diff --git a/web-access-control/protected-operation/read-access-agent.feature b/web-access-control/protected-operation/read-access-agent.feature index 0e90ccd..e6d0749 100644 --- a/web-access-control/protected-operation/read-access-agent.feature +++ b/web-access-control/protected-operation/read-access-agent.feature @@ -1,119 +1,209 @@ Feature: Only authenticated agents can read (and only that) a resource when granted read access # Grant authenticated agents (setAuthenticatedAccess): - # - full access to the parent container (to ensure the tests are specific to the resource) - # - restricted access to the test resources + # - restricted access or no access to the parent container + # - restricted access to the test resources, or inherited access for fictive resources Background: Create test resources with correct access modes - * def authHeaders = (method, url, public) => !public ? clients.bob.getAuthHeaders(method, url) : {} - * def createResources = + * def authHeaders = + """ + function (method, url, agent) { + const agentLowerCase = agent.toLowerCase() + return agentLowerCase != 'public' ? clients[agentLowerCase].getAuthHeaders(method, url) : {} + } + """ + * def resourcePermissions = """ function (modes) { + if (modes && modes != 'inherited' && modes != 'no') { + return Object.entries({ R: 'read', W: 'write', A: 'append', C: 'control' }) + .filter(([mode, permission]) => modes.includes(mode)) + .map(([mode, permission]) => permission) + } + return undefined + } + """ + * def resourceEntry = + """ + function (container, type) { + switch (type) { + case 'plain': + return container.createResource('.txt', 'Hello', 'text/plain') + case 'fictive': + return container.reserveResource('.txt') + case 'rdf': + return container.createResource('.ttl', karate.readAsString('../fixtures/example.ttl'), 'text/turtle') + case 'container': + return container.createContainer() + default: + return undefined + } + } + """ + * def createResource = + """ + function (containerModes, resourceModes, resourceType) { + + const testContainerPermissions = resourcePermissions(containerModes == 'all' ? 'RWAC' : containerModes) + const testResourcePermissions = resourcePermissions(resourceModes) + + const testContainerInheritablePermissions = resourceModes == 'inherited' + ? testContainerPermissions + : undefined + const testContainer = rootTestContainer.createContainer() + const testResource = resourceEntry(testContainer, resourceType) + testContainer.accessDataset = testContainer.accessDatasetBuilder - .setAuthenticatedAccess(testContainer.url, ['read', 'write', 'append', 'control']).build() - const plainResource = testContainer.createResource('.txt', 'Hello', 'text/plain') - plainResource.accessDataset = plainResource.accessDatasetBuilder.setAuthenticatedAccess(plainResource.url, modes).build() - const rdfResource = testContainer.createResource('.ttl', karate.readAsString('../fixtures/example.ttl'), 'text/turtle') - rdfResource.accessDataset = rdfResource.accessDatasetBuilder.setAuthenticatedAccess(rdfResource.url, modes).build() - const container = testContainer.createContainer() - container.accessDataset = container.accessDatasetBuilder.setAuthenticatedAccess(container.url, modes).build() - return { plain: plainResource, rdf: rdfResource, container: container } + .setAuthenticatedAccess(testContainer.url, testContainerPermissions) + .setInheritableAuthenticatedAccess(testContainer.url, testContainerInheritablePermissions) + .build() + + if (resourceType != 'fictive' && resourceModes != 'inherited') { + testResource.accessDataset = testResource.accessDatasetBuilder + .setAuthenticatedAccess(testResource.url, testResourcePermissions) + .build() + } + + return testResource } """ - # Create 3 test resources with read access for authenticated agents - * def testsR = callonce createResources ['read'] - # Create 3 test resources with append, write, control access for authenticated agents - * def testsAWC = callonce createResources ['append', 'write', 'control'] - Scenario Outline: read a resource () to which an authenticated agent has access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) + Scenario Outline: read a resource (), when an authenticated agent has access to the container and access to the resource + * def testResource = createResource(container, resource, type) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) When method Then status Examples: - | agent | type | mode | method | public! | status | - | Bob can | plain | R | GET | false | 200 | - | Bob can | rdf | R | GET | false | 200 | - | Bob can | container | R | GET | false | 200 | - | Bob can | plain | R | HEAD | false | 200 | - | Bob can | rdf | R | HEAD | false | 200 | - | Bob can | container | R | HEAD | false | 200 | - | Public cannot | plain | R | GET | true | 401 | - | Public cannot | rdf | R | GET | true | 401 | - | Public cannot | container | R | GET | true | 401 | - | Public cannot | plain | R | HEAD | true | 401 | - | Public cannot | rdf | R | HEAD | true | 401 | - | Public cannot | container | R | HEAD | true | 401 | - | Bob cannot | plain | AWC | GET | false | 403 | - | Bob cannot | rdf | AWC | GET | false | 403 | - | Bob cannot | container | AWC | GET | false | 403 | - | Bob cannot | plain | AWC | HEAD | false | 403 | - | Bob cannot | rdf | AWC | HEAD | false | 403 | - | Bob cannot | container | AWC | HEAD | false | 403 | - | Public cannot | plain | AWC | GET | true | 401 | - | Public cannot | rdf | AWC | GET | true | 401 | - | Public cannot | container | AWC | GET | true | 401 | - | Public cannot | plain | AWC | HEAD | true | 401 | - | Public cannot | rdf | AWC | HEAD | true | 401 | - | Public cannot | container | AWC | HEAD | true | 401 | + | agent | result | method | type | container | resource | status | + | Bob | can | GET | plain | no | R | 200 | + | Bob | can | GET | plain | R | inherited | 200 | + | Bob | can | GET | fictive | R | inherited | 404 | + | Bob | can | GET | rdf | no | R | 200 | + | Bob | can | GET | rdf | R | inherited | 200 | + | Bob | can | GET | container | no | R | 200 | + | Bob | can | GET | container | R | inherited | 200 | + | Bob | can | HEAD | plain | no | R | 200 | + | Bob | can | HEAD | plain | R | inherited | 200 | + | Bob | can | HEAD | fictive | R | inherited | 404 | + | Bob | can | HEAD | rdf | no | R | 200 | + | Bob | can | HEAD | rdf | R | inherited | 200 | + | Bob | can | HEAD | container | no | R | 200 | + | Bob | can | HEAD | container | R | inherited | 200 | + | Public | cannot | GET | plain | no | R | 401 | + | Public | cannot | GET | plain | R | inherited | 401 | + | Public | cannot | GET | fictive | R | inherited | 401 | + | Public | cannot | GET | rdf | no | R | 401 | + | Public | cannot | GET | rdf | R | inherited | 401 | + | Public | cannot | GET | container | no | R | 401 | + | Public | cannot | GET | container | R | inherited | 401 | + | Public | cannot | HEAD | plain | no | R | 401 | + | Public | cannot | HEAD | plain | R | inherited | 401 | + | Public | cannot | HEAD | fictive | R | inherited | 401 | + | Public | cannot | HEAD | rdf | no | R | 401 | + | Public | cannot | HEAD | rdf | R | inherited | 401 | + | Public | cannot | HEAD | container | no | R | 401 | + | Public | cannot | HEAD | container | R | inherited | 401 | - Scenario Outline: cannot to a resource to which an authenticated agent has access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) + Scenario Outline: to a resource, when an authenticated agent has access to the container and access to the resource + * def testResource = createResource(container, resource, type) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) And header Content-Type = 'text/turtle' And request '@prefix rdfs: . <> rdfs:comment "Bob added this.".' When method Then status Examples: - | agent | type | mode | method | public! | status | - | Bob | rdf | R | PUT | false | 403 | - | Bob | container | R | PUT | false | 403 | - | Bob | rdf | R | POST | false | 403 | - | Bob | container | R | POST | false | 403 | - | Public | rdf | R | PUT | true | 401 | - | Public | container | R | PUT | true | 401 | - | Public | rdf | R | POST | true | 401 | - | Public | container | R | POST | true | 401 | + | agent | result | method | type | container | resource | status | + | Bob | cannot | PUT | rdf | no | R | 403 | + | Bob | cannot | PUT | rdf | R | inherited | 403 | + | Bob | cannot | PUT | fictive | R | inherited | 403 | + | Bob | cannot | PUT | container | no | R | 403 | + | Bob | cannot | PUT | container | R | inherited | 403 | + | Bob | cannot | POST | rdf | no | R | 403 | + | Bob | cannot | POST | rdf | R | inherited | 403 | + | Bob | cannot | POST | fictive | R | inherited | 404 | + | Bob | cannot | POST | container | no | R | 403 | + | Bob | cannot | POST | container | R | inherited | 403 | + | Public | cannot | PUT | rdf | no | R | 401 | + | Public | cannot | PUT | rdf | R | inherited | 401 | + | Public | cannot | PUT | fictive | R | inherited | 401 | + | Public | cannot | PUT | container | no | R | 401 | + | Public | cannot | PUT | container | R | inherited | 401 | + | Public | cannot | POST | rdf | no | R | 401 | + | Public | cannot | POST | rdf | R | inherited | 401 | + | Public | cannot | POST | fictive | R | inherited | 401 | + | Public | cannot | POST | container | no | R | 401 | + | Public | cannot | POST | container | R | inherited | 401 | - Scenario Outline: cannot to a resource to which an authenticated agent has access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) + Scenario Outline: to a resource, when an authenticated agent has access to the container and access to the resource + * def testResource = createResource(container, resource, type) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) And header Content-Type = 'text/n3' And request '@prefix solid: . _:insert a solid:InsertDeletePatch; solid:inserts { <> a . }.' When method Then status Examples: - | agent | type | mode | method | public! | status | - | Bob | rdf | R | PATCH | false | 403 | - | Bob | container | R | PATCH | false | 403 | - | Public | rdf | R | PATCH | true | 401 | - | Public | container | R | PATCH | true | 401 | + | agent | result | method | type | container | resource | status | + | Bob | cannot | PATCH | rdf | no | R | 403 | + | Bob | cannot | PATCH | rdf | R | inherited | 403 | + | Bob | cannot | PATCH | fictive | R | inherited | 403 | + | Bob | cannot | PATCH | container | no | R | 403 | + | Bob | cannot | PATCH | container | R | inherited | 403 | + | Public | cannot | PATCH | rdf | no | R | 401 | + | Public | cannot | PATCH | rdf | R | inherited | 401 | + | Public | cannot | PATCH | fictive | R | inherited | 401 | + | Public | cannot | PATCH | container | no | R | 401 | + | Public | cannot | PATCH | container | R | inherited | 401 | - Scenario Outline: cannot to a resource to which an authenticated agent has access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) + Scenario Outline: to a resource, when an authenticated agent has access to the container and access to the resource + * def testResource = createResource(container, resource, type) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) And header Content-Type = 'text/plain' And request "Bob's text" When method Then match contains responseStatus Examples: - | agent | type | mode | method | public! | status | - | Bob | plain | R | PUT | false | [403] | - | Bob | plain | R | POST | false | [403] | - | Bob | plain | R | PATCH | false | [403, 405, 415] | - | Public | plain | R | PUT | true | [401] | - | Public | plain | R | POST | true | [401] | - | Public | plain | R | PATCH | true | [401, 405, 415] | + | agent | result | method | type | container | resource | status | + | Bob | cannot | PUT | plain | no | R | [403] | + | Bob | cannot | PUT | plain | R | inherited | [403] | + | Bob | cannot | PUT | fictive | R | inherited | [403] | + | Bob | cannot | POST | plain | no | R | [403] | + | Bob | cannot | POST | plain | R | inherited | [403] | + | Bob | cannot | POST | fictive | R | inherited | [404] | + | Bob | cannot | PATCH | plain | no | R | [403, 405, 415] | + | Bob | cannot | PATCH | plain | R | inherited | [403, 405, 415] | + | Bob | cannot | PATCH | fictive | R | inherited | [403, 405, 415] | + | Public | cannot | PUT | plain | no | R | [401] | + | Public | cannot | PUT | plain | R | inherited | [401] | + | Public | cannot | PUT | fictive | R | inherited | [401] | + | Public | cannot | POST | plain | no | R | [401] | + | Public | cannot | POST | plain | R | inherited | [401] | + | Public | cannot | POST | fictive | R | inherited | [401] | + | Public | cannot | PATCH | plain | no | R | [401, 405, 415] | + | Public | cannot | PATCH | plain | R | inherited | [401, 405, 415] | + | Public | cannot | PATCH | fictive | R | inherited | [401, 405, 415] | - Scenario Outline: cannot a resource to which an authenticated agent has access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) + Scenario Outline: a resource, when an authenticated agent has access to the container and access to the resource + * def testResource = createResource(container, resource, type) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) When method Then status Examples: - | agent | type | mode | method | public! | status | - | Bob | plain | R | DELETE | false | 403 | - | Bob | rdf | R | DELETE | false | 403 | - | Bob | container | R | DELETE | false | 403 | - | Public | plain | R | DELETE | true | 401 | - | Public | rdf | R | DELETE | true | 401 | - | Public | container | R | DELETE | true | 401 | + | agent | result | method | type | container | resource | status | + | Bob | cannot | DELETE | plain | no | R | 403 | + | Bob | cannot | DELETE | plain | R | inherited | 403 | + | Bob | cannot | DELETE | fictive | R | inherited | 404 | + | Bob | cannot | DELETE | rdf | no | R | 403 | + | Bob | cannot | DELETE | rdf | R | inherited | 403 | + | Bob | cannot | DELETE | container | no | R | 403 | + | Bob | cannot | DELETE | container | R | inherited | 403 | + | Public | cannot | DELETE | plain | no | R | 401 | + | Public | cannot | DELETE | plain | R | inherited | 401 | + | Public | cannot | DELETE | fictive | R | inherited | 401 | + | Public | cannot | DELETE | rdf | no | R | 401 | + | Public | cannot | DELETE | rdf | R | inherited | 401 | + | Public | cannot | DELETE | container | no | R | 401 | + | Public | cannot | DELETE | container | R | inherited | 401 | diff --git a/web-access-control/protected-operation/read-access-bob.feature b/web-access-control/protected-operation/read-access-bob.feature index b0f75a2..313fb8e 100644 --- a/web-access-control/protected-operation/read-access-bob.feature +++ b/web-access-control/protected-operation/read-access-bob.feature @@ -1,119 +1,210 @@ Feature: Only Bob can read (and only that) a resource when granted read access # Grant a specific agent (setAgentAccess): - # - full access to the parent container (to ensure the tests are specific to the resource) - # - restricted access to the test resources + # - restricted access or no access to the parent container + # - restricted access to the test resources, or inherited access for fictive resources Background: Create test resources with correct access modes - * def authHeaders = (method, url, public) => !public ? clients.bob.getAuthHeaders(method, url) : {} - * def createResources = + * def authHeaders = + """ + function (method, url, agent) { + const agentLowerCase = agent.toLowerCase() + return agentLowerCase != 'public' ? clients[agentLowerCase].getAuthHeaders(method, url) : {} + } + """ + * def resourcePermissions = """ function (modes) { + if (modes && modes != 'inherited' && modes != 'no') { + return Object.entries({ R: 'read', W: 'write', A: 'append', C: 'control' }) + .filter(([mode, permission]) => modes.includes(mode)) + .map(([mode, permission]) => permission) + } + return undefined + } + """ + * def resourceEntry = + """ + function (container, type) { + switch (type) { + case 'plain': + return container.createResource('.txt', 'Hello', 'text/plain') + case 'fictive': + return container.reserveResource('.txt') + case 'rdf': + return container.createResource('.ttl', karate.readAsString('../fixtures/example.ttl'), 'text/turtle') + case 'container': + return container.createContainer() + default: + return undefined + } + } + """ + * def createResource = + """ + function (containerModes, resourceModes, resourceType, agent) { + + const agentWebId = webIds.bob + const testContainerPermissions = resourcePermissions(containerModes == 'all' ? 'RWAC' : containerModes) + const testResourcePermissions = resourcePermissions(resourceModes) + + const testContainerInheritablePermissions = resourceModes == 'inherited' + ? testContainerPermissions + : undefined + const testContainer = rootTestContainer.createContainer() + const testResource = resourceEntry(testContainer, resourceType) + testContainer.accessDataset = testContainer.accessDatasetBuilder - .setAgentAccess(testContainer.url, webIds.bob, ['read', 'write', 'append', 'control']).build() - const plainResource = testContainer.createResource('.txt', 'Hello', 'text/plain') - plainResource.accessDataset = plainResource.accessDatasetBuilder.setAgentAccess(plainResource.url, webIds.bob, modes).build() - const rdfResource = testContainer.createResource('.ttl', karate.readAsString('../fixtures/example.ttl'), 'text/turtle') - rdfResource.accessDataset = rdfResource.accessDatasetBuilder.setAgentAccess(rdfResource.url, webIds.bob, modes).build() - const container = testContainer.createContainer() - container.accessDataset = container.accessDatasetBuilder.setAgentAccess(container.url, webIds.bob, modes).build() - return { plain: plainResource, rdf: rdfResource, container: container } + .setAgentAccess(testContainer.url, agentWebId, testContainerPermissions) + .setInheritableAgentAccess(testContainer.url, agentWebId, testContainerInheritablePermissions) + .build() + + if (resourceType != 'fictive' && resourceModes != 'inherited') { + testResource.accessDataset = testResource.accessDatasetBuilder + .setAgentAccess(testResource.url, agentWebId, testResourcePermissions) + .build() + } + + return testResource } """ - # Create 3 test resources with read access for Bob - * def testsR = callonce createResources ['read'] - # Create 3 test resources with append, write, control access for Bob - * def testsAWC = callonce createResources ['append', 'write', 'control'] - Scenario Outline: read a resource () to which Bob has access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) + Scenario Outline: read a resource (), when Bob has access to the container and access to the resource + * def testResource = createResource(container, resource, type, agent) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) When method Then status Examples: - | agent | type | mode | method | public! | status | - | Bob can | plain | R | GET | false | 200 | - | Bob can | rdf | R | GET | false | 200 | - | Bob can | container | R | GET | false | 200 | - | Bob can | plain | R | HEAD | false | 200 | - | Bob can | rdf | R | HEAD | false | 200 | - | Bob can | container | R | HEAD | false | 200 | - | Public cannot | plain | R | GET | true | 401 | - | Public cannot | rdf | R | GET | true | 401 | - | Public cannot | container | R | GET | true | 401 | - | Public cannot | plain | R | HEAD | true | 401 | - | Public cannot | rdf | R | HEAD | true | 401 | - | Public cannot | container | R | HEAD | true | 401 | - | Bob cannot | plain | AWC | GET | false | 403 | - | Bob cannot | rdf | AWC | GET | false | 403 | - | Bob cannot | container | AWC | GET | false | 403 | - | Bob cannot | plain | AWC | HEAD | false | 403 | - | Bob cannot | rdf | AWC | HEAD | false | 403 | - | Bob cannot | container | AWC | HEAD | false | 403 | - | Public cannot | plain | AWC | GET | true | 401 | - | Public cannot | rdf | AWC | GET | true | 401 | - | Public cannot | container | AWC | GET | true | 401 | - | Public cannot | plain | AWC | HEAD | true | 401 | - | Public cannot | rdf | AWC | HEAD | true | 401 | - | Public cannot | container | AWC | HEAD | true | 401 | + | agent | result | method | type | container | resource | status | + | Bob | can | GET | plain | no | R | 200 | + | Bob | can | GET | plain | R | inherited | 200 | + | Bob | can | GET | fictive | R | inherited | 404 | + | Bob | can | GET | rdf | no | R | 200 | + | Bob | can | GET | rdf | R | inherited | 200 | + | Bob | can | GET | container | no | R | 200 | + | Bob | can | GET | container | R | inherited | 200 | + | Bob | can | HEAD | plain | no | R | 200 | + | Bob | can | HEAD | plain | R | inherited | 200 | + | Bob | can | HEAD | fictive | R | inherited | 404 | + | Bob | can | HEAD | rdf | no | R | 200 | + | Bob | can | HEAD | rdf | R | inherited | 200 | + | Bob | can | HEAD | container | no | R | 200 | + | Bob | can | HEAD | container | R | inherited | 200 | + | Public | cannot | GET | plain | no | R | 401 | + | Public | cannot | GET | plain | R | inherited | 401 | + | Public | cannot | GET | fictive | R | inherited | 401 | + | Public | cannot | GET | rdf | no | R | 401 | + | Public | cannot | GET | rdf | R | inherited | 401 | + | Public | cannot | GET | container | no | R | 401 | + | Public | cannot | GET | container | R | inherited | 401 | + | Public | cannot | HEAD | plain | no | R | 401 | + | Public | cannot | HEAD | plain | R | inherited | 401 | + | Public | cannot | HEAD | fictive | R | inherited | 401 | + | Public | cannot | HEAD | rdf | no | R | 401 | + | Public | cannot | HEAD | rdf | R | inherited | 401 | + | Public | cannot | HEAD | container | no | R | 401 | + | Public | cannot | HEAD | container | R | inherited | 401 | - Scenario Outline: cannot to a resource to which Bob has access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) + Scenario Outline: to a resource, when Bob has access to the container and access to the resource + * def testResource = createResource(container, resource, type, agent) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) And header Content-Type = 'text/turtle' And request '@prefix rdfs: . <> rdfs:comment "Bob added this.".' When method Then status Examples: - | agent | type | mode | method | public! | status | - | Bob | rdf | R | PUT | false | 403 | - | Bob | container | R | PUT | false | 403 | - | Bob | rdf | R | POST | false | 403 | - | Bob | container | R | POST | false | 403 | - | Public | rdf | R | PUT | true | 401 | - | Public | container | R | PUT | true | 401 | - | Public | rdf | R | POST | true | 401 | - | Public | container | R | POST | true | 401 | + | agent | result | method | type | container | resource | status | + | Bob | cannot | PUT | rdf | no | R | 403 | + | Bob | cannot | PUT | rdf | R | inherited | 403 | + | Bob | cannot | PUT | fictive | R | inherited | 403 | + | Bob | cannot | PUT | container | no | R | 403 | + | Bob | cannot | PUT | container | R | inherited | 403 | + | Bob | cannot | POST | rdf | no | R | 403 | + | Bob | cannot | POST | rdf | R | inherited | 403 | + | Bob | cannot | POST | fictive | R | inherited | 404 | + | Bob | cannot | POST | container | no | R | 403 | + | Bob | cannot | POST | container | R | inherited | 403 | + | Public | cannot | PUT | rdf | no | R | 401 | + | Public | cannot | PUT | rdf | R | inherited | 401 | + | Public | cannot | PUT | fictive | R | inherited | 401 | + | Public | cannot | PUT | container | no | R | 401 | + | Public | cannot | PUT | container | R | inherited | 401 | + | Public | cannot | POST | rdf | no | R | 401 | + | Public | cannot | POST | rdf | R | inherited | 401 | + | Public | cannot | POST | fictive | R | inherited | 401 | + | Public | cannot | POST | container | no | R | 401 | + | Public | cannot | POST | container | R | inherited | 401 | - Scenario Outline: cannot to a resource to which Bob has access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) + Scenario Outline: to a resource, when Bob has access to the container and access to the resource + * def testResource = createResource(container, resource, type, agent) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) And header Content-Type = 'text/n3' And request '@prefix solid: . _:insert a solid:InsertDeletePatch; solid:inserts { <> a . }.' When method Then status Examples: - | agent | type | mode | method | public! | status | - | Bob | rdf | R | PATCH | false | 403 | - | Bob | container | R | PATCH | false | 403 | - | Public | rdf | R | PATCH | true | 401 | - | Public | container | R | PATCH | true | 401 | + | agent | result | method | type | container | resource | status | + | Bob | cannot | PATCH | rdf | no | R | 403 | + | Bob | cannot | PATCH | rdf | R | inherited | 403 | + | Bob | cannot | PATCH | fictive | R | inherited | 403 | + | Bob | cannot | PATCH | container | no | R | 403 | + | Bob | cannot | PATCH | container | R | inherited | 403 | + | Public | cannot | PATCH | rdf | no | R | 401 | + | Public | cannot | PATCH | rdf | R | inherited | 401 | + | Public | cannot | PATCH | fictive | R | inherited | 401 | + | Public | cannot | PATCH | container | no | R | 401 | + | Public | cannot | PATCH | container | R | inherited | 401 | - Scenario Outline: cannot to a resource to which Bob has access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) + Scenario Outline: to a resource, when Bob has access to the container and access to the resource + * def testResource = createResource(container, resource, type, agent) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) And header Content-Type = 'text/plain' And request "Bob's text" When method Then match contains responseStatus Examples: - | agent | type | mode | method | public! | status | - | Bob | plain | R | PUT | false | [403] | - | Bob | plain | R | POST | false | [403] | - | Bob | plain | R | PATCH | false | [403, 405, 415] | - | Public | plain | R | PUT | true | [401] | - | Public | plain | R | POST | true | [401] | - | Public | plain | R | PATCH | true | [401, 405, 415] | + | agent | result | method | type | container | resource | status | + | Bob | cannot | PUT | plain | no | R | [403] | + | Bob | cannot | PUT | plain | R | inherited | [403] | + | Bob | cannot | PUT | fictive | R | inherited | [403] | + | Bob | cannot | POST | plain | no | R | [403] | + | Bob | cannot | POST | plain | R | inherited | [403] | + | Bob | cannot | POST | fictive | R | inherited | [404] | + | Bob | cannot | PATCH | plain | no | R | [403, 405, 415] | + | Bob | cannot | PATCH | plain | R | inherited | [403, 405, 415] | + | Bob | cannot | PATCH | fictive | R | inherited | [403, 405, 415] | + | Public | cannot | PUT | plain | no | R | [401] | + | Public | cannot | PUT | plain | R | inherited | [401] | + | Public | cannot | PUT | fictive | R | inherited | [401] | + | Public | cannot | POST | plain | no | R | [401] | + | Public | cannot | POST | plain | R | inherited | [401] | + | Public | cannot | POST | fictive | R | inherited | [401] | + | Public | cannot | PATCH | plain | no | R | [401, 405, 415] | + | Public | cannot | PATCH | plain | R | inherited | [401, 405, 415] | + | Public | cannot | PATCH | fictive | R | inherited | [401, 405, 415] | - Scenario Outline: cannot a resource to which Bob has access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) + Scenario Outline: a resource, when Bob has access to the container and access to the resource + * def testResource = createResource(container, resource, type, agent) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) When method Then status Examples: - | agent | type | mode | method | public! | status | - | Bob | plain | R | DELETE | false | 403 | - | Bob | rdf | R | DELETE | false | 403 | - | Bob | container | R | DELETE | false | 403 | - | Public | plain | R | DELETE | true | 401 | - | Public | rdf | R | DELETE | true | 401 | - | Public | container | R | DELETE | true | 401 | + | agent | result | method | type | container | resource | status | + | Bob | cannot | DELETE | plain | no | R | 403 | + | Bob | cannot | DELETE | plain | R | inherited | 403 | + | Bob | cannot | DELETE | fictive | R | inherited | 404 | + | Bob | cannot | DELETE | rdf | no | R | 403 | + | Bob | cannot | DELETE | rdf | R | inherited | 403 | + | Bob | cannot | DELETE | container | no | R | 403 | + | Bob | cannot | DELETE | container | R | inherited | 403 | + | Public | cannot | DELETE | plain | no | R | 401 | + | Public | cannot | DELETE | plain | R | inherited | 401 | + | Public | cannot | DELETE | fictive | R | inherited | 401 | + | Public | cannot | DELETE | rdf | no | R | 401 | + | Public | cannot | DELETE | rdf | R | inherited | 401 | + | Public | cannot | DELETE | container | no | R | 401 | + | Public | cannot | DELETE | container | R | inherited | 401 | diff --git a/web-access-control/protected-operation/read-access-public.feature b/web-access-control/protected-operation/read-access-public.feature index 31eda78..d0f1bc0 100644 --- a/web-access-control/protected-operation/read-access-public.feature +++ b/web-access-control/protected-operation/read-access-public.feature @@ -1,123 +1,213 @@ Feature: Public agents can read (and only that) a resource when granted read access # Grant public agents (setPublicAccess): - # - full access to the parent container (to ensure the tests are specific to the resource) - # - restricted access to the test resources + # - restricted access or no access to the parent container + # - restricted access to the test resources, or inherited access for fictive resources Background: Create test resources with correct access modes - * def authHeaders = (method, url, public) => !public ? clients.bob.getAuthHeaders(method, url) : {} - * def createResources = + * def authHeaders = + """ + function (method, url, agent) { + const agentLowerCase = agent.toLowerCase() + return agentLowerCase != 'public' ? clients[agentLowerCase].getAuthHeaders(method, url) : {} + } + """ + * def resourcePermissions = """ function (modes) { + if (modes && modes != 'inherited' && modes != 'no') { + return Object.entries({ R: 'read', W: 'write', A: 'append', C: 'control' }) + .filter(([mode, permission]) => modes.includes(mode)) + .map(([mode, permission]) => permission) + } + return undefined + } + """ + * def resourceEntry = + """ + function (container, type) { + switch (type) { + case 'plain': + return container.createResource('.txt', 'Hello', 'text/plain') + case 'fictive': + return container.reserveResource('.txt') + case 'rdf': + return container.createResource('.ttl', karate.readAsString('../fixtures/example.ttl'), 'text/turtle') + case 'container': + return container.createContainer() + default: + return undefined + } + } + """ + * def createResource = + """ + function (containerModes, resourceModes, resourceType) { + + const testContainerPermissions = resourcePermissions(containerModes == 'all' ? 'RWAC' : containerModes) + const testResourcePermissions = resourcePermissions(resourceModes) + + const testContainerInheritablePermissions = resourceModes == 'inherited' + ? testContainerPermissions + : undefined + const testContainer = rootTestContainer.createContainer() + const testResource = resourceEntry(testContainer, resourceType) + testContainer.accessDataset = testContainer.accessDatasetBuilder - .setPublicAccess(testContainer.url, ['read', 'write', 'append', 'control']).build() - const plainResource = testContainer.createResource('.txt', 'Hello', 'text/plain') - plainResource.accessDataset = plainResource.accessDatasetBuilder.setPublicAccess(plainResource.url, modes).build() - const rdfResource = testContainer.createResource('.ttl', karate.readAsString('../fixtures/example.ttl'), 'text/turtle') - rdfResource.accessDataset = rdfResource.accessDatasetBuilder.setPublicAccess(rdfResource.url, modes).build() - const container = testContainer.createContainer() - container.accessDataset = container.accessDatasetBuilder.setPublicAccess(container.url, modes).build() - return { plain: plainResource, rdf: rdfResource, container: container } + .setPublicAccess(testContainer.url, testContainerPermissions) + .setInheritablePublicAccess(testContainer.url, testContainerInheritablePermissions) + .build() + + if (resourceType != 'fictive' && resourceModes != 'inherited') { + testResource.accessDataset = testResource.accessDatasetBuilder + .setPublicAccess(testResource.url, testResourcePermissions) + .build() + } + + return testResource } """ - # Create 3 test resources with read access for public agents - * def testsR = callonce createResources ['read'] - # Create 3 test resources with append, write, control access for public agents - * def testsAWC = callonce createResources ['append', 'write', 'control'] - Scenario Outline: read a resource () to which a public agent has access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) + Scenario Outline: read a resource (), when a public agent has access to the container and access to the resource + * def testResource = createResource(container, resource, type) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) When method Then status Examples: - | agent | type | mode | method | public! | status | - | Bob can | plain | R | GET | false | 200 | - | Bob can | rdf | R | GET | false | 200 | - | Bob can | container | R | GET | false | 200 | - | Bob can | plain | R | HEAD | false | 200 | - | Bob can | rdf | R | HEAD | false | 200 | - | Bob can | container | R | HEAD | false | 200 | - | Bob cannot | plain | AWC | GET | false | 403 | - | Bob cannot | rdf | AWC | GET | false | 403 | - | Bob cannot | container | AWC | GET | false | 403 | - | Bob cannot | plain | AWC | HEAD | false | 403 | - | Bob cannot | rdf | AWC | HEAD | false | 403 | - | Bob cannot | container | AWC | HEAD | false | 403 | - | Public cannot | plain | AWC | GET | true | 401 | - | Public cannot | rdf | AWC | GET | true | 401 | - | Public cannot | container | AWC | GET | true | 401 | - | Public cannot | plain | AWC | HEAD | true | 401 | - | Public cannot | rdf | AWC | HEAD | true | 401 | - | Public cannot | container | AWC | HEAD | true | 401 | + | agent | result | method | type | container | resource | status | + | Bob | can | GET | plain | no | R | 200 | + | Bob | can | GET | plain | R | inherited | 200 | + | Bob | can | GET | fictive | R | inherited | 404 | + | Bob | can | GET | rdf | no | R | 200 | + | Bob | can | GET | rdf | R | inherited | 200 | + | Bob | can | GET | container | no | R | 200 | + | Bob | can | GET | container | R | inherited | 200 | + | Bob | can | HEAD | plain | no | R | 200 | + | Bob | can | HEAD | plain | R | inherited | 200 | + | Bob | can | HEAD | fictive | R | inherited | 404 | + | Bob | can | HEAD | rdf | no | R | 200 | + | Bob | can | HEAD | rdf | R | inherited | 200 | + | Bob | can | HEAD | container | no | R | 200 | + | Bob | can | HEAD | container | R | inherited | 200 | @publicagent Examples: - | agent | type | mode | method | public! | status | - | Public can | plain | R | GET | true | 200 | - | Public can | rdf | R | GET | true | 200 | - | Public can | container | R | GET | true | 200 | - | Public can | plain | R | HEAD | true | 200 | - | Public can | rdf | R | HEAD | true | 200 | - | Public can | container | R | HEAD | true | 200 | + | agent | result | method | type | container | resource | status | + | Public | can | GET | plain | no | R | 200 | + | Public | can | GET | plain | R | inherited | 200 | + | Public | can | GET | fictive | R | inherited | 404 | + | Public | can | GET | rdf | no | R | 200 | + | Public | can | GET | rdf | R | inherited | 200 | + | Public | can | GET | container | no | R | 200 | + | Public | can | GET | container | R | inherited | 200 | + | Public | can | HEAD | plain | no | R | 200 | + | Public | can | HEAD | plain | R | inherited | 200 | + | Public | can | HEAD | fictive | R | inherited | 404 | + | Public | can | HEAD | rdf | no | R | 200 | + | Public | can | HEAD | rdf | R | inherited | 200 | + | Public | can | HEAD | container | no | R | 200 | + | Public | can | HEAD | container | R | inherited | 200 | - Scenario Outline: cannot to a resource to which a public agent has access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) + Scenario Outline: to a resource, when a public agent has access to the container and access to the resource + * def testResource = createResource(container, resource, type) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) And header Content-Type = 'text/turtle' And request '@prefix rdfs: . <> rdfs:comment "Bob added this.".' When method Then status Examples: - | agent | type | mode | method | public! | status | - | Bob | rdf | R | PUT | false | 403 | - | Bob | container | R | PUT | false | 403 | - | Bob | rdf | R | POST | false | 403 | - | Bob | container | R | POST | false | 403 | - | Public | rdf | R | PUT | true | 401 | - | Public | container | R | PUT | true | 401 | - | Public | rdf | R | POST | true | 401 | - | Public | container | R | POST | true | 401 | + | agent | result | method | type | container | resource | status | + | Bob | cannot | PUT | rdf | no | R | 403 | + | Bob | cannot | PUT | rdf | R | inherited | 403 | + | Bob | cannot | PUT | fictive | R | inherited | 403 | + | Bob | cannot | PUT | container | no | R | 403 | + | Bob | cannot | PUT | container | R | inherited | 403 | + | Bob | cannot | POST | rdf | no | R | 403 | + | Bob | cannot | POST | rdf | R | inherited | 403 | + | Bob | cannot | POST | fictive | R | inherited | 404 | + | Bob | cannot | POST | container | no | R | 403 | + | Bob | cannot | POST | container | R | inherited | 403 | + | Public | cannot | PUT | rdf | no | R | 401 | + | Public | cannot | PUT | rdf | R | inherited | 401 | + | Public | cannot | PUT | fictive | R | inherited | 401 | + | Public | cannot | PUT | container | no | R | 401 | + | Public | cannot | PUT | container | R | inherited | 401 | + | Public | cannot | POST | rdf | no | R | 401 | + | Public | cannot | POST | rdf | R | inherited | 401 | + | Public | cannot | POST | fictive | R | inherited | 404 | + | Public | cannot | POST | container | no | R | 401 | + | Public | cannot | POST | container | R | inherited | 401 | - Scenario Outline: cannot to a resource to which a public agent has access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) + Scenario Outline: to a resource, when a public agent has access to the container and access to the resource + * def testResource = createResource(container, resource, type) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) And header Content-Type = 'text/n3' And request '@prefix solid: . _:insert a solid:InsertDeletePatch; solid:inserts { <> a . }.' When method Then status Examples: - | agent | type | mode | method | public! | status | - | Bob | rdf | R | PATCH | false | 403 | - | Bob | container | R | PATCH | false | 403 | - | Public | rdf | R | PATCH | true | 401 | - | Public | container | R | PATCH | true | 401 | + | agent | result | method | type | container | resource | status | + | Bob | cannot | PATCH | rdf | no | R | 403 | + | Bob | cannot | PATCH | rdf | R | inherited | 403 | + | Bob | cannot | PATCH | fictive | R | inherited | 403 | + | Bob | cannot | PATCH | container | no | R | 403 | + | Bob | cannot | PATCH | container | R | inherited | 403 | + | Public | cannot | PATCH | rdf | no | R | 401 | + | Public | cannot | PATCH | rdf | R | inherited | 401 | + | Public | cannot | PATCH | fictive | R | inherited | 401 | + | Public | cannot | PATCH | container | no | R | 401 | + | Public | cannot | PATCH | container | R | inherited | 401 | - Scenario Outline: cannot to a resource to which a public agent has access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) + Scenario Outline: to a resource, when a public agent has access to the container and access to the resource + * def testResource = createResource(container, resource, type) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) And header Content-Type = 'text/plain' And request "Bob's text" When method Then match contains responseStatus Examples: - | agent | type | mode | method | public! | status | - | Bob | plain | R | PUT | false | [403] | - | Bob | plain | R | POST | false | [403] | - | Bob | plain | R | PATCH | false | [403, 405, 415] | - | Public | plain | R | PUT | true | [401] | - | Public | plain | R | POST | true | [401] | - | Public | plain | R | PATCH | true | [401, 405, 415] | + | agent | result | method | type | container | resource | status | + | Bob | cannot | PUT | plain | no | R | [403] | + | Bob | cannot | PUT | plain | R | inherited | [403] | + | Bob | cannot | PUT | fictive | R | inherited | [403] | + | Bob | cannot | POST | plain | no | R | [403] | + | Bob | cannot | POST | plain | R | inherited | [403] | + | Bob | cannot | POST | fictive | R | inherited | [404] | + | Bob | cannot | PATCH | plain | no | R | [403, 405, 415] | + | Bob | cannot | PATCH | plain | R | inherited | [403, 405, 415] | + | Bob | cannot | PATCH | fictive | R | inherited | [403, 405, 415] | + | Public | cannot | PUT | plain | no | R | [401] | + | Public | cannot | PUT | plain | R | inherited | [401] | + | Public | cannot | PUT | fictive | R | inherited | [401] | + | Public | cannot | POST | plain | no | R | [401] | + | Public | cannot | POST | plain | R | inherited | [401] | + | Public | cannot | POST | fictive | R | inherited | [404] | + | Public | cannot | PATCH | plain | no | R | [401, 405, 415] | + | Public | cannot | PATCH | plain | R | inherited | [401, 405, 415] | + | Public | cannot | PATCH | fictive | R | inherited | [401, 405, 415] | - Scenario Outline: cannot a resource to which a public agent has access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) + Scenario Outline: a resource, when a public agent has access to the container and access to the resource + * def testResource = createResource(container, resource, type) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) When method Then status Examples: - | agent | type | mode | method | public! | status | - | Bob | plain | R | DELETE | false | 403 | - | Bob | rdf | R | DELETE | false | 403 | - | Bob | container | R | DELETE | false | 403 | - | Public | plain | R | DELETE | true | 401 | - | Public | rdf | R | DELETE | true | 401 | - | Public | container | R | DELETE | true | 401 | + | agent | result | method | type | container | resource | status | + | Bob | cannot | DELETE | plain | no | R | 403 | + | Bob | cannot | DELETE | plain | R | inherited | 403 | + | Bob | cannot | DELETE | fictive | R | inherited | 404 | + | Bob | cannot | DELETE | rdf | no | R | 403 | + | Bob | cannot | DELETE | rdf | R | inherited | 403 | + | Bob | cannot | DELETE | container | no | R | 403 | + | Bob | cannot | DELETE | container | R | inherited | 403 | + | Public | cannot | DELETE | plain | no | R | 401 | + | Public | cannot | DELETE | plain | R | inherited | 401 | + | Public | cannot | DELETE | fictive | R | inherited | 404 | + | Public | cannot | DELETE | rdf | no | R | 401 | + | Public | cannot | DELETE | rdf | R | inherited | 401 | + | Public | cannot | DELETE | container | no | R | 401 | + | Public | cannot | DELETE | container | R | inherited | 401 | diff --git a/web-access-control/protected-operation/read-inherited-access-agent.feature b/web-access-control/protected-operation/read-inherited-access-agent.feature deleted file mode 100644 index a6f85e0..0000000 --- a/web-access-control/protected-operation/read-inherited-access-agent.feature +++ /dev/null @@ -1,117 +0,0 @@ -Feature: Only authenticated agents can read (and only that) a resource when granted inherited read access - # Grant authenticated agents (setAuthenticatedAccess/setInheritableAuthenticatedAccess): - # - full access to the parent container (to ensure the tests are specific to the resource) - # - restricted access to the any contained resources via the parent - Background: Create test resources with correct access modes - * def authHeaders = (method, url, public) => !public ? clients.bob.getAuthHeaders(method, url) : {} - * def createResources = - """ - function (modes) { - const testContainer = rootTestContainer.createContainer() - testContainer.accessDataset = testContainer.accessDatasetBuilder - .setAuthenticatedAccess(testContainer.url, ['read', 'write', 'append', 'control']) - .setInheritableAuthenticatedAccess(testContainer.url, modes).build() - const plainResource = testContainer.createResource('.txt', 'Hello', 'text/plain') - const rdfResource = testContainer.createResource('.ttl', karate.readAsString('../fixtures/example.ttl'), 'text/turtle') - const container = testContainer.createContainer() - return { plain: plainResource, rdf: rdfResource, container: container } - } - """ - # Create 3 test resources with read access for authenticated agents - * def testsR = callonce createResources ['read'] - # Create 3 test resources with append, write, control access for authenticated agents - * def testsAWC = callonce createResources ['append', 'write', 'control'] - - Scenario Outline: read a resource () to which an authenticated agent has inherited access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) - When method - Then status - Examples: - | agent | type | mode | method | public! | status | - | Bob can | plain | R | GET | false | 200 | - | Bob can | rdf | R | GET | false | 200 | - | Bob can | container | R | GET | false | 200 | - | Bob can | plain | R | HEAD | false | 200 | - | Bob can | rdf | R | HEAD | false | 200 | - | Bob can | container | R | HEAD | false | 200 | - | Public cannot | plain | R | GET | true | 401 | - | Public cannot | rdf | R | GET | true | 401 | - | Public cannot | container | R | GET | true | 401 | - | Public cannot | plain | R | HEAD | true | 401 | - | Public cannot | rdf | R | HEAD | true | 401 | - | Public cannot | container | R | HEAD | true | 401 | - | Bob cannot | plain | AWC | GET | false | 403 | - | Bob cannot | rdf | AWC | GET | false | 403 | - | Bob cannot | container | AWC | GET | false | 403 | - | Bob cannot | plain | AWC | HEAD | false | 403 | - | Bob cannot | rdf | AWC | HEAD | false | 403 | - | Bob cannot | container | AWC | HEAD | false | 403 | - | Public cannot | plain | AWC | GET | true | 401 | - | Public cannot | rdf | AWC | GET | true | 401 | - | Public cannot | container | AWC | GET | true | 401 | - | Public cannot | plain | AWC | HEAD | true | 401 | - | Public cannot | rdf | AWC | HEAD | true | 401 | - | Public cannot | container | AWC | HEAD | true | 401 | - - Scenario Outline: cannot to a resource to which an authenticated agent has inherited access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) - And header Content-Type = 'text/turtle' - And request '@prefix rdfs: . <> rdfs:comment "Bob added this.".' - When method - Then status - Examples: - | agent | type | mode | method | public! | status | - | Bob | rdf | R | PUT | false | 403 | - | Bob | container | R | PUT | false | 403 | - | Bob | rdf | R | POST | false | 403 | - | Bob | container | R | POST | false | 403 | - | Public | rdf | R | PUT | true | 401 | - | Public | container | R | PUT | true | 401 | - | Public | rdf | R | POST | true | 401 | - | Public | container | R | POST | true | 401 | - - Scenario Outline: cannot to a resource to which an authenticated agent has inherited access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) - And header Content-Type = 'text/n3' - And request '@prefix solid: . _:insert a solid:InsertDeletePatch; solid:inserts { <> a . }.' - When method - Then status - Examples: - | agent | type | mode | method | public! | status | - | Bob | rdf | R | PATCH | false | 403 | - | Bob | container | R | PATCH | false | 403 | - | Public | rdf | R | PATCH | true | 401 | - | Public | container | R | PATCH | true | 401 | - - Scenario Outline: cannot to a resource to which an authenticated agent has inherited access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) - And header Content-Type = 'text/plain' - And request "Bob's text" - When method - Then match contains responseStatus - Examples: - | agent | type | mode | method | public! | status | - | Bob | plain | R | PUT | false | [403] | - | Bob | plain | R | POST | false | [403] | - | Bob | plain | R | PATCH | false | [403, 405, 415] | - | Public | plain | R | PUT | true | [401] | - | Public | plain | R | POST | true | [401] | - | Public | plain | R | PATCH | true | [401, 405, 415] | - - Scenario Outline: cannot a resource to which an authenticated agent has inherited access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) - When method - Then status - Examples: - | agent | type | mode | method | public! | status | - | Bob | plain | R | DELETE | false | 403 | - | Bob | rdf | R | DELETE | false | 403 | - | Bob | container | R | DELETE | false | 403 | - | Public | plain | R | DELETE | true | 401 | - | Public | rdf | R | DELETE | true | 401 | - | Public | container | R | DELETE | true | 401 | diff --git a/web-access-control/protected-operation/read-inherited-access-bob.feature b/web-access-control/protected-operation/read-inherited-access-bob.feature deleted file mode 100644 index 1298968..0000000 --- a/web-access-control/protected-operation/read-inherited-access-bob.feature +++ /dev/null @@ -1,117 +0,0 @@ -Feature: Only Bob can read (and only that) a resource when granted inherited read access - # Grant a specific agent (setAgentAccess/setInheritableAgentAccess): - # - full access to the parent container (to ensure the tests are specific to the resource) - # - restricted access to the any contained resources via the parent - Background: Create test resources with correct access modes - * def authHeaders = (method, url, public) => !public ? clients.bob.getAuthHeaders(method, url) : {} - * def createResources = - """ - function (modes) { - const testContainer = rootTestContainer.createContainer() - testContainer.accessDataset = testContainer.accessDatasetBuilder - .setAgentAccess(testContainer.url, webIds.bob, ['read', 'write', 'append', 'control']) - .setInheritableAgentAccess(testContainer.url, webIds.bob, modes).build() - const plainResource = testContainer.createResource('.txt', 'Hello', 'text/plain') - const rdfResource = testContainer.createResource('.ttl', karate.readAsString('../fixtures/example.ttl'), 'text/turtle') - const container = testContainer.createContainer() - return { plain: plainResource, rdf: rdfResource, container: container } - } - """ - # Create 3 test resources with read access for Bob - * def testsR = callonce createResources ['read'] - # Create 3 test resources with append, write, control access for Bob - * def testsAWC = callonce createResources ['append', 'write', 'control'] - - Scenario Outline: read a resource () to which Bob has inherited access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) - When method - Then status - Examples: - | agent | type | mode | method | public! | status | - | Bob can | plain | R | GET | false | 200 | - | Bob can | rdf | R | GET | false | 200 | - | Bob can | container | R | GET | false | 200 | - | Bob can | plain | R | HEAD | false | 200 | - | Bob can | rdf | R | HEAD | false | 200 | - | Bob can | container | R | HEAD | false | 200 | - | Public cannot | plain | R | GET | true | 401 | - | Public cannot | rdf | R | GET | true | 401 | - | Public cannot | container | R | GET | true | 401 | - | Public cannot | plain | R | HEAD | true | 401 | - | Public cannot | rdf | R | HEAD | true | 401 | - | Public cannot | container | R | HEAD | true | 401 | - | Bob cannot | plain | AWC | GET | false | 403 | - | Bob cannot | rdf | AWC | GET | false | 403 | - | Bob cannot | container | AWC | GET | false | 403 | - | Bob cannot | plain | AWC | HEAD | false | 403 | - | Bob cannot | rdf | AWC | HEAD | false | 403 | - | Bob cannot | container | AWC | HEAD | false | 403 | - | Public cannot | plain | AWC | GET | true | 401 | - | Public cannot | rdf | AWC | GET | true | 401 | - | Public cannot | container | AWC | GET | true | 401 | - | Public cannot | plain | AWC | HEAD | true | 401 | - | Public cannot | rdf | AWC | HEAD | true | 401 | - | Public cannot | container | AWC | HEAD | true | 401 | - - Scenario Outline: cannot to a resource to which Bob has inherited access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) - And header Content-Type = 'text/turtle' - And request '@prefix rdfs: . <> rdfs:comment "Bob added this.".' - When method - Then status - Examples: - | agent | type | mode | method | public! | status | - | Bob | rdf | R | PUT | false | 403 | - | Bob | container | R | PUT | false | 403 | - | Bob | rdf | R | POST | false | 403 | - | Bob | container | R | POST | false | 403 | - | Public | rdf | R | PUT | true | 401 | - | Public | container | R | PUT | true | 401 | - | Public | rdf | R | POST | true | 401 | - | Public | container | R | POST | true | 401 | - - Scenario Outline: cannot to a resource to which Bob has inherited access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) - And header Content-Type = 'text/n3' - And request '@prefix solid: . _:insert a solid:InsertDeletePatch; solid:inserts { <> a . }.' - When method - Then status - Examples: - | agent | type | mode | method | public! | status | - | Bob | rdf | R | PATCH | false | 403 | - | Bob | container | R | PATCH | false | 403 | - | Public | rdf | R | PATCH | true | 401 | - | Public | container | R | PATCH | true | 401 | - - Scenario Outline: cannot to a resource to which Bob has inherited access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) - And header Content-Type = 'text/plain' - And request "Bob's text" - When method - Then match contains responseStatus - Examples: - | agent | type | mode | method | public! | status | - | Bob | plain | R | PUT | false | [403] | - | Bob | plain | R | POST | false | [403] | - | Bob | plain | R | PATCH | false | [403, 405, 415] | - | Public | plain | R | PUT | true | [401] | - | Public | plain | R | POST | true | [401] | - | Public | plain | R | PATCH | true | [401, 405, 415] | - - Scenario Outline: cannot a resource to which Bob has inherited access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) - When method - Then status - Examples: - | agent | type | mode | method | public! | status | - | Bob | plain | R | DELETE | false | 403 | - | Bob | rdf | R | DELETE | false | 403 | - | Bob | container | R | DELETE | false | 403 | - | Public | plain | R | DELETE | true | 401 | - | Public | rdf | R | DELETE | true | 401 | - | Public | container | R | DELETE | true | 401 | diff --git a/web-access-control/protected-operation/read-inherited-access-public.feature b/web-access-control/protected-operation/read-inherited-access-public.feature deleted file mode 100644 index 1ca5f0d..0000000 --- a/web-access-control/protected-operation/read-inherited-access-public.feature +++ /dev/null @@ -1,121 +0,0 @@ -Feature: Public agents can read (and only that) a resource when granted inherited read access - # Grant public agents (setPublicAccess/setInheritablePublicAccess): - # - full access to the parent container (to ensure the tests are specific to the resource) - # - restricted access to the any contained resources via the parent - Background: Create test resources with correct access modes - * def authHeaders = (method, url, public) => !public ? clients.bob.getAuthHeaders(method, url) : {} - * def createResources = - """ - function (modes) { - const testContainer = rootTestContainer.createContainer() - testContainer.accessDataset = testContainer.accessDatasetBuilder - .setPublicAccess(testContainer.url, ['read', 'write', 'append', 'control']) - .setInheritablePublicAccess(testContainer.url, modes).build() - const plainResource = testContainer.createResource('.txt', 'Hello', 'text/plain') - const rdfResource = testContainer.createResource('.ttl', karate.readAsString('../fixtures/example.ttl'), 'text/turtle') - const container = testContainer.createContainer() - return { plain: plainResource, rdf: rdfResource, container: container } - } - """ - # Create 3 test resources with read access for public agents - * def testsR = callonce createResources ['read'] - # Create 3 test resources with append, write, control access for public agents - * def testsAWC = callonce createResources ['append', 'write', 'control'] - - Scenario Outline: read a resource () to which a public agent has inherited access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) - When method - Then status - Examples: - | agent | type | mode | method | public! | status | - | Bob can | plain | R | GET | false | 200 | - | Bob can | rdf | R | GET | false | 200 | - | Bob can | container | R | GET | false | 200 | - | Bob can | plain | R | HEAD | false | 200 | - | Bob can | rdf | R | HEAD | false | 200 | - | Bob can | container | R | HEAD | false | 200 | - | Bob cannot | plain | AWC | GET | false | 403 | - | Bob cannot | rdf | AWC | GET | false | 403 | - | Bob cannot | container | AWC | GET | false | 403 | - | Bob cannot | plain | AWC | HEAD | false | 403 | - | Bob cannot | rdf | AWC | HEAD | false | 403 | - | Bob cannot | container | AWC | HEAD | false | 403 | - | Public cannot | plain | AWC | GET | true | 401 | - | Public cannot | rdf | AWC | GET | true | 401 | - | Public cannot | container | AWC | GET | true | 401 | - | Public cannot | plain | AWC | HEAD | true | 401 | - | Public cannot | rdf | AWC | HEAD | true | 401 | - | Public cannot | container | AWC | HEAD | true | 401 | - - @publicagent - Examples: - | agent | type | mode | method | public! | status | - | Public can | plain | R | GET | true | 200 | - | Public can | rdf | R | GET | true | 200 | - | Public can | container | R | GET | true | 200 | - | Public can | plain | R | HEAD | true | 200 | - | Public can | rdf | R | HEAD | true | 200 | - | Public can | container | R | HEAD | true | 200 | - - Scenario Outline: cannot to a resource to which a public agent has inherited access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) - And header Content-Type = 'text/turtle' - And request '@prefix rdfs: . <> rdfs:comment "Bob added this.".' - When method - Then status - Examples: - | agent | type | mode | method | public! | status | - | Bob | rdf | R | PUT | false | 403 | - | Bob | container | R | PUT | false | 403 | - | Bob | rdf | R | POST | false | 403 | - | Bob | container | R | POST | false | 403 | - | Public | rdf | R | PUT | true | 401 | - | Public | container | R | PUT | true | 401 | - | Public | rdf | R | POST | true | 401 | - | Public | container | R | POST | true | 401 | - - Scenario Outline: cannot to a resource to which a public agent has inherited access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) - And header Content-Type = 'text/n3' - And request '@prefix solid: . _:insert a solid:InsertDeletePatch; solid:inserts { <> a . }.' - When method - Then status - Examples: - | agent | type | mode | method | public! | status | - | Bob | rdf | R | PATCH | false | 403 | - | Bob | container | R | PATCH | false | 403 | - | Public | rdf | R | PATCH | true | 401 | - | Public | container | R | PATCH | true | 401 | - - Scenario Outline: cannot to a resource to which a public agent has inherited access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) - And header Content-Type = 'text/plain' - And request "Bob's text" - When method - Then match contains responseStatus - Examples: - | agent | type | mode | method | public! | status | - | Bob | plain | R | PUT | false | [403] | - | Bob | plain | R | POST | false | [403] | - | Bob | plain | R | PATCH | false | [403, 405, 415] | - | Public | plain | R | PUT | true | [401] | - | Public | plain | R | POST | true | [401] | - | Public | plain | R | PATCH | true | [401, 405, 415] | - - Scenario Outline: cannot a resource to which a public agent has inherited access - Given url tests[type].url - And headers authHeaders(method, tests[type].url, public) - When method - Then status - Examples: - | agent | type | mode | method | public! | status | - | Bob | plain | R | DELETE | false | 403 | - | Bob | rdf | R | DELETE | false | 403 | - | Bob | container | R | DELETE | false | 403 | - | Public | plain | R | DELETE | true | 401 | - | Public | rdf | R | DELETE | true | 401 | - | Public | container | R | DELETE | true | 401 | diff --git a/web-access-control/protected-operation/write-access-agent.feature b/web-access-control/protected-operation/write-access-agent.feature new file mode 100644 index 0000000..35ddc20 --- /dev/null +++ b/web-access-control/protected-operation/write-access-agent.feature @@ -0,0 +1,230 @@ +Feature: Only authenticated agents can write (and only that) a resource when granted write access + # Grant authenticated agents (setAuthenticatedAccess): + # - restricted access or no access to the parent container + # - restricted access to the test resources, or inherited access for fictive resources + Background: Create test resources with correct access modes + * def authHeaders = + """ + function (method, url, agent) { + const agentLowerCase = agent.toLowerCase() + return agentLowerCase != 'public' ? clients[agentLowerCase].getAuthHeaders(method, url) : {} + } + """ + * def resourcePermissions = + """ + function (modes) { + if (modes && modes != 'inherited' && modes != 'no') { + return Object.entries({ R: 'read', W: 'write', A: 'append', C: 'control' }) + .filter(([mode, permission]) => modes.includes(mode)) + .map(([mode, permission]) => permission) + } + return undefined + } + """ + * def getRequestData = + """ + function (type) { + switch(type) { + case 'rdf': + return { + contentType: 'text/turtle', + requestBody: '<> "Bob replaced it." .', + responseShouldNotContain: "Bob replaced it" + } + default: + return { + contentType: 'text/plain', + requestBody: "Bob's text", + responseShouldNotContain: "Bob's text" + } + } + } + """ + * def resourceEntry = + """ + function (container, type) { + switch (type) { + case 'plain': + return container.createResource('.txt', 'Hello', 'text/plain') + case 'fictive': + return container.reserveResource('.txt') + case 'rdf': + return container.createResource('.ttl', karate.readAsString('../fixtures/example.ttl'), 'text/turtle') + case 'container': + return container.createContainer() + default: + return undefined + } + } + """ + * def createResource = + """ + function (containerModes, resourceModes, resourceType) { + + const testContainerPermissions = resourcePermissions(containerModes == 'all' ? 'RWAC' : containerModes) + const testResourcePermissions = resourcePermissions(resourceModes) + + const testContainerInheritablePermissions = resourceModes == 'inherited' + ? testContainerPermissions + : resourceType == 'fictive' + ? testResourcePermissions + : undefined + + const testContainer = rootTestContainer.createContainer() + const testResource = resourceEntry(testContainer, resourceType) + + testContainer.accessDataset = testContainer.accessDatasetBuilder + .setAuthenticatedAccess(testContainer.url, testContainerPermissions) + .setInheritableAuthenticatedAccess(testContainer.url, testContainerInheritablePermissions) + .build() + + if (resourceType != 'fictive' && resourceModes != 'inherited') { + testResource.accessDataset = testResource.accessDatasetBuilder + .setAuthenticatedAccess(testResource.url, testResourcePermissions) + .build() + } + + return testResource + } + """ + + Scenario Outline: read a resource (), when an authenticated agent has access to the container and access to the resource + * def testResource = createResource(container, resource, type) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) + When method + Then status + Examples: + | agent | result | method | type | container | resource | status | + | Bob | cannot | GET | plain | no | AWC | 403 | + | Bob | cannot | GET | plain | AWC | inherited | 403 | + | Bob | cannot | GET | fictive | AWC | inherited | 403 | + | Bob | cannot | GET | rdf | no | AWC | 403 | + | Bob | cannot | GET | rdf | AWC | inherited | 403 | + | Bob | cannot | GET | container | no | AWC | 403 | + | Bob | cannot | GET | container | AWC | inherited | 403 | + | Bob | cannot | HEAD | plain | no | AWC | 403 | + | Bob | cannot | HEAD | plain | AWC | inherited | 403 | + | Bob | cannot | HEAD | fictive | AWC | inherited | 403 | + | Bob | cannot | HEAD | rdf | no | AWC | 403 | + | Bob | cannot | HEAD | rdf | AWC | inherited | 403 | + | Bob | cannot | HEAD | container | no | AWC | 403 | + | Bob | cannot | HEAD | container | AWC | inherited | 403 | + | Public | cannot | GET | plain | no | AWC | 401 | + | Public | cannot | GET | plain | AWC | inherited | 401 | + | Public | cannot | GET | fictive | AWC | inherited | 401 | + | Public | cannot | GET | rdf | no | AWC | 401 | + | Public | cannot | GET | rdf | AWC | inherited | 401 | + | Public | cannot | GET | container | no | AWC | 401 | + | Public | cannot | GET | container | AWC | inherited | 401 | + | Public | cannot | HEAD | plain | no | AWC | 401 | + | Public | cannot | HEAD | plain | AWC | inherited | 401 | + | Public | cannot | HEAD | fictive | AWC | inherited | 401 | + | Public | cannot | HEAD | rdf | no | AWC | 401 | + | Public | cannot | HEAD | rdf | AWC | inherited | 401 | + | Public | cannot | HEAD | container | no | AWC | 401 | + | Public | cannot | HEAD | container | AWC | inherited | 401 | + + Scenario Outline: write a resource () and cannot read it, when an authenticated agent has access to the container and access to the resource + * def testResource = createResource(container, resource, type) + * def requestData = getRequestData(type) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) + And header Content-Type = requestData.contentType + And request requestData.requestBody + When method + Then match contains responseStatus + # Server may return payload with information about the operation e.g. "Created" so check it hasn't leaked the data which was PUT + And string responseString = response + And match responseString !contains requestData.responseShouldNotContain + + Given headers authHeaders('GET', testResource.url, agent) + When method GET + Then status + + Examples: + | agent | result | method | type | container | resource | writeStatus | readStatus | + | Bob | can | PUT | rdf | no | W | [201, 204, 205] | 403 | + | Bob | can | PUT | rdf | W | inherited | [201, 204, 205] | 403 | + | Bob | can | PUT | plain | no | W | [201, 204, 205] | 403 | + | Bob | can | PUT | plain | W | inherited | [201, 204, 205] | 403 | + | Bob | can | PUT | fictive | W | inherited | [201, 204, 205] | 403 | + | Bob | can | POST | container | no | W | [201, 204, 205] | 403 | + | Bob | can | POST | container | W | inherited | [201, 204, 205] | 403 | + | Bob | can | POST | container | no | A | [201, 204, 205] | 403 | + | Bob | can | POST | container | A | inherited | [201, 204, 205] | 403 | + | Public | cannot | PUT | rdf | no | WAC | [401] | 401 | + | Public | cannot | PUT | rdf | WAC | inherited | [401] | 401 | + | Public | cannot | PUT | plain | no | WAC | [401] | 401 | + | Public | cannot | PUT | plain | WAC | inherited | [401] | 401 | + | Public | cannot | PUT | fictive | WAC | inherited | [401] | 401 | + | Public | cannot | POST | container | no | WAC | [401] | 401 | + | Public | cannot | POST | container | WAC | inherited | [401] | 401 | + + Scenario Outline: to a resource, when an authenticated agent has access to the container and access to the resource + * def testResource = createResource(container, resource, type) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) + And header Content-Type = 'text/n3' + And request '@prefix solid: . _:insert a solid:InsertDeletePatch; solid:inserts { <> a . }.' + When method + Then match contains responseStatus + # Server may return payload with information about the operation e.g. "Created" so check it hasn't leaked the data which was PUT + And string responseString = response + And match responseString !contains 'http://example.org#Foo' + + Given headers authHeaders('GET', testResource.url, agent) + When method GET + Then status + + Examples: + | agent | result | method | type | container | resource | writeStatus | readStatus | + | Bob | can | PATCH | rdf | no | W | [201, 204, 205] | 403 | + | Bob | can | PATCH | rdf | W | inherited | [201, 204, 205] | 403 | + | Bob | can | PATCH | fictive | W | inherited | [201, 204, 205] | 403 | + | Bob | can | PATCH | rdf | no | A | [201, 204, 205] | 403 | + | Bob | can | PATCH | rdf | A | inherited | [201, 204, 205] | 403 | + | Bob | can | PATCH | fictive | A | inherited | [201, 204, 205] | 403 | + | Bob | cannot | PATCH | rdf | no | C | [403] | 403 | + | Bob | cannot | PATCH | rdf | C | inherited | [403] | 403 | + | Bob | cannot | PATCH | fictive | C | inherited | [403] | 403 | + | Public | cannot | PATCH | rdf | no | WAC | [401] | 401 | + | Public | cannot | PATCH | rdf | WAC | inherited | [401] | 401 | + | Public | cannot | PATCH | fictive | WAC | inherited | [401] | 401 | + + Scenario Outline: a resource, when an authenticated agent has access to the container and access to the resource + * def testResource = createResource(container, resource, type) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) + When method + Then status + Examples: + | agent | result | method | type | container | resource | status | + | Bob | cannot | DELETE | plain | no | C | 403 | + | Bob | cannot | DELETE | plain | C | inherited | 403 | + | Bob | cannot | DELETE | fictive | C | inherited | 403 | + | Bob | cannot | DELETE | rdf | no | C | 403 | + | Bob | cannot | DELETE | rdf | C | inherited | 403 | + | Bob | cannot | DELETE | container | no | C | 403 | + | Bob | cannot | DELETE | container | C | inherited | 403 | + | Bob | cannot | DELETE | plain | no | A | 403 | + | Bob | cannot | DELETE | plain | A | inherited | 403 | + | Bob | cannot | DELETE | fictive | A | inherited | 403 | + | Bob | cannot | DELETE | rdf | no | A | 403 | + | Bob | cannot | DELETE | rdf | A | inherited | 403 | + | Bob | cannot | DELETE | container | no | A | 403 | + | Bob | cannot | DELETE | container | A | inherited | 403 | + | Bob | cannot | DELETE | plain | no | W | 403 | + | Bob | can | DELETE | plain | W | inherited | 205 | + | Bob | cannot | DELETE | fictive | W | inherited | 404 | + | Bob | cannot | DELETE | rdf | no | W | 403 | + | Bob | can | DELETE | rdf | W | inherited | 205 | + | Bob | cannot | DELETE | container | no | W | 403 | + | Bob | cannot | DELETE | container | W | inherited | 403 | + | Public | cannot | DELETE | plain | no | WAC | 401 | + | Public | cannot | DELETE | plain | WAC | inherited | 401 | + | Public | cannot | DELETE | fictive | WAC | inherited | 401 | + | Public | cannot | DELETE | rdf | no | WAC | 401 | + | Public | cannot | DELETE | rdf | WAC | inherited | 401 | + | Public | cannot | DELETE | container | no | WAC | 401 | + | Public | cannot | DELETE | container | WAC | inherited | 401 | diff --git a/web-access-control/protected-operation/write-access-bob.feature b/web-access-control/protected-operation/write-access-bob.feature new file mode 100644 index 0000000..d30589a --- /dev/null +++ b/web-access-control/protected-operation/write-access-bob.feature @@ -0,0 +1,232 @@ +Feature: Only Bob can write (and only that) a resource when granted write access + # Grant a specific agent (setAgentAccess): + # - restricted access or no access to the parent container + # - restricted access to the test resources, or inherited access for fictive resources + Background: Create test resources with correct access modes + * def authHeaders = + """ + function (method, url, agent) { + const agentLowerCase = agent.toLowerCase() + return agentLowerCase != 'public' ? clients[agentLowerCase].getAuthHeaders(method, url) : {} + } + """ + * def resourcePermissions = + """ + function (modes) { + if (modes && modes != 'inherited' && modes != 'no') { + return Object.entries({ R: 'read', W: 'write', A: 'append', C: 'control' }) + .filter(([mode, permission]) => modes.includes(mode)) + .map(([mode, permission]) => permission) + } + return undefined + } + """ + * def getRequestData = + """ + function (type) { + switch(type) { + case 'rdf': + return { + contentType: 'text/turtle', + requestBody: '<> "Bob replaced it." .', + responseShouldNotContain: "Bob replaced it" + } + default: + return { + contentType: 'text/plain', + requestBody: "Bob's text", + responseShouldNotContain: "Bob's text" + } + } + } + """ + * def resourceEntry = + """ + function (container, type) { + switch (type) { + case 'plain': + return container.createResource('.txt', 'Hello', 'text/plain') + case 'fictive': + return container.reserveResource('.txt') + case 'rdf': + return container.createResource('.ttl', karate.readAsString('../fixtures/example.ttl'), 'text/turtle') + case 'container': + return container.createContainer() + default: + return undefined + } + } + """ + * def createResource = + """ + function (containerModes, resourceModes, resourceType, agent) { + + const agentWebId = webIds.bob + + const testContainerPermissions = resourcePermissions(containerModes == 'all' ? 'RWAC' : containerModes) + const testResourcePermissions = resourcePermissions(resourceModes) + + const testContainerInheritablePermissions = resourceModes == 'inherited' + ? testContainerPermissions + : resourceType == 'fictive' + ? testResourcePermissions + : undefined + + const testContainer = rootTestContainer.createContainer() + const testResource = resourceEntry(testContainer, resourceType) + + testContainer.accessDataset = testContainer.accessDatasetBuilder + .setAgentAccess(testContainer.url, agentWebId, testContainerPermissions) + .setInheritableAgentAccess(testContainer.url, agentWebId, testContainerInheritablePermissions) + .build() + + if (resourceType != 'fictive' && resourceModes != 'inherited') { + testResource.accessDataset = testResource.accessDatasetBuilder + .setAgentAccess(testResource.url, agentWebId, testResourcePermissions) + .build() + } + + return testResource + } + """ + + Scenario Outline: read a resource (), when an authenticated agent has access to the container and access to the resource + * def testResource = createResource(container, resource, type) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) + When method + Then status + Examples: + | agent | result | method | type | container | resource | status | + | Bob | cannot | GET | plain | no | AWC | 403 | + | Bob | cannot | GET | plain | AWC | inherited | 403 | + | Bob | cannot | GET | fictive | AWC | inherited | 403 | + | Bob | cannot | GET | rdf | no | AWC | 403 | + | Bob | cannot | GET | rdf | AWC | inherited | 403 | + | Bob | cannot | GET | container | no | AWC | 403 | + | Bob | cannot | GET | container | AWC | inherited | 403 | + | Bob | cannot | HEAD | plain | no | AWC | 403 | + | Bob | cannot | HEAD | plain | AWC | inherited | 403 | + | Bob | cannot | HEAD | fictive | AWC | inherited | 403 | + | Bob | cannot | HEAD | rdf | no | AWC | 403 | + | Bob | cannot | HEAD | rdf | AWC | inherited | 403 | + | Bob | cannot | HEAD | container | no | AWC | 403 | + | Bob | cannot | HEAD | container | AWC | inherited | 403 | + | Public | cannot | GET | plain | no | AWC | 401 | + | Public | cannot | GET | plain | AWC | inherited | 401 | + | Public | cannot | GET | fictive | AWC | inherited | 401 | + | Public | cannot | GET | rdf | no | AWC | 401 | + | Public | cannot | GET | rdf | AWC | inherited | 401 | + | Public | cannot | GET | container | no | AWC | 401 | + | Public | cannot | GET | container | AWC | inherited | 401 | + | Public | cannot | HEAD | plain | no | AWC | 401 | + | Public | cannot | HEAD | plain | AWC | inherited | 401 | + | Public | cannot | HEAD | fictive | AWC | inherited | 401 | + | Public | cannot | HEAD | rdf | no | AWC | 401 | + | Public | cannot | HEAD | rdf | AWC | inherited | 401 | + | Public | cannot | HEAD | container | no | AWC | 401 | + | Public | cannot | HEAD | container | AWC | inherited | 401 | + + Scenario Outline: write a resource () and cannot read it, when an authenticated agent has access to the container and access to the resource + * def testResource = createResource(container, resource, type) + * def requestData = getRequestData(type) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) + And header Content-Type = requestData.contentType + And request requestData.requestBody + When method + Then match contains responseStatus + # Server may return payload with information about the operation e.g. "Created" so check it hasn't leaked the data which was PUT + And string responseString = response + And match responseString !contains requestData.responseShouldNotContain + + Given headers authHeaders('GET', testResource.url, agent) + When method GET + Then status + + Examples: + | agent | result | method | type | container | resource | writeStatus | readStatus | + | Bob | can | PUT | rdf | no | W | [201, 204, 205] | 403 | + | Bob | can | PUT | rdf | W | inherited | [201, 204, 205] | 403 | + | Bob | can | PUT | plain | no | W | [201, 204, 205] | 403 | + | Bob | can | PUT | plain | W | inherited | [201, 204, 205] | 403 | + | Bob | can | PUT | fictive | W | inherited | [201, 204, 205] | 403 | + | Bob | can | POST | container | no | W | [201, 204, 205] | 403 | + | Bob | can | POST | container | W | inherited | [201, 204, 205] | 403 | + | Bob | can | POST | container | no | A | [201, 204, 205] | 403 | + | Bob | can | POST | container | A | inherited | [201, 204, 205] | 403 | + | Public | cannot | PUT | rdf | no | WAC | [401] | 401 | + | Public | cannot | PUT | rdf | WAC | inherited | [401] | 401 | + | Public | cannot | PUT | plain | no | WAC | [401] | 401 | + | Public | cannot | PUT | plain | WAC | inherited | [401] | 401 | + | Public | cannot | PUT | fictive | WAC | inherited | [401] | 401 | + | Public | cannot | POST | container | no | WAC | [401] | 401 | + | Public | cannot | POST | container | WAC | inherited | [401] | 401 | + + Scenario Outline: to a resource, when an authenticated agent has access to the container and access to the resource + * def testResource = createResource(container, resource, type) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) + And header Content-Type = 'text/n3' + And request '@prefix solid: . _:insert a solid:InsertDeletePatch; solid:inserts { <> a . }.' + When method + Then match contains responseStatus + # Server may return payload with information about the operation e.g. "Created" so check it hasn't leaked the data which was PUT + And string responseString = response + And match responseString !contains 'http://example.org#Foo' + + Given headers authHeaders('GET', testResource.url, agent) + When method GET + Then status + + Examples: + | agent | result | method | type | container | resource | writeStatus | readStatus | + | Bob | can | PATCH | rdf | no | W | [201, 204, 205] | 403 | + | Bob | can | PATCH | rdf | W | inherited | [201, 204, 205] | 403 | + | Bob | can | PATCH | fictive | W | inherited | [201, 204, 205] | 403 | + | Bob | can | PATCH | rdf | no | A | [201, 204, 205] | 403 | + | Bob | can | PATCH | rdf | A | inherited | [201, 204, 205] | 403 | + | Bob | can | PATCH | fictive | A | inherited | [201, 204, 205] | 403 | + | Bob | cannot | PATCH | rdf | no | C | [403] | 403 | + | Bob | cannot | PATCH | rdf | C | inherited | [403] | 403 | + | Bob | cannot | PATCH | fictive | C | inherited | [403] | 403 | + | Public | cannot | PATCH | rdf | no | WAC | [401] | 401 | + | Public | cannot | PATCH | rdf | WAC | inherited | [401] | 401 | + | Public | cannot | PATCH | fictive | WAC | inherited | [401] | 401 | + + Scenario Outline: a resource, when an authenticated agent has access to the container and access to the resource + * def testResource = createResource(container, resource, type) + Given url testResource.url + And headers authHeaders(method, testResource.url, agent) + When method + Then status + Examples: + | agent | result | method | type | container | resource | status | + | Bob | cannot | DELETE | plain | no | C | 403 | + | Bob | cannot | DELETE | plain | C | inherited | 403 | + | Bob | cannot | DELETE | fictive | C | inherited | 403 | + | Bob | cannot | DELETE | rdf | no | C | 403 | + | Bob | cannot | DELETE | rdf | C | inherited | 403 | + | Bob | cannot | DELETE | container | no | C | 403 | + | Bob | cannot | DELETE | container | C | inherited | 403 | + | Bob | cannot | DELETE | plain | no | A | 403 | + | Bob | cannot | DELETE | plain | A | inherited | 403 | + | Bob | cannot | DELETE | fictive | A | inherited | 403 | + | Bob | cannot | DELETE | rdf | no | A | 403 | + | Bob | cannot | DELETE | rdf | A | inherited | 403 | + | Bob | cannot | DELETE | container | no | A | 403 | + | Bob | cannot | DELETE | container | A | inherited | 403 | + | Bob | cannot | DELETE | plain | no | W | 403 | + | Bob | can | DELETE | plain | W | inherited | 205 | + | Bob | cannot | DELETE | fictive | W | inherited | 404 | + | Bob | cannot | DELETE | rdf | no | W | 403 | + | Bob | can | DELETE | rdf | W | inherited | 205 | + | Bob | cannot | DELETE | container | no | W | 403 | + | Bob | cannot | DELETE | container | W | inherited | 403 | + | Public | cannot | DELETE | plain | no | WAC | 401 | + | Public | cannot | DELETE | plain | WAC | inherited | 401 | + | Public | cannot | DELETE | fictive | WAC | inherited | 401 | + | Public | cannot | DELETE | rdf | no | WAC | 401 | + | Public | cannot | DELETE | rdf | WAC | inherited | 401 | + | Public | cannot | DELETE | container | no | WAC | 401 | + | Public | cannot | DELETE | container | WAC | inherited | 401 | diff --git a/web-access-control/protected-operation/write-access-public.feature b/web-access-control/protected-operation/write-access-public.feature new file mode 100644 index 0000000..cbadc50 --- /dev/null +++ b/web-access-control/protected-operation/write-access-public.feature @@ -0,0 +1,186 @@ +Feature: Only authenticated agents can write (and only that) a resource when granted write access + # Grant public agents (setPublicAccess): + # - restricted access or no access to the parent container + # - restricted access to the test resources, or inherited access for fictive resources + Background: Create test resources with correct access modes + * def resourcePermissions = + """ + function (modes) { + if (modes && modes != 'inherited' && modes != 'no') { + return Object.entries({ R: 'read', W: 'write', A: 'append', C: 'control' }) + .filter(([mode, permission]) => modes.includes(mode)) + .map(([mode, permission]) => permission) + } + return undefined + } + """ + * def getRequestData = + """ + function (type) { + switch(type) { + case 'rdf': + return { + contentType: 'text/turtle', + requestBody: '<> "Bob replaced it." .', + responseShouldNotContain: "Bob replaced it" + } + default: + return { + contentType: 'text/plain', + requestBody: "Bob's text", + responseShouldNotContain: "Bob's text" + } + } + } + """ + * def resourceEntry = + """ + function (container, type) { + switch (type) { + case 'plain': + return container.createResource('.txt', 'Hello', 'text/plain') + case 'fictive': + return container.reserveResource('.txt') + case 'rdf': + return container.createResource('.ttl', karate.readAsString('../fixtures/example.ttl'), 'text/turtle') + case 'container': + return container.createContainer() + default: + return undefined + } + } + """ + * def createResource = + """ + function (containerModes, resourceModes, resourceType) { + + const testContainerPermissions = resourcePermissions(containerModes == 'all' ? 'RWAC' : containerModes) + const testResourcePermissions = resourcePermissions(resourceModes) + + const testContainerInheritablePermissions = resourceModes == 'inherited' + ? testContainerPermissions + : resourceType == 'fictive' + ? testResourcePermissions + : undefined + + const testContainer = rootTestContainer.createContainer() + const testResource = resourceEntry(testContainer, resourceType) + + testContainer.accessDataset = testContainer.accessDatasetBuilder + .setPublicAccess(testContainer.url, testContainerPermissions) + .setInheritablePublicAccess(testContainer.url, testContainerInheritablePermissions) + .build() + + if (resourceType != 'fictive' && resourceModes != 'inherited') { + testResource.accessDataset = testResource.accessDatasetBuilder + .setPublicAccess(testResource.url, testResourcePermissions) + .build() + } + + return testResource + } + """ + + Scenario Outline: read a resource (), when an authenticated agent has access to the container and access to the resource + * def testResource = createResource(container, resource, type) + Given url testResource.url + When method + Then status + Examples: + | agent | result | method | type | container | resource | status | + | Public | cannot | GET | plain | no | AWC | 401 | + | Public | cannot | GET | plain | AWC | inherited | 401 | + | Public | cannot | GET | fictive | AWC | inherited | 401 | + | Public | cannot | GET | rdf | no | AWC | 401 | + | Public | cannot | GET | rdf | AWC | inherited | 401 | + | Public | cannot | GET | container | no | AWC | 401 | + | Public | cannot | GET | container | AWC | inherited | 401 | + | Public | cannot | HEAD | plain | no | AWC | 401 | + | Public | cannot | HEAD | plain | AWC | inherited | 401 | + | Public | cannot | HEAD | fictive | AWC | inherited | 401 | + | Public | cannot | HEAD | rdf | no | AWC | 401 | + | Public | cannot | HEAD | rdf | AWC | inherited | 401 | + | Public | cannot | HEAD | container | no | AWC | 401 | + | Public | cannot | HEAD | container | AWC | inherited | 401 | + + Scenario Outline: write a resource () and cannot read it, when an authenticated agent has access to the container and access to the resource + * def testResource = createResource(container, resource, type) + * def requestData = getRequestData(type) + Given url testResource.url + And header Content-Type = requestData.contentType + And request requestData.requestBody + When method + Then match contains responseStatus + # Server may return payload with information about the operation e.g. "Created" so check it hasn't leaked the data which was PUT + And string responseString = response + And match responseString !contains requestData.responseShouldNotContain + + When method GET + Then status + + Examples: + | agent | result | method | type | container | resource | writeStatus | readStatus | + | Public | can | PUT | rdf | no | W | [201, 204, 205] | 401 | + | Public | can | PUT | rdf | W | inherited | [201, 204, 205] | 401 | + | Public | can | PUT | plain | no | W | [201, 204, 205] | 401 | + | Public | can | PUT | plain | W | inherited | [201, 204, 205] | 401 | + | Public | can | PUT | fictive | W | inherited | [201, 204, 205] | 401 | + | Public | can | POST | container | no | W | [201, 204, 205] | 401 | + | Public | can | POST | container | W | inherited | [201, 204, 205] | 401 | + | Public | can | POST | container | no | A | [201, 204, 205] | 401 | + | Public | can | POST | container | A | inherited | [201, 204, 205] | 401 | + + Scenario Outline: to a resource, when an authenticated agent has access to the container and access to the resource + * def testResource = createResource(container, resource, type) + Given url testResource.url + And header Content-Type = 'text/n3' + And request '@prefix solid: . _:insert a solid:InsertDeletePatch; solid:inserts { <> a . }.' + When method + Then match contains responseStatus + # Server may return payload with information about the operation e.g. "Created" so check it hasn't leaked the data which was PUT + And string responseString = response + And match responseString !contains 'http://example.org#Foo' + + When method GET + Then status + + Examples: + | agent | result | method | type | container | resource | writeStatus | readStatus | + | Public | can | PATCH | rdf | no | W | [201, 204, 205] | 401 | + | Public | can | PATCH | rdf | W | inherited | [201, 204, 205] | 401 | + | Public | can | PATCH | fictive | W | inherited | [201, 204, 205] | 401 | + | Public | can | PATCH | rdf | no | A | [201, 204, 205] | 401 | + | Public | can | PATCH | rdf | A | inherited | [201, 204, 205] | 401 | + | Public | can | PATCH | fictive | A | inherited | [201, 204, 205] | 401 | + | Public | cannot | PATCH | rdf | no | C | [401] | 401 | + | Public | cannot | PATCH | rdf | C | inherited | [401] | 401 | + | Public | cannot | PATCH | fictive | C | inherited | [401] | 401 | + + Scenario Outline: a resource, when an authenticated agent has access to the container and access to the resource + * def testResource = createResource(container, resource, type) + Given url testResource.url + When method + Then status + Examples: + | agent | result | method | type | container | resource | status | + | Public | cannot | DELETE | plain | no | C | 401 | + | Public | cannot | DELETE | plain | C | inherited | 401 | + | Public | cannot | DELETE | fictive | C | inherited | 401 | + | Public | cannot | DELETE | rdf | no | C | 401 | + | Public | cannot | DELETE | rdf | C | inherited | 401 | + | Public | cannot | DELETE | container | no | C | 401 | + | Public | cannot | DELETE | container | C | inherited | 401 | + | Public | cannot | DELETE | plain | no | A | 401 | + | Public | cannot | DELETE | plain | A | inherited | 401 | + | Public | cannot | DELETE | fictive | A | inherited | 401 | + | Public | cannot | DELETE | rdf | no | A | 401 | + | Public | cannot | DELETE | rdf | A | inherited | 401 | + | Public | cannot | DELETE | container | no | A | 401 | + | Public | cannot | DELETE | container | A | inherited | 401 | + | Public | cannot | DELETE | plain | no | W | 401 | + | Public | can | DELETE | plain | W | inherited | 205 | + | Public | cannot | DELETE | fictive | W | inherited | 404 | + | Public | cannot | DELETE | rdf | no | W | 401 | + | Public | can | DELETE | rdf | W | inherited | 205 | + | Public | cannot | DELETE | container | no | W | 401 | + | Public | cannot | DELETE | container | W | inherited | 401 | diff --git a/web-access-control/web-access-control-test-manifest.ttl b/web-access-control/web-access-control-test-manifest.ttl index 99d12c1..7876613 100644 --- a/web-access-control/web-access-control-test-manifest.ttl +++ b/web-access-control/web-access-control-test-manifest.ttl @@ -111,23 +111,23 @@ manifest:read-access-public spec:testScript . -manifest:read-inherited-access-bob +manifest:write-access-agent a td:TestCase ; spec:requirementReference wac:server-read-operation ; td:reviewStatus td:unreviewed ; spec:testScript - . + . -manifest:read-inherited-access-agent +manifest:write-access-bob a td:TestCase ; spec:requirementReference wac:server-read-operation ; td:reviewStatus td:unreviewed ; spec:testScript - . + . -manifest:read-inherited-access-public +manifest:write-access-public a td:TestCase ; spec:requirementReference wac:server-read-operation ; td:reviewStatus td:unreviewed ; spec:testScript - . + .