diff --git a/bin/run_server_locally b/bin/run_server_locally index 46e96051..45cbfbed 100755 --- a/bin/run_server_locally +++ b/bin/run_server_locally @@ -7,6 +7,12 @@ function error_handler() { trap 'error_handler ${LINENO} $?' ERR set -o errtrace -o errexit -o nounset -o pipefail +arg="${1:-}" +if [[ "$arg" == "acceptance" ]]; then + export SPIFFWORKFLOW_BACKEND_LOAD_FIXTURE_DATA=true + export SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME=acceptance_tests.yml +fi + if [[ -z "${SPIFFWORKFLOW_BACKEND_ENV:-}" ]]; then export SPIFFWORKFLOW_BACKEND_ENV=development fi @@ -25,5 +31,6 @@ else if [[ -z "${PROCESS_WAITING_MESSAGES:-}" ]]; then export PROCESS_WAITING_MESSAGES="true" fi + export FLASK_DEBUG=1 FLASK_APP=src/spiffworkflow_backend poetry run flask run -p 7000 fi diff --git a/bin/spiffworkflow-realm.json b/bin/spiffworkflow-realm.json index a2778fee..ee2bceaa 100644 --- a/bin/spiffworkflow-realm.json +++ b/bin/spiffworkflow-realm.json @@ -470,6 +470,50 @@ "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, "webAuthnPolicyPasswordlessAcceptableAaguids": [], "users": [ + { + "id": "4048e9a7-8afa-4e69-9904-389657221abe", + "createdTimestamp": 1665517741516, + "username": "alex", + "enabled": true, + "totp": false, + "emailVerified": false, + "credentials": [ + { + "id": "81a61a3b-228d-42b3-b39a-f62d8e7f57ca", + "type": "password", + "createdDate": 1665517748308, + "secretData": "{\"value\":\"13OdXlB1S1EqHL+3/0y4LYp/LGCn0UW8/Wh9ykgpUbRrwdX6dY3iiMlKePfTy5nXoH/ISmPlxNKOe5z7FWXsgg==\",\"salt\":\"pv0SEb7Ctk5tpu2y32L2kw==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-spiffworkflow"], + "notBefore": 0, + "groups": [] + }, + { + "id": "b4dc5a30-4bd7-44fc-88b5-839fbb8567ea", + "createdTimestamp": 1665518311550, + "username": "amir", + "enabled": true, + "totp": false, + "emailVerified": false, + "credentials": [ + { + "id": "e589f3ad-bf7b-4756-89f7-7894c03c2831", + "type": "password", + "createdDate": 1665518319210, + "secretData": "{\"value\":\"mamd7Hi6nV5suylSrUgwWon3Gw3WeOIvAJu9g39Mq1iYoXWj2rI870bGHiSITLaFBpdjLOEmlu9feKkULOXNpQ==\",\"salt\":\"wG7tkMQfPKRW9ymu4ekujQ==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-spiffworkflow"], + "notBefore": 0, + "groups": [] + }, { "id": "4c436296-8471-4105-b551-80eee96b43bb", "createdTimestamp": 1657139858075, @@ -520,6 +564,248 @@ "notBefore": 0, "groups": [] }, + { + "id": "99e7e4ea-d4ae-4944-bd31-873dac7b004c", + "createdTimestamp": 1665517024483, + "username": "dan", + "enabled": true, + "totp": false, + "emailVerified": false, + "credentials": [ + { + "id": "d517c520-f500-4542-80e5-7144daef1e32", + "type": "password", + "createdDate": 1665517033429, + "secretData": "{\"value\":\"rgWPI1YobMfDaaT3di2+af3gHU8bkreRElAHgYFA+dXHw0skiGVd1t57kNLEP49M6zKYjZzlOKr0qvAxQF0oSg==\",\"salt\":\"usMZebZnPYXhD6ID95bizg==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-spiffworkflow"], + "notBefore": 0, + "groups": [] + }, + { + "id": "1834a79d-917f-4e4c-ab38-8ec376179fe9", + "createdTimestamp": 1665517805115, + "username": "daniel", + "enabled": true, + "totp": false, + "emailVerified": false, + "credentials": [ + { + "id": "f240495c-265b-42fc-99db-46928580d07d", + "type": "password", + "createdDate": 1665517812636, + "secretData": "{\"value\":\"sRCF3tFOZrUbEW220cVHhQ7e89iKqjgAMyO0BaYCPZZw1tEjZ+drGj+bfwRbuuK0Nps3t//YGVELsejRogWkcw==\",\"salt\":\"XQtLR9oZctkyRTi2Be+Z0g==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-spiffworkflow"], + "notBefore": 0, + "groups": [] + }, + { + "id": "72d32cba-e2e2-489d-9141-4d94e3bb2cda", + "createdTimestamp": 1665517787787, + "username": "elizabeth", + "enabled": true, + "totp": false, + "emailVerified": false, + "credentials": [ + { + "id": "ae951ec8-9fc9-4f1b-b340-bbbe463ae5c2", + "type": "password", + "createdDate": 1665517794484, + "secretData": "{\"value\":\"oudGUsbh8utUavZ8OmoUvggCYxr+RHCgwcqpub5AgbITsK4DgY01X0SlDGRTdNGOIqoHse8zGBNmcyBNPWjC0w==\",\"salt\":\"auHilaAS2Lo7oa0UaA7L6A==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-spiffworkflow"], + "notBefore": 0, + "groups": [] + }, + { + "id": "087bdc16-e362-4340-aa60-1ff71a45f844", + "createdTimestamp": 1665516884829, + "username": "harmeet", + "enabled": true, + "totp": false, + "emailVerified": false, + "credentials": [ + { + "id": "89c26090-9bd3-46ac-b038-883d02e3f125", + "type": "password", + "createdDate": 1665516905862, + "secretData": "{\"value\":\"vDzTFQhjg8l8XgQ/YFYZSMLxQovFc/wflVBiRtAk/UWRKhJwuz3XInFbQ64wbYppBlXDYSmYis3luKv6YyUWjQ==\",\"salt\":\"58OQLETS0sM9VpXWoNa6rQ==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-spiffworkflow"], + "notBefore": 0, + "groups": [] + }, + { + "id": "13f5481e-c6b5-450d-8aaf-e13c1c1f5914", + "createdTimestamp": 1665518332327, + "username": "jakub", + "enabled": true, + "totp": false, + "emailVerified": false, + "credentials": [ + { + "id": "ce141fa5-b8d5-4bbe-93e7-22e7119f97c2", + "type": "password", + "createdDate": 1665518338651, + "secretData": "{\"value\":\"+L4TmIGURzFtyRMFyKbPmQ8iYSC639K0GLNHXM+T/cLiMGxVr/wvWj5j435c1V9P+kwO2CnGtd09IsSN8cXuXg==\",\"salt\":\"a2eNeYyoci5fpkPJJy735g==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-spiffworkflow"], + "notBefore": 0, + "groups": [] + }, + { + "id": "3965a6c8-31df-474f-9a45-c268ed98e3fd", + "createdTimestamp": 1665518284693, + "username": "jarrad", + "enabled": true, + "totp": false, + "emailVerified": false, + "credentials": [ + { + "id": "113e0343-1069-476d-83f9-21d98edb9cfa", + "type": "password", + "createdDate": 1665518292234, + "secretData": "{\"value\":\"1CeBMYC3yiJ/cmIxHs/bSea3kxItLNnaIkPNRk2HefZiCdfUKcJ/QLI0O9QO108G2Lzg9McR33EB72zbFAfYUw==\",\"salt\":\"2kWgItvYvzJkgJU9ICWMAw==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-spiffworkflow"], + "notBefore": 0, + "groups": [] + }, + { + "id": "58bcce19-41ec-4ae7-b930-b37be7ad4ba3", + "createdTimestamp": 1665516949583, + "username": "jason", + "enabled": true, + "totp": false, + "emailVerified": false, + "credentials": [ + { + "id": "40abf32e-f0cc-4a17-8231-1a69a02c1b0b", + "type": "password", + "createdDate": 1665516957192, + "secretData": "{\"value\":\"nCnRYH5rLRMu1E7C260SowAdvJfQCSdf4LigcIzSkoPwT+qfLT5ut5m99zakNLeHLoCtGhO2lSVGUQWhdCUYJw==\",\"salt\":\"mW5QN/RSr55I04VI6FTERA==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-spiffworkflow"], + "notBefore": 0, + "groups": [] + }, + { + "id": "29c11638-3b32-4024-8594-91c8b09e713c", + "createdTimestamp": 1665518366585, + "username": "jon", + "enabled": true, + "totp": false, + "emailVerified": false, + "credentials": [ + { + "id": "8b520e01-5b9b-44ab-9ee8-505bd0831a45", + "type": "password", + "createdDate": 1665518373016, + "secretData": "{\"value\":\"lZBDnz49zW6EkT2t7JSQjOzBlYhjhkw3hHefcOC4tmet+h/dAuxSGRuLibJHBap2j6G9Z2SoRqtyS8bwGbR42g==\",\"salt\":\"MI90jmxbLAno0g5O4BCeHw==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-spiffworkflow"], + "notBefore": 0, + "groups": [] + }, + { + "id": "af15c167-d0e7-4a41-ac2c-109188dd7166", + "createdTimestamp": 1665516966482, + "username": "kb", + "enabled": true, + "totp": false, + "emailVerified": false, + "credentials": [ + { + "id": "2c0be363-038f-48f1-86d6-91fdd28657cf", + "type": "password", + "createdDate": 1665516982394, + "secretData": "{\"value\":\"yvliX8Mn+lgpxfMpkjfsV8CASgghEgPA2P1/DR1GP5LSFoGwGCEwj0SmeQAo+MQjBsn3nfvtL9asQvmIYdNZwQ==\",\"salt\":\"kFr1K94QCEx9eGD25rZR9g==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-spiffworkflow"], + "notBefore": 0, + "groups": [] + }, + { + "id": "6f5bfa09-7494-4a2f-b871-cf327048cac7", + "createdTimestamp": 1665517010600, + "username": "manuchehr", + "enabled": true, + "totp": false, + "emailVerified": false, + "credentials": [ + { + "id": "07dabf55-b5d3-4f98-abba-3334086ecf5e", + "type": "password", + "createdDate": 1665517017682, + "secretData": "{\"value\":\"1btDXHraz9l0Gp4g1xxdcuZffLsuKsW0tHwQGzoEtTlI/iZdrKPG9WFlCEFd84qtpdYPJD/tvzn6ZK6zU4/GlQ==\",\"salt\":\"jHtMiO+4jMv9GqLhC9wg4w==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-spiffworkflow"], + "notBefore": 0, + "groups": [] + }, + { + "id": "d1c46b47-67c4-4d07-9cf4-6b1ceac88fc1", + "createdTimestamp": 1665517760255, + "username": "mike", + "enabled": true, + "totp": false, + "emailVerified": false, + "credentials": [ + { + "id": "1ed375fb-0f1a-4c2a-9243-2477242cf7bd", + "type": "password", + "createdDate": 1665517768715, + "secretData": "{\"value\":\"S1cxZ3dgNB+A6yfMchDWEGP8OyZaaAOU/IUKn+QWFt255yoFqs28pfmwCsevdzuh0YfygO9GBgBv7qZQ2pknNQ==\",\"salt\":\"i+Q9zEHNxfi8TAHw17Dv6w==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-spiffworkflow"], + "notBefore": 0, + "groups": [] + }, { "id": "a15da457-7ebb-49d4-9dcc-6876cb71600d", "createdTimestamp": 1657115919770, @@ -545,6 +831,28 @@ "notBefore": 0, "groups": [] }, + { + "id": "f3852a7d-8adf-494f-b39d-96ad4c899ee5", + "createdTimestamp": 1665516926300, + "username": "sasha", + "enabled": true, + "totp": false, + "emailVerified": false, + "credentials": [ + { + "id": "4a170af4-6f0c-4e7b-b70c-e674edf619df", + "type": "password", + "createdDate": 1665516934662, + "secretData": "{\"value\":\"/cimS+PL6p+YnOCF9ZSA6UuwmmLZ7aVUZUthiFDqp/sn0c8GTpWmAdDIbJy2Ut+D4Rx605kRFQaekzRgSYPxcg==\",\"salt\":\"0dmUnLfqK745YHVSz6HOZg==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-spiffworkflow"], + "notBefore": 0, + "groups": [] + }, { "id": "487d3a85-89dd-4839-957a-c3f6d70551f6", "createdTimestamp": 1657115173081, @@ -1056,14 +1364,6 @@ "allowRemoteResourceManagement": true, "policyEnforcementMode": "ENFORCING", "resources": [ - { - "name": "Default Resource", - "type": "urn:spiffworkflow-backend:resources:default", - "ownerManagedAccess": false, - "attributes": {}, - "_id": "8e00e4a3-3fff-4521-b7f0-95f66c2f79d2", - "uris": ["/*"] - }, { "name": "everything", "ownerManagedAccess": false, @@ -1085,6 +1385,14 @@ } ] }, + { + "name": "Default Resource", + "type": "urn:spiffworkflow-backend:resources:default", + "ownerManagedAccess": false, + "attributes": {}, + "_id": "8e00e4a3-3fff-4521-b7f0-95f66c2f79d2", + "uris": ["/*"] + }, { "name": "process-model-with-repeating-form-crud", "type": "process-model", @@ -1994,14 +2302,14 @@ "subComponents": {}, "config": { "allowed-protocol-mapper-types": [ - "oidc-usermodel-attribute-mapper", - "oidc-address-mapper", - "oidc-full-name-mapper", - "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-property-mapper", - "saml-role-list-mapper", + "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", - "saml-user-attribute-mapper" + "saml-role-list-mapper", + "oidc-full-name-mapper", + "saml-user-attribute-mapper", + "oidc-address-mapper", + "oidc-sha256-pairwise-sub-mapper" ] } }, @@ -2023,14 +2331,14 @@ "subComponents": {}, "config": { "allowed-protocol-mapper-types": [ - "saml-user-property-mapper", "saml-user-attribute-mapper", - "oidc-full-name-mapper", - "oidc-usermodel-attribute-mapper", - "oidc-sha256-pairwise-sub-mapper", - "oidc-usermodel-property-mapper", "saml-role-list-mapper", - "oidc-address-mapper" + "oidc-sha256-pairwise-sub-mapper", + "oidc-address-mapper", + "saml-user-property-mapper", + "oidc-full-name-mapper", + "oidc-usermodel-property-mapper", + "oidc-usermodel-attribute-mapper" ] } }, @@ -2144,7 +2452,7 @@ "supportedLocales": [], "authenticationFlows": [ { - "id": "a2e35646-200f-4d14-98ba-c9b5150d8753", + "id": "24ffe820-51bc-402b-b165-7745b6363275", "alias": "Account verification options", "description": "Method with which to verity the existing account", "providerId": "basic-flow", @@ -2170,7 +2478,7 @@ ] }, { - "id": "d85a3c40-8cc9-43a1-ba04-0c8ca2c072da", + "id": "a1e19975-9f44-4ddd-ab5a-2315afa028b1", "alias": "Authentication Options", "description": "Authentication options.", "providerId": "basic-flow", @@ -2204,7 +2512,7 @@ ] }, { - "id": "e127feb1-c4d8-471a-9afc-c21df984462e", + "id": "88ee8214-27f8-4da3-ba54-cb69053bf593", "alias": "Browser - Conditional OTP", "description": "Flow to determine if the OTP is required for the authentication", "providerId": "basic-flow", @@ -2230,7 +2538,7 @@ ] }, { - "id": "f8f6347b-7eb1-44ca-a912-a826a8f93b6d", + "id": "2a720f72-2f6f-4e64-906c-2be5e2fd95fb", "alias": "Direct Grant - Conditional OTP", "description": "Flow to determine if the OTP is required for the authentication", "providerId": "basic-flow", @@ -2256,7 +2564,7 @@ ] }, { - "id": "d2bb8529-3fb8-4085-9153-b56a930829cd", + "id": "b6f70fef-da90-4033-9f0e-d1b7f8619e68", "alias": "First broker login - Conditional OTP", "description": "Flow to determine if the OTP is required for the authentication", "providerId": "basic-flow", @@ -2282,7 +2590,7 @@ ] }, { - "id": "6ccd1a2e-0184-43d4-80e4-7400a008408f", + "id": "c3869d8d-dda3-4b13-a7f5-55f29195d03a", "alias": "Handle Existing Account", "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", "providerId": "basic-flow", @@ -2308,7 +2616,7 @@ ] }, { - "id": "f13bd8b5-895a-44a0-82a6-067dffdcffa9", + "id": "e2855580-7582-4835-b2af-de34215532fe", "alias": "Reset - Conditional OTP", "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "providerId": "basic-flow", @@ -2334,7 +2642,7 @@ ] }, { - "id": "3ef752df-8070-4864-9f1e-2900317924b2", + "id": "4224394c-485e-42ee-a65a-2bdc6eb092fd", "alias": "User creation or linking", "description": "Flow for the existing/non-existing user alternatives", "providerId": "basic-flow", @@ -2361,7 +2669,7 @@ ] }, { - "id": "9adb8fbe-b778-4ee1-9a1b-c01021aee03e", + "id": "fef8981c-e419-4564-ae91-755e489e6d60", "alias": "Verify Existing Account by Re-authentication", "description": "Reauthentication of existing account", "providerId": "basic-flow", @@ -2387,7 +2695,7 @@ ] }, { - "id": "1958f0c6-aaa0-41df-bbe1-be12668286f5", + "id": "f214f005-ad6c-4314-86b9-8d973fbaa3d2", "alias": "browser", "description": "browser based authentication", "providerId": "basic-flow", @@ -2429,7 +2737,7 @@ ] }, { - "id": "c4a0fb82-e755-465f-a0d1-c87846836397", + "id": "7a4f7246-66dd-44f6-9c57-917ba6e62197", "alias": "clients", "description": "Base authentication for clients", "providerId": "client-flow", @@ -2471,7 +2779,7 @@ ] }, { - "id": "3d377bcf-c7b0-4356-bf2f-f83fb1e4aca9", + "id": "2ff421f8-d280-4d56-bd34-25b2a5c3148e", "alias": "direct grant", "description": "OpenID Connect Resource Owner Grant", "providerId": "basic-flow", @@ -2505,7 +2813,7 @@ ] }, { - "id": "97d2ac80-b725-44f8-b171-655bc28cac2a", + "id": "ae42aaf0-f2a7-4e38-81be-c9fc06dea76e", "alias": "docker auth", "description": "Used by Docker clients to authenticate against the IDP", "providerId": "basic-flow", @@ -2523,7 +2831,7 @@ ] }, { - "id": "0fcc3a08-ea77-42e4-a1fb-858abcf1759a", + "id": "e5aa743d-c889-422e-ba9f-90fee8c7f5d9", "alias": "first broker login", "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", "providerId": "basic-flow", @@ -2550,7 +2858,7 @@ ] }, { - "id": "ac743fa7-98df-4933-898f-44b716ff55e2", + "id": "a54ebefa-6ef6-4e42-a016-2b56af3f8aaa", "alias": "forms", "description": "Username, password, otp and other auth forms.", "providerId": "basic-flow", @@ -2576,7 +2884,7 @@ ] }, { - "id": "65451a14-aa9d-49da-807a-f934b10775cb", + "id": "b5d4595a-88b2-4ea9-aeea-d796b0b9085d", "alias": "http challenge", "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", "providerId": "basic-flow", @@ -2602,7 +2910,7 @@ ] }, { - "id": "733a256d-0ccb-4197-852c-91bf62f80e4b", + "id": "da2eba73-45d5-4f0f-bfe8-8812481cde93", "alias": "registration", "description": "registration flow", "providerId": "basic-flow", @@ -2621,7 +2929,7 @@ ] }, { - "id": "d34e94db-5cfd-412b-9555-bfcf3ab7b21b", + "id": "6d49fc23-14db-49a2-89b5-58439022e649", "alias": "registration form", "description": "registration form", "providerId": "form-flow", @@ -2663,7 +2971,7 @@ ] }, { - "id": "2c90ffbf-2de2-41df-bfb0-ddd089bf8c57", + "id": "a0615de2-cf4a-4812-a9ef-fbc4e38e3d10", "alias": "reset credentials", "description": "Reset credentials for a user if they forgot their password or something", "providerId": "basic-flow", @@ -2705,7 +3013,7 @@ ] }, { - "id": "a779f34a-421c-4b7c-b94a-5b8736cf485b", + "id": "69f5f241-2b8a-4fe0-a38d-e4abee38add2", "alias": "saml ecp", "description": "SAML ECP Profile Authentication Flow", "providerId": "basic-flow", @@ -2725,14 +3033,14 @@ ], "authenticatorConfig": [ { - "id": "d99b0848-0378-4a5d-9a72-6efd758e935f", + "id": "7257ea10-3ff4-4001-8171-edc7a7e5b751", "alias": "create unique user config", "config": { "require.password.update.after.registration": "false" } }, { - "id": "ab775beb-09ca-4f94-b62b-16f0692269e9", + "id": "105a6011-5d34-4b70-aaf1-52833e8f62b6", "alias": "review profile config", "config": { "update.profile.on.first.login": "missing" diff --git a/migrations/versions/88e30afd19ac_.py b/migrations/versions/5f7d61fa371c_.py similarity index 98% rename from migrations/versions/88e30afd19ac_.py rename to migrations/versions/5f7d61fa371c_.py index 9e088be7..8098392d 100644 --- a/migrations/versions/88e30afd19ac_.py +++ b/migrations/versions/5f7d61fa371c_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: 88e30afd19ac +Revision ID: 5f7d61fa371c Revises: -Create Date: 2022-10-11 09:39:40.882490 +Create Date: 2022-10-11 14:45:41.213890 """ from alembic import op @@ -10,7 +10,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '88e30afd19ac' +revision = '5f7d61fa371c' down_revision = None branch_labels = None depends_on = None @@ -226,8 +226,8 @@ def upgrade(): sa.Column('id', sa.Integer(), nullable=False), sa.Column('principal_id', sa.Integer(), nullable=False), sa.Column('permission_target_id', sa.Integer(), nullable=False), - sa.Column('grant_type', sa.Enum('permit', 'deny', name='permitdeny'), nullable=True), - sa.Column('permission', sa.Enum('create', 'read', 'update', 'delete', 'list', 'instantiate', name='permission'), nullable=True), + sa.Column('grant_type', sa.String(length=50), nullable=True), + sa.Column('permission', sa.String(length=50), nullable=True), sa.ForeignKeyConstraint(['permission_target_id'], ['permission_target.id'], ), sa.ForeignKeyConstraint(['principal_id'], ['principal.id'], ), sa.PrimaryKeyConstraint('id'), diff --git a/perms.yml b/perms.yml deleted file mode 100644 index 99931856..00000000 --- a/perms.yml +++ /dev/null @@ -1,32 +0,0 @@ -group-admin: - type: Group - users: [jakub, kb, alex, dan, mike, jason] - -group-finance: - type: Group - users: [harmeet, sasha] - -group-hr: - type: Group - users: [manuchehr] - -permission-admin: - type: Permission - groups: [group-admin] - users: [] - allowed_permissions: [CREATE, READ, UPDATE, DELETE, LIST, INSTANTIATE] - uri: /* - -permission-finance-admin: - type: Permission - groups: [group-a] - users: [] - allowed_permissions: [CREATE, READ, UPDATE, DELETE] - uri: /v1.0/process-groups/finance/* - -permission-read-all: - type: Permission - groups: [group-finance, group-hr, group-admin] - users: [] - allowed_permissions: [READ] - uri: /* diff --git a/poetry.lock b/poetry.lock index b1d46768..b8be1708 100644 --- a/poetry.lock +++ b/poetry.lock @@ -639,7 +639,7 @@ werkzeug = "*" type = "git" url = "https://github.com/sartography/flask-bpmn" reference = "main" -resolved_reference = "f3fc539423a3522d142146d2a039c0cd49badaf5" +resolved_reference = "d15e26c289a97d089a5cf2c95ebf034a4f2f425a" [[package]] name = "Flask-Cors" @@ -1986,6 +1986,14 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "types-PyYAML" +version = "6.0.12" +description = "Typing stubs for PyYAML" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "types-requests" version = "2.28.11.1" @@ -2178,7 +2186,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "ba476dd0748bb440b522d1bf24fb62eb30ce3cfbd48b9e3d8f7b5069ddc78ba9" +content-hash = "6f6d015d830e33b06f47989691fa99d6d42536356decbbed3f980f0b43649cce" [metadata.files] alabaster = [ @@ -3396,6 +3404,10 @@ types-pytz = [ {file = "types-pytz-2022.4.0.0.tar.gz", hash = "sha256:17d66e4b16e80ceae0787726f3a22288df7d3f9fdebeb091dc64b92c0e4ea09d"}, {file = "types_pytz-2022.4.0.0-py3-none-any.whl", hash = "sha256:950b0f3d64ed5b03a3e29c1e38fe2be8371c933c8e97922d0352345336eb8af4"}, ] +types-PyYAML = [ + {file = "types-PyYAML-6.0.12.tar.gz", hash = "sha256:f6f350418125872f3f0409d96a62a5a5ceb45231af5cc07ee0034ec48a3c82fa"}, + {file = "types_PyYAML-6.0.12-py3-none-any.whl", hash = "sha256:29228db9f82df4f1b7febee06bbfb601677882e98a3da98132e31c6874163e15"}, +] types-requests = [ {file = "types-requests-2.28.11.1.tar.gz", hash = "sha256:02b1806c5b9904edcd87fa29236164aea0e6cdc4d93ea020cd615ef65cb43d65"}, {file = "types_requests-2.28.11.1-py3-none-any.whl", hash = "sha256:1ff2c1301f6fe58b5d1c66cdf631ca19734cb3b1a4bbadc878d75557d183291a"}, diff --git a/pyproject.toml b/pyproject.toml index 3ff12de8..967a8965 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ Jinja2 = "^3.1.2" RestrictedPython = "^5.2" Flask-SQLAlchemy = "^3" orjson = "^3.8.0" +types-PyYAML = "^6.0.12" [tool.poetry.dev-dependencies] diff --git a/src/spiffworkflow_backend/config/__init__.py b/src/spiffworkflow_backend/config/__init__.py index aa71d207..0383f995 100644 --- a/src/spiffworkflow_backend/config/__init__.py +++ b/src/spiffworkflow_backend/config/__init__.py @@ -54,9 +54,6 @@ def setup_config(app: Flask) -> None: else: app.config.from_pyfile(f"{app.instance_path}/config.py", silent=True) - setup_database_uri(app) - setup_logger(app) - env_config_module = "spiffworkflow_backend.config." + app.config["ENV_IDENTIFIER"] try: app.config.from_object(env_config_module) @@ -65,6 +62,18 @@ def setup_config(app: Flask) -> None: f"Cannot find config module: {env_config_module}" ) from exception + setup_database_uri(app) + setup_logger(app) + + app.config["PERMISSIONS_FILE_FULLPATH"] = None + if app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME"]: + app.config["PERMISSIONS_FILE_FULLPATH"] = os.path.join( + app.root_path, + "config", + "permissions", + app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME"], + ) + # unversioned (see .gitignore) config that can override everything and include secrets. # src/spiffworkflow_backend/config/secrets.py app.config.from_pyfile(os.path.join("config", "secrets.py"), silent=True) diff --git a/src/spiffworkflow_backend/config/default.py b/src/spiffworkflow_backend/config/default.py index 30459b4b..3500ec65 100644 --- a/src/spiffworkflow_backend/config/default.py +++ b/src/spiffworkflow_backend/config/default.py @@ -42,6 +42,10 @@ CONNECTOR_PROXY_URL = environ.get( "CONNECTOR_PROXY_URL", default="http://localhost:7004" ) +SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get( + "SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME" +) + # Sentry Configuration SENTRY_DSN = environ.get("SENTRY_DSN", default="") SENTRY_SAMPLE_RATE = environ.get("SENTRY_SAMPLE_RATE", default="1.0") diff --git a/src/spiffworkflow_backend/config/development.py b/src/spiffworkflow_backend/config/development.py index 5ddd1a28..6877b6de 100644 --- a/src/spiffworkflow_backend/config/development.py +++ b/src/spiffworkflow_backend/config/development.py @@ -1 +1,6 @@ """Development.""" +from os import environ + +SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get( + "SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME", default="staging.yml" +) diff --git a/src/spiffworkflow_backend/config/permissions/acceptance_tests.yml b/src/spiffworkflow_backend/config/permissions/acceptance_tests.yml new file mode 100644 index 00000000..a10b5685 --- /dev/null +++ b/src/spiffworkflow_backend/config/permissions/acceptance_tests.yml @@ -0,0 +1,13 @@ +groups: + admin: + users: [ciadmin1] + + common-user: + users: [ciuser1] + +permissions: + admin: + groups: [admin, common-user] + users: [] + allowed_permissions: [create, read, update, delete, list, instantiate] + uri: /* diff --git a/src/spiffworkflow_backend/config/permissions/staging.yml b/src/spiffworkflow_backend/config/permissions/staging.yml new file mode 100644 index 00000000..9fe49997 --- /dev/null +++ b/src/spiffworkflow_backend/config/permissions/staging.yml @@ -0,0 +1,28 @@ +groups: + admin: + users: [jakub, kb, alex, dan, mike, jason, amir, jarrad, elizabeth, jon] + + finance: + users: [harmeet, sasha] + + hr: + users: [manuchehr] + +permissions: + admin: + groups: [admin] + users: [] + allowed_permissions: [create, read, update, delete, list, instantiate] + uri: /* + + finance-admin: + groups: [finance] + users: [] + allowed_permissions: [create, read, update, delete] + uri: /v1.0/process-groups/finance/* + + read-all: + groups: [finance, hr, admin] + users: [] + allowed_permissions: [read] + uri: /* diff --git a/src/spiffworkflow_backend/config/permissions/testing.yml b/src/spiffworkflow_backend/config/permissions/testing.yml new file mode 100644 index 00000000..0d3f3806 --- /dev/null +++ b/src/spiffworkflow_backend/config/permissions/testing.yml @@ -0,0 +1,28 @@ +groups: + admin: + users: [testadmin1, testadmin2] + + finance: + users: [testuser1, testuser2] + + hr: + users: [testuser2, testuser3, testuser4] + +permissions: + admin: + groups: [admin] + users: [] + allowed_permissions: [create, read, update, delete, list, instantiate] + uri: /* + + read-all: + groups: [finance, hr, admin] + users: [] + allowed_permissions: [read] + uri: /* + + finance-admin: + groups: [finance] + users: [testuser4] + allowed_permissions: [create, read, update, delete] + uri: /v1.0/process-groups/finance/* diff --git a/src/spiffworkflow_backend/config/testing.py b/src/spiffworkflow_backend/config/testing.py index a9b04327..9501cafc 100644 --- a/src/spiffworkflow_backend/config/testing.py +++ b/src/spiffworkflow_backend/config/testing.py @@ -7,3 +7,7 @@ SECRET_KEY = "the_secret_key" SPIFFWORKFLOW_BACKEND_LOG_TO_FILE = ( environ.get("SPIFFWORKFLOW_BACKEND_LOG_TO_FILE", default="true") == "true" ) + +SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get( + "SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME", default="testing.yml" +) diff --git a/src/spiffworkflow_backend/models/permission_assignment.py b/src/spiffworkflow_backend/models/permission_assignment.py index 006d63ce..5fc7ae31 100644 --- a/src/spiffworkflow_backend/models/permission_assignment.py +++ b/src/spiffworkflow_backend/models/permission_assignment.py @@ -1,10 +1,11 @@ """PermissionAssignment.""" import enum +from typing import Any from flask_bpmn.models.db import db from flask_bpmn.models.db import SpiffworkflowBaseDBModel -from sqlalchemy import Enum from sqlalchemy import ForeignKey +from sqlalchemy.orm import validates from spiffworkflow_backend.models.permission_target import PermissionTargetModel from spiffworkflow_backend.models.principal import PrincipalModel @@ -26,12 +27,12 @@ class Permission(enum.Enum): # administer = 2 # view_instance = 3 - create = 1 - read = 2 - update = 3 - delete = 4 - list = 5 - instantiate = 6 # this is something you do to a process model + create = "create" + read = "read" + update = "update" + delete = "delete" + list = "list" + instantiate = "instantiate" # this is something you do to a process model class PermissionAssignmentModel(SpiffworkflowBaseDBModel): @@ -51,5 +52,15 @@ class PermissionAssignmentModel(SpiffworkflowBaseDBModel): permission_target_id = db.Column( ForeignKey(PermissionTargetModel.id), nullable=False ) - grant_type = db.Column(Enum(PermitDeny)) - permission = db.Column(Enum(Permission)) + grant_type = db.Column(db.String(50)) + permission = db.Column(db.String(50)) + + @validates("grant_type") + def validate_grant_type(self, key: str, value: str) -> Any: + """Validate_grant_type.""" + return self.validate_enum_field(key, value, PermitDeny) + + @validates("permission") + def validate_permission(self, key: str, value: str) -> Any: + """Validate_permission.""" + return self.validate_enum_field(key, value, Permission) diff --git a/src/spiffworkflow_backend/models/permission_target.py b/src/spiffworkflow_backend/models/permission_target.py index 0e576cf8..3a341c28 100644 --- a/src/spiffworkflow_backend/models/permission_target.py +++ b/src/spiffworkflow_backend/models/permission_target.py @@ -1,26 +1,28 @@ """PermissionTarget.""" +import re + from flask_bpmn.models.db import db from flask_bpmn.models.db import SpiffworkflowBaseDBModel +from sqlalchemy.orm import validates -# process groups and models are not in the db -# from sqlalchemy import ForeignKey # type: ignore -# -# from spiffworkflow_backend.models.process_group import ProcessGroupModel -# from spiffworkflow_backend.models.process_model import ProcessModel + +class InvalidPermissionTargetUriError(Exception): + """InvalidPermissionTargetUriError.""" class PermissionTargetModel(SpiffworkflowBaseDBModel): """PermissionTargetModel.""" __tablename__ = "permission_target" - # __table_args__ = ( - # CheckConstraint( - # "NOT(process_group_id IS NULL AND process_model_identifier IS NULL AND process_instance_id IS NULL)" - # ), - # ) id = db.Column(db.Integer, primary_key=True) uri = db.Column(db.String(255), unique=True, nullable=False) - # process_group_id = db.Column(ForeignKey(ProcessGroupModel.id), nullable=True) # type: ignore - # process_model_identifier = db.Column(ForeignKey(ProcessModel.id), nullable=True) # type: ignore - # process_instance_id = db.Column(ForeignKey(ProcessInstanceModel.id), nullable=True) # type: ignore + + @validates("uri") + def validate_uri(self, key: str, value: str) -> str: + """Validate_uri.""" + if re.search(r"%.", value): + raise InvalidPermissionTargetUriError( + f"Wildcard must appear at end: {value}" + ) + return value diff --git a/src/spiffworkflow_backend/models/user.py b/src/spiffworkflow_backend/models/user.py index 47711c33..30d60a6f 100644 --- a/src/spiffworkflow_backend/models/user.py +++ b/src/spiffworkflow_backend/models/user.py @@ -17,6 +17,10 @@ from spiffworkflow_backend.services.authentication_service import ( ) +class UserNotFoundError(Exception): + """UserNotFoundError.""" + + class UserModel(SpiffworkflowBaseDBModel): """UserModel.""" diff --git a/src/spiffworkflow_backend/routes/user.py b/src/spiffworkflow_backend/routes/user.py index e5c08d41..a87f7b72 100644 --- a/src/spiffworkflow_backend/routes/user.py +++ b/src/spiffworkflow_backend/routes/user.py @@ -17,6 +17,7 @@ from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.authentication_service import ( PublicAuthenticationService, ) +from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.user_service import UserService """ @@ -250,6 +251,14 @@ def login_return(code: str, state: str, session_state: str) -> Optional[Response if user_model: g.user = user_model.id + # this may eventually get too slow. + # when it does, be careful about backgrounding, because + # the user will immediately need permissions to use the site. + # we are also a little apprehensive about pre-creating users + # before the user signs in, because we won't know things like + # the external service user identifier. + AuthorizationService.import_permissions_from_yaml_file() + redirect_url = ( f"{state_redirect_url}?" + f"access_token={id_token_object['access_token']}&" diff --git a/src/spiffworkflow_backend/services/authorization_service.py b/src/spiffworkflow_backend/services/authorization_service.py index 0209e3f9..f1272ef0 100644 --- a/src/spiffworkflow_backend/services/authorization_service.py +++ b/src/spiffworkflow_backend/services/authorization_service.py @@ -1,15 +1,24 @@ """Authorization_service.""" +import re +from typing import Optional from typing import Union import jwt +import yaml from flask import current_app from flask_bpmn.api.api_error import ApiError +from flask_bpmn.models.db import db +from sqlalchemy import text +from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.permission_assignment import PermissionAssignmentModel from spiffworkflow_backend.models.permission_target import PermissionTargetModel from spiffworkflow_backend.models.principal import MissingPrincipalError from spiffworkflow_backend.models.principal import PrincipalModel from spiffworkflow_backend.models.user import UserModel +from spiffworkflow_backend.models.user import UserNotFoundError +from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel +from spiffworkflow_backend.services.user_service import UserService class AuthorizationService: @@ -21,20 +30,21 @@ class AuthorizationService: ) -> bool: """Has_permission.""" principal_ids = [p.id for p in principals] + permission_assignments = ( PermissionAssignmentModel.query.filter( PermissionAssignmentModel.principal_id.in_(principal_ids) ) .filter_by(permission=permission) .join(PermissionTargetModel) - .filter_by(uri=target_uri) + .filter(text(f"'{target_uri}' LIKE permission_target.uri")) .all() ) for permission_assignment in permission_assignments: - if permission_assignment.grant_type.value == "permit": + if permission_assignment.grant_type == "permit": return True - elif permission_assignment.grant_type.value == "deny": + elif permission_assignment.grant_type == "deny": return False else: raise Exception("Unknown grant type") @@ -61,7 +71,105 @@ class AuthorizationService: principals.append(group.principal) return cls.has_permission(principals, permission, target_uri) - # return False + + @classmethod + def import_permissions_from_yaml_file( + cls, raise_if_missing_user: bool = False + ) -> None: + """Import_permissions_from_yaml_file.""" + permission_configs = None + with open(current_app.config["PERMISSIONS_FILE_FULLPATH"]) as file: + permission_configs = yaml.safe_load(file) + + if "groups" in permission_configs: + for group_identifier, group_config in permission_configs["groups"].items(): + group = GroupModel.query.filter_by(identifier=group_identifier).first() + if group is None: + group = GroupModel(identifier=group_identifier) + db.session.add(group) + db.session.commit() + UserService.create_principal(group.id, id_column_name="group_id") + for username in group_config["users"]: + user = UserModel.query.filter_by(username=username).first() + if user is None: + if raise_if_missing_user: + raise ( + UserNotFoundError( + f"Could not find a user with name: {username}" + ) + ) + continue + user_group_assignemnt = UserGroupAssignmentModel.query.filter_by( + user_id=user.id, group_id=group.id + ).first() + if user_group_assignemnt is None: + user_group_assignemnt = UserGroupAssignmentModel( + user_id=user.id, group_id=group.id + ) + db.session.add(user_group_assignemnt) + db.session.commit() + + if "permissions" in permission_configs: + for _permission_identifier, permission_config in permission_configs[ + "permissions" + ].items(): + uri = permission_config["uri"] + uri_with_percent = re.sub(r"\*", "%", uri) + permission_target = PermissionTargetModel.query.filter_by( + uri=uri_with_percent + ).first() + if permission_target is None: + permission_target = PermissionTargetModel(uri=uri_with_percent) + db.session.add(permission_target) + db.session.commit() + + for allowed_permission in permission_config["allowed_permissions"]: + if "groups" in permission_config: + for group_identifier in permission_config["groups"]: + principal = ( + PrincipalModel.query.join(GroupModel) + .filter(GroupModel.identifier == group_identifier) + .first() + ) + cls.create_permission_for_principal( + principal, permission_target, allowed_permission + ) + if "users" in permission_config: + for username in permission_config["users"]: + principal = ( + PrincipalModel.query.join(UserModel) + .filter(UserModel.username == username) + .first() + ) + cls.create_permission_for_principal( + principal, permission_target, allowed_permission + ) + + @classmethod + def create_permission_for_principal( + cls, + principal: PrincipalModel, + permission_target: PermissionTargetModel, + permission: str, + ) -> PermissionAssignmentModel: + """Create_permission_for_principal.""" + permission_assignment: Optional[ + PermissionAssignmentModel + ] = PermissionAssignmentModel.query.filter_by( + principal_id=principal.id, + permission_target_id=permission_target.id, + permission=permission, + ).first() + if permission_assignment is None: + permission_assignment = PermissionAssignmentModel( + principal_id=principal.id, + permission_target_id=permission_target.id, + permission=permission, + grant_type="permit", + ) + db.session.add(permission_assignment) + db.session.commit() + return permission_assignment # def refresh_token(self, token: str) -> str: # """Refresh_token.""" diff --git a/src/spiffworkflow_backend/services/user_service.py b/src/spiffworkflow_backend/services/user_service.py index b0dcec35..3bf7f092 100644 --- a/src/spiffworkflow_backend/services/user_service.py +++ b/src/spiffworkflow_backend/services/user_service.py @@ -270,13 +270,17 @@ class UserService: ) @classmethod - def create_principal(cls, user_id: int) -> PrincipalModel: + def create_principal( + cls, child_id: int, id_column_name: str = "user_id" + ) -> PrincipalModel: """Create_principal.""" - principal: Optional[PrincipalModel] = PrincipalModel.query.filter_by( - user_id=user_id + column = PrincipalModel.__table__.columns[id_column_name] + principal: Optional[PrincipalModel] = PrincipalModel.query.filter( + column == child_id ).first() if principal is None: - principal = PrincipalModel(user_id=user_id) + principal = PrincipalModel() + setattr(principal, id_column_name, child_id) db.session.add(principal) try: db.session.commit() @@ -285,7 +289,7 @@ class UserService: current_app.logger.error(f"Exception in create_principal: {e}") raise ApiError( error_code="add_principal_error", - message=f"Could not create principal {user_id}", + message=f"Could not create principal {child_id}", ) from e return principal diff --git a/tests/spiffworkflow_backend/helpers/base_test.py b/tests/spiffworkflow_backend/helpers/base_test.py index b7ca4dd0..f323a497 100644 --- a/tests/spiffworkflow_backend/helpers/base_test.py +++ b/tests/spiffworkflow_backend/helpers/base_test.py @@ -22,6 +22,7 @@ from spiffworkflow_backend.models.process_model import NotificationType from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.process_model import ProcessModelInfoSchema from spiffworkflow_backend.models.user import UserModel +from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.process_model_service import ProcessModelService from spiffworkflow_backend.services.user_service import UserService @@ -262,3 +263,18 @@ class BaseTest: ) with open(file_full_path, "rb") as file: return file.read() + + def assert_user_has_permission( + self, + user: UserModel, + permission: str, + target_uri: str, + expected_result: bool = True, + ) -> None: + """Assert_user_has_permission.""" + has_permission = AuthorizationService.user_has_permission( + user=user, + permission=permission, + target_uri=target_uri, + ) + assert has_permission is expected_result diff --git a/tests/spiffworkflow_backend/unit/test_authorization_service.py b/tests/spiffworkflow_backend/unit/test_authorization_service.py new file mode 100644 index 00000000..43949d60 --- /dev/null +++ b/tests/spiffworkflow_backend/unit/test_authorization_service.py @@ -0,0 +1,69 @@ +"""Test_message_service.""" +import pytest +from flask import Flask +from tests.spiffworkflow_backend.helpers.base_test import BaseTest + +from spiffworkflow_backend.models.user import UserNotFoundError +from spiffworkflow_backend.services.authorization_service import AuthorizationService + + +class TestAuthorizationService(BaseTest): + """TestAuthorizationService.""" + + def test_can_raise_if_missing_user( + self, app: Flask, with_db_and_bpmn_file_cleanup: None + ) -> None: + """Test_can_raise_if_missing_user.""" + with pytest.raises(UserNotFoundError): + AuthorizationService.import_permissions_from_yaml_file( + raise_if_missing_user=True + ) + + def test_can_import_permissions_from_yaml( + self, app: Flask, with_db_and_bpmn_file_cleanup: None + ) -> None: + """Test_can_import_permissions_from_yaml.""" + usernames = [ + "testadmin1", + "testadmin2", + "testuser1", + "testuser2", + "testuser3", + "testuser4", + ] + users = {} + for username in usernames: + user = self.find_or_create_user(username=username) + users[username] = user + + AuthorizationService.import_permissions_from_yaml_file() + assert len(users["testadmin1"].groups) == 1 + assert users["testadmin1"].groups[0].identifier == "admin" + assert len(users["testuser1"].groups) == 1 + assert users["testuser1"].groups[0].identifier == "finance" + assert len(users["testuser2"].groups) == 2 + + self.assert_user_has_permission( + users["testuser1"], "update", "/v1.0/process-groups/finance/model1" + ) + self.assert_user_has_permission( + users["testuser1"], "update", "/v1.0/process-groups/finance/" + ) + self.assert_user_has_permission( + users["testuser1"], "update", "/v1.0/process-groups/", expected_result=False + ) + self.assert_user_has_permission( + users["testuser4"], "update", "/v1.0/process-groups/finance/model1" + ) + self.assert_user_has_permission( + users["testuser4"], "read", "/v1.0/process-groups/finance/model1" + ) + self.assert_user_has_permission( + users["testuser2"], "update", "/v1.0/process-groups/finance/model1" + ) + self.assert_user_has_permission( + users["testuser2"], "update", "/v1.0/process-groups/", expected_result=False + ) + self.assert_user_has_permission( + users["testuser2"], "read", "/v1.0/process-groups/" + ) diff --git a/tests/spiffworkflow_backend/unit/test_permission_target.py b/tests/spiffworkflow_backend/unit/test_permission_target.py new file mode 100644 index 00000000..a2f222a4 --- /dev/null +++ b/tests/spiffworkflow_backend/unit/test_permission_target.py @@ -0,0 +1,32 @@ +"""Process Model.""" +import pytest +from flask.app import Flask +from flask_bpmn.models.db import db +from tests.spiffworkflow_backend.helpers.base_test import BaseTest + +from spiffworkflow_backend.models.permission_target import ( + InvalidPermissionTargetUriError, +) +from spiffworkflow_backend.models.permission_target import PermissionTargetModel + + +class TestPermissionTarget(BaseTest): + """TestPermissionTarget.""" + + def test_asterisk_must_go_at_the_end_of_uri( + self, app: Flask, with_db_and_bpmn_file_cleanup: None + ) -> None: + """Test_asterisk_must_go_at_the_end_of_uri.""" + permission_target = PermissionTargetModel(uri="/test_group/%") + db.session.add(permission_target) + db.session.commit() + + permission_target = PermissionTargetModel(uri="/test_group") + db.session.add(permission_target) + db.session.commit() + + with pytest.raises(InvalidPermissionTargetUriError) as exception: + PermissionTargetModel(uri="/test_group/%/model") + assert ( + str(exception.value) == "Wildcard must appear at end: /test_group/%/model" + ) diff --git a/tests/spiffworkflow_backend/unit/test_permissions.py b/tests/spiffworkflow_backend/unit/test_permissions.py index b3a31989..39f857e2 100644 --- a/tests/spiffworkflow_backend/unit/test_permissions.py +++ b/tests/spiffworkflow_backend/unit/test_permissions.py @@ -8,7 +8,6 @@ from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.permission_assignment import PermissionAssignmentModel from spiffworkflow_backend.models.permission_target import PermissionTargetModel from spiffworkflow_backend.models.principal import PrincipalModel -from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.user_service import UserService @@ -74,23 +73,17 @@ class TestPermissions(BaseTest): db.session.add(permission_assignment) db.session.commit() - has_permission_to_a = AuthorizationService.user_has_permission( - user=group_a_admin, - permission="update", - target_uri=f"/{process_group_a_id}", + self.assert_user_has_permission( + group_a_admin, "update", f"/{process_group_a_id}" ) - assert has_permission_to_a is True - has_permission_to_b = AuthorizationService.user_has_permission( - user=group_a_admin, - permission="update", - target_uri=f"/{process_group_b_id}", + self.assert_user_has_permission( + group_a_admin, "update", f"/{process_group_b_id}", expected_result=False ) - assert has_permission_to_b is False def test_user_can_be_granted_access_through_a_group( self, app: Flask, with_db_and_bpmn_file_cleanup: None ) -> None: - """Test_group_a_admin_needs_to_stay_away_from_group_b.""" + """Test_user_can_be_granted_access_through_a_group.""" process_group_ids = ["group-a", "group-b"] process_group_a_id = process_group_ids[0] process_group_ids[1] @@ -123,9 +116,38 @@ class TestPermissions(BaseTest): db.session.add(permission_assignment) db.session.commit() - has_permission_to_a = AuthorizationService.user_has_permission( - user=user, + self.assert_user_has_permission(user, "update", f"/{process_group_a_id}") + + def test_user_can_be_read_models_with_global_permission( + self, app: Flask, with_db_and_bpmn_file_cleanup: None + ) -> None: + """Test_user_can_be_read_models_with_global_permission.""" + process_group_ids = ["group-a", "group-b"] + process_group_a_id = process_group_ids[0] + process_group_b_id = process_group_ids[1] + for process_group_id in process_group_ids: + load_test_spec( + "timers_intermediate_catch_event", + process_group_id=process_group_id, + ) + group_a_admin = self.find_or_create_user() + + permission_target = PermissionTargetModel(uri="/%") + db.session.add(permission_target) + db.session.commit() + + permission_assignment = PermissionAssignmentModel( + permission_target_id=permission_target.id, + principal_id=group_a_admin.principal.id, permission="update", - target_uri=f"/{process_group_a_id}", + grant_type="permit", + ) + db.session.add(permission_assignment) + db.session.commit() + + self.assert_user_has_permission( + group_a_admin, "update", f"/{process_group_a_id}" + ) + self.assert_user_has_permission( + group_a_admin, "update", f"/{process_group_b_id}" ) - assert has_permission_to_a is True