Description
Role Based Auth: Feature Discussion
Given some of my feature plans, and the fact that people appear to be deploying local-ai in multi-user scenarios already, I'd like to implement Role Based Authentication (RBAC) in the near future. In particular, I can see a couple of requirements for this already:
- The basics - assigning api keys to hierarchical roles, and then restriction what actions can be performed by role in some way
- Additional conditional attributes of a request:
-
- What model is requested?
-
- Is it loaded or not?
-
- Priority?
-
- ETC?
- non-http servers in the future?
As an "Option 0", before starting major research, I put together a quick "demo" in the form of https://github.com/mudler/LocalAI/pull/2321/files. Rather than rely on any external dependencies, this is a minimal, manual implementation of the pattern. The performance is not optimized, and everyone knows writing your own security code is dubious, but this should be remembered as a "back pocket" option if we find we need something simpler to digest or a near-instant solution.
With that in mind, I did some research on our options for "real, battle-tested" RBAC, and I think our best contender is casbin.
- Industry standard and battle tested.
- Flexible enough to support advanced logic for some of the features I'm planning
- Has a middleware already supported by fiber, our http server.
However, there are some limits of that existing middleware - and on different projects I've worked on, people have had very different opinions on style for this sort of thing. Rather than pick one myself, I'm creating this issue to discuss some of our options for using casbin.
At the core of the issue is that the existing fiber middleware takes care of the call to casbin's Enforce()
function. This is normally a good thing, as it has a way to customize the subject of the request. However, in order to implement some of the more advanced rules, it might be nice to specify additional request/policy definition objects, rather than packing everything into a single subject - for example, something more like
[request_definition]
r = key, permission/path, model, loaded, action
Even if we don't use a customized definition here, we have a lot of flexibility with casbin to serialize our state into a subject
string
With that in mind, I think we have a couple different choices here:
-
Write our own
casbin
middleware heavily based on the example
I know I just said "don't write your own auth code" above... but the existing middleware is a very thin wrapper around casbin. A custom middleware would allow us to pass whateverrequest_parameters
to the engine we like, allowing auth decisions to be fully handled within casbin, in a single pass. While we're on our own for the fiber side of things, the actual "enforcement" code is where the performance and security implementation details lie, so I feel relatively confident that this gives us the most flexibility going forward at an acceptable cost. -
Use RequirePermissions
This is the most complex of the built in middlewares, but thePermissionParserFunc func(str string) []string
should let us actually use the exact same level of control as above. I'm the least sure about exactly how to use this option... so I'm doing some prototyping as it may end up being an ideal blend. The main risk is purely the unknown aspects - I think this basically gives us so much freedom that we're largely writing our own security code with the potential risk of not recognizing that fact. I also need to do some more research - off the cuff it doesn't look like runtime parameters can be passed into this function without reregistering the middleware? -
RequireRoles :: the explicit option
We could use this existing middleware to handle the authorization for http requests, with a consistent set of roles across all request modalities. Conditional authorization is somewhat messy in this scenario - I'm assuming that to implement that, we may need multiple passes through casbin here, once via the standard RequireRoles middleware, and once via a customEnforce()
call, which somewhat negates the value vs option 1. The primary advantage here is that we would know that at least the basic access control to our most important http endpoints were battle-tested. The primary disadvantage is that this middleware requires developers to manually annotate what roles are required - I have found this to be a source of accidental errors in the past, as everyone makes mistakes and runs the risk of forgetting the correct requirements... -
Use RoutePermissions :: entirely separate casbin rule chains
Of the options, this is probably the "least complex", but also has the most unknowns at the same time. RoutePermissions allow us to bind http endpoints to authentication, so we could have a set of policies and permissions that only covers that layer. This leaves anything to do with my MQTT proposal out of scope, and means that if we do want to implement any type of per-model or other conditional authorization, we likely will need to run through casbin a second time, on an entirely separate set of casbin policies. The only real advantage this option has is that it's more likely to fail-secure - if we as developers add new endpoints without creating policies for them first, no users will be able to hit them until that's changed.
With all this in mind, I'd love to hear some more opinions from the rest of the team on our path forward here. I think the biggest choice is really between options 1 and 2/3: between the latter two is really a matter of style and scoping.
I'd also love to hear other requirements people have for RBAC in general.