Implement permissions via relationships?


#1

Hi I am working on a unix-style permissions system where every row in the database has a unique id so that a resource_user_permissions table like [resource_id][user_id][permissions] can be implemented to provide user-level access to resources. Each resource is analogous to an inode from unix.

I’m running into a situation where a resource may contain other resources that are public. I can’t reveal the other resource’s id, because any user can go to a /resources/{resource_id} endpoint to look that resource up by id. I think I am hitting a problem similar to wanting read permissions but not execute (list contents) permissions on a unix directory. I need something even more granular to limit listing a container’s specific relationships.

So for example imagine a user’s directory (1 my_user) that contains references to other resources on the site owned by other users. If I want to make it so that a different user (5 some_user) can’t see a resource in the first directory, I can’t assign a no_access permission to resource 5 because I’m not that resource’s owner. I need to somehow assign no_access to the directory-resource relationship. It appears I have two options:


OPTION 1) list resources as relationships so that multiple ids correspond to one resource and permissions can be applied to the relationship id:

Structure:

2 my_directory
    37 my_resource (a relationship resource that points to 3 my_resource)
    42 someone_elses_resource (a relationship resource that points to 4 someone_elses_resource)

JSON:

{
	"data": {
		"type": "directories",
		"id": 2,
		"relationships": {
			"resources": {
				"data": [
					{
						"type": "resources",
						"id": 37
					},
					{
						"type": "resources",
						"id": 42
					}
				]
			}
		}
	}
}

So a request to add 5 some_user to a blocks collection to prevent it from reading 4 someone_elses_resource might look like:

POST /directories/2/resources/42/blocks
{
	"data": {
		"type": "users",
		"id": 5
	}
}

This would add a row like [42][5][no_access] to the permissions table.

The downside is that all ids are now auto-generated and unique per path so none of the usual POST/PUT/DELETE calls to endpoints would work (because the client only knows resource ids, not path-dependent relationship ids). Requests would require two trips to fetch a relationship id for a resource and then submit that relationship id.


OPTION 2) list resources directly and set permissions on relationships:

Structure:

2 my_directory
    3 my_resource
    4 someone_elses_resource

JSON:

{
	"data": {
		"type": "directories",
		"id": 2,
		"relationships": {
			"resources": {
				"data": [
					{
						"type": "resources",
						"id": 3
					},
					{
						"type": "resources",
						"id": 4
					}
				]
			}
		}
	}
}

So a request to add 5 some_user to a blocks collection to prevent it from reading 4 someone_elses_resource might look like:

POST /directories/2/relationships/resources/4/blocks
{
	data:
	{
		type: "users",
		id: "5"
	}
}

This would add a row like [42][5][no_access] to the permissions table by looking up relationship id 42 for 4 someone_elses_resource behind the scenes.


I’m leaning towards option 2 but I’m having a hard time understanding how relationships work from the documentation. I understand how a one-to-one relationship is specified, but are we allowed to append additional path elements beyond the /relationships/{relationship}/ portion of the URL? For example blocking 5 some_user from reading 1 my_user's profile might look like:

POST /users/1/relationships/profile/blocks
{
	data:
	{
		type: "users",
		id: "5"
	}
}

But I am not clear if it’s ok to use an endpoint like /directories/2/relationships/resources/4/blocks where I specify the related resource’s id in the URL.

I think the gist of the problem is that I would like to set permissions on a relationship like it’s a first-class resource, but I don’t want to reveal the relationship id to the client (due to the added complexity of doing so).

I have found some examples on the web where REST APIs have attribute-level permissions. I’m concerned though that this could be a can of worms and nonstandard. If it’s possible to treat everything as an inode and stick with unix-style permissions, I’d prefer to do that.

I realize this touches on both permissions and relationships which can be nebulous, and that my examples and terminology might not be that great. I feel like I’m not seeing something fundamental here. Any insights you can provide about permissions with respect to REST API design would be greatly appreciated, thanks!


#2

There are no restrictions on URL design; however, /directories/2/relationships/resources/4/blocks is not an idiomatic JSON API URL.

Idiomatically, if you want to modify a resource you can do so via its self URL (which it seems would be something like /resources/4 in your scenario), or via a related URL (where a resource is in a 1-to-1 relationship).
If you want to modify a collection, such as blocks in your example, the idiomatic URL would be something like this: /users/1/profile/blocks (assuming a user has only 1 profile, and that profile can have many related blocks).

self refers to an associative entity / pointer, related refers to the resource (or collection) that is associated / pointed at by self. Modifying related changes the related resource (or collection), modifying self changes which resource (or collection) is related.

I notice you say:

[quote]the client only knows resource ids, not path-dependent relationship ids[/quote] In a REST API the client should not be generating any URLs itself (other than the initial URL to access the API); it should be following URLs provided to it by the API.


#3

Ok that makes sense, thanks for the confirmation on the relationships endpoint structure.

I understand now how /directories/2/relationships/resources takes resource ids:

http://jsonapi.org/format/#crud-updating-relationships
http://jsonapi.org/format/#crud-updating-to-many-relationships

Assign 4 someone_elses_resource to 2 my_directory:

POST /directories/2/relationships/resources
{
	"data": {
		"type": "resources",
		"id": 4
	}
}

Assign 3 my_resource and 4 someone_elses_resource to 2 my_directory:

PATCH /directories/2/relationships/resources
{
	"data": [{
		"type": "resources",
		"id": 3
	},
	{
		"type": "resources",
		"id": 4
	}]
}

Remove 4 someone_elses_resource from 2 my_directory:

DELETE /directories/2/relationships/resources
{
	"data": {
		"type": "resources",
		"id": 4
	}
}

Personally I feel that PUT should replace all of the resources in the directory and that PATCH should only insert the specified resources in the directory (leaving any previous resources in the directory alone). Otherwise there is no distinction between PUT and PATCH, which doesn’t make a lot of sense to me. However, a POST with an array payload can be used to insert multiple resources the way I was envisioning PATCH to work sparsely. So oh well THESE THINGS ARE HARD.

It sounds like if I want to operate on relationships using their ids, when only resource ids are exposed to the client, there is no way to do so under the current spec. Perhaps the JSON API maintainers could consider making /directories/{directory_id}/relationships/resources/{resource_id} an idiomatic JSON API URL. I understand that relationships must be promoted to first-class resources if they contain attributes, based on this response:

https://github.com/json-api/json-api/issues/1042#issuecomment-217335990

But I may have hit a use case where relationships have relationships to other relationships or resources, due to permissions and access control lists (ACLs). I don’t know if my need to hide relationships for permissions is a general need that REST APIs have or if I have just messed up somewhere. I feel like promoting my directory-resource relationships to first-class resources doesn’t make sense in this case because they don’t have attributes. But I’ve been struggling with this for several days so I’d be grateful for any outside ideas on how to solve this.


#4

If you’d said: "if I want to operate on resources using their ids"
I’d reply: There’s nothing in the spec that prevents you from doing that. If you do then your API isn’t RESTful, but REST compliance might not be important to you.

But I’m not clear exactly why you say “relationships” here.

Then I think you do need to promote your relationships to resources.

Are you building an API to manage permissions on resources outside the API, or an API to manage resources which also allows permissions for those resources to be managed through the same API?


#5

The API will have public permissions endpoints so that a user can grant other users read/write access and also block them from both. I haven’t exactly decided on how the endpoints will look yet though.

Just FYI after thinking about this for a few days, I realized that I can set a permission on a public resource and even though users will still be able to see it if they look it up by id, the API can decide not to list that resource id in the relationships field. In other words, it can always use resource ids rather than relationship ids to infer when a relationship should be hidden. No need to map some notion of the unix executable flag for directory listing onto a relationship.

I think I was just overthinking it. Thanks for your help, hopefully this will help someone in the future.