diff --git a/README.md b/README.md index 47b62fd95a3bb0c98694c2dbfc4b7d84c64c9594..7cadda7dd38d90e0e0d4f49fe835c811d21124fc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ + + # POLARIS - Rights Engine / Consent Engine This is the repository contains the source code of the rights engine of the Polaris project. The Consent Engine is a software solution designed to provide a secure and efficient way for obtaining and managing user consent. The project is divided into two main components: frontend and backend. The frontend component is responsible for providing a user-friendly interface for obtaining consent, while the backend component is responsible for storing and managing user consent data. diff --git a/docs/docs/deployment.md b/docs/docs/deployment.md index e60de0ff95c2af9448952674c81979380075298d..d6d1f5c8bfe1486a35448b30b3f4b162fcc837a1 100644 --- a/docs/docs/deployment.md +++ b/docs/docs/deployment.md @@ -372,3 +372,18 @@ Content `traefik.toml` statusCodes = ["200", "300-302"] ``` +## Adjust deployment + +### Number of gunicorn workers +The number of Gunicorn workers can be adjusted using the WORKERS environment variable. +By default, the application starts with 4 workers. You can override this value during deployment: + +```docker-compose.yml +services: + web: + image: registry.git.rwth-aachen.de/polaris/rights-engine/rights-engine:${POLARIS_VERSION} + ports: + - "80:80" + environment: + - WORKERS=10 +``` \ No newline at end of file diff --git a/docs/docs/third_party_access.md b/docs/docs/third_party_access.md index 93328c1a71a2cf314b405aaad21b839304d5b4fe..cdd896d87b1aa8546e31fc6395e449e63f7689d2 100644 --- a/docs/docs/third_party_access.md +++ b/docs/docs/third_party_access.md @@ -18,7 +18,7 @@ In cases were neither a provider schema is available or the provider hasn't yet ### Get user consent -This endpoint returns the current user consent for a given user along with the provider schema the user accepted and a flag for data recording pausation. +This endpoint returns the current user consent for a given user, along with a flag indicating data recording pausation. - HTTP Method: **GET** - URL `/api/v1/consents/user/status/user1@polaris.com/third-party` @@ -31,11 +31,11 @@ This endpoint returns the current user consent for a given user along with the p ###### Returns -| Code | Type | Description | -| ----- | ---- | -------------------------------------------------------------------------------------------- | -| `200` | dict | dict containing `user_consent`, `provider_schema` and `paused_data_recording` (boolean flag) | -| `401` | dict | invalid token | -| `400` | dict | invalid request data e.g. `{"user_id":["Object with email=user1@polaris does not exist."]}` | +| Code | Type | Description | +| ----- | ---- |------------------------------------------------------------------------------------------| +| `200` | dict | dict containing `consent` and `paused_data_recording` (boolean flag) | +| `401` | dict | invalid token | +| `400` | dict | invalid request data e.g. `{"user_id":["Object with email=user1@polaris does not exist."]}` | ###### Example @@ -45,7 +45,7 @@ $ curl -X GET http://[RIGHT_ENGINE_URL]/api/v1/consents/user/status/user1@polari Response -```js +```json { "consent": { "id": 1, @@ -138,181 +138,13 @@ Response "objects": [] }] }, - "provider_schema": { - "id": 1, - "definition": { - "id": "h5p-0", - "name": "H5P", - "description": "Open-source content collaboration framework", - "groups": [{ - "label": "Default group", - "description": "default", - "verbs": [{ - "id": "http://h5p.example.com/expapi/verbs/experienced", - "label": "Experienced", - "description": "Experienced", - "defaultConsent": true, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", - "label": "1.1.1 Funktionen", - "defaultConsent": true, - "definition": { - "name": { - "enUS": "1.1.1 Funktionen" - } - } - }] - }, { - "id": "http://h5p.example.com/expapi/verbs/attempted", - "label": "Attempted", - "description": "Attempted", - "defaultConsent": true, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", - "label": "2.3.1 Funktion Zirkulationsleitung", - "defaultConsent": true, - "definition": { - "name": { - "enUS": "2.3.1 Funktion Zirkulationsleitung" - } - } - }] - }], - "isDefault": true - }, { - "label": "Group 2", - "description": "Lorem ipsum", - "verbs": [{ - "id": "http://h5p.example.com/expapi/verbs/interacted", - "label": "Interacted", - "description": "Lorem ipsum", - "defaultConsent": true, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", - "label": "1.2.3 Kappenventil", - "defaultConsent": true, - "definition": { - "name": { - "enUS": "1.2.3 Kappenventil" - } - } - }] - }, { - "id": "http://h5p.example.com/expapi/verbs/answered", - "label": "Answered", - "description": "lorem ipsum", - "defaultConsent": false, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", - "label": "7.2.1 Ventil Basics", - "defaultConsent": false, - "definition": { - "name": { - "enUS": "7.2.1 Ventil Basics" - } - } - }] - }], - "isDefault": false - }], - "essential_verbs": [{ - "id": "http://h5p.example.com/expapi/verbs/liked", - "label": "Liked", - "description": "Like interaction", - "defaultConsent": true, - "objects": [] - }] - }, - "groups": [{ - "label": "Default group", - "description": "default", - "verbs": [{ - "id": "http://h5p.example.com/expapi/verbs/experienced", - "label": "Experienced", - "description": "Experienced", - "defaultConsent": true, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", - "label": "1.1.1 Funktionen", - "defaultConsent": true, - "definition": { - "name": { - "enUS": "1.1.1 Funktionen" - } - } - }] - }, { - "id": "http://h5p.example.com/expapi/verbs/attempted", - "label": "Attempted", - "description": "Attempted", - "defaultConsent": true, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", - "label": "2.3.1 Funktion Zirkulationsleitung", - "defaultConsent": true, - "definition": { - "name": { - "enUS": "2.3.1 Funktion Zirkulationsleitung" - } - } - }] - }], - "isDefault": true - }, { - "label": "Group 2", - "description": "Lorem ipsum", - "verbs": [{ - "id": "http://h5p.example.com/expapi/verbs/interacted", - "label": "Interacted", - "description": "Lorem ipsum", - "defaultConsent": true, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", - "label": "1.2.3 Kappenventil", - "defaultConsent": true, - "definition": { - "name": { - "enUS": "1.2.3 Kappenventil" - } - } - }] - }, { - "id": "http://h5p.example.com/expapi/verbs/answered", - "label": "Answered", - "description": "lorem ipsum", - "defaultConsent": false, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", - "label": "7.2.1 Ventil Basics", - "defaultConsent": false, - "definition": { - "name": { - "enUS": "7.2.1 Ventil Basics" - } - } - }] - }], - "isDefault": false - }], - "essential_verbs": [{ - "id": "http://h5p.example.com/expapi/verbs/liked", - "label": "Liked", - "description": "Like interaction", - "defaultConsent": true, - "objects": [] - }], - "updated": "2023-02-09T12:45:57.562000Z", - "created": "2023-02-09T12:45:57.562000Z", - "provider": 1, - "superseded_by": null - }, "paused_data_recording": false } ``` ### Get provider details -Allows a third party to query the details of one's provider. Among other things, all created schemas are supplied here. +Allows a third party to query the details of the current provider. Among other things, all associated groups and created schemas are supplied here. - HTTP Method: **GET** - URL `/api/v1/consents/provider-status/third-party` @@ -325,383 +157,254 @@ $ curl -X GET 127.0.0.1:8003/api/v1/consents/provider-status/third-party -H "Con Response -```js +```json { - "id": 1, - "name": "H5P", - "versions": [{ - "id": 5, - "definition": { - "id": "h5p-0", - "name": "H5P", - "description": "Open-source content collaboration framework", - "groups": [{ - "label": "Default group", - "description": "default", - "verbs": [{ - "id": "http://h5p.example.com/expapi/verbs/experienced", - "label": "Experienced", - "description": "Experienced", - "defaultConsent": false, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", - "label": "1.1.1 Funktionen", - "defaultConsent": true, - "definition": { - "name": { - "enUS": "1.1.1 Funktionen" - } - } - }] - }, { - "id": "http://h5p.example.com/expapi/verbs/attempted", - "label": "Attempted", - "description": "Attempted", - "defaultConsent": true, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", - "label": "2.3.1 Funktion Zirkulationsleitung", - "defaultConsent": true, - "definition": { - "name": { - "enUS": "2.3.1 Funktion Zirkulationsleitung" - } - } - }] - }], - "isDefault": true - }, { - "label": "Group 2", - "description": "Lorem ipsum", - "verbs": [{ - "id": "http://h5p.example.com/expapi/verbs/interacted", - "label": "Interacted", - "description": "Lorem ipsum", - "defaultConsent": true, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", - "label": "1.2.3 Kappenventil", - "defaultConsent": true, - "definition": { - "name": { - "enUS": "1.2.3 Kappenventil" - } - } - }] - }, { - "id": "http://h5p.example.com/expapi/verbs/answered", - "label": "Answered", - "description": "lorem ipsum", - "defaultConsent": false, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", - "label": "7.2.1 Ventil Basics", - "defaultConsent": false, - "definition": { - "name": { - "enUS": "7.2.1 Ventil Basics" - } - } - }] - }], - "isDefault": false - }], - "essential_verbs": [{ - "id": "http://h5p.example.com/expapi/verbs/liked", - "label": "Liked", - "description": "Like interaction", - "defaultConsent": true, - "objects": [] - }, { - "id": "http://h5p.example.com/expapi/verbs/clicked", - "label": "Clicked", - "description": "Clicked interaction", - "defaultConsent": true, - "objects": [] - }] + "id": 1, + "name": "H5P", + "groups": [ + { + "id": 1, + "verbs": [ + { + "id": "http://h5p.example.com/expapi/verbs/experienced", + "label": "Experienced", + "description": "Experienced", + "defaultConsent": true, + "objects": [ + { + "id": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", + "objectType": "Activity", + "defaultConsent": true, + "definition": { + "type": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", + "name": { + "enUS": "1.1.1 Funktionen" + } + }, + "object_type": "Activity", + "matching": "definitionType", + "label": "" + } + ] }, - "groups": [{ - "label": "Default group", - "description": "default", - "verbs": [{ - "id": "http://h5p.example.com/expapi/verbs/experienced", - "label": "Experienced", - "description": "Experienced", - "defaultConsent": false, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", - "label": "1.1.1 Funktionen", - "defaultConsent": true, - "definition": { - "name": { - "enUS": "1.1.1 Funktionen" - } - } - }] - }, { - "id": "http://h5p.example.com/expapi/verbs/attempted", - "label": "Attempted", - "description": "Attempted", - "defaultConsent": true, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", - "label": "2.3.1 Funktion Zirkulationsleitung", - "defaultConsent": true, - "definition": { - "name": { - "enUS": "2.3.1 Funktion Zirkulationsleitung" - } - } - }] - }], - "isDefault": true - }, { - "label": "Group 2", - "description": "Lorem ipsum", - "verbs": [{ - "id": "http://h5p.example.com/expapi/verbs/interacted", - "label": "Interacted", - "description": "Lorem ipsum", - "defaultConsent": true, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", - "label": "1.2.3 Kappenventil", - "defaultConsent": true, - "definition": { - "name": { - "enUS": "1.2.3 Kappenventil" - } - } - }] - }, { - "id": "http://h5p.example.com/expapi/verbs/answered", - "label": "Answered", - "description": "lorem ipsum", - "defaultConsent": false, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", - "label": "7.2.1 Ventil Basics", - "defaultConsent": false, - "definition": { - "name": { - "enUS": "7.2.1 Ventil Basics" - } - } - }] - }], - "isDefault": false - }], - "essential_verbs": [{ - "id": "http://h5p.example.com/expapi/verbs/liked", - "label": "Liked", - "description": "Like interaction", - "defaultConsent": true, - "objects": [] - }, { - "id": "http://h5p.example.com/expapi/verbs/clicked", - "label": "Clicked", - "description": "Clicked interaction", - "defaultConsent": true, - "objects": [] - }], - "updated": "2023-02-09T10:19:51.566456Z", - "created": "2023-02-09T10:19:51.566471Z", - "provider": 1, - "superseded_by": null - }, { - "id": 1, - "definition": { - "id": "h5p-0", - "name": "H5P", - "description": "Open-source content collaboration framework", - "groups": [{ - "label": "Default group", - "description": "default", - "verbs": [{ - "id": "http://h5p.example.com/expapi/verbs/experienced", - "label": "Experienced", - "description": "Experienced", - "defaultConsent": true, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", - "label": "1.1.1 Funktionen", - "defaultConsent": true, - "definition": { - "name": { - "enUS": "1.1.1 Funktionen" - } - } - }] - }, { - "id": "http://h5p.example.com/expapi/verbs/attempted", - "label": "Attempted", - "description": "Attempted", - "defaultConsent": true, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", - "label": "2.3.1 Funktion Zirkulationsleitung", - "defaultConsent": true, - "definition": { - "name": { - "enUS": "2.3.1 Funktion Zirkulationsleitung" - } - } - }] - }], - "isDefault": true - }, { - "label": "Group 2", - "description": "Lorem ipsum", - "verbs": [{ - "id": "http://h5p.example.com/expapi/verbs/interacted", - "label": "Interacted", - "description": "Lorem ipsum", - "defaultConsent": true, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", - "label": "1.2.3 Kappenventil", - "defaultConsent": true, - "definition": { - "name": { - "enUS": "1.2.3 Kappenventil" - } - } - }] - }, { - "id": "http://h5p.example.com/expapi/verbs/answered", - "label": "Answered", - "description": "lorem ipsum", - "defaultConsent": false, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", - "label": "7.2.1 Ventil Basics", - "defaultConsent": false, - "definition": { - "name": { - "enUS": "7.2.1 Ventil Basics" - } - } - }] - }], - "isDefault": false - }], - "essential_verbs": [{ - "id": "http://h5p.example.com/expapi/verbs/liked", - "label": "Liked", - "description": "Like interaction", - "defaultConsent": true, - "objects": [] - }] + { + "id": "http://h5p.example.com/expapi/verbs/attempted", + "label": "Attempted", + "description": "Attempted", + "defaultConsent": true, + "objects": [ + { + "id": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", + "objectType": "Activity", + "defaultConsent": true, + "definition": { + "type": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", + "name": { + "enUS": "2.3.1 Funktion Zirkulationsleitung" + } + }, + "object_type": "Activity", + "matching": "definitionType", + "label": "" + } + ] }, - "groups": [{ - "label": "Default group", - "description": "default", - "verbs": [{ - "id": "http://h5p.example.com/expapi/verbs/experienced", - "label": "Experienced", - "description": "Experienced", + { + "id": "http://h5p.example.com/expapi/verbs/interacted", + "label": "Interacted", + "description": "Lorem ipsum", + "defaultConsent": true, + "objects": [ + { + "id": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", + "objectType": "Activity", + "defaultConsent": true, + "definition": { + "type": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", + "name": { + "enUS": "1.2.3 Kappenventil" + } + }, + "object_type": "Activity", + "matching": "definitionType", + "label": "" + } + ] + }, + { + "id": "http://h5p.example.com/expapi/verbs/answered", + "label": "Answered", + "description": "lorem ipsum", + "defaultConsent": false, + "objects": [ + { + "id": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", + "objectType": "Activity", + "defaultConsent": false, + "definition": { + "type": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", + "name": { + "enUS": "7.2.1 Ventil Basics" + } + }, + "object_type": "Activity", + "matching": "definitionType", + "label": "" + } + ] + } + ], + "purposeOfCollection": "Lorem Ipsum", + "group_id": "372f255714963f49f0f8e7acca75e0fc55140d00cae2d4328f5a9a6bf090c359", + "label": "Default group", + "description": "default", + "requires_consent": true, + "updated": "2025-03-27T16:01:20.025590Z", + "created": "2025-03-27T16:01:20.025602Z", + "provider": 1, + "provider_schema": 1 + } + ], + "versions": [ + { + "id": 1, + "definition": { + "id": "h5p-0", + "name": "H5P", + "description": "Open-source content collaboration framework", + "verbs": [ + { + "id": "http://h5p.example.com/expapi/verbs/experienced", + "label": "Experienced", + "description": "Experienced", + "defaultConsent": true, + "objects": [ + { + "id": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", + "objectType": "Activity", "defaultConsent": true, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", - "label": "1.1.1 Funktionen", - "defaultConsent": true, - "definition": { - "name": { - "enUS": "1.1.1 Funktionen" - } - } - }] - }, { - "id": "http://h5p.example.com/expapi/verbs/attempted", - "label": "Attempted", - "description": "Attempted", + "definition": { + "type": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", + "name": { + "enUS": "1.1.1 Funktionen" + } + }, + "object_type": "Activity", + "matching": "definitionType", + "label": "" + } + ] + }, + { + "id": "http://h5p.example.com/expapi/verbs/attempted", + "label": "Attempted", + "description": "Attempted", + "defaultConsent": true, + "objects": [ + { + "id": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", + "objectType": "Activity", "defaultConsent": true, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", - "label": "2.3.1 Funktion Zirkulationsleitung", - "defaultConsent": true, - "definition": { - "name": { - "enUS": "2.3.1 Funktion Zirkulationsleitung" - } - } - }] - }], - "isDefault": true - }, { - "label": "Group 2", + "definition": { + "type": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", + "name": { + "enUS": "2.3.1 Funktion Zirkulationsleitung" + } + }, + "object_type": "Activity", + "matching": "definitionType", + "label": "" + } + ] + }, + { + "id": "http://h5p.example.com/expapi/verbs/interacted", + "label": "Interacted", "description": "Lorem ipsum", - "verbs": [{ - "id": "http://h5p.example.com/expapi/verbs/interacted", - "label": "Interacted", - "description": "Lorem ipsum", + "defaultConsent": true, + "objects": [ + { + "id": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", + "objectType": "Activity", "defaultConsent": true, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", - "label": "1.2.3 Kappenventil", - "defaultConsent": true, - "definition": { - "name": { - "enUS": "1.2.3 Kappenventil" - } - } - }] - }, { - "id": "http://h5p.example.com/expapi/verbs/answered", - "label": "Answered", - "description": "lorem ipsum", + "definition": { + "type": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", + "name": { + "enUS": "1.2.3 Kappenventil" + } + }, + "object_type": "Activity", + "matching": "definitionType", + "label": "" + } + ] + }, + { + "id": "http://h5p.example.com/expapi/verbs/answered", + "label": "Answered", + "description": "lorem ipsum", + "defaultConsent": false, + "objects": [ + { + "id": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", + "objectType": "Activity", "defaultConsent": false, - "objects": [{ - "id": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", - "label": "7.2.1 Ventil Basics", - "defaultConsent": false, - "definition": { - "name": { - "enUS": "7.2.1 Ventil Basics" - } - } - }] - }], - "isDefault": false - }], - "essential_verbs": [{ + "definition": { + "type": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", + "name": { + "enUS": "7.2.1 Ventil Basics" + } + }, + "object_type": "Activity", + "matching": "definitionType", + "label": "" + } + ] + } + ], + "essential_verbs": [ + { "id": "http://h5p.example.com/expapi/verbs/liked", "label": "Liked", "description": "Like interaction", "defaultConsent": true, "objects": [] - }], - "updated": "2023-02-09T10:19:51.568203Z", - "created": "2023-02-09T09:45:15.405764Z", - "provider": 1, - "superseded_by": 5 - }] + } + ], + "additional_lrs": [] + }, + "additional_lrs": [], + "updated": "2025-03-27T16:01:20.013790Z", + "created": "2025-03-27T16:01:20.013799Z", + "provider": 1, + "superseded_by": null + } + ] } ``` ### Save user consent -The provider schema id for each provider must always refer to the latest schema version of the respective provider, otherwise the request will be rejected +Save consent declarations for one or multiple verb groups for a user. - HTTP Method: **POST** - URL `/api/v1/consents/user/save/third-party` ###### Parameters -| Name | Type | Required | Default | -| -------------------- | ---------- | -------- | ------- | -| `user_id` | str | Yes | None | -| `provider_schema_id` | int | Yes | None | -| `verbs` | list[dict] | Yes | None | +| Name | Type | Required | Default | +|----------------------|-----------| -------- | ------- | +| `user_id` | str | Yes | None | +| `groups` | list[int] | Yes | None | ###### Returns -| Code | Type | Description | -| ----- | ---- | ----------------------------------------------------------------------------------------------------- | -| `200` | dict | dict with entry for each user contain list of consented verb and object ids | -| `401` | dict | invalid token | -| `403` | dict | reaised if a verb for a provider other than the one associated with the application token is provided | -| `400` | dict | invalid request data e.g. `{"user_id":["Object with email=user1@polaris does not exist."]}` | +| Code | Type | Description | +| ----- | ---- |------------------------------------------------------------------------------------------------------| +| `200` | dict | dict with the key "message" stating that the consent was saved | +| `401` | dict | invalid token | +| `403` | dict | raised if a verb for a provider other than the one associated with the application token is provided | +| `400` | dict | invalid request data e.g. `{"user_id":["Object with email=user1@polaris does not exist."]}` | ###### Examples @@ -710,36 +413,14 @@ curl -X POST 127.0.0.1:8003/api/v1/consents/user/save/third-party --data '{ "use ``` -Formatted request payload -```js +Formatted request payload (group IDs have to be retrieved beforehand) +```json { "user_id": "user1@polaris.com", - "provider_schema_id": 3, - "verbs": [ - { - "provider": 1, - "id": "http://h5p.example.com/expapi/verbs/experienced", - "consented": false, - "objects": "[{\"id\":\"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF\",\"label\":\"1.1.1 Funktionen\",\"defaultConsent\":true,\"definition\":{\"name\":{\"enUS\":\"1.1.1 Funktionen\"}},\"consented\":true}]" - }, - { - "provider": 1, - "id": "http://h5p.example.com/expapi/verbs/attempted", - "consented": true, - "objects": "[{\"id\":\"http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm\",\"label\":\"2.3.1 Funktion Zirkulationsleitung\",\"defaultConsent\":true,\"definition\":{\"name\":{\"enUS\":\"2.3.1 Funktion Zirkulationsleitung\"}},\"consented\":true}]" - }, - { - "provider": 1, - "id": "http://h5p.example.com/expapi/verbs/interacted", - "consented": true, - "objects": "[{\"id\":\"http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46\",\"label\":\"1.2.3 Kappenventil\",\"defaultConsent\":true,\"definition\":{\"name\":{\"enUS\":\"1.2.3 Kappenventil\"}},\"consented\":true}]" - }, - { - "provider": 1, - "id": "http://h5p.example.com/expapi/verbs/answered", - "consented": false, - "objects": "[{\"id\":\"http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b\",\"label\":\"7.2.1 Ventil Basics\",\"defaultConsent\":false,\"definition\":{\"name\":{\"enUS\":\"7.2.1 Ventil Basics\"}},\"consented\":false}]" - } + "groups": [ + 1, + 2, + 5 ] } ``` diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/backend/management/commands/create_mongo_index.py b/src/backend/management/commands/create_mongo_index.py index a5edf0ad599c14005249e17321f6e5c5948ca930..848a24db5ff7a56d48eeea21f212be40d001bc1c 100644 --- a/src/backend/management/commands/create_mongo_index.py +++ b/src/backend/management/commands/create_mongo_index.py @@ -14,6 +14,7 @@ class Command(BaseCommand): {"key": [("name", ASCENDING)], "name": "name_1"}, {"key": [("created_at", DESCENDING)], "name": "created_at_-1"}, {"key": [("name", ASCENDING), ("created_at", DESCENDING)], "name": "name_1_created_at_-1"}, + {"key": [("context_id", HASHED), ("name", ASCENDING), ("created_at", DESCENDING)], "name": "context_name_1_created_at_-1"}, ], "statement": [ {"key": [("_id", ASCENDING)], "name": "_id_"}, @@ -25,6 +26,14 @@ class Command(BaseCommand): # Iterate through each collection and create indexes for collection_name, indexes in collections_indexes.items(): + if collection_name not in lrs_db.list_collection_names(): + self.stdout.write(self.style.WARNING( + f"Collection '{collection_name}' does not exist. Creating collection..." + )) + # Force collection creation by inserting and deleting a dummy doc + lrs_db[collection_name].insert_one({"_init": True}) + lrs_db[collection_name].delete_one({"_init": True}) + collection = lrs_db[collection_name] self.stdout.write(self.style.SUCCESS(f"Creating indexes for collection: {collection_name}")) for index in indexes: diff --git a/src/backend/settings.py b/src/backend/settings.py index 9dcb9c95c5357b06702f3801275028b9ce0cdf83..31a0c99758035722afe519d7d5b84622c26cd09b 100644 --- a/src/backend/settings.py +++ b/src/backend/settings.py @@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/4.1/ref/settings/ """ import mimetypes import os +import sys from pathlib import Path import environ @@ -135,11 +136,12 @@ else: # Cache(s) # https://docs.djangoproject.com/en/4.1/topics/cache/ +use_file_based_cache = 'test' in sys.argv or os.getenv('DJANGO_TEST_ENV') or env("CACHE_BACKEND", default="file") != "redis" CACHES = { 'default': { - 'BACKEND': 'django.core.cache.backends.redis.RedisCache' if env("CACHE_BACKEND", default="file") == "redis" + 'BACKEND': 'django.core.cache.backends.redis.RedisCache' if not use_file_based_cache else 'django.core.cache.backends.filebased.FileBasedCache', - 'LOCATION': '/tmp/django_cache' if env("CACHE_BACKEND", default="file") == 'file' else env("CACHE_URI", default='redis://127.0.0.1:6379') , + 'LOCATION': '/tmp/django_cache' if use_file_based_cache else env("CACHE_URI", default='redis://127.0.0.1:6379') , } } diff --git a/src/consents/migrations/0005_remove_userconsents_provider_schema_and_more.py b/src/consents/migrations/0005_remove_userconsents_provider_schema_and_more.py index 2dffac17584025b4d77354926d5a2c83493260c3..feb994a33f184da5882d6a8c00b3b1ca2dd0d588 100644 --- a/src/consents/migrations/0005_remove_userconsents_provider_schema_and_more.py +++ b/src/consents/migrations/0005_remove_userconsents_provider_schema_and_more.py @@ -1,4 +1,4 @@ -from django.db import migrations, models +from django.db import migrations, models, connection import django.db.models.deletion def migrate_user_consent_to_verb(apps, schema_editor): @@ -8,10 +8,12 @@ def migrate_user_consent_to_verb(apps, schema_editor): schema = consent.provider_schema verb_id = consent.verb verb = Verb.objects.get(verb_id=verb_id, provider_schema=schema) - consent.verb = verb.id + #consent.verb = verb.id group = verb.providerverbgroup_set.first() - consent.verb_group_id = group.id - consent.save() + #consent.verb_group_id = group.id + #consent.save() + with connection.cursor() as cursor: + cursor.execute("UPDATE consents_userconsents SET verb = %s, verb_group_id = %s WHERE id = %s", [verb.id, group.id, consent.id]) class Migration(migrations.Migration): diff --git a/src/consents/tests/tests_consent_operations.py b/src/consents/tests/tests_consent_operations.py index 13c22c4927d1f8422a821c2c39a2dc2878d452f4..72887c8900856c3591b92fc17a3d4dc317f17474 100644 --- a/src/consents/tests/tests_consent_operations.py +++ b/src/consents/tests/tests_consent_operations.py @@ -11,7 +11,7 @@ from providers.models import Provider, ProviderAuthorization, ProviderSchema, Pr from users.models import CustomUser from django.core.management import call_command -from ..models import UserConsents +from consents.models import UserConsents PROJECT_PATH = os.path.abspath(os.path.dirname(__name__)) diff --git a/src/consents/tests/tests_provider_endpoints.py b/src/consents/tests/tests_provider_endpoints.py new file mode 100644 index 0000000000000000000000000000000000000000..adf6a9f5de081357a2252feee9027a217b1b0944 --- /dev/null +++ b/src/consents/tests/tests_provider_endpoints.py @@ -0,0 +1,190 @@ +import json +import os +from io import StringIO + +from rest_framework.test import APIClient +from rolepermissions.roles import assign_role +from django.core.management import call_command + +from consents.tests.tests_consent_operations import BaseTestCase +from providers.models import ProviderAuthorization, Provider, ProviderVerbGroup, ProviderSchema +from users.models import CustomUser + + +class TestVerbGroups(BaseTestCase): + verb_groups = [ + { + "provider_id": 1, + "id": "default_group", + "label": "Default group", + "description": "default", + "showVerbDetails": True, + "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, + "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/attempted"}, + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"} + ] + }, + ] + + def setUp(self): + call_command('check_and_apply_migrations') + normal_user = CustomUser.objects.create_user( + self.test_user_email, self.test_user_password + ) + provider_user = CustomUser.objects.create_user( + self.test_provider_email, self.test_provider_password + ) + + assign_role(normal_user, "user") + assign_role(provider_user, "provider_manager") + + response = self.client.post( + "/api/v1/auth/token", + {"email": self.test_user_email, "password": self.test_user_password}, + ) # obtain token + self.assertEqual(response.status_code, 200) + self.assertTrue("access" in response.data) + user_token = response.data["access"] + + response = self.client.post( + "/api/v1/auth/token", + { + "email": self.test_provider_email, + "password": self.test_provider_password, + }, + ) # obtain token + self.assertEqual(response.status_code, 200) + self.assertTrue("access" in response.data) + provider_token = response.data["access"] + + self.user_client = APIClient() + self.user_client.credentials(HTTP_AUTHORIZATION="Bearer " + user_token) + + self.provider_client = APIClient() + self.provider_client.credentials(HTTP_AUTHORIZATION="Bearer " + provider_token) + + # Create provider schema + with StringIO(json.dumps(self.h5p_provider_schema)) as fp: + response = self.provider_client.put( + "/api/v1/consents/provider/create", + {"provider-schema": fp}, + format="multipart", + ) + self.assertEqual(response.status_code, 201) + + # Get application token for created provider + self.application_token = ProviderAuthorization.objects.get( + provider__name="H5P" + ).key + self.third_party_client = APIClient() + self.third_party_client.credentials( + HTTP_AUTHORIZATION="Basic " + self.application_token + ) + + # create some verb groups + for group in self.verb_groups: + response = self.provider_client.post( + "/api/v1/consents/provider/" + str(Provider.objects.latest('id').id) + "/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + + + def test_get_verb_groups(self): + """ + Ensure correct verb groups are returned for a given provider. + """ + provider_id = Provider.objects.latest('id').id + # create a verb group + group = {"id": "default_group", "label": "Group 2", + "description": "Lorem ipsum", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/attempted"}, + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"}, + ]} + response = self.provider_client.post( + f"/api/v1/consents/provider/{provider_id}/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + + # test permissions + response = self.user_client.get( + f"/api/v1/consents/provider/{provider_id}/verb-groups" + ) + self.assertEqual(response.status_code, 403) + + # check if group exists + response = self.provider_client.get( + f"/api/v1/consents/provider/{provider_id}/verb-groups" + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertTrue(isinstance(data, list)) + self.assertEqual(len(data), 2) # another group is created during setup + self.assertEqual(data[-1]["purposeOfCollection"], "Lorem Ipsum") + self.assertTrue(isinstance(data[-1]["verbs"], list)) + self.assertEqual(data[-1]["verbs"][0]["id"], "http://h5p.example.com/expapi/verbs/experienced") + + def test_create_invalid_verb_group(self): + provider_id = Provider.objects.latest('id').id + group = {"id": "default_group", "label": "Group 2", + "description": "Lorem ipsum", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/atztempted"}, # mistyped on purpose + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"}, + ]} + response = self.provider_client.post( + f"/api/v1/consents/provider/{provider_id}/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 400) + + + def test_update_existing_verb_group(self): + provider_id = Provider.objects.latest('id').id + group = {"id": "default_group", "label": "Group 2", + "description": "Lorem ipsum", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"}, + ]} + response = self.provider_client.post( + f"/api/v1/consents/provider/{provider_id}/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + + group_id = ProviderVerbGroup.objects.latest('group_id').group_id # generated on server, has to be supplied for update + + group = {"id": group_id, "label": "Group 2", # id has to be the same and is generated when creating the group + "description": "Lorem ipsum!", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum!", # changed + "requiresConsent": True, "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/attempted"}, # added + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"}, + ]} + response = self.provider_client.post( + f"/api/v1/consents/provider/{provider_id}/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + + groups = ProviderVerbGroup.objects.filter(provider_id=provider_id).all() + + self.assertEqual(len(groups), 2) \ No newline at end of file diff --git a/src/consents/tests/tests_third_party.py b/src/consents/tests/tests_third_party.py index a1aae6a90770af88a329196e4a36cda57fd7aaa1..547841e4bf294e8a587f9f4ef630e5f7eaaf604d 100644 --- a/src/consents/tests/tests_third_party.py +++ b/src/consents/tests/tests_third_party.py @@ -303,3 +303,272 @@ class TestThirdPartyUserConsentUpdate(BaseTestCase): "/api/v1/consents/user/save/third-party", data=payload, format="json" ) self.assertEqual(response.status_code, 400) + + + + +class TestThirdPartyGetProviderStatus(BaseTestCase): + verb_groups = [ + { + "provider_id": 1, + "id": "default_group", + "label": "Default group", + "description": "default", + "showVerbDetails": True, + "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, + "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/attempted"}, + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"} + ] + }, + ] + def setUp(self): + call_command('check_and_apply_migrations') + normal_user = CustomUser.objects.create_user( + self.test_user_email, self.test_user_password + ) + provider_user = CustomUser.objects.create_user( + self.test_provider_email, self.test_provider_password + ) + + assign_role(normal_user, "user") + assign_role(provider_user, "provider_manager") + + response = self.client.post( + "/api/v1/auth/token", + {"email": self.test_user_email, "password": self.test_user_password}, + ) # obtain token + self.assertEqual(response.status_code, 200) + self.assertTrue("access" in response.data) + user_token = response.data["access"] + + response = self.client.post( + "/api/v1/auth/token", + { + "email": self.test_provider_email, + "password": self.test_provider_password, + }, + ) # obtain token + self.assertEqual(response.status_code, 200) + self.assertTrue("access" in response.data) + provider_token = response.data["access"] + + self.user_client = APIClient() + self.user_client.credentials(HTTP_AUTHORIZATION="Bearer " + user_token) + + self.provider_client = APIClient() + self.provider_client.credentials(HTTP_AUTHORIZATION="Bearer " + provider_token) + + # Create provider schema + with StringIO(json.dumps(self.h5p_provider_schema)) as fp: + response = self.provider_client.put( + "/api/v1/consents/provider/create", + {"provider-schema": fp}, + format="multipart", + ) + self.assertEqual(response.status_code, 201) + + # Get application token for created provider + self.application_token = ProviderAuthorization.objects.get( + provider__name="H5P" + ).key + self.third_party_client = APIClient() + self.third_party_client.credentials( + HTTP_AUTHORIZATION="Basic " + self.application_token + ) + + # create some verb groups + for group in self.verb_groups: + response = self.provider_client.post( + "/api/v1/consents/provider/" + str(Provider.objects.latest('id').id) + "/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + + def test_provider_status_401(self): + """ + Ensure requests including an invalid application token are rejected. + """ + response = self.user_client.get( + "/api/v1/consents/provider-status/third-party" + ) + self.assertEqual(response.status_code, 401) + + def test_provider_status(self): + response = self.third_party_client.get( + "/api/v1/consents/provider-status/third-party" + ) + self.assertEqual(response.status_code, 200) + + data = json.loads(response.content.decode()) + self.assertTrue(all(key in data.keys() for key in ["id", "name", "groups", "versions"])) + self.assertEqual(data["name"], "H5P") + +class TestThirdPartyUserManagement(BaseTestCase): + verb_groups = [ + { + "provider_id": 1, + "id": "default_group", + "label": "Default group", + "description": "default", + "showVerbDetails": True, + "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, + "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/attempted"}, + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"} + ] + }, + ] + + def setUp(self): + call_command('check_and_apply_migrations') + normal_user = CustomUser.objects.create_user( + self.test_user_email, self.test_user_password + ) + provider_user = CustomUser.objects.create_user( + self.test_provider_email, self.test_provider_password + ) + + assign_role(normal_user, "user") + assign_role(provider_user, "provider_manager") + + response = self.client.post( + "/api/v1/auth/token", + {"email": self.test_user_email, "password": self.test_user_password}, + ) # obtain token + self.assertEqual(response.status_code, 200) + self.assertTrue("access" in response.data) + user_token = response.data["access"] + + response = self.client.post( + "/api/v1/auth/token", + { + "email": self.test_provider_email, + "password": self.test_provider_password, + }, + ) # obtain token + self.assertEqual(response.status_code, 200) + self.assertTrue("access" in response.data) + provider_token = response.data["access"] + + self.user_client = APIClient() + self.user_client.credentials(HTTP_AUTHORIZATION="Bearer " + user_token) + + self.provider_client = APIClient() + self.provider_client.credentials(HTTP_AUTHORIZATION="Bearer " + provider_token) + + # Create provider schema + with StringIO(json.dumps(self.h5p_provider_schema)) as fp: + response = self.provider_client.put( + "/api/v1/consents/provider/create", + {"provider-schema": fp}, + format="multipart", + ) + self.assertEqual(response.status_code, 201) + + # Get application token for created provider + self.application_token = ProviderAuthorization.objects.get( + provider__name="H5P" + ).key + self.third_party_client = APIClient() + self.third_party_client.credentials( + HTTP_AUTHORIZATION="Basic " + self.application_token + ) + + # create some verb groups + for group in self.verb_groups: + response = self.provider_client.post( + "/api/v1/consents/provider/" + str(Provider.objects.latest('id').id) + "/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + + + def test_create_user(self): + # test permission check + response = self.user_client.post( + "/api/v1/consents/user/create", + {} + ) + self.assertEqual(response.status_code, 401) + + # check validation + response = self.third_party_client.post( + "/api/v1/consents/user/create", + {} + ) + self.assertEqual(response.status_code, 400) + + response = self.third_party_client.post( + "/api/v1/consents/user/create", + { + "email": "test@polaris.com", + "first_name": "Max", + "last_name": "Mustermann" + } + ) + self.assertEqual(response.status_code, 200) + + count_before = CustomUser.objects.count() + 0 + + # try to create with same email again + response = self.third_party_client.post( + "/api/v1/consents/user/create", + { + "email": "test@polaris.com", + "first_name": "Max", + "last_name": "Mustermann" + } + ) + self.assertEqual(response.status_code, 200) + # no new user should have been created + self.assertEqual(CustomUser.objects.count(), count_before) + + def test_create_user_via_connect_service(self): + # test permission check + response = self.user_client.post( + "/api/v1/consents/user/create-via-connect-service", + {} + ) + self.assertEqual(response.status_code, 401) + + # check validation + response = self.third_party_client.post( + "/api/v1/consents/user/create-via-connect-service", + {} + ) + self.assertEqual(response.status_code, 400) + + # create user + response = self.third_party_client.post( + "/api/v1/consents/user/create-via-connect-service", + { + "email": "test@polaris.com", + } + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertEqual(data["message"], "user has been created") + + count_before = CustomUser.objects.count() + 0 + + # try to create with same email again + response = self.third_party_client.post( + "/api/v1/consents/user/create-via-connect-service", + { + "email": "test@polaris.com", + } + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertEqual(data["message"], "user exists") + # no new user should have been created + self.assertEqual(CustomUser.objects.count(), count_before) \ No newline at end of file diff --git a/src/consents/tests/tests_user_endpoints.py b/src/consents/tests/tests_user_endpoints.py new file mode 100644 index 0000000000000000000000000000000000000000..6c9f17584ede554104b1e5dc26aa11190f8d5521 --- /dev/null +++ b/src/consents/tests/tests_user_endpoints.py @@ -0,0 +1,327 @@ +import json +import os +from io import StringIO + +from rest_framework.test import APIClient +from rolepermissions.roles import assign_role +from django.core.management import call_command + +from consents.tests.tests_consent_operations import BaseTestCase +from providers.models import ProviderAuthorization, Provider, ProviderVerbGroup, ProviderSchema +from users.models import CustomUser + +from consents.models import UserConsents + + +class TestUserEndpoints(BaseTestCase): + verb_groups = [ + { + "provider_id": 1, + "id": "default_group", + "label": "Default group", + "description": "default", + "showVerbDetails": True, + "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, + "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/attempted"}, + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"} + ] + }, + ] + + def setUp(self): + call_command('check_and_apply_migrations') + normal_user = CustomUser.objects.create_user( + self.test_user_email, self.test_user_password + ) + provider_user = CustomUser.objects.create_user( + self.test_provider_email, self.test_provider_password + ) + + assign_role(normal_user, "user") + assign_role(provider_user, "provider_manager") + assign_role(provider_user, 'analyst') # needed to create analytics tokens later on + + response = self.client.post( + "/api/v1/auth/token", + {"email": self.test_user_email, "password": self.test_user_password}, + ) # obtain token + self.assertEqual(response.status_code, 200) + self.assertTrue("access" in response.data) + user_token = response.data["access"] + + response = self.client.post( + "/api/v1/auth/token", + { + "email": self.test_provider_email, + "password": self.test_provider_password, + }, + ) # obtain token + self.assertEqual(response.status_code, 200) + self.assertTrue("access" in response.data) + provider_token = response.data["access"] + + self.user_client = APIClient() + self.user_client.credentials(HTTP_AUTHORIZATION="Bearer " + user_token) + + self.provider_client = APIClient() + self.provider_client.credentials(HTTP_AUTHORIZATION="Bearer " + provider_token) + + # Create provider schema + with StringIO(json.dumps(self.h5p_provider_schema)) as fp: + response = self.provider_client.put( + "/api/v1/consents/provider/create", + {"provider-schema": fp}, + format="multipart", + ) + self.assertEqual(response.status_code, 201) + + # Get application token for created provider + self.application_token = ProviderAuthorization.objects.get( + provider__name="H5P" + ).key + self.third_party_client = APIClient() + self.third_party_client.credentials( + HTTP_AUTHORIZATION="Basic " + self.application_token + ) + + # create some verb groups + for group in self.verb_groups: + response = self.provider_client.post( + "/api/v1/consents/provider/" + str(Provider.objects.latest('id').id) + "/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + + def test_get_consent_for_provider(self): + # Note: this route is currently not being used in the frontend since the consent history replaces most of the functionality + provider_id = Provider.objects.latest('id').id + + response = self.user_client.get( + f"/api/v1/consents/user/{provider_id}", + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + + # right now we haven't declared consent for any groups + self.assertEqual(data["consent"], None) + + # create consent + group_id = ProviderVerbGroup.objects.latest('id').id # maybe todo: rework frontend to use the "group_id" attribute instead of primary keys + response = self.user_client.post( + "/api/v1/consents/user/save", {"groups": [group_id]}, format="json" + ) + self.assertEqual(response.status_code, 200) + + # now we would expect the consent to be present in the response + response = self.user_client.get( + f"/api/v1/consents/user/{provider_id}", + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + + self.assertTrue(all(key in data.keys() for key in ["consent", "provider_schema", "paused_data_recording"])) + self.assertFalse(data["consent"] is None) + + def test_get_consent_history(self): + provider_id = Provider.objects.latest('id').id + + # consent history should be empty + response = self.user_client.get( + "/api/v1/consents/user/history", + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertTrue("groups" in data.keys()) + self.assertEqual(len(data["groups"]), 0) + + # create consent + group_id = ProviderVerbGroup.objects.latest('id').id + response = self.user_client.post( + "/api/v1/consents/user/save", {"groups": [group_id]}, format="json" + ) + self.assertEqual(response.status_code, 200) + + response = self.user_client.get( + "/api/v1/consents/user/history", + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertTrue("groups" in data.keys()) + self.assertEqual(len(data["groups"]), 1) + + # create another group + group = {"id": "default_group", "label": "Group 2", + "description": "Lorem ipsum", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"}, + ]} + response = self.provider_client.post( + f"/api/v1/consents/provider/{provider_id}/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + + # create consent for second group + group_id = ProviderVerbGroup.objects.latest('id').id + response = self.user_client.post( + "/api/v1/consents/user/save", {"groups": [group_id]}, format="json" + ) + self.assertEqual(response.status_code, 200) + + # check that both consents are present in history + response = self.user_client.get( + "/api/v1/consents/user/history", + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertTrue("groups" in data.keys()) + self.assertEqual(len(data["groups"]), 2) + + def test_update_consent_group_active(self): + provider_id = Provider.objects.latest('id').id + + response = self.user_client.get( + f"/api/v1/consents/user/{provider_id}", + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + + # right now we haven't declared consent for any groups + self.assertEqual(data["consent"], None) + + # create consent + group_id = ProviderVerbGroup.objects.latest('id').id + response = self.user_client.post( + "/api/v1/consents/user/save", {"groups": [group_id]}, format="json" + ) + self.assertEqual(response.status_code, 200) + + # check if incorrect request is rejected + response = self.user_client.post( + "/api/v1/consents/user/update-consent-group-active", {}, format="json" + ) + self.assertEqual(response.status_code, 400) + + # update group consent + response = self.user_client.post( + "/api/v1/consents/user/update-consent-group-active", {"id": group_id, "active": False}, format="json" + ) + self.assertEqual(response.status_code, 200) + # check if status correctly stored in DB + self.assertFalse(UserConsents.objects.last().active) + + def test_revoke_consent_group(self): + provider_id = Provider.objects.latest('id').id + + response = self.user_client.get( + f"/api/v1/consents/user/{provider_id}", + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + + # right now we haven't declared consent for any groups + self.assertEqual(data["consent"], None) + + # create consent + group_id = ProviderVerbGroup.objects.latest('id').id + response = self.user_client.post( + "/api/v1/consents/user/save", {"groups": [group_id]}, format="json" + ) + self.assertEqual(response.status_code, 200) + + # check if incorrect request is rejected + response = self.user_client.post( + "/api/v1/consents/user/revoke-consent-group", {}, format="json" + ) + self.assertEqual(response.status_code, 400) + + # update group consent + response = self.user_client.post( + "/api/v1/consents/user/revoke-consent-group", {"id": group_id}, format="json" + ) + self.assertEqual(response.status_code, 200) + # check if consent correctly updated in DB + self.assertFalse(UserConsents.objects.last().consented) + + def test_get_analytics_tokens(self): + response = self.provider_client.get( + f"/api/v1/consents/user/analytics-tokens", + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertEqual(len(data), 0) + + response = self.provider_client.post( + f"/api/v1/provider/analytics-tokens/create", + { + "name": "Test analytics token", + "description": "Test analytics token", + "expires": "12/31/2099", + "can_access": [] + } + ) + self.assertEqual(response.status_code, 201) + + response = self.provider_client.get( + f"/api/v1/consents/user/analytics-tokens", + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertEqual(len(data), 1) + + def test_get_analytics_token_consent(self): + response = self.provider_client.get( + f"/api/v1/consents/user/analytics-tokens", + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertEqual(len(data), 0) + + response = self.provider_client.post( + f"/api/v1/provider/analytics-tokens/create", + { + "name": "Test analytics token", + "description": "Test analytics token", + "expires": "12/31/2099", + "can_access": [] + } + ) + self.assertEqual(response.status_code, 201) + data = json.loads(response.content.decode()) + analyticTokenId = data["id"] + + response = self.user_client.post( + f"/api/v1/consents/user/analytics-tokens/consent", + { + "analyticTokenId": analyticTokenId + } + ) + self.assertEqual(response.status_code, 200) + # maybe todo: check if consents created correctly (?) + + def test_get_providers(self): + response = self.user_client.get( + f"/api/v1/consents/user/providers", + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertEqual(len(data), 1) + + + def test_get_status(self): + response = self.user_client.get( + f"/api/v1/consents/user/status", + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertTrue("result" in data.keys()) + self.assertTrue("1" in data["result"].keys()) + self.assertEqual(data["result"]["1"]["status"], "none") # since consent is coupled with groups, consent for a whole provider schema can not exist \ No newline at end of file diff --git a/src/consents/urls.py b/src/consents/urls.py index a1e4f71d98a566c174f130c13187c3c8dc5e86d1..fc0ea1c0ea78a2ea882a34271b2b5304165fb3a9 100644 --- a/src/consents/urls.py +++ b/src/consents/urls.py @@ -6,8 +6,8 @@ urlpatterns = [ path('provider', views.GetProviderSchemasView.as_view()), path('provider-status/third-party', views.GetProviderStatusThirdPartyView.as_view()), path('provider/create', views.CreateProviderConsentView.as_view()), - path('provider/<provider_id>/create-verb-group', views.CreateProviderVerbGroupView.as_view()), # TODO - path('provider/<provider_id>/verb-groups', views.GetProviderVerbGroupsView.as_view()), # TODO + path('provider/<provider_id>/create-verb-group', views.CreateProviderVerbGroupView.as_view()), + path('provider/<provider_id>/verb-groups', views.GetProviderVerbGroupsView.as_view()), path('user/save', views.SaveUserConsentView.as_view()), path('user/create', views.CreateUserConsentView.as_view()), path('user/create-via-connect-service', views.CreateUserConsentViaConnectServiceView.as_view()), diff --git a/src/consents/views.py b/src/consents/views.py index 6e140bcbf60fef60cb64c7406be5380ce33cf0b8..b6e570344a4cf7e9b441e70aadd37500ddd41905 100644 --- a/src/consents/views.py +++ b/src/consents/views.py @@ -199,14 +199,16 @@ class GetProviderVerbGroupsView(generics.ListAPIView): """ Lists all verb groups for the current provider. """ - def get(self, request): - application_token = request.headers.get("Authorization", "").split("Basic ")[-1] - provider = ProviderAuthorization.objects.filter(key=application_token).first() + permission_classes = [IsAuthenticated, IsProvider] + + def get(self, request, provider_id): + + provider = ProviderAuthorization.objects.filter(pk=provider_id).first() if provider is None: return JsonResponse( - {"message": "invalid access token"}, + {"message": "invalid provider supplied"}, safe=False, - status=status.HTTP_401_UNAUTHORIZED, + status=status.HTTP_404_NOT_FOUND, ) verb_groups = ProviderVerbGroup.objects.filter(provider=provider.provider).all() serializer = ProviderVerbGroupSerializer(verb_groups, many=True) @@ -487,7 +489,7 @@ class GetUserConsentHistoryView(APIView): if not user_consents.first(): return JsonResponse( { - "consents": [], + "groups": [], }, safe=False, status=status.HTTP_200_OK, @@ -504,6 +506,7 @@ class GetUserConsentHistoryView(APIView): except ObjectDoesNotExist: return JsonResponse( { + "groups": [], "message": "User has no consent declaration record.", "no_consent_record": True, }, @@ -804,23 +807,9 @@ class GetUsersConsentsThirdPartyView(APIView): status=status.HTTP_200_OK, ) else: - accepted_provider_schemas = user.accepted_provider_schemas.all() - provider_schemas = list( - filter( - lambda schema: schema.provider.id == int(provider.provider.id), - accepted_provider_schemas, - ) - ) - - provider_schema = ( - ProviderSchemaSerializer(provider_schemas, many=True).data[0] - if len(provider_schemas) > 0 - else None - ) return JsonResponse( { "consent": user_consent, - "provider_schema": provider_schema, "paused_data_recording": user.paused_data_recording, }, safe=False, diff --git a/src/data_disclosure/tests.py b/src/data_disclosure/tests.py deleted file mode 100644 index 7ce503c2dd97ba78597f6ff6e4393132753573f6..0000000000000000000000000000000000000000 --- a/src/data_disclosure/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/src/data_disclosure/tests/__init__.py b/src/data_disclosure/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/data_disclosure/tests/tests_data_disclosure.py b/src/data_disclosure/tests/tests_data_disclosure.py new file mode 100644 index 0000000000000000000000000000000000000000..b32ca89eca277e0e4bce298b4476f6d8e7300dc8 --- /dev/null +++ b/src/data_disclosure/tests/tests_data_disclosure.py @@ -0,0 +1,266 @@ +import json +import os +from io import StringIO +from unittest.mock import patch + +from django.conf import settings +from django.core.files.base import ContentFile +from django.core.files.storage import default_storage +from rest_framework.test import APIClient +from rolepermissions.roles import assign_role +from django.core.management import call_command + +from consents.tests.tests_consent_operations import BaseTestCase +from providers.models import ProviderAuthorization, Provider, ProviderVerbGroup, ProviderSchema +from users.models import CustomUser + +from data_disclosure.models import DataDisclosure + + + +class TestsDataDisclosure(BaseTestCase): + verb_groups = [ + { + "provider_id": 1, + "id": "default_group", + "label": "Default group", + "description": "default", + "showVerbDetails": True, + "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, + "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/attempted"}, + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"} + ] + }, + ] + + def setUp(self): + call_command('check_and_apply_migrations') + normal_user = CustomUser.objects.create_user( + self.test_user_email, self.test_user_password + ) + provider_user = CustomUser.objects.create_user( + self.test_provider_email, self.test_provider_password + ) + + assign_role(normal_user, "user") + assign_role(provider_user, "provider_manager") + + response = self.client.post( + "/api/v1/auth/token", + {"email": self.test_user_email, "password": self.test_user_password}, + ) # obtain token + self.assertEqual(response.status_code, 200) + self.assertTrue("access" in response.data) + user_token = response.data["access"] + + response = self.client.post( + "/api/v1/auth/token", + { + "email": self.test_provider_email, + "password": self.test_provider_password, + }, + ) # obtain token + self.assertEqual(response.status_code, 200) + self.assertTrue("access" in response.data) + provider_token = response.data["access"] + + self.user_client = APIClient() + self.user_client.credentials(HTTP_AUTHORIZATION="Bearer " + user_token) + + self.provider_client = APIClient() + self.provider_client.credentials(HTTP_AUTHORIZATION="Bearer " + provider_token) + + # Create provider schema + with StringIO(json.dumps(self.h5p_provider_schema)) as fp: + response = self.provider_client.put( + "/api/v1/consents/provider/create", + {"provider-schema": fp}, + format="multipart", + ) + self.assertEqual(response.status_code, 201) + + # Get application token for created provider + self.application_token = ProviderAuthorization.objects.get( + provider__name="H5P" + ).key + self.third_party_client = APIClient() + self.third_party_client.credentials( + HTTP_AUTHORIZATION="Basic " + self.application_token + ) + + # create some verb groups + for group in self.verb_groups: + response = self.provider_client.post( + "/api/v1/consents/provider/" + str(Provider.objects.latest('id').id) + "/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + + @patch('celery.app.task.Task.apply_async') + def test_create_job(self, celery_mock): + self.assertEqual(DataDisclosure.objects.count(), 0) + + response = self.user_client.post("/api/v1/data-disclosure/create") + self.assertEqual(response.status_code, 200) + + self.assertEqual(DataDisclosure.objects.count(), 1) + self.assertTrue(celery_mock.called) + + + @patch('celery.app.task.Task.apply_async') + def test_list_jobs(self, celery_mock): + response = self.user_client.get("/api/v1/data-disclosure/list") + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertEqual(len(data), 0) + + response = self.user_client.post("/api/v1/data-disclosure/create") + self.assertEqual(response.status_code, 200) + self.assertTrue(celery_mock.called) + + response = self.user_client.get("/api/v1/data-disclosure/list") + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertEqual(len(data), 1) + + @patch('celery.app.task.Task.apply_async') + def test_get_secret(self, celery_mock): + response = self.user_client.get("/api/v1/data-disclosure/list") + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertEqual(len(data), 0) + + response = self.user_client.post("/api/v1/data-disclosure/create") + self.assertEqual(response.status_code, 200) + self.assertTrue(celery_mock.called) + + # create second job from another account + response = self.provider_client.post("/api/v1/data-disclosure/create") + self.assertEqual(response.status_code, 200) + + response = self.user_client.get("/api/v1/data-disclosure/list") + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertEqual(len(data), 1) + + # check if we can get the key + response = self.user_client.get(f"/api/v1/data-disclosure/file_secret/{data[0]['id']}") + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertTrue("secret" in data.keys()) + + # get ID for other account to test permission check + response = self.provider_client.get("/api/v1/data-disclosure/list") + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertEqual(len(data), 1) + + response = self.user_client.get(f"/api/v1/data-disclosure/file_secret/{data[0]['id']}") # note the "user_client" + self.assertEqual(response.status_code, 403) + + @patch('celery.app.task.Task.apply_async') + def test_get_file(self, celery_mock): + response = self.user_client.get("/api/v1/data-disclosure/list") + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertEqual(len(data), 0) + + response = self.user_client.post("/api/v1/data-disclosure/create") + self.assertEqual(response.status_code, 200) + self.assertTrue(celery_mock.called) + + response = self.user_client.get("/api/v1/data-disclosure/list") + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertEqual(len(data), 1) + + file_id = data[0]["id"] + + # get the key + response = self.user_client.get(f"/api/v1/data-disclosure/file_secret/{file_id}") + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertTrue("secret" in data.keys()) + + secret = data["secret"] + + # create zip file to fulfil requirements + data_disclosure_location = settings.DATA_DISCLOSURE_LOCATION + if not data_disclosure_location or data_disclosure_location is None or data_disclosure_location == "": + data_disclosure_location = "./data_disclosure_zips" + empty_zip_data = ContentFile(b'PK\x05\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') + file_name = default_storage.save( + f"{data_disclosure_location}/{file_id}.zip", empty_zip_data + ) + + disc = DataDisclosure.objects.get(id=file_id) + disc.filename = file_name + disc.save() + + # try with wrong secret + response = self.user_client.get(f"/api/v1/data-disclosure/files/{file_id}/somerandomstring") + self.assertEqual(response.status_code, 403) + + + response = self.user_client.get(f"/api/v1/data-disclosure/files/{file_id}/{secret}") + self.assertEqual(response.status_code, 200) + + @patch('celery.app.task.Task.apply_async') + def test_delete_file(self, celery_mock): + response = self.user_client.get("/api/v1/data-disclosure/list") + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertEqual(len(data), 0) + + response = self.user_client.post("/api/v1/data-disclosure/create") + self.assertEqual(response.status_code, 200) + self.assertTrue(celery_mock.called) + + response = self.user_client.get("/api/v1/data-disclosure/list") + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertEqual(len(data), 1) + + file_id = data[0]["id"] + + # get the key + response = self.user_client.get(f"/api/v1/data-disclosure/file_secret/{file_id}") + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertTrue("secret" in data.keys()) + + secret = data["secret"] + + # try with wrong secret + response = self.user_client.get(f"/api/v1/data-disclosure/files/{file_id}/somerandomstring") + self.assertEqual(response.status_code, 403) + + # create zip file to fulfil requirements + data_disclosure_location = settings.DATA_DISCLOSURE_LOCATION + if not data_disclosure_location or data_disclosure_location is None or data_disclosure_location == "": + data_disclosure_location = "./data_disclosure_zips" + empty_zip_data = ContentFile(b'PK\x05\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') + file_name = default_storage.save( + f"{data_disclosure_location}/{file_id}.zip", empty_zip_data + ) + + disc = DataDisclosure.objects.get(id=file_id) + disc.filename = file_name + disc.save() + + response = self.user_client.get(f"/api/v1/data-disclosure/files/{file_id}/{secret}") + self.assertEqual(response.status_code, 200) + + # try wrong account for deletion + response = self.provider_client.delete(f"/api/v1/data-disclosure/{file_id}/delete") + self.assertEqual(response.status_code, 403) + + response = self.user_client.delete(f"/api/v1/data-disclosure/{file_id}/delete") + self.assertEqual(response.status_code, 200) + + self.assertFalse(default_storage.exists(file_name)) \ No newline at end of file diff --git a/src/data_removal/tests.py b/src/data_removal/tests.py deleted file mode 100644 index 7ce503c2dd97ba78597f6ff6e4393132753573f6..0000000000000000000000000000000000000000 --- a/src/data_removal/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/src/data_removal/tests/__init__.py b/src/data_removal/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/data_removal/tests/tests_data_removal.py b/src/data_removal/tests/tests_data_removal.py new file mode 100644 index 0000000000000000000000000000000000000000..1774e7e6ce1a09a55382eec16047a56b4371ca80 --- /dev/null +++ b/src/data_removal/tests/tests_data_removal.py @@ -0,0 +1,130 @@ +import json +import os +from io import StringIO + +from django.conf import settings +from django.core.files.base import ContentFile +from django.core.files.storage import default_storage +from rest_framework.test import APIClient +from rolepermissions.roles import assign_role +from django.core.management import call_command + +from consents.tests.tests_consent_operations import BaseTestCase +from providers.models import ProviderAuthorization, Provider, ProviderVerbGroup, ProviderSchema +from users.models import CustomUser + +from data_removal.models import DataRemovalJob + + +class TestsDataRemoval(BaseTestCase): + verb_groups = [ + { + "provider_id": 1, + "id": "default_group", + "label": "Default group", + "description": "default", + "showVerbDetails": True, + "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, + "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/attempted"}, + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"} + ] + }, + ] + + def setUp(self): + call_command('check_and_apply_migrations') + normal_user = CustomUser.objects.create_user( + self.test_user_email, self.test_user_password + ) + provider_user = CustomUser.objects.create_user( + self.test_provider_email, self.test_provider_password + ) + + assign_role(normal_user, "user") + assign_role(provider_user, "provider_manager") + + response = self.client.post( + "/api/v1/auth/token", + {"email": self.test_user_email, "password": self.test_user_password}, + ) # obtain token + self.assertEqual(response.status_code, 200) + self.assertTrue("access" in response.data) + user_token = response.data["access"] + + response = self.client.post( + "/api/v1/auth/token", + { + "email": self.test_provider_email, + "password": self.test_provider_password, + }, + ) # obtain token + self.assertEqual(response.status_code, 200) + self.assertTrue("access" in response.data) + provider_token = response.data["access"] + + self.user_client = APIClient() + self.user_client.credentials(HTTP_AUTHORIZATION="Bearer " + user_token) + + self.provider_client = APIClient() + self.provider_client.credentials(HTTP_AUTHORIZATION="Bearer " + provider_token) + + # Create provider schema + with StringIO(json.dumps(self.h5p_provider_schema)) as fp: + response = self.provider_client.put( + "/api/v1/consents/provider/create", + {"provider-schema": fp}, + format="multipart", + ) + self.assertEqual(response.status_code, 201) + + # Get application token for created provider + self.application_token = ProviderAuthorization.objects.get( + provider__name="H5P" + ).key + self.third_party_client = APIClient() + self.third_party_client.credentials( + HTTP_AUTHORIZATION="Basic " + self.application_token + ) + + # create some verb groups + for group in self.verb_groups: + response = self.provider_client.post( + "/api/v1/consents/provider/" + str(Provider.objects.latest('id').id) + "/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + + + def test_create_job(self): + self.assertEqual(DataRemovalJob.objects.count(), 0) + + response = self.user_client.post("/api/v1/data-removal/create", { + "scope": {}, + "immediately": True, + }, format="json") + self.assertEqual(response.status_code, 200) + + self.assertEqual(DataRemovalJob.objects.count(), 1) + + + def test_list_jobs(self): + response = self.user_client.get("/api/v1/data-removal/list", ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertEqual(len(data), 0) + + response = self.user_client.post("/api/v1/data-removal/create", { + "scope": {}, + "immediately": True, + }, format="json") + self.assertEqual(response.status_code, 200) + + response = self.user_client.get("/api/v1/data-removal/list",) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertEqual(len(data), 1) \ No newline at end of file diff --git a/src/data_removal/views.py b/src/data_removal/views.py index dd3d7f0c6518b91d3425f9b4eda9ee44b222d6b1..a84e922d7cacb577817536ef90ff383dd4cb6ceb 100644 --- a/src/data_removal/views.py +++ b/src/data_removal/views.py @@ -61,7 +61,7 @@ class CreateDataRemovalJob(APIView): [statement_filter, description] = get_scope_filter( request.user.id, request.data["scope"] ) - if request.data["immedialty"]: + if request.data["immediately"]: DataRemovalJob.objects.create( user=request.user, filter=statement_filter, diff --git a/src/entrypoint.sh b/src/entrypoint.sh index 1fd51c861b44f140e6db0c1c4a140e1e4eefaf2b..47930e539a5f6147e743816c04b22d208148390b 100644 --- a/src/entrypoint.sh +++ b/src/entrypoint.sh @@ -1,7 +1,22 @@ #!/bin/sh +# Set default number of workers to 4 if not provided via environment +: "${WORKERS:=4}" + echo "Running database migrations and role setup..." python manage.py check_and_apply_migrations -echo "Starting Gunicorn server..." -exec env "$@" gunicorn --bind :80 --workers 3 --timeout 240 --access-logfile - --error-logfile - backend.wsgi +echo "Running mongodb check..." +python manage.py create_mongo_index + +echo "Starting Gunicorn server with $WORKERS workers..." +exec env "$@" gunicorn \ + --bind :80 \ + --workers "$WORKERS" \ + --timeout 300 \ + --graceful-timeout 300 \ + --max-requests 1000 \ + --max-requests-jitter 50 \ + --access-logfile - \ + --error-logfile - \ + backend.wsgi diff --git a/src/frontend/src/app/data-removal.service.ts b/src/frontend/src/app/data-removal.service.ts index cd518c62b42f0011e461afd8d653809ddf1228d7..650f6b7e53cdda82bbcf4b97290bafd6532ca137 100644 --- a/src/frontend/src/app/data-removal.service.ts +++ b/src/frontend/src/app/data-removal.service.ts @@ -35,7 +35,7 @@ export class DataRemovalService { scope: DeleteAllUserStatements | DeleteVerbStatements | DeleteObjectStatements }): Observable<void> { return this.http.post<void>(`${environment.apiUrl}/api/v1/data-removal/create`, { - immedialty: data.immediately, + immediately: data.immediately, scope: data.scope }) } diff --git a/src/providers/models.py b/src/providers/models.py index ab80e24e68d4313b44985a30038c9ec1695373ba..c7f7b126b995c87ba76b30b25a914d8ced79ff87 100644 --- a/src/providers/models.py +++ b/src/providers/models.py @@ -71,8 +71,10 @@ class ProviderVerbGroup(models.Model): def __str__(self): return (f"Provider Verb Group {self.id}:\n" + f"- label {self.label}\n" f"- contains {len(self.verbs.all())} verbs\n" f"- group ID: {self.group_id}\n" + f"- provider ID: {self.provider_id}\n" f"- provider Schema ID: {self.provider_schema_id}\n") diff --git a/src/providers/tests/__init__.py b/src/providers/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/providers/tests/tests_analytics_tokens.py b/src/providers/tests/tests_analytics_tokens.py new file mode 100644 index 0000000000000000000000000000000000000000..3c011d702c14b8792c000a84148aa848ad97cbb6 --- /dev/null +++ b/src/providers/tests/tests_analytics_tokens.py @@ -0,0 +1,241 @@ +import json +import os + +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import TestCase +from rest_framework.test import APIClient +from rolepermissions.roles import assign_role +from django.core.management import call_command + +from users.models import CustomUser + +from providers.models import Provider, ProviderAuthorization, AnalyticsToken + +PROJECT_PATH = os.path.abspath(os.path.dirname(__name__)) + +class ProviderTestCase(TestCase): + + test_user_email = "test@mail.com" + test_user_password = "test123" + + test_provider_email = "test2@mail.com" + test_provider_password = "test123" + + def setUp(self): + call_command('check_and_apply_migrations') + normal_user = CustomUser.objects.create_user(self.test_user_email, self.test_user_password) + provider_user = CustomUser.objects.create_user(self.test_provider_email, self.test_provider_password) + + assign_role(normal_user, 'user') + assign_role(provider_user, 'provider_manager') + assign_role(provider_user, 'analyst') # needed to create analytics tokens + + response=self.client.post('/api/v1/auth/token', {"email": self.test_user_email, "password": self.test_user_password}) # obtain token + self.assertEqual(response.status_code, 200) + self.assertTrue('access' in response.data) + user_token = response.data['access'] + + response=self.client.post('/api/v1/auth/token', {"email": self.test_provider_email, "password": self.test_provider_password}) # obtain token + self.assertEqual(response.status_code, 200) + self.assertTrue('access' in response.data) + provider_token = response.data['access'] + + self.user_client = APIClient() + self.user_client.credentials(HTTP_AUTHORIZATION='Bearer ' + user_token) + + self.provider_client = APIClient() + self.provider_client.credentials(HTTP_AUTHORIZATION='Bearer ' + provider_token) + + # we need provider schemas for many of these tests + with open(os.path.join(PROJECT_PATH, "static/provider_schema_h5p_v1.example.json")) as fp: + response=self.provider_client.put('/api/v1/consents/provider/create', { + "provider-schema": fp + }, format='multipart') + self.assertEqual(response.status_code, 201) + self.assertTrue('message' in response.json()) + + def test_create_token(self): + response = self.provider_client.post( + f"/api/v1/provider/analytics-tokens/create", + { + "name": "Test analytics token", + "description": "Test analytics token", + "expires": "12/31/2099", + "can_access": [] + }, format="json" + ) + self.assertEqual(response.status_code, 201) + + + def test_token_name_available(self): + response = self.provider_client.post( + f"/api/v1/provider/analytics-tokens/create", + { + "name": "Test analytics token", + "description": "Test analytics token", + "expires": "12/31/2099", + "can_access": [] + }, format="json" + ) + self.assertEqual(response.status_code, 201) + + # test unavailable + response = self.provider_client.post( + f"/api/v1/provider/analytics-tokens/name-available", + { + "name": "Test analytics token" + }, format="json" + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertFalse(data["is_available"]) + + # test available + response = self.provider_client.post( + f"/api/v1/provider/analytics-tokens/name-available", + { + "name": "Test analytics token other" + }, format="json" + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertTrue(data["is_available"]) + + + # test failure case + response = self.provider_client.post( + f"/api/v1/provider/analytics-tokens/name-available", + { + "name": None + }, format="json" + ) + self.assertEqual(response.status_code, 400) + + def test_delete_token(self): + response = self.provider_client.post( + f"/api/v1/provider/analytics-tokens/create", + { + "name": "Test analytics token", + "description": "Test analytics token", + "expires": "12/31/2099", + "can_access": [] + }, format="json" + ) + self.assertEqual(response.status_code, 201) + self.assertEqual(AnalyticsToken.objects.count(), 1) + + token_id = AnalyticsToken.objects.latest('id').id + + response = self.provider_client.delete(f"/api/v1/provider/analytics-tokens/{token_id}/delete") + self.assertEqual(response.status_code, 204) + self.assertEqual(AnalyticsToken.objects.count(), 0) + + def test_update_token_verbs(self): + provider_id = Provider.objects.latest('id').id + + response = self.provider_client.post( + f"/api/v1/provider/analytics-tokens/create", + { + "name": "Test analytics token", + "description": "Test analytics token", + "expires": "12/31/2099", + "can_access": [] + }, format="json" + ) + self.assertEqual(response.status_code, 201) + + token_id = AnalyticsToken.objects.latest('id').id + + # test wrong token ID + response = self.provider_client.post(f"/api/v1/provider/analytics-tokens/1000000/update-verbs", + { + "active_verbs": [ + {"verb": "http://h5p.example.com/expapi/verbs/experienced", + "provider": provider_id}, + {"verb": "http://h5p.example.com/expapi/verbs/attempted", + "provider": provider_id}, + {"verb": "http://h5p.example.com/expapi/verbs/interacted", + "provider": provider_id}, + {"verb": "http://h5p.example.com/expapi/verbs/answered", + "provider": provider_id}, + ] + }, format="json") + self.assertEqual(response.status_code, 404) + + response = self.provider_client.post(f"/api/v1/provider/analytics-tokens/{token_id}/update-verbs", + { + "active_verbs": [ + {"verb": "http://h5p.example.com/expapi/verbs/experienced", "provider": provider_id}, + {"verb": "http://h5p.example.com/expapi/verbs/attempted", "provider": provider_id}, + {"verb": "http://h5p.example.com/expapi/verbs/interacted", "provider": provider_id}, + {"verb": "http://h5p.example.com/expapi/verbs/answered", "provider": provider_id}, + ] + }, format="json") + self.assertEqual(response.status_code, 200) + + token = AnalyticsToken.objects.filter(pk=token_id).get() + self.assertEqual(token.analyticstokenverb_set.count(), 4) + + def test_update_and_delete_token_image(self): + provider_id = Provider.objects.latest('id').id + + response = self.provider_client.post( + f"/api/v1/provider/analytics-tokens/create", + { + "name": "Test analytics token", + "description": "Test analytics token", + "expires": "12/31/2099", + "can_access": [] + }, format="json" + ) + self.assertEqual(response.status_code, 201) + + token_id = AnalyticsToken.objects.latest('id').id + + response = self.provider_client.post(f"/api/v1/provider/analytics-tokens/{token_id}/update-verbs", + { + "active_verbs": [ + {"verb": "http://h5p.example.com/expapi/verbs/experienced", "provider": provider_id}, + {"verb": "http://h5p.example.com/expapi/verbs/attempted", "provider": provider_id}, + {"verb": "http://h5p.example.com/expapi/verbs/interacted", "provider": provider_id}, + {"verb": "http://h5p.example.com/expapi/verbs/answered", "provider": provider_id}, + ] + }, format="json") + self.assertEqual(response.status_code, 200) + + token = AnalyticsToken.objects.filter(pk=token_id).get() + self.assertEqual(token.analyticstokenverb_set.count(), 4) + + # test upload invalid image + upload_image = SimpleUploadedFile("image.mp4", b"aslkdjnwekjgfnwkgbwekrjgbwgbkwjebg", content_type="video/mp4") + response = self.provider_client.post(f"/api/v1/provider/analytics-tokens/{token_id}/image", + {"image": upload_image}) + self.assertEqual(response.status_code, 400) + + # test upload "valid" image + upload_image = SimpleUploadedFile("image.png", b"aslkdjnwekjgfnwkgbwekrjgbwgbkwjebg", content_type="image/png") + response = self.provider_client.post(f"/api/v1/provider/analytics-tokens/{token_id}/image", + {"image": upload_image}) + self.assertEqual(response.status_code, 200) + + token = AnalyticsToken.objects.filter(pk=token_id).get() + + # test get image + response = self.provider_client.get(f"/api/v1/provider/analytics-tokens/{token_id}/image/{token.image_path}") + self.assertEqual(response.status_code, 200) + + # test get wrong image + response = self.provider_client.get(f"/api/v1/provider/analytics-tokens/{token_id}/image/nonexistent.png") + self.assertEqual(response.status_code, 404) + + # test deletion failure for nonexistent token + response = self.provider_client.delete(f"/api/v1/provider/analytics-tokens/1000000/image") + self.assertEqual(response.status_code, 404) + + # test deletion + response = self.provider_client.delete(f"/api/v1/provider/analytics-tokens/{token_id}/image") + self.assertEqual(response.status_code, 200) + + # test get image + response = self.provider_client.get(f"/api/v1/provider/analytics-tokens/{token_id}/image/{token.image_path}") + self.assertEqual(response.status_code, 404) \ No newline at end of file diff --git a/src/providers/tests.py b/src/providers/tests/tests_provider_management.py similarity index 98% rename from src/providers/tests.py rename to src/providers/tests/tests_provider_management.py index c7d75f04935f8a88de00c5b28e51108322a8d158..a6ea6b04bcc0dd3b379823fe284bddb7e584f289 100644 --- a/src/providers/tests.py +++ b/src/providers/tests/tests_provider_management.py @@ -8,7 +8,7 @@ from django.core.management import call_command from users.models import CustomUser -from .models import Provider, ProviderAuthorization +from providers.models import Provider, ProviderAuthorization PROJECT_PATH = os.path.abspath(os.path.dirname(__name__)) diff --git a/src/providers/views.py b/src/providers/views.py index 9f6a42c3d4393f7032489ee5cc8a378dfb29601c..a210f30b9dc9e35657954eadfe4afade095c0767 100644 --- a/src/providers/views.py +++ b/src/providers/views.py @@ -238,8 +238,8 @@ class UpdateAnalyticsTokensVerbs(APIView): status=status.HTTP_401_UNAUTHORIZED, ) - # delete all currently assignes verbs - AnalyticsTokenVerb.objects.filter(analytics_token=analytics_token.id).delete(); + # delete all currently assigned verbs + AnalyticsTokenVerb.objects.filter(analytics_token=analytics_token.id).delete() for activeverb in request.data["active_verbs"]: v = AnalyticsTokenVerb() v.verb = activeverb["verb"] @@ -248,7 +248,7 @@ class UpdateAnalyticsTokensVerbs(APIView): v.save() - return Response({"message": "sucess"}, status=status.HTTP_200_OK) + return Response({"message": "success"}, status=status.HTTP_200_OK) except ObjectDoesNotExist: return Response( {"message": "token doesn't exist"}, status=status.HTTP_404_NOT_FOUND diff --git a/src/settings/tests/__init__.py b/src/settings/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/settings/tests/tests_privacy_policy.py b/src/settings/tests/tests_privacy_policy.py new file mode 100644 index 0000000000000000000000000000000000000000..9a67194bcbaad5c02129b9a5632742ec371e1370 --- /dev/null +++ b/src/settings/tests/tests_privacy_policy.py @@ -0,0 +1,136 @@ +import json +import os +from io import StringIO + +from rest_framework.test import APIClient +from rolepermissions.roles import assign_role +from django.core.management import call_command + +from consents.tests.tests_consent_operations import BaseTestCase +from providers.models import ProviderAuthorization, Provider, ProviderVerbGroup, ProviderSchema +from users.models import CustomUser + +from consents.models import UserConsents + + +class TestUserEndpoints(BaseTestCase): + verb_groups = [ + { + "provider_id": 1, + "id": "default_group", + "label": "Default group", + "description": "default", + "showVerbDetails": True, + "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, + "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/attempted"}, + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"} + ] + }, + ] + + def setUp(self): + call_command('check_and_apply_migrations') + normal_user = CustomUser.objects.create_user( + self.test_user_email, self.test_user_password + ) + provider_user = CustomUser.objects.create_superuser( # super user, since we need extended permissions + self.test_provider_email, self.test_provider_password + ) + + assign_role(normal_user, "user") + assign_role(provider_user, "provider_manager") + assign_role(provider_user, 'polaris_administrator') + + response = self.client.post( + "/api/v1/auth/token", + {"email": self.test_user_email, "password": self.test_user_password}, + ) # obtain token + self.assertEqual(response.status_code, 200) + self.assertTrue("access" in response.data) + user_token = response.data["access"] + + response = self.client.post( + "/api/v1/auth/token", + { + "email": self.test_provider_email, + "password": self.test_provider_password, + }, + ) # obtain token + self.assertEqual(response.status_code, 200) + self.assertTrue("access" in response.data) + provider_token = response.data["access"] + + self.user_client = APIClient() + self.user_client.credentials(HTTP_AUTHORIZATION="Bearer " + user_token) + + self.provider_client = APIClient() + self.provider_client.credentials(HTTP_AUTHORIZATION="Bearer " + provider_token) + + # Create provider schema + with StringIO(json.dumps(self.h5p_provider_schema)) as fp: + response = self.provider_client.put( + "/api/v1/consents/provider/create", + {"provider-schema": fp}, + format="multipart", + ) + self.assertEqual(response.status_code, 201) + + # Get application token for created provider + self.application_token = ProviderAuthorization.objects.get( + provider__name="H5P" + ).key + self.third_party_client = APIClient() + self.third_party_client.credentials( + HTTP_AUTHORIZATION="Basic " + self.application_token + ) + + # create some verb groups + for group in self.verb_groups: + response = self.provider_client.post( + "/api/v1/consents/provider/" + str(Provider.objects.latest('id').id) + "/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + + def test_set_privacy_policy(self): + # test unauthorized user + response = self.user_client.post( + f"/api/v1/settings/privacy-policy", + {"content": "test"}, + format="json" + ) + self.assertEqual(response.status_code, 403) + + # test authorized user + response = self.provider_client.post( + f"/api/v1/settings/privacy-policy", + {"content": "test"}, + format="json" + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertEqual(data["content"], "test") + + def test_get_privacy_policy(self): + # test authorized user + response = self.provider_client.post( + f"/api/v1/settings/privacy-policy", + {"content": "test"}, + format="json" + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertEqual(data["content"], "test") + + # test unauthorized user + response = self.user_client.get( + f"/api/v1/settings/privacy-policy", + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertEqual(data["content"], "test") \ No newline at end of file diff --git a/src/users/tests.py b/src/users/tests.py index 6a49d12c6446ea3e274bbb8644707ccffdee2797..012b4001219d6b0ccd39fca5df51403d9d16ad0e 100644 --- a/src/users/tests.py +++ b/src/users/tests.py @@ -3,7 +3,7 @@ from rest_framework.test import APIClient from rolepermissions.roles import assign_role from django.core.management import call_command -from .models import CustomUser +from users.models import CustomUser class UserTests(TestCase):