Ejemplo n.º 1
0
def test_get_protocol_with_more_than_one_value():
    """
    Are we able to determine the original protocol from the client's launch request?
    """
    utils = LTIUtils()
    handler = Mock(
        spec=RequestHandler, request=Mock(headers={'x-forwarded-proto': 'https,http,http'}, protocol='https',),
    )
    expected = 'https'
    protocol = utils.get_client_protocol(handler)

    assert expected == protocol
Ejemplo n.º 2
0
    async def get(self) -> None:
        """
        Gets the JSON config which is used by LTI platforms
        to install the external tool.
        
        - The extensions key contains settings for specific vendors, such as canvas,
        moodle, edx, among others.
        - The tool uses public settings by default. Users that wish to install the tool with
        private settings should either copy/paste the json or toggle the application to private
        after it is installed with the platform.
        - Usernames are obtained by first attempting to get and normalize values sent when
        tools are installed with public settings. If private, the username is set using the
        anonumized user data when requests are sent with private installation settings.
        """
        lti_utils = LTIUtils()
        self.set_header('Content-Type', 'application/json')

        # get the origin protocol
        protocol = lti_utils.get_client_protocol(self)
        self.log.debug('Origin protocol is: %s' % protocol)
        # build the full target link url value required for the jwks endpoint
        target_link_url = f'{protocol}://{self.request.host}/'
        self.log.debug('Target link url is: %s' % target_link_url)
        keys = {
            'title':
            'IllumiDesk',
            'scopes': [
                'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem',
                'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly',
                'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly',
                'https://purl.imsglobal.org/spec/lti-ags/scope/score',
                'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly',
                'https://canvas.instructure.com/lti/public_jwk/scope/update',
                'https://canvas.instructure.com/lti/data_services/scope/create',
                'https://canvas.instructure.com/lti/data_services/scope/show',
                'https://canvas.instructure.com/lti/data_services/scope/update',
                'https://canvas.instructure.com/lti/data_services/scope/list',
                'https://canvas.instructure.com/lti/data_services/scope/destroy',
                'https://canvas.instructure.com/lti/data_services/scope/list_event_types',
                'https://canvas.instructure.com/lti/feature_flags/scope/show',
                'https://canvas.instructure.com/lti/account_lookup/scope/show',
            ],
            'extensions': [{
                'platform': 'canvas.instructure.com',
                'settings': {
                    'platform':
                    'canvas.instructure.com',
                    'placements': [
                        {
                            'placement': 'course_navigation',
                            'message_type': 'LtiResourceLinkRequest',
                            'windowTarget': '_blank',
                            'target_link_uri': target_link_url,
                            'custom_fields': {
                                'email': '$Person.email.primary',
                                'lms_user_id': '$User.id',
                            },  # noqa: E231
                        },
                        {
                            'placement': 'assignment_selection',
                            'message_type': 'LtiResourceLinkRequest',
                            'target_link_uri': target_link_url,
                        },
                    ],
                },
                'privacy_level': 'public',
            }],
            'description':
            'IllumiDesk Learning Tools Interoperability (LTI) v1.3 tool.',
            'custom_fields': {
                'email': '$Person.email.primary',
                'lms_user_id': '$User.id',
            },  # noqa: E231
            'public_jwk_url':
            f'{target_link_url}hub/lti13/jwks',
            'target_link_uri':
            target_link_url,
            'oidc_initiation_url':
            f'{target_link_url}hub/oauth_login',
        }
        self.write(json.dumps(keys))
Ejemplo n.º 3
0
    async def get(self) -> None:
        """
        Gets the JSON config which is used by LTI platforms
        to install the external tool.

        - The extensions key contains settings for specific vendors, such as canvas,
        moodle, edx, among others.
        - The tool uses public settings by default. Users that wish to install the tool with
        private settings should either copy/paste the json or toggle the application to private
        after it is installed with the platform.
        - Usernames are obtained by first attempting to get and normalize values sent when
        tools are installed with public settings. If private, the username is set using the
        anonumized user data when requests are sent with private installation settings.
        """
        lti_utils = LTIUtils()
        self.set_header("Content-Type", "application/json")

        # get the origin protocol
        protocol = lti_utils.get_client_protocol(self)
        self.log.debug("Origin protocol is: %s" % protocol)
        # build the full target link url value required for the jwks endpoint
        target_link_url = f"{protocol}://{self.request.host}/"
        self.log.debug("Target link url is: %s" % target_link_url)
        keys = {
            "title": "IllumiDesk",
            "scopes": [
                "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
                "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly",
                "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly",
                "https://purl.imsglobal.org/spec/lti-ags/scope/score",
                "https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly",
                "https://canvas.instructure.com/lti/public_jwk/scope/update",
                "https://canvas.instructure.com/lti/data_services/scope/create",
                "https://canvas.instructure.com/lti/data_services/scope/show",
                "https://canvas.instructure.com/lti/data_services/scope/update",
                "https://canvas.instructure.com/lti/data_services/scope/list",
                "https://canvas.instructure.com/lti/data_services/scope/destroy",
                "https://canvas.instructure.com/lti/data_services/scope/list_event_types",
                "https://canvas.instructure.com/lti/feature_flags/scope/show",
                "https://canvas.instructure.com/lti/account_lookup/scope/show",
            ],
            "extensions": [
                {
                    "platform": "canvas.instructure.com",
                    "settings": {
                        "platform": "canvas.instructure.com",
                        "placements": [
                            {
                                "placement": "course_navigation",
                                "message_type": "LtiResourceLinkRequest",
                                "windowTarget": "_blank",
                                "target_link_uri": target_link_url,
                                "custom_fields": {
                                    "email": "$Person.email.primary",
                                    "lms_user_id": "$User.id",
                                },  # noqa: E231
                            },
                            {
                                "placement": "assignment_selection",
                                "message_type": "LtiResourceLinkRequest",
                                "target_link_uri": target_link_url,
                            },
                        ],
                    },
                    "privacy_level": "public",
                }
            ],
            "description": "IllumiDesk Learning Tools Interoperability (LTI) v1.3 tool.",
            "custom_fields": {
                "email": "$Person.email.primary",
                "lms_user_id": "$User.id",
            },  # noqa: E231
            "public_jwk_url": f"{target_link_url}hub/lti13/jwks",
            "target_link_uri": target_link_url,
            "oidc_initiation_url": f"{target_link_url}hub/oauth_login",
        }
        self.write(json.dumps(keys))
Ejemplo n.º 4
0
    async def authenticate(self, handler: BaseHandler, data: Dict[str, str] = None) -> Dict[str, str]:  # noqa: C901
        """
        LTI 1.1 authenticator which overrides authenticate function from base LTIAuthenticator.
        After validating the LTI 1.1 signuature, this function decodes the dictionary object
        from the request payload to set normalized strings for the course_id, username,
        user role, and lms user id. Once those values are set they are added to the auth_state
        and returned as a dictionary for further processing by hooks defined in jupyterhub_config.

        One or more consumer keys/values must be set in the jupyterhub config with the
        LTIAuthenticator.consumers dict.

        Args:
            handler: JupyterHub's Authenticator handler object. For LTI 1.1 requests, the handler is
              an instance of LTIAuthenticateHandler.
            data: optional data object

        Returns:
            Authentication's auth_state dictionary

        Raises:
            HTTPError if the required values are not in the request
        """
        validator = LTI11LaunchValidator(self.consumers)
        lti_utils = LTIUtils()

        self.log.debug('Original arguments received in request: %s' % handler.request.arguments)

        # extract the request arguments to a dict
        args = lti_utils.convert_request_to_dict(handler.request.arguments)
        self.log.debug('Decoded args from request: %s' % args)

        # get the origin protocol
        protocol = lti_utils.get_client_protocol(handler)
        self.log.debug('Origin protocol is: %s' % protocol)

        # build the full launch url value required for oauth1 signatures
        launch_url = f'{protocol}://{handler.request.host}{handler.request.uri}'
        self.log.debug('Launch url is: %s' % launch_url)

        if validator.validate_launch_request(launch_url, handler.request.headers, args):
            # get the lms vendor to implement optional logic for said vendor
            lms_vendor = ''
            if 'tool_consumer_info_product_family_code' in args and args['tool_consumer_info_product_family_code']:
                lms_vendor = args['tool_consumer_info_product_family_code']

            # We use the course_id to setup the grader service notebook. Since this service
            # runs as a container we need to normalize the string so we can use it
            # as a container name.
            if 'context_label' in args and args['context_label']:
                course_id = args['context_label']
                self.log.debug('Course context_label normalized to: %s' % course_id)
            elif 'context_title' in args and args['context_title']:
                course_id = args['context_title']
                self.log.debug('Course context_title normalized to: %s' % course_id)
            else:
                raise HTTPError(400, 'course_label or course_title not included in the LTI request')

            # Get the user's role, assign to Learner role by default. Roles are sent as institution
            # roles, where the roles' value is <handle>,<full URN>.
            # https://www.imsglobal.org/specs/ltiv1p0/implementation-guide#toc-16
            user_role = 'Learner'
            if 'roles' in args and args['roles']:
                user_role = args['roles'].split(',')[0]
                self.log.debug('User LTI role is: %s' % user_role)
            else:
                raise HTTPError(400, 'User role not included in the LTI request')

            # Assign the user_id. Check the tool consumer (lms) vendor. If canvas use their
            # custom user id extension by default, else use standar lti values.
            username = ''
            if lms_vendor == 'canvas':
                login_id = ''
                user_id = ''
                self.log.debug('TC is a Canvas LMS instance')
                if (
                    'custom_canvas_user_login_id' in args
                    and args['custom_canvas_user_login_id']
                    and 'custom_canvas_user_id' in args
                    and args['custom_canvas_user_id']
                ):
                    custom_canvas_user_login_id = args['custom_canvas_user_login_id']
                    login_id = lti_utils.email_to_username(custom_canvas_user_login_id)
                    self.log.debug('using custom_canvas_user_login_id for username')
                if 'custom_canvas_user_id' in args and args['custom_canvas_user_id']:
                    custom_canvas_user_id = args['custom_canvas_user_id']
                    user_id = lti_utils.email_to_username(custom_canvas_user_id)
                    self.log.debug('using custom_canvas_user_id for username')
                username = f'{login_id}-{user_id}'
            if (
                not username
                and 'lis_person_contact_email_primary' in args
                and args['lis_person_contact_email_primary']
            ):
                email = args['lis_person_contact_email_primary']
                username = lti_utils.email_to_username(email)
                self.log.debug('using lis_person_contact_email_primary for username')
            elif not username and 'lis_person_name_given' in args and args['lis_person_name_given']:
                username = args['lis_person_name_given']
                self.log.debug('using lis_person_name_given for username')
            elif not username and 'lis_person_sourcedid' in args and args['lis_person_sourcedid']:
                username = args['lis_person_sourcedid']
                self.log.debug('using lis_person_sourcedid for username')
            elif not username and 'lis_person_name_family' in args and args['lis_person_name_family']:
                username = args['lis_person_name_family']
                self.log.debug('using lis_person_name_family for username')
            elif not username and 'lis_person_name_full' in args and args['lis_person_name_full']:
                username = args['lis_person_name_full']
                self.log.debug('using lis_person_name_full for username')
            elif not username and 'user_id' in args and args['user_id']:
                username = args['user_id']
            elif not username:
                raise HTTPError(400, 'Unable to get username from request arguments')

            # use the user_id to identify the unique user id, if its not sent with the request
            # then default to the username
            lms_user_id = args['user_id'] if 'user_id' in args else username

            # GRADES-SENDER: fetch the information needed to register assignments within the control file
            # retrieve assignment_name from standard property vs custom lms properties
            assignment_name = ''
            # the next fields must come in args
            if 'custom_canvas_assignment_title' in args and args['custom_canvas_assignment_title']:
                assignment_name = lti_utils.normalize_string(args['custom_canvas_assignment_title'])
            # this requires adding a the assignment_title as a custom parameter in the tool consumer (lms)
            elif 'custom_assignment_title' in args and args['custom_assignment_title']:
                assignment_name = lti_utils.normalize_string(args['custom_assignment_title'])
            elif 'resource_link_title' in args and args['resource_link_title']:
                assignment_name = lti_utils.normalize_string(args['resource_link_title'])
            elif 'resource_link_id' in args and args['resource_link_id']:
                assignment_name = lti_utils.normalize_string(args['resource_link_id'])

            # Get lis_outcome_service_url and lis_result_sourcedid values that will help us to submit grades later
            lis_outcome_service_url = ''
            lis_result_sourcedid = ''

            # the next fields must come in args
            if 'lis_outcome_service_url' in args and args['lis_outcome_service_url']:
                lis_outcome_service_url = args['lis_outcome_service_url']
            if 'lis_result_sourcedid' in args and args['lis_result_sourcedid']:
                lis_result_sourcedid = args['lis_result_sourcedid']
            # only if both values exist we can register them to submit grades later
            if lis_outcome_service_url and lis_result_sourcedid:
                control_file = LTIGradesSenderControlFile(f'/home/grader-{course_id}/{course_id}')
                control_file.register_data(assignment_name, lis_outcome_service_url, lms_user_id, lis_result_sourcedid)
            # Assignment creation
            if assignment_name:
                nbgrader_service = NbGraderServiceHelper(course_id, True)
                self.log.debug(
                    'Creating a new assignment from the Authentication flow with title %s' % assignment_name
                )
                nbgrader_service.register_assignment(assignment_name)
            # ensure the user name is normalized
            username_normalized = lti_utils.normalize_string(username)
            self.log.debug('Assigned username is: %s' % username_normalized)

            return {
                'name': username_normalized,
                'auth_state': {
                    'course_id': course_id,
                    'lms_user_id': lms_user_id,
                    'user_role': user_role,
                },  # noqa: E231
            }
Ejemplo n.º 5
0
    async def authenticate(
            self,
            handler: BaseHandler,
            data: Dict[str, str] = None) -> Dict[str, str]:  # noqa: C901
        """
        LTI 1.1 authenticator which overrides authenticate function from base LTIAuthenticator.
        After validating the LTI 1.1 signuature, this function decodes the dictionary object
        from the request payload to set normalized strings for the course_id, username,
        user role, and lms user id. Once those values are set they are added to the auth_state
        and returned as a dictionary for further processing by hooks defined in jupyterhub_config.

        One or more consumer keys/values must be set in the jupyterhub config with the
        LTIAuthenticator.consumers dict.

        Args:
            handler: JupyterHub's Authenticator handler object. For LTI 1.1 requests, the handler is
              an instance of LTIAuthenticateHandler.
            data: optional data object

        Returns:
            Authentication's auth_state dictionary

        Raises:
            HTTPError if the required values are not in the request
        """
        validator = LTI11LaunchValidator(self.consumers)
        lti_utils = LTIUtils()

        self.log.debug('Original arguments received in request: %s' %
                       handler.request.arguments)

        # extract the request arguments to a dict
        args = lti_utils.convert_request_to_dict(handler.request.arguments)
        self.log.debug('Decoded args from request: %s' % args)

        # get the origin protocol
        protocol = lti_utils.get_client_protocol(handler)
        self.log.debug('Origin protocol is: %s' % protocol)

        # build the full launch url value required for oauth1 signatures
        launch_url = f'{protocol}://{handler.request.host}{handler.request.uri}'
        self.log.debug('Launch url is: %s' % launch_url)

        if validator.validate_launch_request(launch_url,
                                             handler.request.headers, args):
            # get the lms vendor to implement optional logic for said vendor
            lms_vendor = ''
            if ('tool_consumer_info_product_family_code' in args
                    and args['tool_consumer_info_product_family_code']
                    is not None):
                lms_vendor = args['tool_consumer_info_product_family_code']

            # We use the course_id to setup the grader service notebook. Since this service
            # runs as a docker container we need to normalize the string so we can use it
            # as a container name.
            if 'context_label' in args and args['context_label'] is not None:
                course_id = args['context_label']
                self.log.debug('Course context_label normalized to: %s' %
                               course_id)
            else:
                raise HTTPError(
                    400, 'Course label not included in the LTI request')

            # Users have the option to initiate a launch request with the workspace_type they would like
            # to launch. edX prepends arguments with custom_*, so we need to check for those too.
            workspace_type = ''
            if 'custom_workspace_type' in args or 'workspace_type' in args:
                workspace_type = (args['custom_workspace_type']
                                  if 'custom_workspace_type' in args else
                                  args['workspace_type'])
            if workspace_type is None or workspace_type not in WORKSPACE_TYPES:
                workspace_type = 'notebook'
            self.log.debug('Workspace type assigned as: %s' % workspace_type)

            # Get the user's role, assign to Learner role by default. Roles are sent as institution
            # roles, where the roles' value is <handle>,<full URN>.
            # https://www.imsglobal.org/specs/ltiv1p0/implementation-guide#toc-16
            user_role = 'Learner'
            if 'roles' in args and args['roles'] is not None:
                user_role = args['roles'].split(',')[0]
                self.log.debug('User LTI role is: %s' % user_role)
            else:
                raise HTTPError(400,
                                'User role not included in the LTI request')

            # Assign the user_id. Check the tool consumer (lms) vendor. If canvas use their
            # custom user id extension by default, else use standar lti values.
            username = ''
            # GRADES-SENDER: retrieve assignment_name from standard property vs custom lms
            # properties, such as custom_canvas_...
            assignment_name = args[
                'resource_link_title'] if 'resource_link_title' in args else 'unknown'
            if lms_vendor == 'canvas':
                self.log.debug('TC is a Canvas LMS instance')
                if 'custom_canvas_user_login_id' in args and args[
                        'custom_canvas_user_login_id'] is not None:
                    custom_canvas_user_id = args['custom_canvas_user_login_id']
                    username = lti_utils.email_to_username(
                        custom_canvas_user_id)
                    self.log.debug('using custom_canvas_user_id for username')
                # GRADES-SENDER >>>> retrieve assignment_name from custom property
                assignment_name = (args['custom_canvas_assignment_title']
                                   if 'custom_canvas_assignment_title' in args
                                   else 'unknown')
            else:
                if (username == ''
                        and 'lis_person_contact_email_primary' in args and
                        args['lis_person_contact_email_primary'] is not None):
                    email = args['lis_person_contact_email_primary']
                    username = lti_utils.email_to_username(email)
                    self.log.debug(
                        'using lis_person_contact_email_primary for username')
                elif 'lis_person_name_given' in args and args[
                        'lis_person_name_given'] is not None:
                    username = args['lis_person_name_given']
                    self.log.debug('using lis_person_name_given for username')
                elif 'lis_person_sourcedid' in args and args[
                        'lis_person_sourcedid'] is not None:
                    username = args['lis_person_sourcedid']
                    self.log.debug('using lis_person_sourcedid for username')
            if username == '':
                self.log.debug('using user_id for username')
                if 'user_id' in args and args['user_id'] is not None:
                    username = args['user_id']
                else:
                    raise HTTPError(
                        400, 'Unable to get username from request arguments')
            self.log.debug('Assigned username is: %s' % username)

            # use the user_id to identify the unique user id, if its not sent with the request
            # then default to the username
            lms_user_id = args['user_id'] if 'user_id' in args else username

            # with all info extracted from lms request, register info for grades sender only if the user has
            # the Learner role
            lis_outcome_service_url = None
            lis_result_sourcedid = None
            if user_role == 'Learner' or user_role == 'Student':
                # the next fields must come in args
                if 'lis_outcome_service_url' in args and args[
                        'lis_outcome_service_url'] is not None:
                    lis_outcome_service_url = args['lis_outcome_service_url']
                if 'lis_result_sourcedid' in args and args[
                        'lis_result_sourcedid'] is not None:
                    lis_result_sourcedid = args['lis_result_sourcedid']
                # only if both values exist we can register them to submit grades later
                if lis_outcome_service_url and lis_result_sourcedid:
                    control_file = LTIGradesSenderControlFile(
                        f'/home/grader-{course_id}/{course_id}')
                    control_file.register_data(assignment_name,
                                               lis_outcome_service_url,
                                               lms_user_id,
                                               lis_result_sourcedid)

            return {
                'name': username,
                'auth_state': {
                    'course_id': course_id,
                    'lms_user_id': lms_user_id,
                    'user_role': user_role,
                    'workspace_type': workspace_type,
                },  # noqa: E231
            }