diff --git a/back/boxtribute_server/business_logic/warehouse/qr_code/fields.py b/back/boxtribute_server/business_logic/warehouse/qr_code/fields.py index a66cc0c43..b6a03eb73 100644 --- a/back/boxtribute_server/business_logic/warehouse/qr_code/fields.py +++ b/back/boxtribute_server/business_logic/warehouse/qr_code/fields.py @@ -1,6 +1,6 @@ from ariadne import ObjectType -from ....authz import authorize_for_reading_box +from ....authz import authorize_for_reading_box, handle_unauthorized from ....models.definitions.box import Box from ....models.definitions.location import Location @@ -8,6 +8,7 @@ @qr_code.field("box") +@handle_unauthorized def resolve_qr_code_box(qr_code_obj, _): try: box = ( diff --git a/back/boxtribute_server/business_logic/warehouse/qr_code/queries.py b/back/boxtribute_server/business_logic/warehouse/qr_code/queries.py index fa808cc76..df9483f82 100644 --- a/back/boxtribute_server/business_logic/warehouse/qr_code/queries.py +++ b/back/boxtribute_server/business_logic/warehouse/qr_code/queries.py @@ -1,7 +1,8 @@ from ariadne import QueryType -from ....authz import authorize +from ....authz import authorize, handle_unauthorized from ....models.definitions.qr_code import QrCode +from ....models.utils import handle_non_existing_resource query = QueryType() @@ -17,6 +18,8 @@ def resolve_qr_exists(*_, qr_code): @query.field("qrCode") -def resolve_qr_code(*_, qr_code): +@handle_unauthorized +@handle_non_existing_resource +def resolve_qr_code(*_, code): authorize(permission="qr:read") - return QrCode.get(QrCode.code == qr_code) + return QrCode.get(QrCode.code == code) diff --git a/back/boxtribute_server/graph_ql/bindables.py b/back/boxtribute_server/graph_ql/bindables.py index da57ae017..3484f265f 100644 --- a/back/boxtribute_server/graph_ql/bindables.py +++ b/back/boxtribute_server/graph_ql/bindables.py @@ -180,6 +180,8 @@ def resolve_location_type(obj, *_): UnionType("MoveBoxesResult", resolve_type_by_class_name), UnionType("AssignTagToBoxesResult", resolve_type_by_class_name), UnionType("UnassignTagFromBoxesResult", resolve_type_by_class_name), + UnionType("QrCodeResult", resolve_type_by_class_name), + UnionType("BoxResult", resolve_type_by_class_name), ) interface_types = ( InterfaceType("Location", resolve_location_type), diff --git a/back/boxtribute_server/graph_ql/definitions/protected/queries.graphql b/back/boxtribute_server/graph_ql/definitions/protected/queries.graphql index 8dca5d779..0ed1e1396 100644 --- a/back/boxtribute_server/graph_ql/definitions/protected/queries.graphql +++ b/back/boxtribute_server/graph_ql/definitions/protected/queries.graphql @@ -15,8 +15,8 @@ type Query { box(labelIdentifier: String!): Box " Return page of non-deleted [`Boxes`]({{Types.Box}}) in base with specified ID. Optionally pass filters " boxes(baseId: ID!, paginationInput: PaginationInput, filterInput: FilterBoxInput): BoxPage! - " Return [`QrCode`]({{Types.QrCode}}) with specified code (an MD5 hash in hex format of length 32) " - qrCode(qrCode: String!): QrCode + " Return [`QrCode`]({{Types.QrCode}}) with specified code (an MD5 hash in hex format of length 32), or an error in case of insufficient permission or missing resource. " + qrCode(code: String!): QrCodeResult! qrExists(qrCode: String): Boolean " Return [`ClassicLocation`]({{Types.ClassicLocation}}) with specified ID. Accessible for clients who are members of the location's base " location(id: ID!): ClassicLocation diff --git a/back/boxtribute_server/graph_ql/definitions/protected/types.graphql b/back/boxtribute_server/graph_ql/definitions/protected/types.graphql index 17a6c9453..a13d13791 100644 --- a/back/boxtribute_server/graph_ql/definitions/protected/types.graphql +++ b/back/boxtribute_server/graph_ql/definitions/protected/types.graphql @@ -65,8 +65,8 @@ Representation of a QR code, possibly associated with a [`Box`]({{Types.Box}}). type QrCode { id: ID! code: String! - " [`Box`]({{Types.Box}}) associated with the QR code (`null` if none associated) " - box: Box + " [`Box`]({{Types.Box}}) associated with the QR code (`null` if none associated), or an error in case of insufficient permission or missing authorization for box's base " + box: BoxResult createdOn: Datetime } @@ -617,8 +617,11 @@ union DeleteProductResult = Product | InsufficientPermissionError | ResourceDoes union EnableStandardProductResult = Product | InsufficientPermissionError | ResourceDoesNotExistError | UnauthorizedForBaseError | InvalidPriceError | StandardProductAlreadyEnabledForBaseError union EditStandardProductInstantiationResult = Product | InsufficientPermissionError | ResourceDoesNotExistError | UnauthorizedForBaseError | InvalidPriceError | ProductTypeMismatchError union DisableStandardProductResult = Product | InsufficientPermissionError | ResourceDoesNotExistError | UnauthorizedForBaseError | BoxesStillAssignedToProductError | ProductTypeMismatchError + union StandardProductResult = StandardProduct | InsufficientPermissionError | ResourceDoesNotExistError union StandardProductsResult = StandardProductPage | InsufficientPermissionError | UnauthorizedForBaseError +union QrCodeResult = QrCode | InsufficientPermissionError | ResourceDoesNotExistError +union BoxResult = Box | InsufficientPermissionError | UnauthorizedForBaseError type Metrics { """ diff --git a/back/test/auth0_integration_tests/test_operations.py b/back/test/auth0_integration_tests/test_operations.py index 5da948e82..18d19a33e 100644 --- a/back/test/auth0_integration_tests/test_operations.py +++ b/back/test/auth0_integration_tests/test_operations.py @@ -15,7 +15,8 @@ def _assert_successful_request(*args, **kwargs): return assert_successful_request(*args, **kwargs, endpoint=endpoint) query = """query BoxIdAndItems { - qrCode(qrCode: "093f65e080a295f8076b1c5722a46aa2") { box { id } } + qrCode(code: "093f65e080a295f8076b1c5722a46aa2") { + ...on QrCode { box { id } } } }""" queried_box = _assert_successful_request(auth0_client, query)["box"] assert queried_box == {"id": "100000000"} diff --git a/back/test/endpoint_tests/test_app.py b/back/test/endpoint_tests/test_app.py index 298b1ac6c..4ba3d675c 100644 --- a/back/test/endpoint_tests/test_app.py +++ b/back/test/endpoint_tests/test_app.py @@ -113,13 +113,6 @@ def test_query_non_existent_box(read_only_client): assert "SQL" not in response.json["errors"][0]["message"] -def test_query_non_existent_qr_code(read_only_client): - # Test case 8.1.31 - query = """query { qrCode(qrCode: "-1") { id } }""" - response = assert_bad_user_input(read_only_client, query) - assert "SQL" not in response.json["errors"][0]["message"] - - @pytest.mark.parametrize("resource", ["base", "organisation", "user"]) def test_query_non_existent_resource_for_god_user(read_only_client, mocker, resource): # Test case 99.1.3, 10.1.3 @@ -352,6 +345,13 @@ def test_mutate_resource_does_not_exist( @pytest.mark.parametrize( "operation,query_input,field,response", [ + # Test case 8.1.31 + [ + "qrCode", + 'code: "0"', + "...on ResourceDoesNotExistError { id name }", + {"id": None, "name": "QrCode"}, + ], # Test case 8.1.42 [ "standardProduct", diff --git a/back/test/endpoint_tests/test_box.py b/back/test/endpoint_tests/test_box.py index 61d7bf3c7..d5c90ef9f 100644 --- a/back/test/endpoint_tests/test_box.py +++ b/back/test/endpoint_tests/test_box.py @@ -84,11 +84,8 @@ def test_box_query_by_label_identifier( def test_box_query_by_qr_code(read_only_client, default_box, default_qr_code): # Test case 8.1.5 query = f"""query {{ - qrCode(qrCode: "{default_qr_code['code']}") {{ - box {{ - labelIdentifier - }} - }} + qrCode(code: "{default_qr_code['code']}") {{ + ...on QrCode {{ box {{ ...on Box {{ labelIdentifier }} }} }} }} }}""" queried_box = assert_successful_request(read_only_client, query)["box"] assert queried_box["labelIdentifier"] == default_box["label_identifier"] @@ -984,7 +981,8 @@ def _create_query(label_identifier): return f"""query {{ box(labelIdentifier: "{label_identifier}") {{ id }} }}""" def _create_qr_query(qr_code): - return f"""query {{ qrCode(qrCode: "{qr_code['code']}") {{ box {{ id }} }} }}""" + return f"""query {{ qrCode(code: "{qr_code['code']}") {{ + ...on QrCode {{ box {{ ...on Box {{ id }} }} }} }} }}""" queries = { str(in_transit_box["id"]): _create_query(in_transit_box["label_identifier"]), diff --git a/back/test/endpoint_tests/test_cron.py b/back/test/endpoint_tests/test_cron.py index 4b3074d8f..afc056e5e 100644 --- a/back/test/endpoint_tests/test_cron.py +++ b/back/test/endpoint_tests/test_cron.py @@ -13,7 +13,7 @@ NR_OF_CREATED_TAGS_PER_BASE, NR_OF_DELETED_TAGS_PER_BASE, ) -from utils import assert_bad_user_input, assert_successful_request +from utils import assert_successful_request reseed_db_path = f"{CRON_PATH}/reseed-db" headers = [("X-AppEngine-Cron", "true")] @@ -45,14 +45,16 @@ def test_reseed_db(cron_client, monkeypatch, mocker): # Success; perform actual sourcing of seed (takes about 2s) # Create QR code and verify that it is removed after reseeding - mutation = "mutation { createQrCode { code } }" + mutation = "mutation { createQrCode { id code } }" response = assert_successful_request(cron_client, mutation) code = response["code"] response = cron_client.get(reseed_db_path, headers=headers) assert response.status_code == 200 assert response.json == {"message": "reseed-db job executed"} - query = f"""query {{ qrCode(qrCode: "{code}") {{ id }} }}""" - response = assert_bad_user_input(cron_client, query) + query = f"""query {{ qrCode(code: "{code}") {{ + ...on ResourceDoesNotExistError {{ id name }} }} }}""" + response = assert_successful_request(cron_client, query) + assert response == {"id": None, "name": "QrCode"} # Verify generation of fake data query = "query { tags { id } }" diff --git a/back/test/endpoint_tests/test_permissions.py b/back/test/endpoint_tests/test_permissions.py index 92a0baaf4..24798b47d 100644 --- a/back/test/endpoint_tests/test_permissions.py +++ b/back/test/endpoint_tests/test_permissions.py @@ -58,8 +58,6 @@ def operation_name(operation): [ # Test case 8.1.3 """box( labelIdentifier: "12345678") { id }""", - # Test case 8.1.32 - """qrCode( qrCode: "1337beef" ) { id }""", # Test case 8.1.35 """qrExists( qrCode: "1337beef" )""", ], @@ -306,8 +304,12 @@ def test_invalid_permission_for_qr_code_box( # Verify missing stock:read permission mock_user_for_request(mocker, permissions=["qr:read"]) code = default_qr_code["code"] - query = f"""query {{ qrCode(qrCode: "{code}") {{ box {{ id }} }} }}""" - assert_forbidden_request(read_only_client, query, value={"box": None}) + query = f"""query {{ qrCode(code: "{code}") {{ + ...on QrCode {{ + box {{ ...on InsufficientPermissionError {{ name }} }} + }} }} }}""" + response = assert_successful_request(read_only_client, query) + assert response == {"box": {"name": "stock:read"}} # Test case 8.1.11 # Verify missing base-specific stock:read permission (the QR code belongs to a box @@ -325,9 +327,15 @@ def test_invalid_permission_for_qr_code_box( base_ids=[1], ) code = another_qr_code_with_box["code"] # the associated box is in base ID 3 - query = f"""query {{ qrCode(qrCode: "{code}") {{ - box {{ tags {{ taggedResources {{ ...on Beneficiary {{ id }} }} }} }} }} }}""" - assert_forbidden_request(read_only_client, query, value={"box": None}) + query = f"""query {{ qrCode(code: "{code}") {{ + ...on QrCode {{ + box {{ + ...on UnauthorizedForBaseError {{ id name }} + ...on Box {{ + tags {{ taggedResources {{ ...on Beneficiary {{ id }} }} }} + }} }} }} }} }}""" + response = assert_successful_request(read_only_client, query) + assert response == {"box": {"id": "3", "name": ""}} def test_invalid_permission_for_organisation_bases( @@ -648,6 +656,13 @@ def test_mutate_unauthorized_for_base( @pytest.mark.parametrize( "operation,query_input,field,response", [ + # Test case 8.1.32 + [ + "qrCode", + 'code: "1337beef"', + "...on InsufficientPermissionError { name }", + {"name": "qr:read"}, + ], # Test case 8.1.43 [ "standardProduct", diff --git a/back/test/endpoint_tests/test_qr.py b/back/test/endpoint_tests/test_qr.py index 1b87fe563..3993feb1a 100644 --- a/back/test/endpoint_tests/test_qr.py +++ b/back/test/endpoint_tests/test_qr.py @@ -25,12 +25,12 @@ def test_qr_code_query(read_only_client, default_box, default_qr_code): # Test case 8.1.30 code = default_qr_code["code"] query = f"""query {{ - qrCode(qrCode: "{code}") {{ + qrCode(code: "{code}") {{ ...on QrCode {{ id code - box {{ id }} + box {{ ...on Box {{ id }} }} createdOn - }} + }} }} }}""" queried_code = assert_successful_request(read_only_client, query) assert queried_code == { @@ -44,7 +44,8 @@ def test_qr_code_query(read_only_client, default_box, default_qr_code): def test_code_not_associated_with_box(read_only_client, qr_code_without_box): # Test case 8.1.2a code = qr_code_without_box["code"] - query = f"""query {{ qrCode(qrCode: "{code}") {{ box {{ id }} }} }}""" + query = f"""query {{ qrCode(code: "{code}") {{ + ...on QrCode {{ box {{ ...on Box {{ id }} }} }} }} }}""" qr_code = assert_successful_request(read_only_client, query) assert qr_code == {"box": None} @@ -61,10 +62,10 @@ def test_qr_code_mutation(client, box_without_qr_code): createQrCode(boxLabelIdentifier: "{box_without_qr_code['label_identifier']}") {{ id - box {{ + box {{ ...on Box {{ id numberOfItems - }} + }} }} }} }}""" created_qr_code = assert_successful_request(client, mutation) diff --git a/front/src/queries/queries.ts b/front/src/queries/queries.ts index 4c80f0427..2be559eab 100644 --- a/front/src/queries/queries.ts +++ b/front/src/queries/queries.ts @@ -38,10 +38,28 @@ export const BOX_DETAILS_BY_LABEL_IDENTIFIER_QUERY = gql` export const GET_BOX_LABEL_IDENTIFIER_BY_QR_CODE = gql` ${BOX_BASIC_FIELDS_FRAGMENT} query GetBoxLabelIdentifierForQrCode($qrCode: String!) { - qrCode(qrCode: $qrCode) { - code - box { - ...BoxBasicFields + qrCode(code: $qrCode) { + __typename + ... on QrCode { + code + box { + __typename + ...on Box { + ...BoxBasicFields + } + ...on InsufficientPermissionError { + name + } + ...on UnauthorizedForBaseError { + name + } + } + } + ... on InsufficientPermissionError { + name + } + ... on ResourceDoesNotExistError { + name } } } diff --git a/front/src/types/generated/graphql.ts b/front/src/types/generated/graphql.ts index 7beb6e6f8..d02496f9a 100755 --- a/front/src/types/generated/graphql.ts +++ b/front/src/types/generated/graphql.ts @@ -246,12 +246,7 @@ export type BoxPage = { totalCount: Scalars['Int']; }; -/** Utility response type for box bulk-update mutations, containing both updated boxes and invalid boxes (ignored due to e.g. being deleted, in prohibited base, and/or non-existing). */ -export type BoxesResult = { - __typename?: 'BoxesResult'; - invalidBoxLabelIdentifiers: Array; - updatedBoxes: Array; -}; +export type BoxResult = Box | InsufficientPermissionError | UnauthorizedForBaseError; /** Classificators for [`Box`]({{Types.Box}}) state. */ export enum BoxState { @@ -279,6 +274,13 @@ export type BoxUpdateInput = { tagIdsToBeAdded?: InputMaybe>; }; +/** Utility response type for box bulk-update mutations, containing both updated boxes and invalid boxes (ignored due to e.g. being deleted, in prohibited base, and/or non-existing). */ +export type BoxesResult = { + __typename?: 'BoxesResult'; + invalidBoxLabelIdentifiers: Array; + updatedBoxes: Array; +}; + export type BoxesStillAssignedToProductError = { __typename?: 'BoxesStillAssignedToProductError'; labelIdentifiers: Array; @@ -1194,13 +1196,15 @@ export type ProductTypeMismatchError = { /** Representation of a QR code, possibly associated with a [`Box`]({{Types.Box}}). */ export type QrCode = { __typename?: 'QrCode'; - /** [`Box`]({{Types.Box}}) associated with the QR code (`null` if none associated) */ - box?: Maybe; + /** [`Box`]({{Types.Box}}) associated with the QR code (`null` if none associated), or an error in case of insufficient permission or missing authorization for box's base */ + box?: Maybe; code: Scalars['String']; createdOn?: Maybe; id: Scalars['ID']; }; +export type QrCodeResult = InsufficientPermissionError | QrCode | ResourceDoesNotExistError; + export type Query = { __typename?: 'Query'; /** Return [`Base`]({{Types.Base}}) with specified ID. Accessible for clients who are members of this base. */ @@ -1241,8 +1245,8 @@ export type Query = { productCategory?: Maybe; /** Return all [`Products`]({{Types.Product}}) (incl. deleted) that the client is authorized to view. */ products: ProductPage; - /** Return [`QrCode`]({{Types.QrCode}}) with specified code (an MD5 hash in hex format of length 32) */ - qrCode?: Maybe; + /** Return [`QrCode`]({{Types.QrCode}}) with specified code (an MD5 hash in hex format of length 32), or an error in case of insufficient permission or missing resource. */ + qrCode: QrCodeResult; qrExists?: Maybe; /** Return [`Shipment`]({{Types.Shipment}}) with specified ID. Clients are authorized to view a shipment if they're member of either the source or the target base */ shipment?: Maybe; @@ -1373,7 +1377,7 @@ export type QueryProductsArgs = { export type QueryQrCodeArgs = { - qrCode: Scalars['String']; + code: Scalars['String']; }; @@ -1945,7 +1949,7 @@ export type GetBoxLabelIdentifierForQrCodeQueryVariables = Exact<{ }>; -export type GetBoxLabelIdentifierForQrCodeQuery = { __typename?: 'Query', qrCode?: { __typename?: 'QrCode', code: string, box?: { __typename?: 'Box', labelIdentifier: string, state: BoxState, comment?: string | null, lastModifiedOn?: any | null, location?: { __typename?: 'ClassicLocation', id: string, base?: { __typename?: 'Base', id: string } | null } | { __typename?: 'DistributionSpot', id: string, base?: { __typename?: 'Base', id: string } | null } | null, shipmentDetail?: { __typename?: 'ShipmentDetail', id: string, shipment: { __typename?: 'Shipment', id: string } } | null } | null } | null }; +export type GetBoxLabelIdentifierForQrCodeQuery = { __typename?: 'Query', qrCode: { __typename: 'InsufficientPermissionError', name: string } | { __typename: 'QrCode', code: string, box?: { __typename: 'Box', labelIdentifier: string, state: BoxState, comment?: string | null, lastModifiedOn?: any | null, location?: { __typename?: 'ClassicLocation', id: string, base?: { __typename?: 'Base', id: string } | null } | { __typename?: 'DistributionSpot', id: string, base?: { __typename?: 'Base', id: string } | null } | null, shipmentDetail?: { __typename?: 'ShipmentDetail', id: string, shipment: { __typename?: 'Shipment', id: string } } | null } | { __typename: 'InsufficientPermissionError', name: string } | { __typename: 'UnauthorizedForBaseError', name: string } | null } | { __typename: 'ResourceDoesNotExistError', name: string } }; export type CheckIfQrExistsInDbQueryVariables = Exact<{ qrCode: Scalars['String']; @@ -2030,7 +2034,7 @@ export type CreateBoxMutationVariables = Exact<{ }>; -export type CreateBoxMutation = { __typename?: 'Mutation', createBox?: { __typename?: 'Box', labelIdentifier: string, qrCode?: { __typename?: 'QrCode', code: string, box?: { __typename?: 'Box', labelIdentifier: string } | null } | null } | null }; +export type CreateBoxMutation = { __typename?: 'Mutation', createBox?: { __typename?: 'Box', labelIdentifier: string, qrCode?: { __typename?: 'QrCode', code: string, box?: { __typename?: 'Box', labelIdentifier: string } | { __typename?: 'InsufficientPermissionError' } | { __typename?: 'UnauthorizedForBaseError' } | null } | null } | null }; export type BoxByLabelIdentifierAndAllProductsWithBaseIdQueryVariables = Exact<{ baseId: Scalars['ID']; diff --git a/front/src/views/BoxCreate/BoxCreateView.tsx b/front/src/views/BoxCreate/BoxCreateView.tsx index ad9d5c7c9..ffe08bdf4 100644 --- a/front/src/views/BoxCreate/BoxCreateView.tsx +++ b/front/src/views/BoxCreate/BoxCreateView.tsx @@ -72,7 +72,9 @@ export const CREATE_BOX_MUTATION = gql` qrCode { code box { - labelIdentifier + ...on Box { + labelIdentifier + } } } }