Пример #1
0
    def test_editing_as_subscriber(self):
        """Test that we can set certain fields, but not all."""

        from application.utils import remove_private_keys, PillarJSONEncoder
        dumps = functools.partial(json.dumps, cls=PillarJSONEncoder)

        project_info = self._create_user_and_project([u'subscriber'])
        project_url = '/projects/%(_id)s' % project_info

        resp = self.client.get(project_url,
                               headers={'Authorization': self.make_header('token')})
        project = json.loads(resp.data.decode('utf-8'))

        # Create another user we can try and assign the project to.
        other_user_id = 'f00dd00df00dd00df00dd00d'
        self._create_user_with_token(['subscriber'], 'other-token', user_id=other_user_id)

        # Unauthenticated should be forbidden
        resp = self.client.put('/projects/%s' % project['_id'],
                               data=dumps(remove_private_keys(project)),
                               headers={'Content-Type': 'application/json'})
        self.assertEqual(403, resp.status_code)

        # Regular user should be able to PUT, but only be able to edit certain fields.
        put_project = remove_private_keys(project)
        put_project['url'] = u'very-offensive-url'
        put_project['description'] = u'Blender je besplatan set alata za izradu interaktivnog 3D ' \
                                     u'sadržaja pod različitim operativnim sustavima.'
        put_project['name'] = u'โครงการปั่นเมฆ'
        put_project['summary'] = u'Это переведена на Google'
        put_project['status'] = 'pending'
        put_project['category'] = 'software'
        put_project['user'] = other_user_id

        # Try making the project public. This should update is_private as well.
        put_project['permissions']['world'] = ['GET']

        resp = self.client.put(project_url,
                               data=dumps(put_project),
                               headers={'Authorization': self.make_header('token'),
                                        'Content-Type': 'application/json',
                                        'If-Match': project['_etag']})
        self.assertEqual(200, resp.status_code, resp.data)

        # Re-fetch from database to see which fields actually made it there.
        # equal to put_project -> changed in DB
        # equal to project -> not changed in DB
        resp = self.client.get(project_url,
                               headers={'Authorization': self.make_header('token')})
        db_proj = json.loads(resp.data)
        self.assertEqual(project['url'], db_proj['url'])
        self.assertEqual(put_project['description'], db_proj['description'])
        self.assertEqual(put_project['name'], db_proj['name'])
        self.assertEqual(put_project['summary'], db_proj['summary'])
        self.assertEqual(project['status'], db_proj['status'])
        self.assertEqual(project['category'], db_proj['category'])

        # Project should be consistent.
        self.assertEqual(False, db_proj['is_private'])
        self.assertEqual(['GET'], db_proj['permissions']['world'])
Пример #2
0
def _generate_all_links(response, now):
    """Generate a new link for the file and all its variations.

    :param response: the file document that should be updated.
    :param now: datetime that reflects 'now', for consistent expiry generation.
    """

    project_id = str(
        response['project']) if 'project' in response else None  # TODO: add project id to all files
    backend = response['backend']
    response['link'] = generate_link(backend, response['file_path'], project_id)

    variations = response.get('variations')
    if variations:
        for variation in variations:
            variation['link'] = generate_link(backend, variation['file_path'], project_id)

    # Construct the new expiry datetime.
    validity_secs = current_app.config['FILE_LINK_VALIDITY'][backend]
    response['link_expires'] = now + datetime.timedelta(seconds=validity_secs)

    patch_info = remove_private_keys(response)
    file_id = ObjectId(response['_id'])
    (patch_resp, _, _, _) = patch_internal('files', patch_info, _id=file_id)
    if patch_resp.get('_status') == 'ERR':
        log.warning('Unable to save new links for file %s: %r', response['_id'], patch_resp)
        # TODO: raise a snag.
        response['_updated'] = now
    else:
        response['_updated'] = patch_resp['_updated']

    # Be silly and re-fetch the etag ourselves. TODO: handle this better.
    etag_doc = current_app.data.driver.db['files'].find_one({'_id': file_id}, {'_etag': 1})
    response['_etag'] = etag_doc['_etag']
Пример #3
0
    def test_edits_by_nonowner_subscriber(self):
        """A subscriber should only be able to edit their own projects."""

        from application.utils import remove_private_keys, PillarJSONEncoder
        dumps = functools.partial(json.dumps, cls=PillarJSONEncoder)

        # Create test project.
        project = self._create_user_and_project([u'subscriber'])
        project_id = project['_id']
        project_url = '/projects/%s' % project_id

        # Create test user.
        my_user_id = 'cafef00dbeefcafef00dbeef'
        self._create_user_with_token(['subscriber'], 'mortal-token', user_id=my_user_id)

        # Regular subscriber should not be able to do this.
        put_project = remove_private_keys(project)
        put_project['name'] = u'Болту́н -- нахо́дка для шпио́на.'
        put_project['user'] = my_user_id
        resp = self.client.put(project_url,
                               data=dumps(put_project),
                               headers={'Authorization': self.make_header('mortal-token'),
                                        'Content-Type': 'application/json',
                                        'If-Match': project['_etag']})
        self.assertEqual(403, resp.status_code, resp.data)
    def test_edits_by_nonowner_subscriber(self):
        """A subscriber should only be able to edit their own projects."""

        from application.utils import remove_private_keys, PillarJSONEncoder
        dumps = functools.partial(json.dumps, cls=PillarJSONEncoder)

        # Create test project.
        project = self._create_user_and_project([u'subscriber'])
        project_id = project['_id']
        project_url = '/projects/%s' % project_id

        # Create test user.
        my_user_id = 'cafef00dbeefcafef00dbeef'
        self._create_user_with_token(['subscriber'],
                                     'mortal-token',
                                     user_id=my_user_id)

        # Regular subscriber should not be able to do this.
        put_project = remove_private_keys(project)
        put_project['name'] = u'Болту́н -- нахо́дка для шпио́на.'
        put_project['user'] = my_user_id
        resp = self.client.put(project_url,
                               data=dumps(put_project),
                               headers={
                                   'Authorization':
                                   self.make_header('mortal-token'),
                                   'Content-Type':
                                   'application/json',
                                   'If-Match':
                                   project['_etag']
                               })
        self.assertEqual(403, resp.status_code, resp.data)
    def test_edits_by_nonowner_admin(self):
        """Any admin should be able to edit any project."""

        from application.utils import remove_private_keys, PillarJSONEncoder
        dumps = functools.partial(json.dumps, cls=PillarJSONEncoder)

        # Create test project.
        project = self._create_user_and_project([u'subscriber'])
        project_id = project['_id']
        project_url = '/projects/%s' % project_id

        # Create test user.
        self._create_user_with_token(['admin'],
                                     'admin-token',
                                     user_id='cafef00dbeef')

        # Admin user should be able to PUT.
        put_project = remove_private_keys(project)
        put_project['name'] = u'โครงการปั่นเมฆ'

        resp = self.client.put(project_url,
                               data=dumps(put_project),
                               headers={
                                   'Authorization':
                                   self.make_header('admin-token'),
                                   'Content-Type': 'application/json',
                                   'If-Match': project['_etag']
                               })
        self.assertEqual(200, resp.status_code, resp.data)
Пример #6
0
    def test_put_admin(self):
        from application.utils import remove_private_keys

        # PUTting a user should work, and not mess up the auth field.
        resp = self.client.get(
            '/users/123456789abc123456789abc',
            headers={'Authorization': self.make_header('token')})
        user_info = json.loads(resp.data)
        put_user = remove_private_keys(user_info)

        resp = self.client.put('/users/123456789abc123456789abc',
                               headers={
                                   'Authorization':
                                   self.make_header('admin-token'),
                                   'Content-Type': 'application/json',
                                   'If-Match': user_info['_etag']
                               },
                               data=json.dumps(put_user))
        self.assertEqual(200, resp.status_code, resp.data)

        # Get directly from MongoDB, Eve blocks access to the auth field.
        with self.app.test_request_context():
            users = self.app.data.driver.db['users']
            db_user = users.find_one(ObjectId('123456789abc123456789abc'))
            self.assertIn('auth', db_user)
Пример #7
0
    def test_user_anonymous(self):
        from application.utils import remove_private_keys

        # Getting a user should be limited to certain fields
        resp = self.client.get('/users/123456789abc123456789abc')
        self.assertEqual(200, resp.status_code)

        user_info = json.loads(resp.data)
        regular_info = remove_private_keys(user_info)
        self.assertEqual(PUBLIC_USER_FIELDS, set(regular_info.keys()))
Пример #8
0
    def test_user_anonymous(self):
        from application.utils import remove_private_keys

        # Getting a user should be limited to certain fields
        resp = self.client.get('/users/123456789abc123456789abc')
        self.assertEqual(200, resp.status_code)

        user_info = json.loads(resp.data)
        regular_info = remove_private_keys(user_info)
        self.assertEqual(PUBLIC_USER_FIELDS, set(regular_info.keys()))
Пример #9
0
    def test_save_own_user(self):
        """Tests that a user can't change their own fields."""

        from application.utils import authentication as auth
        from application.utils import PillarJSONEncoder, remove_private_keys

        user_id = self.create_user(roles=[u'subscriber'])

        now = datetime.datetime.now(tz_util.utc)
        future = now + datetime.timedelta(days=1)

        with self.app.test_request_context():
            auth.store_token(user_id, 'nonexpired-main', future, None)

        with self.app.test_request_context(
                headers={'Authorization': self.make_header('nonexpired-main')}):
            self.assertTrue(auth.validate_token())

            users = self.app.data.driver.db['users']
            db_user = users.find_one(user_id)

        updated_fields = remove_private_keys(db_user)
        updated_fields['roles'] = ['admin', 'subscriber', 'demo']  # Try to elevate our roles.

        # POSTing updated info to a specific user URL is not allowed by Eve.
        resp = self.client.post('/users/%s' % user_id,
                                data=json.dumps(updated_fields, cls=PillarJSONEncoder),
                                headers={'Authorization': self.make_header('nonexpired-main'),
                                         'Content-Type': 'application/json'})
        self.assertEqual(405, resp.status_code)

        # PUT and PATCH should not be allowed.
        resp = self.client.put('/users/%s' % user_id,
                               data=json.dumps(updated_fields, cls=PillarJSONEncoder),
                               headers={'Authorization': self.make_header('nonexpired-main'),
                                        'Content-Type': 'application/json'})
        self.assertEqual(403, resp.status_code)

        updated_fields = {'roles': ['admin', 'subscriber', 'demo']}
        resp = self.client.patch('/users/%s' % user_id,
                                 data=json.dumps(updated_fields, cls=PillarJSONEncoder),
                                 headers={'Authorization': self.make_header('nonexpired-main'),
                                          'Content-Type': 'application/json'})
        self.assertEqual(405, resp.status_code)

        # After all of this, the roles should be the same.
        with self.app.test_request_context(
                headers={'Authorization': self.make_header('nonexpired-main')}):
            self.assertTrue(auth.validate_token())

            users = self.app.data.driver.db['users']
            db_user = users.find_one(user_id)

            self.assertEqual([u'subscriber'], db_user['roles'])
    def test_editing_as_admin(self):
        """Test that we can set all fields as admin."""

        from application.utils import remove_private_keys, PillarJSONEncoder
        dumps = functools.partial(json.dumps, cls=PillarJSONEncoder)

        project_info = self._create_user_and_project([u'subscriber', u'admin'])
        project_url = '/projects/%(_id)s' % project_info

        resp = self.client.get(project_url)
        project = json.loads(resp.data.decode('utf-8'))

        # Create another user we can try and assign the project to.
        other_user_id = 'f00dd00df00dd00df00dd00d'
        self._create_user_with_token(['subscriber'],
                                     'other-token',
                                     user_id=other_user_id)

        # Admin user should be able to PUT everything.
        put_project = remove_private_keys(project)
        put_project['url'] = u'very-offensive-url'
        put_project['description'] = u'Blender je besplatan set alata za izradu interaktivnog 3D ' \
                                     u'sadržaja pod različitim operativnim sustavima.'
        put_project['name'] = u'โครงการปั่นเมฆ'
        put_project['summary'] = u'Это переведена на Google'
        put_project['is_private'] = False
        put_project['status'] = 'pending'
        put_project['category'] = 'software'
        put_project['user'] = other_user_id

        resp = self.client.put(project_url,
                               data=dumps(put_project),
                               headers={
                                   'Authorization': self.make_header('token'),
                                   'Content-Type': 'application/json',
                                   'If-Match': project['_etag']
                               })
        self.assertEqual(200, resp.status_code, resp.data)

        # Re-fetch from database to see which fields actually made it there.
        # equal to put_project -> changed in DB
        # equal to project -> not changed in DB
        resp = self.client.get('/projects/%s' % project['_id'])
        db_proj = json.loads(resp.data)
        self.assertEqual(put_project['url'], db_proj['url'])
        self.assertEqual(put_project['description'], db_proj['description'])
        self.assertEqual(put_project['name'], db_proj['name'])
        self.assertEqual(put_project['summary'], db_proj['summary'])
        self.assertEqual(put_project['is_private'], db_proj['is_private'])
        self.assertEqual(put_project['status'], db_proj['status'])
        self.assertEqual(put_project['category'], db_proj['category'])
        self.assertEqual(put_project['user'], db_proj['user'])
Пример #11
0
def create_file_doc_for_upload(project_id, uploaded_file):
    """Creates a secure filename and a document in MongoDB for the file.

    The (project_id, filename) tuple should be unique. If such a document already
    exists, it is updated with the new file.

    :param uploaded_file: file from request.files['form-key']
    :type uploaded_file: werkzeug.datastructures.FileStorage
    :returns: a tuple (file_id, filename, status), where 'filename' is the internal
            filename used on GCS.
    """

    project_id = ObjectId(project_id)

    # Hash the filename with path info to get the internal name. This should
    # be unique for the project.
    # internal_filename = uploaded_file.filename
    _, ext = os.path.splitext(uploaded_file.filename)
    internal_filename = uuid.uuid4().hex + ext

    # For now, we don't support overwriting files, and create a new one every time.
    # # See if we can find a pre-existing file doc.
    # files = current_app.data.driver.db['files']
    # file_doc = files.find_one({'project': project_id,
    #                            'name': internal_filename})
    file_doc = None

    # TODO: at some point do name-based and content-based content-type sniffing.
    new_props = {
        'filename': uploaded_file.filename,
        'content_type': uploaded_file.mimetype,
        'length': uploaded_file.content_length,
        'project': project_id,
        'status': 'uploading'
    }

    if file_doc is None:
        # Create a file document on MongoDB for this file.
        file_doc = create_file_doc(name=internal_filename, **new_props)
        file_fields, _, _, status = post_internal('files', file_doc)
    else:
        file_doc.update(new_props)
        file_fields, _, _, status = put_internal('files',
                                                 remove_private_keys(file_doc))

    if status not in (200, 201):
        log.error(
            'Unable to create new file document in MongoDB, status=%i: %s',
            status, file_fields)
        raise InternalServerError()

    return file_fields['_id'], internal_filename, status
Пример #12
0
    def test_other_user_subscriber(self):
        from application.utils import remove_private_keys

        # Requesting another user should be limited to full name and email.
        resp = self.client.get('/users/%s' % '223456789abc123456789abc',
                               headers={'Authorization': self.make_header('token')})
        user_info = json.loads(resp.data)

        self.assertEqual(200, resp.status_code)
        self.assertNotIn('auth', user_info)

        regular_info = remove_private_keys(user_info)
        self.assertEqual(PUBLIC_USER_FIELDS, set(regular_info.keys()))
    def test_is_private_updated_by_world_permissions(self):
        """For backward compatibility, is_private should reflect absence of world-GET"""

        from application.utils import remove_private_keys, dumps

        project_url = '/projects/%s' % self.project_id
        put_project = remove_private_keys(self.project)

        # Create admin user.
        self._create_user_with_token(['admin'],
                                     'admin-token',
                                     user_id='cafef00dbeef')

        # Make the project public
        put_project['permissions']['world'] = ['GET']  # make public
        put_project['is_private'] = True  # This should be overridden.

        resp = self.client.put(project_url,
                               data=dumps(put_project),
                               headers={
                                   'Authorization':
                                   self.make_header('admin-token'),
                                   'Content-Type': 'application/json',
                                   'If-Match': self.project['_etag']
                               })
        self.assertEqual(200, resp.status_code, resp.data)

        with self.app.test_request_context():
            projects = self.app.data.driver.db['projects']
            db_proj = projects.find_one(self.project_id)
            self.assertEqual(['GET'], db_proj['permissions']['world'])
            self.assertFalse(db_proj['is_private'])

        # Make the project private
        put_project['permissions']['world'] = []

        resp = self.client.put(project_url,
                               data=dumps(put_project),
                               headers={
                                   'Authorization':
                                   self.make_header('admin-token'),
                                   'Content-Type': 'application/json',
                                   'If-Match': db_proj['_etag']
                               })
        self.assertEqual(200, resp.status_code, resp.data)

        with self.app.test_request_context():
            projects = self.app.data.driver.db['projects']
            db_proj = projects.find_one(self.project_id)
            self.assertEqual([], db_proj['permissions']['world'])
            self.assertTrue(db_proj['is_private'])
Пример #14
0
    def test_other_user_subscriber(self):
        from application.utils import remove_private_keys

        # Requesting another user should be limited to full name and email.
        resp = self.client.get(
            '/users/%s' % '223456789abc123456789abc',
            headers={'Authorization': self.make_header('token')})
        user_info = json.loads(resp.data)

        self.assertEqual(200, resp.status_code)
        self.assertNotIn('auth', user_info)

        regular_info = remove_private_keys(user_info)
        self.assertEqual(PUBLIC_USER_FIELDS, set(regular_info.keys()))
Пример #15
0
    def test_put_other_user(self):
        from application.utils import remove_private_keys

        # PUTting the user as another user should fail.
        resp = self.client.get('/users/123456789abc123456789abc',
                               headers={'Authorization': self.make_header('token')})
        user_info = json.loads(resp.data)
        put_user = remove_private_keys(user_info)

        resp = self.client.put('/users/123456789abc123456789abc',
                               headers={'Authorization': self.make_header('other-token'),
                                        'Content-Type': 'application/json',
                                        'If-Match': user_info['_etag']},
                               data=json.dumps(put_user))
        self.assertEqual(403, resp.status_code, resp.data)
Пример #16
0
def create_file_doc_for_upload(project_id, uploaded_file):
    """Creates a secure filename and a document in MongoDB for the file.

    The (project_id, filename) tuple should be unique. If such a document already
    exists, it is updated with the new file.

    :param uploaded_file: file from request.files['form-key']
    :type uploaded_file: werkzeug.datastructures.FileStorage
    :returns: a tuple (file_id, filename, status), where 'filename' is the internal
            filename used on GCS.
    """

    project_id = ObjectId(project_id)

    # Hash the filename with path info to get the internal name. This should
    # be unique for the project.
    # internal_filename = uploaded_file.filename
    _, ext = os.path.splitext(uploaded_file.filename)
    internal_filename = uuid.uuid4().hex + ext

    # For now, we don't support overwriting files, and create a new one every time.
    # # See if we can find a pre-existing file doc.
    # files = current_app.data.driver.db['files']
    # file_doc = files.find_one({'project': project_id,
    #                            'name': internal_filename})
    file_doc = None

    # TODO: at some point do name-based and content-based content-type sniffing.
    new_props = {'filename': uploaded_file.filename,
                 'content_type': uploaded_file.mimetype,
                 'length': uploaded_file.content_length,
                 'project': project_id,
                 'status': 'uploading'}

    if file_doc is None:
        # Create a file document on MongoDB for this file.
        file_doc = create_file_doc(name=internal_filename, **new_props)
        file_fields, _, _, status = post_internal('files', file_doc)
    else:
        file_doc.update(new_props)
        file_fields, _, _, status = put_internal('files', remove_private_keys(file_doc))

    if status not in (200, 201):
        log.error('Unable to create new file document in MongoDB, status=%i: %s',
                  status, file_fields)
        raise InternalServerError()

    return file_fields['_id'], internal_filename, status
Пример #17
0
    def test_is_private_updated_by_world_permissions(self):
        """For backward compatibility, is_private should reflect absence of world-GET"""

        from application.utils import remove_private_keys, dumps

        project_url = '/projects/%s' % self.project_id
        put_project = remove_private_keys(self.project)

        # Create admin user.
        self._create_user_with_token(['admin'], 'admin-token', user_id='cafef00dbeef')

        # Make the project public
        put_project['permissions']['world'] = ['GET']  # make public
        put_project['is_private'] = True  # This should be overridden.

        resp = self.client.put(project_url,
                               data=dumps(put_project),
                               headers={'Authorization': self.make_header('admin-token'),
                                        'Content-Type': 'application/json',
                                        'If-Match': self.project['_etag']})
        self.assertEqual(200, resp.status_code, resp.data)

        with self.app.test_request_context():
            projects = self.app.data.driver.db['projects']
            db_proj = projects.find_one(self.project_id)
            self.assertEqual(['GET'], db_proj['permissions']['world'])
            self.assertFalse(db_proj['is_private'])

        # Make the project private
        put_project['permissions']['world'] = []

        resp = self.client.put(project_url,
                               data=dumps(put_project),
                               headers={'Authorization': self.make_header('admin-token'),
                                        'Content-Type': 'application/json',
                                        'If-Match': db_proj['_etag']})
        self.assertEqual(200, resp.status_code, resp.data)

        with self.app.test_request_context():
            projects = self.app.data.driver.db['projects']
            db_proj = projects.find_one(self.project_id)
            self.assertEqual([], db_proj['permissions']['world'])
            self.assertTrue(db_proj['is_private'])
Пример #18
0
    def test_put_other_user(self):
        from application.utils import remove_private_keys

        # PUTting the user as another user should fail.
        resp = self.client.get(
            '/users/123456789abc123456789abc',
            headers={'Authorization': self.make_header('token')})
        user_info = json.loads(resp.data)
        put_user = remove_private_keys(user_info)

        resp = self.client.put('/users/123456789abc123456789abc',
                               headers={
                                   'Authorization':
                                   self.make_header('other-token'),
                                   'Content-Type': 'application/json',
                                   'If-Match': user_info['_etag']
                               },
                               data=json.dumps(put_user))
        self.assertEqual(403, resp.status_code, resp.data)
Пример #19
0
def _update_project(project_uuid, project):
    """Updates a project in the database, or SystemExit()s.

    :param project_uuid: UUID of the project
    :type: str
    :param project: the project data, should be the entire project document
    :type: dict
    :return: the project
    :rtype: dict
    """

    from application.utils import remove_private_keys

    project_id = ObjectId(project_uuid)
    project = remove_private_keys(project)
    result, _, _, _ = put_internal('projects', project, _id=project_id)

    if result['_status'] != 'OK':
        log.error("Can't update project %s, issues: %s", project_uuid, result['_issues'])
        raise SystemExit()
Пример #20
0
    def test_put_admin(self):
        from application.utils import remove_private_keys

        # PUTting a user should work, and not mess up the auth field.
        resp = self.client.get('/users/123456789abc123456789abc',
                               headers={'Authorization': self.make_header('token')})
        user_info = json.loads(resp.data)
        put_user = remove_private_keys(user_info)

        resp = self.client.put('/users/123456789abc123456789abc',
                               headers={'Authorization': self.make_header('admin-token'),
                                        'Content-Type': 'application/json',
                                        'If-Match': user_info['_etag']},
                               data=json.dumps(put_user))
        self.assertEqual(200, resp.status_code, resp.data)

        # Get directly from MongoDB, Eve blocks access to the auth field.
        with self.app.test_request_context():
            users = self.app.data.driver.db['users']
            db_user = users.find_one(ObjectId('123456789abc123456789abc'))
            self.assertIn('auth', db_user)
Пример #21
0
def _generate_all_links(response, now):
    """Generate a new link for the file and all its variations.

    :param response: the file document that should be updated.
    :param now: datetime that reflects 'now', for consistent expiry generation.
    """

    project_id = str(
        response['project']
    ) if 'project' in response else None  # TODO: add project id to all files
    backend = response['backend']
    response['link'] = generate_link(backend, response['file_path'],
                                     project_id)

    variations = response.get('variations')
    if variations:
        for variation in variations:
            variation['link'] = generate_link(backend, variation['file_path'],
                                              project_id)

    # Construct the new expiry datetime.
    validity_secs = current_app.config['FILE_LINK_VALIDITY'][backend]
    response['link_expires'] = now + datetime.timedelta(seconds=validity_secs)

    patch_info = remove_private_keys(response)
    file_id = ObjectId(response['_id'])
    (patch_resp, _, _, _) = patch_internal('files', patch_info, _id=file_id)
    if patch_resp.get('_status') == 'ERR':
        log.warning('Unable to save new links for file %s: %r',
                    response['_id'], patch_resp)
        # TODO: raise a snag.
        response['_updated'] = now
    else:
        response['_updated'] = patch_resp['_updated']

    # Be silly and re-fetch the etag ourselves. TODO: handle this better.
    etag_doc = current_app.data.driver.db['files'].find_one({'_id': file_id},
                                                            {'_etag': 1})
    response['_etag'] = etag_doc['_etag']
Пример #22
0
    def test_edits_by_nonowner_admin(self):
        """Any admin should be able to edit any project."""

        from application.utils import remove_private_keys, PillarJSONEncoder
        dumps = functools.partial(json.dumps, cls=PillarJSONEncoder)

        # Create test project.
        project = self._create_user_and_project([u'subscriber'])
        project_id = project['_id']
        project_url = '/projects/%s' % project_id

        # Create test user.
        self._create_user_with_token(['admin'], 'admin-token', user_id='cafef00dbeef')

        # Admin user should be able to PUT.
        put_project = remove_private_keys(project)
        put_project['name'] = u'โครงการปั่นเมฆ'

        resp = self.client.put(project_url,
                               data=dumps(put_project),
                               headers={'Authorization': self.make_header('admin-token'),
                                        'Content-Type': 'application/json',
                                        'If-Match': project['_etag']})
        self.assertEqual(200, resp.status_code, resp.data)
Пример #23
0
def after_inserting_project(project, db_user):
    project_id = project['_id']
    user_id = db_user['_id']

    # Create a project-specific admin group (with name matching the project id)
    result, _, _, status = post_internal('groups', {'name': str(project_id)})
    if status != 201:
        log.error('Unable to create admin group for new project %s: %s',
                  project_id, result)
        return abort_with_error(status)

    admin_group_id = result['_id']
    log.debug('Created admin group %s for project %s', admin_group_id,
              project_id)

    # Assign the current user to the group
    db_user.setdefault('groups', []).append(admin_group_id)

    result, _, _, status = patch_internal('users',
                                          {'groups': db_user['groups']},
                                          _id=user_id)
    if status != 200:
        log.error(
            'Unable to add user %s as member of admin group %s for new project %s: %s',
            user_id, admin_group_id, project_id, result)
        return abort_with_error(status)
    log.debug('Made user %s member of group %s', user_id, admin_group_id)

    # Assign the group to the project with admin rights
    is_admin = authorization.is_admin(db_user)
    world_permissions = ['GET'] if is_admin else []
    permissions = {
        'world':
        world_permissions,
        'users': [],
        'groups': [
            {
                'group': admin_group_id,
                'methods': DEFAULT_ADMIN_GROUP_PERMISSIONS[:]
            },
        ]
    }

    def with_permissions(node_type):
        copied = copy.deepcopy(node_type)
        copied['permissions'] = permissions
        return copied

    # Assign permissions to the project itself, as well as to the node_types
    project['permissions'] = permissions
    project['node_types'] = [
        with_permissions(node_type_group),
        with_permissions(node_type_asset),
        with_permissions(node_type_comment),
        with_permissions(node_type_texture),
        with_permissions(node_type_group_texture),
    ]

    # Allow admin users to use whatever url they want.
    if not is_admin or not project.get('url'):
        project['url'] = "p-{!s}".format(project_id)

    # Initialize storage page (defaults to GCS)
    if current_app.config.get('TESTING'):
        log.warning(
            'Not creating Google Cloud Storage bucket while running unit tests!'
        )
    else:
        gcs_storage = GoogleCloudStorageBucket(str(project_id))
        if gcs_storage.bucket.exists():
            log.info('Created CGS instance for project %s', project_id)
        else:
            log.warning('Unable to create CGS instance for project %s',
                        project_id)

    # Commit the changes directly to the MongoDB; a PUT is not allowed yet,
    # as the project doesn't have a valid permission structure.
    projects_collection = current_app.data.driver.db['projects']
    result = projects_collection.update_one(
        {'_id': project_id}, {'$set': remove_private_keys(project)})
    if result.matched_count != 1:
        log.warning('Unable to update project %s: %s', project_id,
                    result.raw_result)
        abort_with_error(500)
Пример #24
0
    def test_save_own_user(self):
        """Tests that a user can't change their own fields."""

        from application.utils import authentication as auth
        from application.utils import PillarJSONEncoder, remove_private_keys

        user_id = self.create_user(roles=[u'subscriber'])

        now = datetime.datetime.now(tz_util.utc)
        future = now + datetime.timedelta(days=1)

        with self.app.test_request_context():
            auth.store_token(user_id, 'nonexpired-main', future, None)

        with self.app.test_request_context(
                headers={'Authorization': self.make_header('nonexpired-main')
                         }):
            self.assertTrue(auth.validate_token())

            users = self.app.data.driver.db['users']
            db_user = users.find_one(user_id)

        updated_fields = remove_private_keys(db_user)
        updated_fields['roles'] = ['admin', 'subscriber',
                                   'demo']  # Try to elevate our roles.

        # POSTing updated info to a specific user URL is not allowed by Eve.
        resp = self.client.post('/users/%s' % user_id,
                                data=json.dumps(updated_fields,
                                                cls=PillarJSONEncoder),
                                headers={
                                    'Authorization':
                                    self.make_header('nonexpired-main'),
                                    'Content-Type':
                                    'application/json'
                                })
        self.assertEqual(405, resp.status_code)

        # PUT and PATCH should not be allowed.
        resp = self.client.put('/users/%s' % user_id,
                               data=json.dumps(updated_fields,
                                               cls=PillarJSONEncoder),
                               headers={
                                   'Authorization':
                                   self.make_header('nonexpired-main'),
                                   'Content-Type':
                                   'application/json'
                               })
        self.assertEqual(403, resp.status_code)

        updated_fields = {'roles': ['admin', 'subscriber', 'demo']}
        resp = self.client.patch('/users/%s' % user_id,
                                 data=json.dumps(updated_fields,
                                                 cls=PillarJSONEncoder),
                                 headers={
                                     'Authorization':
                                     self.make_header('nonexpired-main'),
                                     'Content-Type':
                                     'application/json'
                                 })
        self.assertEqual(405, resp.status_code)

        # After all of this, the roles should be the same.
        with self.app.test_request_context(
                headers={'Authorization': self.make_header('nonexpired-main')
                         }):
            self.assertTrue(auth.validate_token())

            users = self.app.data.driver.db['users']
            db_user = users.find_one(user_id)

            self.assertEqual([u'subscriber'], db_user['roles'])
Пример #25
0
def validate_create_user(blender_id_user_id, token, oauth_subclient_id):
    """Validates a user against Blender ID, creating the user in our database.

    :param blender_id_user_id: the user ID at the BlenderID server.
    :param token: the OAuth access token.
    :param oauth_subclient_id: the subclient ID, or empty string if not a subclient.
    :returns: (user in MongoDB, HTTP status 200 or 201)
    """

    # Verify with Blender ID
    log.debug('Storing token for BlenderID user %s', blender_id_user_id)
    user_info, token_expiry = validate_token(blender_id_user_id, token, oauth_subclient_id)

    if user_info is None:
        log.debug('Unable to verify token with Blender ID.')
        return None, None

    # Blender ID can be queried without user ID, and will always include the
    # correct user ID in its response.
    log.debug('Obtained user info from Blender ID: %s', user_info)
    blender_id_user_id = user_info['id']

    # Store the user info in MongoDB.
    db_user = find_user_in_db(blender_id_user_id, user_info)

    r = {}
    for retry in range(5):
        if '_id' in db_user:
            # Update the existing user
            attempted_eve_method = 'PUT'
            db_id = db_user['_id']
            r, _, _, status = put_internal('users', remove_private_keys(db_user),
                                           _id=db_id)
            if status == 422:
                log.error('Status %i trying to PUT user %s with values %s, should not happen! %s',
                          status, db_id, remove_private_keys(db_user), r)
        else:
            # Create a new user, retry for non-unique usernames.
            attempted_eve_method = 'POST'
            r, _, _, status = post_internal('users', db_user)

            db_id = r['_id']
            db_user.update(r)  # update with database/eve-generated fields.

        if status == 422:
            # Probably non-unique username, so retry a few times with different usernames.
            log.info('Error creating new user: %s', r)
            username_issue = r.get('_issues', {}).get(u'username', '')
            if u'not unique' in username_issue:
                # Retry
                db_user['username'] = authentication.make_unique_username(db_user['email'])
                continue

        # Saving was successful, or at least didn't break on a non-unique username.
        break
    else:
        log.error('Unable to create new user %s: %s', db_user, r)
        return abort(500)

    if status not in (200, 201):
        log.error('internal response from %s to Eve: %r %r', attempted_eve_method, status, r)
        return abort(500)

    # Store the token in MongoDB.
    authentication.store_token(db_id, token, token_expiry, oauth_subclient_id)

    return db_user, status
Пример #26
0
def zencoder_notifications():
    """

    See: https://app.zencoder.com/docs/guides/getting-started/notifications#api_version_2

    """
    if current_app.config['ENCODING_BACKEND'] != 'zencoder':
        log.warning(
            'Received notification from Zencoder but app not configured for Zencoder.'
        )
        return abort(403)

    if not current_app.config['DEBUG']:
        # If we are in production, look for the Zencoder header secret
        try:
            notification_secret_request = request.headers[
                'X-Zencoder-Notification-Secret']
        except KeyError:
            log.warning('Received Zencoder notification without secret.')
            return abort(401)
        # If the header is found, check it agains the one in the config
        notification_secret = current_app.config[
            'ZENCODER_NOTIFICATIONS_SECRET']
        if notification_secret_request != notification_secret:
            log.warning(
                'Received Zencoder notification with incorrect secret.')
            return abort(401)

    # Cast request data into a dict
    data = request.get_json()

    if log.isEnabledFor(logging.DEBUG):
        from pprint import pformat
        log.debug('Zencoder job JSON: %s', pformat(data))

    files_collection = current_app.data.driver.db['files']
    # Find the file object based on processing backend and job_id
    zencoder_job_id = data['job']['id']
    lookup = {
        'processing.backend': 'zencoder',
        'processing.job_id': str(zencoder_job_id)
    }
    file_doc = files_collection.find_one(lookup)
    if not file_doc:
        log.warning('Unknown Zencoder job id %r', zencoder_job_id)
        # Return 200 OK when debugging, or Zencoder will keep trying and trying and trying...
        # which is what we want in production.
        return "Not found, but that's okay.", 200 if current_app.config[
            'DEBUG'] else 404

    file_id = ObjectId(file_doc['_id'])
    # Remove internal keys (so that we can run put internal)
    file_doc = utils.remove_private_keys(file_doc)

    # Update processing status
    job_state = data['job']['state']
    file_doc['processing']['status'] = job_state

    if job_state == 'failed':
        log.warning('Zencoder job %i for file %s failed.', zencoder_job_id,
                    file_id)
        # Log what Zencoder told us went wrong.
        for output in data['outputs']:
            if not any('error' in key for key in output):
                continue
            log.warning('Errors for output %s:', output['url'])
            for key in output:
                if 'error' in key:
                    log.info('    %s: %s', key, output[key])

        file_doc['status'] = 'failed'
        put_internal('files', file_doc, _id=file_id)
        return "You failed, but that's okay.", 200

    log.info('Zencoder job %s for file %s completed with status %s.',
             zencoder_job_id, file_id, job_state)

    # For every variation encoded, try to update the file object
    root, _ = os.path.splitext(file_doc['file_path'])

    for output in data['outputs']:
        video_format = output['format']
        # Change the zencoder 'mpeg4' format to 'mp4' used internally
        video_format = 'mp4' if video_format == 'mpeg4' else video_format

        # Find a variation matching format and resolution
        variation = next(
            (v for v in file_doc['variations']
             if v['format'] == format and v['width'] == output['width']), None)
        # Fall back to a variation matching just the format
        if variation is None:
            variation = next((v for v in file_doc['variations']
                              if v['format'] == video_format), None)
        if variation is None:
            log.warning(
                'Unable to find variation for video format %s for file %s',
                video_format, file_id)
            continue

        # Rename the file to include the now-known size descriptor.
        size = size_descriptor(output['width'], output['height'])
        new_fname = '{}-{}.{}'.format(root, size, video_format)

        # Rename on Google Cloud Storage
        try:
            rename_on_gcs(file_doc['project'], '_/' + variation['file_path'],
                          '_/' + new_fname)
        except Exception:
            log.warning(
                'Unable to rename GCS blob %r to %r. Keeping old name.',
                variation['file_path'],
                new_fname,
                exc_info=True)
        else:
            variation['file_path'] = new_fname

        # TODO: calculate md5 on the storage
        variation.update({
            'height': output['height'],
            'width': output['width'],
            'length': output['file_size_in_bytes'],
            'duration': data['input']['duration_in_ms'] / 1000,
            'md5': output['md5_checksum']
            or '',  # they don't do MD5 for GCS...
            'size': size,
        })

    file_doc['status'] = 'complete'

    # Force an update of the links on the next load of the file.
    file_doc['link_expires'] = datetime.datetime.now(
        tz=tz_util.utc) - datetime.timedelta(days=1)

    put_internal('files', file_doc, _id=file_id)

    return '', 204
Пример #27
0
def validate_create_user(blender_id_user_id, token, oauth_subclient_id):
    """Validates a user against Blender ID, creating the user in our database.

    :param blender_id_user_id: the user ID at the BlenderID server.
    :param token: the OAuth access token.
    :param oauth_subclient_id: the subclient ID, or empty string if not a subclient.
    :returns: (user in MongoDB, HTTP status 200 or 201)
    """

    # Verify with Blender ID
    log.debug('Storing token for BlenderID user %s', blender_id_user_id)
    user_info, token_expiry = validate_token(blender_id_user_id, token,
                                             oauth_subclient_id)

    if user_info is None:
        log.debug('Unable to verify token with Blender ID.')
        return None, None

    # Blender ID can be queried without user ID, and will always include the
    # correct user ID in its response.
    log.debug('Obtained user info from Blender ID: %s', user_info)
    blender_id_user_id = user_info['id']

    # Store the user info in MongoDB.
    db_user = find_user_in_db(blender_id_user_id, user_info)

    r = {}
    for retry in range(5):
        if '_id' in db_user:
            # Update the existing user
            attempted_eve_method = 'PUT'
            db_id = db_user['_id']
            r, _, _, status = put_internal('users',
                                           remove_private_keys(db_user),
                                           _id=db_id)
            if status == 422:
                log.error(
                    'Status %i trying to PUT user %s with values %s, should not happen! %s',
                    status, db_id, remove_private_keys(db_user), r)
        else:
            # Create a new user, retry for non-unique usernames.
            attempted_eve_method = 'POST'
            r, _, _, status = post_internal('users', db_user)

            db_id = r['_id']
            db_user.update(r)  # update with database/eve-generated fields.

        if status == 422:
            # Probably non-unique username, so retry a few times with different usernames.
            log.info('Error creating new user: %s', r)
            username_issue = r.get('_issues', {}).get(u'username', '')
            if u'not unique' in username_issue:
                # Retry
                db_user['username'] = authentication.make_unique_username(
                    db_user['email'])
                continue

        # Saving was successful, or at least didn't break on a non-unique username.
        break
    else:
        log.error('Unable to create new user %s: %s', db_user, r)
        return abort(500)

    if status not in (200, 201):
        log.error('internal response from %s to Eve: %r %r',
                  attempted_eve_method, status, r)
        return abort(500)

    # Store the token in MongoDB.
    authentication.store_token(db_id, token, token_expiry, oauth_subclient_id)

    return db_user, status
Пример #28
0
def after_inserting_project(project, db_user):
    project_id = project['_id']
    user_id = db_user['_id']

    # Create a project-specific admin group (with name matching the project id)
    result, _, _, status = post_internal('groups', {'name': str(project_id)})
    if status != 201:
        log.error('Unable to create admin group for new project %s: %s',
                  project_id, result)
        return abort_with_error(status)

    admin_group_id = result['_id']
    log.debug('Created admin group %s for project %s', admin_group_id, project_id)

    # Assign the current user to the group
    db_user.setdefault('groups', []).append(admin_group_id)

    result, _, _, status = patch_internal('users', {'groups': db_user['groups']}, _id=user_id)
    if status != 200:
        log.error('Unable to add user %s as member of admin group %s for new project %s: %s',
                  user_id, admin_group_id, project_id, result)
        return abort_with_error(status)
    log.debug('Made user %s member of group %s', user_id, admin_group_id)

    # Assign the group to the project with admin rights
    is_admin = authorization.is_admin(db_user)
    world_permissions = ['GET'] if is_admin else []
    permissions = {
        'world': world_permissions,
        'users': [],
        'groups': [
            {'group': admin_group_id,
             'methods': ['GET', 'PUT', 'POST', 'DELETE']},
        ]
    }

    def with_permissions(node_type):
        copied = copy.deepcopy(node_type)
        copied['permissions'] = permissions
        return copied

    # Assign permissions to the project itself, as well as to the node_types
    project['permissions'] = permissions
    project['node_types'] = [
        with_permissions(node_type_group),
        with_permissions(node_type_asset),
        with_permissions(node_type_comment),
        with_permissions(node_type_texture),
        with_permissions(node_type_group_texture),
    ]

    # Allow admin users to use whatever url they want.
    if not is_admin or not project.get('url'):
        project['url'] = "p-{!s}".format(project_id)

    # Initialize storage page (defaults to GCS)
    if current_app.config.get('TESTING'):
        log.warning('Not creating Google Cloud Storage bucket while running unit tests!')
    else:
        gcs_storage = GoogleCloudStorageBucket(str(project_id))
        if gcs_storage.bucket.exists():
            log.info('Created CGS instance for project %s', project_id)
        else:
            log.warning('Unable to create CGS instance for project %s', project_id)

    # Commit the changes directly to the MongoDB; a PUT is not allowed yet,
    # as the project doesn't have a valid permission structure.
    projects_collection = current_app.data.driver.db['projects']
    result = projects_collection.update_one({'_id': project_id},
                                            {'$set': remove_private_keys(project)})
    if result.matched_count != 1:
        log.warning('Unable to update project %s: %s', project_id, result.raw_result)
        abort_with_error(500)