Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to design user-facing and admin JSON endpoints? #9

Open
saurabhnanda opened this issue Oct 5, 2016 · 15 comments
Open

How to design user-facing and admin JSON endpoints? #9

saurabhnanda opened this issue Oct 5, 2016 · 15 comments

Comments

@saurabhnanda
Copy link
Contributor

saurabhnanda commented Oct 5, 2016

Note: This is probably closely related to #10

Most domain models in our shopping card application will need two kinds of behaviours/representations/actions:

  • One, for the end-customer
  • Another, for the shop/store owner

Apart from privileged actions, which are relatively easier to handle, privileged views seem be tougher to implement in a type-safe manner. For example, the following fields should never be sent down to a UI for end-customers:

  • Cost-price of the product (as opposed to the selling price)
  • Vendor from which the product is sourced
  • Exact number of units in stock (we are fine with sending the number when we have only a few units left in stock)

Now, there can be two ways in which this can be designed.

Option 1: Separate API endpoints for end-customers and store owners: /products vs /admin/products

This allows us a very easy way to respond with different JSONs for the end-customer vs the store owner. However, what if we want a more fine-grained privilege system? For example:

  • Store owner -- can view all fields
  • Customer support -- can view all fields except product vendor & cost price
  • Shipping personnel -- can view all fields except cost price

Option 2: A single API endpoint which responds with different JSONs based on user permissions/roles

This allows us to implement a more fine-grained privilege system. However, if it's not type-safe, it is easy to get this wrong.

Is there any other options? Is Option 2 possible to implement in a type-safe manner?

@sudhirvkumar
Copy link

@saurabhnanda

I believe Option 1 is the best choice as the product details for the customer will be different from that of the Admin.

I would go with Option 1. It will be simple to understand and implement too also to test.

@sudhirvkumar
Copy link

@saurabhnanda

As we are using Haskell, we can always extract away the common functionality into functions at the time of refactoring / code review.

@saurabhnanda
Copy link
Contributor Author

What about cases where even on the admin side, different user-roles are to
be given view-level access to different fields.

On Wed, Oct 5, 2016 at 7:19 PM, Sudhir Kumar [email protected]
wrote:

@saurabhnanda https://github.com/saurabhnanda

As we are using Haskell, we can always extract away the common
functionality into functions at the time of refactoring / code review.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#9 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AABu0WO5Dcnn-m-zBG5QiQMopQLLefhQks5qw6rSgaJpZM4KObCB
.

@sudhirvkumar
Copy link

/products
/admin/products
/manager/products

I guess the route I would take if have different end points

also, if the details are going to differ then how will we handle it? how will we parse the JSON? how will we be sure that things are the way they should be?

I understand that in RoR this is easy... because its all dynamic.

I believe I will go with different end points depending on the role.

In the future I will move to GraphQL and Code Generation to avoid writing these things by hand.

@saurabhnanda
Copy link
Contributor Author

@sudhirvkumar Option 1 is the easy default. The real question here is whether we can do better with help from Haskell's type-system 😀

@sudhirvkumar
Copy link

@saurabhnanda yeah that's true :D

I had an idea of using jwt token and extracting the role from that and using that in types and also mentioning that in the API endpoint definition in Servant and then define a type which will return the appropriate type depending on the role!

API definition will depend on the role extracted from the jwt token ;)

http://haskell-servant.readthedocs.io/en/stable/tutorial/ApiType.html#request-headers

So yes, I believe Haskell Type System can help! may be we can ask somebody in the servant repo?

We need to write custom combinator(s)?

"/" :> "products" :> Role RoleType :> Get '[JSON] [Product]

RoleType being

data RoleType = Anonymous | Admin | Seller | LoggedInUser | Manager

What do you think?

@sudhirvkumar
Copy link

sudhirvkumar commented Oct 6, 2016

I found this example... yet to understand it though

https://github.com/arianvp/servant-jwt-example

also there is an experimental method in Servant

https://github.com/haskell-servant/servant/blob/master/servant/src/Servant/API/Experimental/Auth.hs

So I believe we should be able to extract the Role along the request.

https://github.com/haskell-servant/servant/blob/master/doc/tutorial/Authentication.lhs

type AuthGenAPI = "private" :> AuthProtect "cookie-auth" :> PrivateAPI
             :<|> "public"  :> PublicAPI

@jfoutz
Copy link

jfoutz commented Oct 11, 2016

So, Servant has some support for different authentication strategies, via that experimental api. I'm hoping to dig into that Wednesday.

Authorization is a whole different problem. After authentication, i know who you are, but i'm still not sure what you're allowed to do. I really like one endpoint per role. I can look at your roles, and then go look at the endpoints and see what you can do. This works well for a relatively small number of roles.

Sometimes it's helpful to put all of the authorization information in one place, a text file, a database, haskell source, whatever. Eventually, someone will have access to something they shouldn't. It's nice to be able to split out, is it a policy problem or is it a bug in the code? A contrived example, a customer can delete another customer's bookings. Clearly bad. If there's one central policy, you can tell right away if the policy is just wrong, "auth:delete-any-booking" vs "auth:delete-own-booking" or if there's a bug somewhere in the code.

In java spring, you can sprinkle authorization around arbitrarily, per endpoint, per method. It can be a handy shortcut when you have a new role that's just like X plus a couple of methods. Unfortunately, that makes it pretty rough to figure out what the rules are supposed to be for a given role.

One endpoint per role is great, it's clean and easy to figure out what a specific role can do. If you wind up have a hundred different roles, it kind of sucks.

@saurabhnanda
Copy link
Contributor Author

@jfoutz just to be clear, what do you mean when you say "role"? Is "admin" or "finance manager" a role? Or, is "view-any-booking" or "view-any-booking" a role?

For me, a role is a tenant-defined collection of permissions. A permission is a fine grained ability to do a specific action in the system, eg can-edit-invoice, can-generate-reports.

So, a different endpoint per role can work when you have 1-3 roles, but can't if you allow a tenant to define his/her own roles.

Any thoughts on how to approach the solution for arbitrarily defined roles?

@jfoutz
Copy link

jfoutz commented Oct 11, 2016

I'd think of "admin" or "finance-manager" as a role. "view-any-booking" would be a permission, something they're allowed to do.

I think we agree, but i might include a "vacation-labs-admin" that could do stuff across all tenants or end users.

For arbitrarily defined roles, I'll go back to the standard suggestion of one table of permissions, one table of user defined roles, and a junction table to map them. each api call would have to check authorization. I think that query can be built into the type so it just happens on every call without developer interaction. Without that central source of truth things would get very complicated.

You can build a nice UI around the permissions. maybe provide a few pre defined bundles of permissions for common roles. Also cache the results so the webservices aren't constantly querying.

I'll keep thinking about it, maybe as i get further along i'll have some better ideas. I do think this is a pretty standard approach that works for lots of people though.

@saurabhnanda
Copy link
Contributor Author

@jfoutz that's pretty much how roles and permissions have been modelled in the starter DB schema we have in the repo. Except perhaps not having permissions in a separate table. I'm envisioning the permission list to come from the Haskell source. Adding a row in the permissions table cannot have any effect till some code is updated to react to that permission, right?

However, the larger question is not how to design the schema, but how to leverage the type system to force every developer to at least think about authorisation at appropriate "levels" in the app: at the JSON level, DB access level, etc.

@jfoutz
Copy link

jfoutz commented Oct 11, 2016

  • Adding a row in the permissions table cannot have any effect till some code is updated to react to that permission, right?

Yeah, the permission (as i see it) would be some code to run, if the code doesn't exist... it couldn't run.

I'll need to think more about the second part of the comment. I'm not sure how that would flow down to actual decisions in code. Since it's dynamic, it would have to start with a DB query. I'm thinking something like a reader monad that carries around a list of permissions. I'll probably have better ideas as i get further along.

@sudhirvkumar
Copy link

@saurabhnanda @jfoutz

theoretically, just like we can use different content type JSON, HTML...

we can vary the output depending on the Role too..

for example... if we can extract Role from the token (jwt) and use pattern matching on the Role then we know how to handle each of those Roles.

data Role = Anonymous | Admin | Customer | Manager

if its anonymous... we can throw an error as required... if its Admin, we can handle it accordingly!

@jfoutz
Copy link

jfoutz commented Oct 11, 2016

@sudhirvkumar Yes! absoulutely.

But if the roles are custom created at runtime things get tricky. Maybe something like

newtype Permission = String
data Role = Anonymous | Admin | Customer | Manager | Dynamic [Permission]

or maybe

data Role a = Anonymous | Admin | Customer | Manager | Dynamic a

So stock roles are straightforward, they can just be hardcoded. I haven't quite figured out how to verify a Dynamic _ without programmer intervention. It sure seems like there's a cute way to handle the Dynamic case without having to start every function with checkPermission I'm thinking maybe we can lift the servant types into a monad that does the permission checking behind the scenes.

@sudhirvkumar
Copy link

@saurabhnanda @jfoutz

I don't think we should keep the permissions dynamic if we need compile time guarantees

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

No branches or pull requests

3 participants