Пример #1
0
    def test_object_pattern_schema(self):
        o = fields.Object(fields.Integer, pattern="[A-Z][0-9]+")

        self.assertEqual(
            {
                "type": "object",
                "additionalProperties": False,
                "patternProperties": {
                    "[A-Z][0-9]+": {
                        "type": "integer"
                    }
                }
            }, o.response)

        o = fields.Object(pattern_properties={"[A-Z][0-9]+": fields.Integer})

        self.assertEqual(
            {
                "type": "object",
                "additionalProperties": False,
                "patternProperties": {
                    "[A-Z][0-9]+": {
                        "type": "integer"
                    }
                }
            }, o.response)
Пример #2
0
    def test_attribute_mapped_no_pattern(self):
        o = fields.AttributeMapped(
            fields.Object({"foo": fields.Integer()}), mapping_attribute="key"
        )

        self.assertEqual(
            [{'foo': 1, 'key': 'A3'}, {'foo': 1, 'key': 'B12'}],
            sorted(
                o.convert({"A3": {"foo": 1}, "B12": {"foo": 1}}), key=itemgetter("key")
            ),
        )

        self.assertEqual(
            {"A3": {"foo": 1}, "B12": {"foo": 2}},
            o.format([{'foo': 1, 'key': 'A3'}, {'foo': 2, 'key': 'B12'}]),
        )

        self.assertEqual(
            {
                "type": "object",
                "additionalProperties": {
                    "additionalProperties": False,
                    "properties": {"foo": {"type": "integer"}},
                    "type": "object",
                },
            },
            o.response,
        )
Пример #3
0
    def test_object_convert_pattern(self):
        o = fields.Object(fields.Integer, pattern="[A-Z][0-9]+")

        self.assertEqual({"A3": 1, "B12": 2}, o.convert({"A3": 1, "B12": 2}))

        with self.assertRaises(ValidationError):
            o.convert({"A2": "string"})

        with self.assertRaises(ValidationError):
            o.convert({"Wrong": 1})
Пример #4
0
        class FooResource(Resource):
            @Route.GET(lambda r: '/<{}:id>'.format(r.meta.id_converter), rel="self")
            def read(self, id):
                return {"id": id}

            read.response_schema = fields.Object({"id": fields.Integer()})

            class Meta:
                name = 'foo'
                id_converter = 'int'
Пример #5
0
    def test_object_convert(self):
        o = fields.Object(fields.Integer, nullable=False)

        self.assertEqual({"x": 123}, o.convert({"x": 123}))

        self.assertEqual(o.request, o.response)
        self.assertEqual(
            {"type": "object", "additionalProperties": {"type": "integer"}}, o.response
        )

        with self.assertRaises(ValidationError):
            o.convert({"y": "string"})

        with self.assertRaises(ValidationError):
            o.convert(None)
Пример #6
0
        class Recipe(ModelResource):
            class Schema:
                name = fields.String()

            class Meta:
                model = 'recipe'
                manager = MemoryManager
                id_field_class = fields.Integer
                include_type = True

            ingredients = ItemAttributeRoute(
                fields.Array(
                    fields.Object({
                        "ingredient": fields.String(),
                        "amount": fields.String()
                    })))
Пример #7
0
        class DrinkResource(ModelResource):
            recipe = ItemAttributeRoute(
                fields.Array(
                    fields.Object(
                        properties={
                            "ingredient": fields.ToOne("ingredient"),
                            "volume": fields.Number()
                        })))

            class Meta:
                name = "drink"
                model = name
                manager = MemoryManager
                id_field_class = fields.Integer
                include_type = True

            class Schema:
                name = fields.String()
Пример #8
0
    def test_object_convert_pattern(self):
        o = fields.Object(fields.Integer, pattern="[A-Z][0-9]+")

        self.assertEqual({"A3": 1, "B12": 2}, o.convert({"A3": 1, "B12": 2}))

        self.assertEqual({
                             "type": "object",
                             "additionalProperties": False,
                             "patternProperties": {
                                 "[A-Z][0-9]+": {"type": "integer"}
                             }
                         }, o.response)

        with self.assertRaises(ValidationError):
            o.convert({"A2": "string"})

        with self.assertRaises(ValidationError):
            o.convert({"Wrong": 1})
Пример #9
0
    'A list of dimension values that the policy holder will be allowed to or prevented '
    'from querying on. To allow the user to query against all values, you would omit '
    'this field and instead set `allowAllValues` to true.',
)

ALL_VALUES_SCHEMA = fields.Boolean(
    description=
    'Set to true if the policy holder should be allowed to query across ALL values '
    'for the specified dimension.',
    default=False,
    nullable=True,
)

DIMENSION_VALUES_SCHEMA = fields.Object({
    'excludeValues': SPECIFIC_VALUES_SCHEMA,
    'includeValues': SPECIFIC_VALUES_SCHEMA,
    'allValues': ALL_VALUES_SCHEMA,
})

POLICY_FILTERS_SCHEMA = fields.Object(
    properties=DIMENSION_VALUES_SCHEMA,
    pattern_properties={
        fields.String(title='dimensionType',
                      description='The dimension to filter on'):
        DIMENSION_VALUES_SCHEMA
    },
    pattern='|'.join(DIMENSIONS),
    attribute='policy_filters',
    description=
    'A mapping of dimension names to the discrete dimension values that the '
    'policy holder is allowed to query data for. Example: For dimension '
Пример #10
0
 def test_object_format_any(self):
     o = fields.Object(fields.Any)
     self.assertEqual({"x": 123}, o.format({"x": 123}))
Пример #11
0
ROLE_NAME_SCHEMA = fields.String(
    title='roleName',
    description=
    'The string representation of a role name. (e.g. `dashboard_admin`)',
)

# The schema for a mapping of resource names to role names
# Structure should be
# { <resourceName>: [<roleName>, <otherRoleName>], <otherResourceName>: [<roleName>], ... }
RESOURCE_MAP_SCHEMA = fields.Object(
    properties=fields.List(
        ROLE_NAME_SCHEMA,
        description='A list of all the roles held for a given resource.',
    ),
    pattern_properties={
        fields.String(title='resourceName',
                      description='An individual resource.'):
        fields.List(ROLE_NAME_SCHEMA)
    },
    nullable=False,
    description='A mapping of resource names to role names. ',
)

# The schema representing the role assignments for an individual resource type
# Structure should be
# {
#   'resources': {
#     <resourceName>: [<roleName>, <otherRoleName>],
#     <otherResourceName>: [<roleName>], ...
#   },
#   'sitewideRoles': [<roleName>, <otherRoleName>, ...]
Пример #12
0
class DashboardResource(PrincipalResource):
    '''The potion class for performing CRUD operations on the `Dashboard` class.
    '''

    resource = Relation('resource', io='r')
    author = Relation('user', io='r')

    class Meta(object):
        manager = principals(DashboardManager)
        model = Dashboard
        natural_key = 'slug'
        excluded_fields = ('id',)
        id_attribute = 'resource_id'

        permissions = {'read': 'view_resource'}

        filters = {
            'slug': True,
            'title': True,
            'created': True,
            'author': {'eq': UserFilter, None: UserFilter},
            'isOfficial': True,
        }

    class Schema(object):
        title = TITLE_SCHEMA

        slug = NULLABLE_SLUG_SCHEMA

        description = DESCRIPTION_SCHEMA

        specification = SPECIFICATION_SCHEMA

        authorUsername = AUTHOR_USERNAME_SCHEMA

        author = AUTHOR_URI_SCHEMA

        resource = RESOURCE_URI_SCHEMA

        created = CREATED_SCHEMA

        isOfficial = IS_OFFICIAL_SCHEMA

        # NOTE(stephen): These fields are now pulled **from metadata** not from
        # the Dashboard model directly. They are only represented here to make
        # flask potion happy EVEN THOUGH THEY WILL NEVER EVER BE SENT BY THE
        # CLIENT, FLASK POTION STILL COMPLAINS.
        lastModified = UNUSED_LAST_MODIFIED_SCHEMA
        totalViews = UNUSED_TOTAL_VIEWS_SCHEMA

    # HACK(stephen): Attaching dashboard metadata to the base dashboard model
    # response is like fitting a square peg into a round hole. To add that
    # information in ways that Flask-Potion would naturally work causes us to
    # issue a huge number of queries per dashboard (previously 14 queries per
    # dashboard with a naive implementation). This query encapsulates all the
    # information needed for the dashboard response into a single query.
    # TODO(stephen): If we have to write workarounds like this, it probably
    # means we shouldn't be jamming too much information into a single API.
    def _attach_metadata_to_query(self, query):
        # NOTE(stephen): I don't think a transaction is necessary for this
        # read only query, but it is an easy way to access the session.
        with Transaction() as transaction:
            session = transaction._session
            subquery = DashboardUserMetadata.summary_by_dashboard_for_user(
                session, current_user.id
            ).subquery()
            return (
                query
                # Join in the summarized metadata for each dashboard.
                .outerjoin(subquery, Dashboard.id == subquery.c.dashboard_id)
                # Attach user info so we can extract the author username.
                .outerjoin(User, Dashboard.author_id == User.id)
                # Make sure all the metadata columns are included.
                .add_columns(subquery)
                # Also include all dashboard columns since otherwise a new query
                # will be issued EACH TIME we access a dashboard in the query
                # result.
                .add_columns(Dashboard.__table__)
                # Manually set up author_username since hybrid properties weren't
                # transferring.
                .add_columns(User.username.label('author_username'))
            )

    # HACK(stephen): To ensure endpoints that return a single dashboard also
    # include the appropriate metadata, we must join in the dashboard metadata
    # to our single dashboard query.
    def _get_single_dashboard_with_metadata(self, resource_id):
        # NOTE(stephen): The resource_id being filtered on here is **not**
        # Dashboard.id. This is because we use the `resource_id` column for
        # lookups and reference.
        query = self.manager._query().filter(self.manager.id_column == resource_id)
        return self._attach_metadata_to_query(query).one()

    # pylint: disable=R0201
    # pylint: disable=E1101
    # Flask Potion does not allow class methods.

    # Override the default "get all dashboards" route to augment the response
    # with dashboard specific metadata.
    @Route.GET(
        '',
        rel='instances',
        title='Something',
        description='Something',
        schema=Instances(),
        response_schema=fields.Array(fields.Object(DASHBOARD_SIMPLE_FIELDS)),
    )
    def get_instances(self, page, per_page, where, sort):
        base_query = self.manager.instances(where, sort)

        # NOTE(stephen): I'm not sure why this wouldn't exist, but I think it
        # only happens when there are no dashboards in the DB.
        if not base_query:
            return []

        return self._attach_metadata_to_query(base_query).paginate(page, per_page).items

    @Route.POST(
        '/upgrade',
        title='Upgrade Dashboard Specification',
        description='Upgrades the provided dashboard specification to the '
        'latest schema version supported by the server.',
        schema=fields.Any(),
        rel='upgrade',
    )
    def upgrade_dashboard(self, dashboard_specification):
        # The validation is being done in the conversion defined by SPECIFICATION_SCHEMA. If there
        # are any errors, they will be thrown as an exception to the client. No action needs to be
        # taken here.
        return format_and_upgrade_specification(dashboard_specification)

    @ItemRoute.POST(
        '/visualization',
        title='Add Simple Query Visualization',
        description='Adds an item from Simple Query Tool to the dashboard.',
        schema=ADD_QUERY_TO_DASHBOARD_SCHEMA,
        response_schema=DETAILED_DASHBOARD_SCHEMA,
        rel='addVisualization',
    )
    def add_visualization(self, dashboard, request):
        resource_id = dashboard.resource.id
        with AuthorizedOperation('edit_resource', 'dashboard', resource_id):
            _add_visualization(dashboard, request, False)
            track_dashboard_access(dashboard.id, True)
            return self._get_single_dashboard_with_metadata(resource_id)

    @ItemRoute.POST(
        '/visualization/advanced',
        title='Add Advanced Query Visualization',
        description='Adds an item from Advanced Query Tool to the dashboard.',
        schema=ADD_QUERY_TO_DASHBOARD_SCHEMA,
        response_schema=DETAILED_DASHBOARD_SCHEMA,
        rel='addAdvancedVisualization',
    )
    def add_advanced_visualization(self, dashboard, request):
        resource_id = dashboard.resource.id
        with AuthorizedOperation('edit_resource', 'dashboard', resource_id):
            _add_visualization(dashboard, request, True)
            track_dashboard_access(dashboard.id, True)
            return self._get_single_dashboard_with_metadata(resource_id)

    @ItemRoute.POST(
        '/transfer',
        title='Transfer Ownership',
        description='Transfers the ownership of this dashboard from the current'
        'owner to the one specified. ',
        schema=USER_URI_SCHEMA,
    )
    def transfer_ownership(self, dashboard, new_author):
        with AuthorizedOperation(
            'update_users', 'dashboard', dashboard.resource_id
        ), AuthorizedOperation('view_resource', 'user', dashboard.author.id):
            new_author = lookup_author(author_id=new_author)
            api_transfer_dashboard_ownership(dashboard, new_author)
            return None, NO_CONTENT

    @ItemRoute.POST(
        '/transfer/username',
        title='Transfer Ownership',
        description='Transfers the ownership of this dashboard from the current'
        'owner to the one specified. ',
        schema=USERNAME_SCHEMA,
    )
    def transfer_ownership_by_username(self, dashboard, new_author):
        with AuthorizedOperation(
            'update_users', 'dashboard', dashboard.resource_id
        ), AuthorizedOperation('view_resource', 'user', dashboard.author.id):
            new_author = lookup_author(author_username=new_author)
            api_transfer_dashboard_ownership(dashboard, new_author)
            return None, NO_CONTENT

    @Route.POST(
        '/transfer',
        title='Transfer Ownership',
        description='Transfers the ownership of ALL dashboards from one user '
        'to another.',
        schema=fields.Object(
            {'sourceAuthor': USER_URI_SCHEMA, 'targetAuthor': USER_URI_SCHEMA}
        ),
    )
    @authorization_required('update_users', 'dashboard')
    def transfer_bulk_ownership(self, request):
        source_author = lookup_author(author_id=request['sourceAuthor'])
        target_author = lookup_author(author_id=request['targetAuthor'])
        api_bulk_transfer_dashboard_ownership(source_author, target_author)
        return None, NO_CONTENT

    @Route.POST(
        '/transfer/username',
        title='Transfer Ownership',
        description='Transfers the ownership of ALL dashboards from one user '
        'to another.',
        schema=fields.Object(
            {'sourceAuthor': USERNAME_SCHEMA, 'targetAuthor': USERNAME_SCHEMA}
        ),
    )
    @authorization_required('update_users', 'dashboard')
    def transfer_bulk_ownership_by_username(self, request):
        source_author = lookup_author(author_username=request['sourceAuthor'])
        target_author = lookup_author(author_username=request['targetAuthor'])
        api_bulk_transfer_dashboard_ownership(source_author, target_author)
        return None, NO_CONTENT

    @ItemRoute.POST(
        '/official',
        title='Update Dashboard \'official\' flag',
        description='Marks a Dashboard as official or not.',
        schema=fields.Boolean(
            description='The updated value of the "isOfficial" flag for the '
            'dashboard.'
        ),
    )
    @authorization_required('publish_resource', 'dashboard')
    def set_official(self, dashboard, is_official):
        self.manager.update(dashboard, {'is_official': is_official})
        return None, NO_CONTENT

    @ItemRoute.POST(
        '/favorite',
        title='Update Dashboard \'favorite\' flag',
        description='Marks a Dashboard as a user favorite or not.',
        schema=fields.Boolean(
            description='The updated value of the "isFavorite" flag for the '
            'dashboard.'
        ),
    )
    def set_favorite(self, dashboard, is_favorite):
        with AuthorizedOperation(
            'view_resource', 'dashboard', dashboard.id
        ), Transaction() as transaction:
            metadata = get_or_create_metadata(transaction, dashboard.id)
            metadata.is_favorite = is_favorite
            transaction.add_or_update(metadata)

        return None, NO_CONTENT

    # NOTE(vedant): Why are we overriding the default potion route for GET <{}:id> and
    # PATCH <{}:id>?
    #
    # We want to only display the dashboard specification when an individual dashboard is
    # requested. To do this, we have to tailor the response schema to include the specification
    # field which would otherwise NOT be included in the default Schema specified in the `Schema`
    # subclass of `DashboardResource`.
    #
    # The rationale for only display the complete dashboard specification is that specifications
    # have a tendency to be very large and we don't want to send a lot of useless data over the
    # wire unless it is specifically asked for by the client. It is also very unlikely that a
    # client will ever be loading more than one dashboard specification at a given time.
    @Route.GET(
        lambda r: '/<{}:id>'.format(r.meta.id_converter),
        rel='self',
        attribute='instance',
        response_schema=DETAILED_DASHBOARD_SCHEMA,
    )
    def read(self, id):
        with Transaction() as transaction:
            dashboard = super(DashboardResource, self).read(id)
            dashboard.total_views += 1
            track_dashboard_access(dashboard.id)
            dashboard = transaction.add_or_update(dashboard, flush=True)
        return self._get_single_dashboard_with_metadata(id)

    @Route.PATCH(
        lambda r: '/<{}:id>'.format(r.meta.id_converter),
        rel='update',
        schema=fields.Inline('self', patchable=True),
        response_schema=DETAILED_DASHBOARD_SCHEMA,
    )
    def update(self, properties, id):
        dashboard = super(DashboardResource, self).update(properties, id)
        track_dashboard_access(dashboard.id, edited=True)
        return self._get_single_dashboard_with_metadata(id)

    @ItemRoute.GET(
        '/history',
        title='Dashboard Update History',
        schema=Instances(),
        description='Gets dashboard history data',
        rel='getDashboardHistory',
        response_schema=DASHBOARD_CHANGES_SCHEMA,
    )
    # pylint: disable=W0613
    # Method signature requires where and sort params
    def get_history(self, dashboard, page, per_page, where, sort):
        records = []
        with Transaction() as transaction:
            records = (
                transaction.find_all_by_fields(
                    HistoryRecord,
                    {'object_id': dashboard.resource_id, 'object_type': self.meta.name},
                )
                .paginate(page, per_page)
                .items
            )
        return records
Пример #13
0
# The schema for the `query_result_spec` object that is POSTed to the
# `visualization` API. This represents the frontend configuration for a query
# result (e.g. custom fields, filters, settings, etc.) We are not specifying a
# detailed schema for this object yet, but we should.
QUERY_RESULT_SPEC_SCHEMA = fields.Custom(
    fields.Any(),
    description='The query result spec object describing a query result and its configuration to be added to a dashboard.',
)

ADD_QUERY_TO_DASHBOARD_SCHEMA = fields.Object(
    properties={
        'activeViewType': fields.String(
            description='The currently active view type when the query was saved',
            nullable=False,
        ),
        'queryResultSpec': QUERY_RESULT_SPEC_SCHEMA,
        'querySelections': SELECTIONS_SCHEMA,
    },
    description='The request object to add a query to a dashboard',
)

# The schema for the dashboard's specification. Because the specification is stored in the database
# as a text value, we must serialize and deserialize as part of retrieval / update.
# TODO(vedant) - We can actually define the entire Dashboard Schema here.
# Determine if it's necessary.
# HACK(vedant) - This is actually a hack. The `io=w` signals to Potion that this is a write-only
# field when it is infact a readable field as well. The reason that we mark it as a write-only
# field is to prevent it from being shown in the `GET` call to the API root. We only want this
# field to be shown in the resource-specific `GET` call. The correct solution is to build this
# behaviour into Flask-Potion (configurable fields depending on query view / resource view).
Пример #14
0
UPDATE_ROLE_PERMISSIONS_RESPONSE_SCHEMA = augment_standard_schema(
    ROLE_PERMISSIONS_FIELDS)
ROLE_LIST_SCHEMA = fields.List(
    ROLE_NAME_SCHEMA,
    title='roles',
    description='A listing of roles held by a user or security group on an '
    'indvidual resource or all resources of a specific type.',
)

DEFAULT_ROLE_SCHEMA = fields.Object({
    'roleName':
    ROLE_NAME_SCHEMA,
    'applyToUnregistered':
    fields.Boolean(
        nullable=False,
        description=
        'Indicates whether or not this role only applies to registered users. If '
        'set to `false` it only applies to registered users. If set to `false`, it '
        'applies to unregistered/anonymous users as well as long as public access '
        'is enabled.',
    ),
})

USER_ROLES_MAPPING = fields.Object(
    properties=ROLE_LIST_SCHEMA,
    pattern_properties={
        fields.String(title='username', description='The user\'s username'):
        ROLE_LIST_SCHEMA
    },
    default=None,
    nullable=True,
Пример #15
0
    Success,
)
from web.server.potion.signals import after_user_role_change

# The schema for an invite user request
INVITE_OBJECT_FIELDS = {
    'name':
    fields.String(
        description='The invited user\'s name.',
        pattern=INTERNATIONALIZED_ALPHANUMERIC_AND_DELIMITER,
    ),
    'email':
    USERNAME_SCHEMA,
}

INVITE_OBJECT_SCHEMA = fields.Custom(fields.Object(INVITE_OBJECT_FIELDS),
                                     converter=lambda value: Invitee(**value))


class UserResource(PrincipalResource):
    '''The potion class for performing CRUD operations on the `User` class.
    '''
    class Meta(object):
        title = 'Users API'
        description = (
            'The API through which CRUD operations can be performed on User(s).'
        )
        model = User
        natural_key = 'username'

        permissions = {'read': 'yes'}