Beispiel #1
0
    def _assert_is_admin(self, org_id):
        om = current_app.org_manager

        if current_user().has_cap('admin'):
            # Always allow admins to edit every organization.
            return

        if not om.user_is_admin(org_id):
            log.warning('User %s uses PATCH to edit organization %s, '
                        'but is not admin of that Organization. Request denied.',
                        current_user().user_id, org_id)
            raise wz_exceptions.Forbidden()
Beispiel #2
0
    def patch_assign_user(self, org_id: bson.ObjectId, patch: dict):
        """Assigns a single user by User ID to an organization.

        The calling user must be admin of the organization.
        """
        from . import NotEnoughSeats
        self._assert_is_admin(org_id)

        # Do some basic validation.
        try:
            user_id = patch['user_id']
        except KeyError:
            raise wz_exceptions.BadRequest('No key "user_id" in patch.')

        user_oid = str2id(user_id)
        log.info('User %s uses PATCH to add user %s to organization %s',
                 current_user().user_id, user_oid, org_id)
        try:
            org_doc = current_app.org_manager.assign_single_user(
                org_id, user_id=user_oid)
        except NotEnoughSeats:
            resp = jsonify(
                {'_message': f'Not enough seats to assign this user'})
            resp.status_code = 422
            return resp

        return jsonify(org_doc)
Beispiel #3
0
    def patch_assign_users(self, org_id: bson.ObjectId, patch: dict):
        """Assigns users to an organization.

        The calling user must be admin of the organization.
        """
        from . import NotEnoughSeats

        self._assert_is_admin(org_id)

        # Do some basic validation.
        try:
            emails = patch['emails']
        except KeyError:
            raise wz_exceptions.BadRequest('No key "email" in patch.')

        # Skip empty emails.
        emails = [
            stripped for stripped in (email.strip() for email in emails)
            if stripped
        ]

        log.info('User %s uses PATCH to add users to organization %s',
                 current_user().user_id, org_id)
        try:
            org_doc = current_app.org_manager.assign_users(org_id, emails)
        except NotEnoughSeats:
            resp = jsonify({
                '_message':
                f'Not enough seats to assign {len(emails)} users'
            })
            resp.status_code = 422
            return resp

        return jsonify(org_doc)
Beispiel #4
0
def pre_get_organizations(request, lookup):
    user = current_user()
    if user.is_anonymous:
        raise wz_exceptions.Forbidden()

    if user.has_cap('admin'):
        # Allow all lookups to admins.
        return

    # Only allow users to see their own organizations.
    lookup['$or'] = [{'admin_uid': user.user_id}, {'members': user.user_id}]
Beispiel #5
0
    def test_current_user_logged_in(self):
        self.enter_app_context()

        from flask import g
        from pillar.auth import UserClass
        from pillar.api.utils.authentication import current_user

        with self.app.test_request_context():
            g.current_user = UserClass.construct('the token', ctd.EXAMPLE_USER)

            user = current_user()
            self.assertIs(g.current_user, user)
Beispiel #6
0
def generate_token(manager_id: str):
    manager_oid = str2id(manager_id)
    manager = mongo.find_one_or_404('flamenco_managers', manager_oid)

    # There are three ways in which a user can get here. One is authenticated via
    # Authorization header (either Bearer token or Basic token:subtoken), and the
    # other is via an already-existing browser session.
    # In the latter case it's a redirect from a Flamenco Manager and we need to
    # check the timeout and HMAC.
    if not request.headers.get('Authorization'):
        hasher = current_flamenco.manager_manager.hasher(manager_oid)
        if hasher is None:
            raise wz_exceptions.InternalServerError(
                'Flamenco Manager not linked to this server')

        expires = request.args.get('expires', '')
        string_to_hash = f'{expires}-{manager_id}'
        hasher.update(string_to_hash.encode('utf8'))
        actual_hmac = hasher.hexdigest()

        query_hmac = request.args.get('hmac', '')
        if not hmac.compare_digest(query_hmac, actual_hmac):
            raise wz_exceptions.Unauthorized('Bad HMAC')

        # Only parse the timestamp after we learned we can trust it.
        expire_timestamp = dateutil.parser.parse(expires)
        validity_seconds_left = (expire_timestamp - utcnow()).total_seconds()
        if validity_seconds_left < 0:
            raise wz_exceptions.Unauthorized('Link expired')
        if validity_seconds_left > 900:
            # Flamenco Manager generates links that are valid for less than a minute, so
            # if it's more than 15 minutes in the future, it's bad.
            raise wz_exceptions.Unauthorized('Link too far in the future')

    user = authentication.current_user()

    if not current_flamenco.manager_manager.user_may_use(mngr_doc=manager):
        log.warning(
            'Account %s called %s for manager %s without access to that manager',
            user.user_id, request.url, manager_oid)
        raise wz_exceptions.Unauthorized()

    jwt = current_flamenco.jwt
    if not jwt.usable:
        raise wz_exceptions.NotImplemented(
            'JWT keystore is not usable at the moment')

    log.info('Generating JWT key for user_id=%s manager_id=%s remote_addr=%s',
             user.user_id, manager_id, request.remote_addr)
    key_for_manager = jwt.generate_key_for_manager(manager_oid, user.user_id)

    return Response(key_for_manager, content_type='text/plain')
Beispiel #7
0
def generate_token(manager_id: str):
    manager_oid = str2id(manager_id)
    manager = mongo.find_one_or_404('flamenco_managers', manager_oid)

    # There are three ways in which a user can get here. One is authenticated via
    # Authorization header (either Bearer token or Basic token:subtoken), and the
    # other is via an already-existing browser session.
    # In the latter case it's a redirect from a Flamenco Manager and we need to
    # check the timeout and HMAC.
    if not request.headers.get('Authorization'):
        hasher = current_flamenco.manager_manager.hasher(manager_oid)
        if hasher is None:
            raise wz_exceptions.InternalServerError('Flamenco Manager not linked to this server')

        expires = request.args.get('expires', '')
        string_to_hash = f'{expires}-{manager_id}'
        hasher.update(string_to_hash.encode('utf8'))
        actual_hmac = hasher.hexdigest()

        query_hmac = request.args.get('hmac', '')
        if not hmac.compare_digest(query_hmac, actual_hmac):
            raise wz_exceptions.Unauthorized('Bad HMAC')

        # Only parse the timestamp after we learned we can trust it.
        expire_timestamp = dateutil.parser.parse(expires)
        validity_seconds_left = (expire_timestamp - utcnow()).total_seconds()
        if validity_seconds_left < 0:
            raise wz_exceptions.Unauthorized('Link expired')
        if validity_seconds_left > 900:
            # Flamenco Manager generates links that are valid for less than a minute, so
            # if it's more than 15 minutes in the future, it's bad.
            raise wz_exceptions.Unauthorized('Link too far in the future')

    user = authentication.current_user()

    if not current_flamenco.manager_manager.user_may_use(mngr_doc=manager):
        log.warning(
            'Account %s called %s for manager %s without access to that manager',
            user.user_id, request.url, manager_oid)
        raise wz_exceptions.Unauthorized()

    jwt = current_flamenco.jwt
    if not jwt.usable:
        raise wz_exceptions.NotImplemented('JWT keystore is not usable at the moment')

    log.info('Generating JWT key for user_id=%s manager_id=%s remote_addr=%s',
             user.user_id, manager_id, request.remote_addr)
    key_for_manager = jwt.generate_key_for_manager(manager_oid, user.user_id)

    return Response(key_for_manager, content_type='text/plain')
Beispiel #8
0
    def test_current_user_anonymous(self):
        self.enter_app_context()

        from flask import g
        from pillar.auth import AnonymousUser
        from pillar.api.utils.authentication import current_user

        with self.app.test_request_context():
            g.current_user = None

            user = current_user()
            self.assertIsInstance(user, AnonymousUser)
            self.assertIsNone(user.user_id)
            self.assertTrue(user.is_anonymous)
            self.assertFalse(user.is_authenticated)
    def patch_set_job_status(self, job_id: bson.ObjectId, patch: dict):
        """Updates a job's status in the database."""
        self.assert_job_access(job_id)

        new_status = patch['status']

        user = current_user()
        log.info('User %s uses PATCH to set job %s status to "%s"',
                 user.user_id, job_id, new_status)
        try:
            current_flamenco.job_manager.api_set_job_status(
                job_id, new_status,
                reason=f'Set to {new_status} by {user.full_name} (@{user.username})')
        except ValueError as ex:
            log.debug('api_set_job_status(%s, %r) raised %s', job_id, new_status, ex)
            raise wz_exceptions.UnprocessableEntity(f'Status {new_status} is invalid')
Beispiel #10
0
    def patch_assign_admin(self, org_id: bson.ObjectId, patch: dict):
        """Assigns a single user by User ID as admin of the organization.

        The calling user must be admin of the organization.
        """

        self._assert_is_admin(org_id)

        # Do some basic validation.
        try:
            user_id = patch['user_id']
        except KeyError:
            raise wz_exceptions.BadRequest('No key "user_id" in patch.')

        user_oid = str2id(user_id)
        log.info('User %s uses PATCH to set user %s as admin for organization %s',
                 current_user().user_id, user_oid, org_id)
        current_app.org_manager.assign_admin(org_id, user_id=user_oid)
Beispiel #11
0
    def patch_remove_user(self, org_id: bson.ObjectId, patch: dict):
        """Removes a user from an organization.

        The calling user must be admin of the organization.
        """

        # Do some basic validation.
        email = patch.get('email') or None
        user_id = patch.get('user_id')
        user_oid = str2id(user_id) if user_id else None

        # Users require admin rights on the org, except when removing themselves.
        current_user_id = current_user().user_id
        if user_oid is None or user_oid != current_user_id:
            self._assert_is_admin(org_id)

        log.info('User %s uses PATCH to remove user %s from organization %s',
                 current_user_id, user_oid, org_id)

        org_doc = current_app.org_manager.remove_user(org_id, user_id=user_oid, email=email)
        return jsonify(org_doc)
    def patch_construct(self, job_id: bson.ObjectId, patch: dict):
        """Send job to 'under-construction' status and compile its tasks.

        The patch can contain extra settings for the job, such as 'filepath'
        for Blender render jobs.
        """
        job = self.assert_job_access(job_id)

        job_status = job.get('status', '-unset-')
        if job_status != 'waiting-for-files':
            log.info('User %s wants to PATCH to construct job %s; rejecting because it is in '
                     'status %r', current_user_id(), job_id, job_status)
            raise wz_exceptions.UnprocessableEntity(
                f'Current job status {job_status} does not allow job construction')

        # TODO(Sybren): add job settings handling.
        user = current_user()
        new_job_settings = patch.get('settings')
        log.info('User %s uses PATCH to construct job %s with settings %s',
                 user.user_id, job_id, new_job_settings)
        current_flamenco.job_manager.api_construct_job(
            job_id, new_job_settings,
            reason=f'Construction initiated by {user.full_name} (@{user.username})')
Beispiel #13
0
    def patch_edit_from_web(self, org_id: bson.ObjectId, patch: dict):
        """Updates Organization fields from the web.

        The PATCH command supports the following payload. The 'name' field must
        be set, all other fields are optional. When an optional field is
        ommitted it will be handled as an instruction to clear that field.
            {'name': str,
             'description': str,
             'website': str,
             'location': str,
             'ip_ranges': list of human-readable IP ranges}
        """

        from pymongo.results import UpdateResult
        from . import ip_ranges

        self._assert_is_admin(org_id)
        user = current_user()
        current_user_id = user.user_id

        # Only take known fields from the patch, don't just copy everything.
        update = {
            'name': patch['name'].strip(),
            'description': patch.get('description', '').strip(),
            'website': patch.get('website', '').strip(),
            'location': patch.get('location', '').strip(),
        }
        unset = {}

        # Special transformation for IP ranges
        iprs = patch.get('ip_ranges')
        if iprs:
            ipr_docs = []
            for r in iprs:
                try:
                    doc = ip_ranges.doc(r, min_prefixlen6=48, min_prefixlen4=8)
                except ValueError as ex:
                    raise wz_exceptions.UnprocessableEntity(
                        f'Invalid IP range {r!r}: {ex}')
                ipr_docs.append(doc)
            update['ip_ranges'] = ipr_docs
        else:
            unset['ip_ranges'] = True

        refresh_user_roles = False
        if user.has_cap('admin'):
            if 'seat_count' in patch:
                update['seat_count'] = int(patch['seat_count'])
            if 'org_roles' in patch:
                org_roles = [
                    stripped for stripped in (role.strip()
                                              for role in patch['org_roles'])
                    if stripped
                ]
                if not all(role.startswith('org-') for role in org_roles):
                    raise wz_exceptions.UnprocessableEntity(
                        'Invalid role given, all roles must start with "org-"')

                update['org_roles'] = org_roles
                refresh_user_roles = True

        self.log.info('User %s edits Organization %s: %s', current_user_id,
                      org_id, update)

        validator = current_app.validator_for_resource('organizations')
        if not validator.validate_update(update, org_id,
                                         persisted_document={}):
            resp = jsonify({
                '_errors':
                validator.errors,
                '_message':
                ', '.join(f'{field}: {error}'
                          for field, error in validator.errors.items()),
            })
            resp.status_code = 422
            return resp

        # Figure out what to set and what to unset
        for_mongo = {'$set': update}
        if unset:
            for_mongo['$unset'] = unset

        organizations_coll = current_app.db('organizations')
        result: UpdateResult = organizations_coll.update_one({'_id': org_id},
                                                             for_mongo)

        if result.matched_count != 1:
            self.log.warning(
                'User %s edits Organization %s but update matched %i items',
                current_user_id, org_id, result.matched_count)
            raise wz_exceptions.BadRequest()

        if refresh_user_roles:
            self.log.info(
                'Organization roles set for org %s, refreshing users', org_id)
            current_app.org_manager.refresh_all_user_roles(org_id)

        return '', 204
Beispiel #14
0
    def patch_edit_from_web(self, org_id: bson.ObjectId, patch: dict):
        """Updates Organization fields from the web."""

        from pymongo.results import UpdateResult

        self._assert_is_admin(org_id)
        user = current_user()
        current_user_id = user.user_id

        # Only take known fields from the patch, don't just copy everything.
        update = {
            'name': patch['name'].strip(),
            'description': patch.get('description', '').strip(),
            'website': patch.get('website', '').strip(),
            'location': patch.get('location', '').strip(),
        }

        refresh_user_roles = False
        if user.has_cap('admin'):
            if 'seat_count' in patch:
                update['seat_count'] = int(patch['seat_count'])
            if 'org_roles' in patch:
                org_roles = [
                    stripped for stripped in (role.strip()
                                              for role in patch['org_roles'])
                    if stripped
                ]
                if not all(role.startswith('org-') for role in org_roles):
                    raise wz_exceptions.UnprocessableEntity(
                        'Invalid role given, all roles must start with "org-"')

                update['org_roles'] = org_roles
                refresh_user_roles = True

        self.log.info('User %s edits Organization %s: %s', current_user_id,
                      org_id, update)

        validator = current_app.validator_for_resource('organizations')
        if not validator.validate_update(update, org_id):
            resp = jsonify({
                '_errors':
                validator.errors,
                '_message':
                ', '.join(f'{field}: {error}'
                          for field, error in validator.errors.items()),
            })
            resp.status_code = 422
            return resp

        organizations_coll = current_app.db('organizations')
        result: UpdateResult = organizations_coll.update_one({'_id': org_id},
                                                             {'$set': update})

        if result.matched_count != 1:
            self.log.warning(
                'User %s edits Organization %s but update matched %i items',
                current_user_id, org_id, result.matched_count)
            raise wz_exceptions.BadRequest()

        if refresh_user_roles:
            self.log.info(
                'Organization roles set for org %s, refreshing users', org_id)
            current_app.org_manager.refresh_all_user_roles(org_id)

        return '', 204
Beispiel #15
0
def pre_post_organizations(request):
    user = current_user()
    if not user.has_cap('create-organization'):
        raise wz_exceptions.Forbidden()