casbin / casbin-cpp

An authorization library that supports access control models like ACL, RBAC, ABAC in C/C++
https://casbin.org
Apache License 2.0
221 stars 61 forks source link

Integrate with Glewlwyd #100

Open hsluoyz opened 3 years ago

hsluoyz commented 3 years ago

See: https://github.com/babelouest/glewlwyd/issues/184#issuecomment-845783765

We should provide help and support including development efforts to help Glewlwyd integrate with Casbin. We need to communicate with them, know our customer's need and help them with the code.

I suggest putting this in a high priority (than the issues proposed by our own people) because we have been "working behind closed doors" for long (we raise issues by ourselves, then solve them) and this can be the first time for us to show Casbin-Cpp is also useful for others.

hsluoyz commented 3 years ago

@EmperorYP7

/cc @xcaptain

ghost commented 3 years ago

thank you @hsluoyz!

If you need any feedback please let me know. It is something I find useful myself.

The target as I see is to be able to configure both OAuth2 and ACLs from a single back-end (Glewlwyd) and be able to re-use any intersecting functionality:

Users Roles Scopes(Permissions) Resources(URLs, topics)

This would help bridge the gap between securing protocols like MQTT and HTTP and even event-brokers such as EventStore or Kafka by exposing the Casbin API as a REST API on the Glewlwyd SSO Server but in the meantime partially re-using existing(intersecting) functionality such as user, scope, and resource management etc.

@babelouest

EmperorYP7 commented 3 years ago

I'd like to work on this. I still need to get into the weeds of Glewlwyd which might take me a while. So I need @xcaptain's help on this.

babelouest commented 3 years ago

Hello, Glewlwyd's author here!

I'm available to help improving casbin and/or Glewlwyd to integrate one with another.

Glewlwyd's ACL design is based on the OAuth2 scopes: When a user asks for an access token to Glewlwyd, Glewlwyd checks that the user is allowed to grant those scopes, and asks for new authentication factors if the scopes require it.

For now, the security model of a user is like a flat list, for example, a user object could look like this:

{
  username: "dev",
  name: "Dave Lopper",
  email: "dev@example.com",
  enabled: true,
  scopes: [ "file:read", "file:write", "mail:read", "profile", "openid" ]
}

The user model is flexible enough to add other properties, example: address, certificate, picture, you name it. The requirement for any additional property is that the values must be a string or an array of strings. You can add any number of additional properties.

I don't know casbin at all, nor its security model so correct me if I'm wrong.

One solution I see for the integration is to create a user backend module, based on the database or ldap module, or both. This new module would behave as the original, but would also include ACLs properties. Those ACLs can be included as one or multiple additional properties, depending on the need, and therefore be included in the access token as Additional claims in the ID Token or the /userinfo endpoint. This solution is elegant because it doesn't adds another protocol to implement for the client or the resource service. Its main problem is that the client must know all the addition claims available to request the correct ones, or potentially get a huge amount of claims and deals with them.

Another solution is to add a new plugin to handle the interface with the casbin backend. This plugin would have its REST API and would be called to answer more precise questions: "Is this user allowed to read A and write B?". The main problem I see with this solution is that the client and the resource server must implement an interface to the casbin plugin in addition to the OAuth2/OIDC standard. This plugin would also be a duplicate of what already exists in the casbin ecosystem, therefore the benefit is limited. i.e. it's not more or less complicated to ask a casbin interface if "user is allowed to read A and write B?" or ask glewlwyd's casbin plugin the exact same question.

A concern I have is that the plugin would not implement a standard but a dedicated interface. And if the casbin interface changes, the plugin must follow the changes.

I personally don't mind, we all make free software, but since I don't intend to use casbin for myself, I hope we can both develop the plugin and maintain it over the month or years of its life.

xcaptain commented 3 years ago

@babelouest Your idea that encoding policies to OAuth token is very clever, I once built an OAuth server, I use a database table to store the relation of scope and correspond API, not very convenient and can't be abstracted as a library.

The OAuth scope model is very simple, it's just an ACL, for example, if a token has scope file:read, then this token can access API like getFile or listFile, a regex match can solve this problem. But if the scopes have intersections, we can consider using casbin, because casbin is very good at handling hierarchical or negative permissions.

babelouest commented 3 years ago

While thinking about it, I came out with a better idea than updating the existing user backends, instead, I'll add a middleware backend system.

These middleware backends would be placed between glewlwyd core and the user backends and would be able to access and update the user objects. Their possibility would be potentially limitless with the user object.

For example, in the case of casbin, the middleware beckend would do something like this:

// user returned by the user backend
{
  username: "dev",
  name: "Dave Lopper",
  email: "dev@example.com",
  enabled: true,
  scopes: [ "file:read", "file:write", "mail:read", "profile", "openid" ]
}

// user updated by the casbin middleware backend
{
  username: "dev",
  name: "Dave Lopper",
  email: "dev@example.com",
  enabled: true,
  scopes: [ "file:read", "file:write", "mail:read", "profile", "openid" ],
  roles: ["admin:it", "admin:legal", "admin:shoes"], // added by the middleware
  profile: ["left", "right"] // added by the middleware
}

This way will be better and easier to implement and to test, and will allow to use backends and middleware in symbiosis, rather than having an updated backend and having to maintain it in parallel of a similar one.

hsluoyz commented 3 years ago

@EmperorYP7 plz work on this.

EmperorYP7 commented 3 years ago

@hsluoyz sure! Should we create the middleware separately, or include it in glewlwyd's source?

Are there any examples of such middleware(s) to which I can refer? Can this be a viable starting point? I'll be glad if someone can educate me/drop-in references on how middlewares work in glewlwyd while I try to figure it out since I am not familiar with it enough.

babelouest commented 3 years ago

@EmperorYP7 ,

The middleware modules implementation is still a WIP, I'm working on the front-end at the moment, the tests will follow, and the documentation too. But you can start working on a middleware module, based on the mock you mention, that's what it's for.

The front-end must have an update too to integrate the casbin parameter, we can make that when we're sure what parameters will be required for it.

I will be happy to include the casbin middleware module in Glewlwyd's project, assuming we agree on a license.

Feel free to ask questions or challenge the interface if you think it should be changed!

A few tips on the functions definitions to help you start:

/**
 * Must inclulde at least "glewlwyd-common.h"
 */
#include "glewlwyd-common.h"

/**
 * function executed once when the module file is loaded
 * can be used to initiate global startup
 * must return a json_t * object with the module description (see mock return value)
 */
json_t * user_middleware_module_load(struct config_module * config);

/**
 * function executed once when the module file is closed
 * must return G_OK on success, an error value on error
 */
int user_middleware_module_unload(struct config_module * config);

/**
 * function executed when a module instance is initiated
 * The variable j_parameters will contain the instance parameters
 * The variable *cls, if used, must be set by the instance and will be passed as parameter for all other module functions,
 * can be used to keep a structure with the connection to a service or anything useful to the module
 * must return a json_t * object of the following format:
 * {result: G_OK} on success
 * {result: G_ERROR*, error: ["error list in a JSON array of strings"]} on error
 */
json_t * user_middleware_module_init(struct config_module * config, json_t * j_parameters, void ** cls);

/**
 * function executed when a module instance is closed
 * the resources contained in cls must be released here
 * must return G_OK on success, an error value on error
 */
int user_middleware_module_close(struct config_module * config, void * cls);

/**
 * function executed on a user_get_iist
 * this function will update the j_user_list content accordingly to its requirements, or leave it as is if needed
 * must return G_OK on success, an error value on error
 * The whole user_get_list result will be cancelled if an error is returned, so an error value shouldn't be returned if one user has error for example, better off removing this user of the list
 */
int user_middleware_module_get_list(struct config_module * config, json_t * j_user_list, void * cls);

/**
 * function executed on a get_user
 * this function will update the j_user content accordingly to its requirements, or leave it as is if needed
 * must return G_OK on success, an error value on error
 */
int user_middleware_module_get(struct config_module * config, const char * username, json_t * j_user, void * cls);

/**
 * function executed on a get_user_profile, the user_profile is the attributes the user has access to
 * this function will update the j_user content accordingly to its requirements, or leave it as is if needed
 * must return G_OK on success, an error value on error
 */
int user_middleware_module_get_profile(struct config_module * config, const char * username, json_t * j_user, void * cls);

/**
 * function executed before a add_user, set_user or set_user_profile
 * this function will update the j_user content accordingly to its requirements, or leave it as is if needed
 * must return G_OK on success, an error value on error
 */
int user_middleware_module_update(struct config_module * config, const char * username, json_t * j_user, void * cls);

/**
 * function executed before a delete_user
 * this function will update the j_user content accordingly to its requirements, or leave it as is if needed
 * must return G_OK on success, an error value on error
 */
int user_middleware_module_delete(struct config_module * config, const char * username, json_t * j_user, void * cls);
EmperorYP7 commented 3 years ago

Feel free to ask questions or challenge the interface if you think it should be changed!

Thank you @babelouest. :smile:

I will be happy to include the casbin middleware module in Glewlwyd's project, assuming we agree on a license.

@hsluoyz I think we should be creating a middleware within glewlwyd's source.

babelouest commented 3 years ago

You can also reach me on IRC, libera network, channel #glewlwyd

hsluoyz commented 3 years ago

@babelouest @EmperorYP7 thanks for the update! The middleware design looks great :)

We usually use Apache 2.0 license in Casbin main repos. But I think it's OK to follow the https://github.com/babelouest/glewlwyd repo's license (which is GPL 3.0) if we put the middleware code directly into it.

babelouest commented 3 years ago

@hsluoyz , no problem for the license.

To explain my point of view, I'm open minded with the licences third parties want to use with the modules, that's why the mock modules and the required files to include use MIT license, while the rest of glewlwyd is using GPL3. Therefore even a closed source license would be possible for a module, although in that case I wouldn't be part of its implementation.

Concerning my glewlwyd repository, I'll include only free-as-in-free-speech and DFSG compliant code, to respect the philosophy. Therefore, Apache 2 license is allowed too, but I might say I prefer GPL3 for my own.

Bottom line, I'm ok with a GPL3 license for the casbin module, but I'm not closed to other compatible license in general ;)

babelouest commented 3 years ago

@EmperorYP7 , the middleware modules management is now ready in the master branch.

EmperorYP7 commented 3 years ago

@EmperorYP7 , the middleware modules management is now ready in the master branch.

That's great! I am on it. Thanks for the update. :smile:

EmperorYP7 commented 3 years ago

Quick update:

casbin-cpp has not yet been configured to be called within C. This would require developing C wrappers for casbin's intrinsic functions and classes. I'll be working on this next week as well along with benchmarking.

babelouest commented 3 years ago

You may not have to wrap casbin functions: I've written a module in a similar context by adding extern "C" to the functions that are exported, see https://github.com/babelouest/angharad/blob/master/src/benoic/device-modules/device-zwave.cpp#L514 for example.

Something like that:

extern "C" json_t * user_middleware_module_init(struct config_module * config, json_t * j_parameters, void ** cls) {
  // Do magic here
}

The Makefile will use g++ instead og gcc and the cmake script won't have a difference if I recall correctly.

EmperorYP7 commented 3 years ago

Thank you so much. Saved me a lot of time! 😄 @babelouest

babelouest commented 3 years ago

My pleasure