Skip to content

Conversation

@RubenVerborgh
Copy link
Contributor

@RubenVerborgh RubenVerborgh commented Jul 2, 2017

This pull request adds support for N3 patches, and contains extensive tests for PATCH.

Added features

  • exposing ACL object and user ID on the request
  • fine-grained permission checking of patches after parsing (for both N3 and SPARQL UPDATE)
  • support for Notation3 patches

Pending decisions

  • A final vocabulary/vocabularies for N3 patches (options on the Wiki)
  • Have the right decisions been taken for status codes and permissions? (See Specification below.)
  • The PATCH handler performs partial permission checking: it assumes that Append permissions are checked, and—after patch parsing—verifies Read and Write permissions when needed for a particular patch. We might want to either double-check Append permissions, or move permission checking into handlers in general.

As future work, we can create an abstraction that allows non-triple-based patches as well. For this, however, I would strongly recommend rewriting handlers in an object-oriented way, as the current pure functional style could become confusing.

Specification


PATCH
  with a patch document
    with an unsupported content type
      ✓ returns HTTP status code 415
      ✓ has "Unsupported patch content type: text/other" in the response
      ✓ does not modify the file
    containing invalid syntax
      ✓ returns HTTP status code 400
      ✓ has "Patch document syntax error" in the response
      ✓ does not modify the file
    without relevant patch element
      ✓ returns HTTP status code 400
      ✓ has "No patch for https://tim.localhost:7777/read-write.ttl found" in the response
      ✓ does not modify the file
    with neither insert nor delete
      ✓ returns HTTP status code 400
      ✓ has "Patch should at least contain inserts or deletes" in the response
      ✓ does not modify the file
  with insert
    on a non-existing file
      ✓ returns HTTP status code 200
      ✓ has "Patch applied successfully" in the response
      ✓ creates the file
      ✓ writes the correct contents
    on a resource with read-only access
      ✓ returns HTTP status code 403
      ✓ has "Access denied" in the response
      ✓ does not modify the file
    on a resource with append-only access
      ✓ returns HTTP status code 200
      ✓ has "Patch applied successfully" in the response
      ✓ patches the file correctly
    on a resource with write-only access
      ✓ returns HTTP status code 200
      ✓ has "Patch applied successfully" in the response
      ✓ patches the file correctly
  with insert and where
    on a non-existing file
      ✓ returns HTTP status code 409
      ✓ has "The patch could not be applied" in the response
      ✓ does not create the file
    on a resource with read-only access
      ✓ returns HTTP status code 403
      ✓ has "Access denied" in the response
      ✓ does not modify the file
    on a resource with append-only access
      ✓ returns HTTP status code 403
      ✓ has "Access denied" in the response
      ✓ does not modify the file
    on a resource with write-only access
      ✓ returns HTTP status code 403
      ✓ has "Access denied" in the response
      ✓ does not modify the file
    on a resource with read-append access
      with a matching WHERE clause
        ✓ returns HTTP status code 200
        ✓ has "Patch applied successfully" in the response
        ✓ patches the file correctly
      with a non-matching WHERE clause
        ✓ returns HTTP status code 409
        ✓ has "The patch could not be applied" in the response
        ✓ does not modify the file
    on a resource with read-write access
      with a matching WHERE clause
        ✓ returns HTTP status code 200
        ✓ has "Patch applied successfully" in the response
        ✓ patches the file correctly
      with a non-matching WHERE clause
        ✓ returns HTTP status code 409
        ✓ has "The patch could not be applied" in the response
        ✓ does not modify the file
  with delete
    on a non-existing file
      ✓ returns HTTP status code 409
      ✓ has "The patch could not be applied" in the response
      ✓ does not create the file
    on a resource with read-only access
      ✓ returns HTTP status code 403
      ✓ has "Access denied" in the response
      ✓ does not modify the file
    on a resource with append-only access
      ✓ returns HTTP status code 403
      ✓ has "Access denied" in the response
      ✓ does not modify the file
    on a resource with write-only access
      ✓ returns HTTP status code 403
      ✓ has "Access denied" in the response
      ✓ does not modify the file
    on a resource with read-append access
      ✓ returns HTTP status code 403
      ✓ has "Access denied" in the response
      ✓ does not modify the file
    on a resource with read-write access
      with a patch for existing data
        ✓ returns HTTP status code 200
        ✓ has "Patch applied successfully" in the response
        ✓ patches the file correctly
      with a patch for non-existing data
        ✓ returns HTTP status code 409
        ✓ has "The patch could not be applied" in the response
        ✓ does not modify the file
      with a matching WHERE clause
        ✓ returns HTTP status code 200
        ✓ has "Patch applied successfully" in the response
        ✓ patches the file correctly
      with a non-matching WHERE clause
        ✓ returns HTTP status code 409
        ✓ has "The patch could not be applied" in the response
        ✓ does not modify the file
  deleting and inserting
    on a non-existing file
      ✓ returns HTTP status code 409
      ✓ has "The patch could not be applied" in the response
      ✓ does not create the file
    on a resource with read-only access
      ✓ returns HTTP status code 403
      ✓ has "Access denied" in the response
      ✓ does not modify the file
    on a resource with append-only access
      ✓ returns HTTP status code 403
      ✓ has "Access denied" in the response
      ✓ does not modify the file
    on a resource with write-only access
      ✓ returns HTTP status code 403
      ✓ has "Access denied" in the response
      ✓ does not modify the file
    on a resource with read-append access
      ✓ returns HTTP status code 403
      ✓ has "Access denied" in the response
      ✓ does not modify the file
    on a resource with read-write access
      executes deletes before inserts
        ✓ returns HTTP status code 409
        ✓ has "The patch could not be applied" in the response
        ✓ does not modify the file
      with a patch for existing data
        ✓ returns HTTP status code 200
        ✓ has "Patch applied successfully" in the response
        ✓ patches the file correctly
      with a patch for non-existing data
        ✓ returns HTTP status code 409
        ✓ has "The patch could not be applied" in the response
        ✓ does not modify the file
      with a matching WHERE clause
        ✓ returns HTTP status code 200
        ✓ has "Patch applied successfully" in the response
        ✓ patches the file correctly
      with a non-matching WHERE clause
        ✓ returns HTTP status code 409
        ✓ has "The patch could not be applied" in the response
        ✓ does not modify the file

router.get('/*', index, acl.allow('Read'), get)
router.post('/*', acl.allow('Append'), post)
router.patch('/*', acl.allow('Write'), patch)
router.patch('/*', acl.allow('Append'), patch)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dmitrizagidulin This is a permission downgrade that, with other patch handlers, could allow unintended access. The patch handler is assumed to check for Write (and Read) permission when needed after parsing the patch.

(Note that this handler did not check for Read yet.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good; the code will check for Write vs Append downstream, as we discussed.

Copy link
Contributor

@timbl timbl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the original, if I remember correctly, it was critical to perform the file read/patch/write without letting go of the async thread. Hence the comment "'PATCH -- applied OK (sync)'". This was to avoid conflict with other PATCH requests being handled concurrently. Concerned that the functions to check permissions being written in the promise style, it will be easy to do some I/O operation in them and lose that atomic operation. If we don't do this sync, then we have have to have some form of locking, which I understood was not really the node style.

@RubenVerborgh
Copy link
Contributor Author

RubenVerborgh commented Jul 4, 2017

Mhm, the sync-ness of the patch handler seems to have been gone for some time already. The file I started working on already performed asynchronous reads and writes (https://github.com/solid/node-solid-server/pull/511/files#diff-d83413e73094e31dc0662b194162262dL160). It seems that the "sync" comment was copied verbatim from above (https://github.com/solid/node-solid-server/pull/511/files#diff-d83413e73094e31dc0662b194162262dL56), but that code never performed any actual writing (synchronous nor asynchronous).

In any case, sync is not an appropriate solution for the atomicity problem, given that any Node.js server can be started with multiple threads, which can interfere. If cross-request atomicity is important for Solid (which I assume it is), we might need to give locking a more prominent place in the overall architecture.

@dmitrizagidulin
Copy link
Contributor

@timbl I agree with @RubenVerborgh - switching the PATCH handler code to synchronous reads & writes won't do anything for atomicity; that has to be handled separately and explicitly (via locks / mutexes, etc).


getUserId(req, function (err, userId) {
if (err) return next(err)
req.userId = userId
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need this part; the userId is already recorded on the request object upstream of this handler.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we discussed, not in all cases (could be in req.session.userId or locals.oidc.webIdFromClaims(req.claims)). Opened #517 for this.

Copy link
Contributor

@dmitrizagidulin dmitrizagidulin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thanks! 👍

@RubenVerborgh RubenVerborgh force-pushed the rv/patch/n3 branch 2 times, most recently from f255521 to 6d9e863 Compare August 16, 2017 21:21
@RubenVerborgh
Copy link
Contributor Author

RubenVerborgh commented Aug 16, 2017

TODO for @dmitrizagidulin and/or @timbl:

  • Check whether the decisions regarding status codes are okay (see Specification > Details at the top of this page).

TODO for @dmitrizagidulin:

  • Check whether the assumptions regarding authentication are fine (see partial permission checking argument above).

TODO For @RubenVerborgh:

  • Create and publish patch vocabulary

@dmitrizagidulin
Copy link
Contributor

Should a PATCH with an insert to a non-existing file return a 201 Created instead of a 200?

@RubenVerborgh
Copy link
Contributor Author

@dmitrizagidulin Seems logical. Only problem might be that a user with only Append permissions (not Read) might not be authorized to know whether the file already existed.

@dmitrizagidulin
Copy link
Contributor

Only problem might be that a user with only Append permissions (not Read) might not be authorized to know whether the file already existed.

Good point. Is that a use case we should support, tho? I can only think of one Append-but-not-Read access, and that's the Inbox. And there the main interaction is via POST, not PATCH.

@dmitrizagidulin
Copy link
Contributor

👍 on the status codes from me. (aside from the minor conversation about the 201 insert via patch)

@RubenVerborgh
Copy link
Contributor Author

Discussed this with @dmitrizagidulin and we decided to keep the 200.

@RubenVerborgh
Copy link
Contributor Author

Ready, pending approval of solid/vocab#25.

@RubenVerborgh
Copy link
Contributor Author

Currently on hold because of #552.

@timbl
Copy link
Contributor

timbl commented Aug 18, 2017

This functionality adds the ability to parse N3 patches. Then, to send them upstream, we need to add an option to the pub/sub protocol to include patches in the upstream info in the websocket. (#554)

@RubenVerborgh
Copy link
Contributor Author

#552 has been merged; the rv/patch/n3 branch has been merged with master, so this pull request is ready to be reviewed and merged.

@RubenVerborgh
Copy link
Contributor Author

@dmitrizagidulin The final commit 1aca23f is currently a merge commit of dz_oidc onto rv/patch/n3 (resolving some conflicts). It might simplify history if I make the same merge, but then from rv/patch/n3 onto dz_oidc and then push (instead of merging on GitHub).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants