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

RBAC using Casbin #256

Merged
merged 33 commits into from
Jul 6, 2020
Merged

RBAC using Casbin #256

merged 33 commits into from
Jul 6, 2020

Conversation

speza
Copy link
Contributor

@speza speza commented Jun 8, 2020

WIP

@Skarlso Skarlso self-requested a review June 9, 2020 08:51
* Use rice instead of pkger
* Move enforcer instantiation to server.go
* Fix makefile issue
@speza
Copy link
Contributor Author

speza commented Jun 9, 2020

Ok, so this is getting there now. This change introduces Casbin to enforce RBAC.

We have a policy model that is defined like so (see static/rbac-model.conf).
p, role:admin, pipelines, get, *, allow

Namespace = pipelines, secrets, etc.
Action = create, get, delete, etc
Resource = pipelineid, etc

We use manage a standard set of policy rules loaded in on server start ( static/rbac-policy.csv).

Summary:

  • Implemented an API mapping yaml file that stores all our endpoints and stores which namespace/action/resource to apply when we enforce RBAC

  • We now instantiate a casbin adapter that sits on top of boltdb and saves each policy rule line as a k/v pair. On server startup we have to iterate over every k/v item within Bolt to populate the in-memory Casbin model - this isn't ideal but is only needed on startup. We use bolt this way because it give us more flexibility using Cabin's auto-save functionality with the persist.batchAdapter interface. It means we don't just store a single blob and can dynamically add/remove policy lines.

  • I have wrapped the Casbin enforcer into our own enforcer service for flexibility (security/rbac/service.go)

  • Use rice to include some more static assets (rbac-model.conf, rbac-policy.csv, rbac-api-mappings.yml

  • Implemented some new endpoints to manage rbac

// RBAC - Management
apiAuthGrp.GET("rbac/roles", rbacHandler.getAllRoles)
apiAuthGrp.PUT("rbac/roles/:role", rbacHandler.addRole)
apiAuthGrp.DELETE("rbac/roles/:role", rbacHandler.deleteRole)
apiAuthGrp.PUT("rbac/roles/:role/attach/:username", rbacHandler.attachRole)
apiAuthGrp.DELETE("rbac/roles/:role/attach/:username", rbacHandler.detatchRole)
apiAuthGrp.GET("rbac/roles/:role/attached", rbacHandler.getRolesAttachedUsers)
// RBAC - Users
apiAuthGrp.GET("users/:username/rbac/roles", rbacHandler.getUserAttachedRoles)

  • Add more logic to the authMiddleware to enforce RBAC

TODO:

  • Add the ability to turn RBAC on/off
  • More testing
  • Another PR to implement the UI

# Policy format:
# p, <role>, <namespace>, <action>, <resource>, <effect = allow/deny>

p, role:admin, pipelines, get, *, allow
Copy link

Choose a reason for hiding this comment

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

If role:admin is assumed to access all, you can just write p, role:admin, *, *, *, allow one rule to allow all.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice! That would definitely be handy here!


import (
"errors"
"github.com/casbin/casbin/v2"
Copy link

Choose a reason for hiding this comment

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

Sort the imports.

Copy link
Member

@Skarlso Skarlso left a comment

Choose a reason for hiding this comment

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

Overall I agree with the approach. Well done. :) Just have a few remarks regarding the code itself and a tiny bit of structure in storage.

return true, nil
}

splitAction := strings.Split(perm, "/")
Copy link
Member

Choose a reason for hiding this comment

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

will we never have something like worker/pipeline/create? When the namespace can be multiple embedded ones for clarity? Not saying we should, I'm just wondering if we should rather just check the last / and everything after that is the action. Since the namespace could be defined as embedded namespaces. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm that's an interesting thought. We should probably handle it you're right.
Unless we name it something like worker::pipeline/create? Actually I think worker/pipeline::create maybe even looks nicer?

My main thoughts are that we should seperate the namespace and action with something unique (in this case /).

Copy link
Member

Choose a reason for hiding this comment

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

A single colon is fine actually, I think. No need to bring PHP into this. :D

Copy link
Contributor Author

Choose a reason for hiding this comment

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

PHP.... Java... anything else? :P

namespace := splitAction[0]
action := splitAction[1]

fullResource := "*"
Copy link
Member

Choose a reason for hiding this comment

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

Does this mean that we are allow all by default? I think that should be deny by default, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So we need to use * for endpoints where there isn't a resource id specified in the api-mappings yaml. Right now that is something like secrets/create that doesn't need the enforcement of a resource id on it.

Copy link
Member

Choose a reason for hiding this comment

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

Wait, why isn't there a secrets and why doesn't that need enforcement? :D

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think you have misunderstood mate! We have permissions on secrets. I just mean that secrets/create doesn't have resource (a key ID) enforced because you haven't created something yet!

So:
not specified < cannot create secrets
p, secrets, create, *, deny < cannot create secrets
p, secrets, create, *, allow < can create secrets

func loadModel() (model.Model, error) {
modelStr, err := assethelper.LoadRBACModel()
if err != nil {
return nil, err
Copy link
Member

Choose a reason for hiding this comment

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

Add logging here as well, and all the rest of the errors. The more messages we get the more we know what went wrong. Pass in the logger as a dependency so you can use it with a method receiver.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

WIll go through and do this as I tidy up mate

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd prefer to add logging at the caller level. So in this case it would be where we call NewEnforcerSvc. The rest of the logging I will add into the handler code, etc.

One thing I will do is wrap the errors though to give more context 👍

Copy link
Member

Choose a reason for hiding this comment

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

That's good as well. High level logging is fine but that logging wraps at least 3 or more different errors. If even one of them is similar we'll have no idea where the error happened. Wrapping is a good solution to that. Love the new error system. 😊

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Where’s a good old Java stacktrace when you need it! Haha

Copy link
Member

Choose a reason for hiding this comment

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

Bwuhahaha, pssss, not so loud.. The other gophers might hear you. 😂😂😂

return gaia.RBACAPIMappings{}, err
}

var rbacapiMappings gaia.RBACAPIMappings
Copy link
Member

Choose a reason for hiding this comment

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

Declare this above and then return it.

}

var rbacapiMappings gaia.RBACAPIMappings
if err := yaml.Unmarshal([]byte(mappings), &rbacapiMappings); err != nil {
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if we should lock this mapping down for tempering with UnmarshalStrict. That will throw errors in case of duplicate fields and mappings which do not have corresponding structs.

return err
}
if !exists {
return errors.New("role does not exists for user")
Copy link
Member

Choose a reason for hiding this comment

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

*exist

helper/assethelper/helper.go Show resolved Hide resolved

// LoadRBACBuiltinPolicy loads the builtin rbac-policy.csv
func LoadRBACBuiltinPolicy() (string, error) {
return loadStaticFile("rbac-policy.csv")
Copy link
Member

Choose a reason for hiding this comment

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

Put the file names into constants.

store/store.go Show resolved Hide resolved
* Updated our bolt dependency (the adapter is supporting latest - so makes sense to bump us too)
* Log errors in handlers and correct wrap errors
* Improve static rbac-policy.csv
@speza
Copy link
Contributor Author

speza commented Jun 12, 2020

Just to top it off @Skarlso circle is still using Go 1.12 so won’t compile the error wrapping!

@Skarlso
Copy link
Member

Skarlso commented Jun 12, 2020

#256 (comment)

Yes. Right now circleci is failing until we can update it and push a new image.

speza added 7 commits June 13, 2020 23:54
* We now load in a different file format at start up for the RBAC api mappings. This one is more flexible, and I believe more user friendly (see the comment in the .yml file). Once loaded, we still have an O(1) lookup available for the endpoints :)
* Moved some models out of Gaia into rbac as they we only being used internally by the rbac package.
* Split the settings store interface out into its own (but nested in store.GaiaStore)
* Added unit tests for settings.go and improved DI on existing ones
* Created a new SettingsHandler to encapsulate the store dependency
* Add ability to save RBAC enabled state into database
* RBAC service is switched to EnforcerService or NoOpService depeneding on enabled state
* RBAC can be enabled or disabled with cmd flag `-rbac-enabled=true/false`
Add RBAC settings flag (enabled/disabled) and some improvements
* Split the settings store interface out into its own (but nested in store.GaiaStore)
* Added unit tests for settings.go and improved DI on existing ones
* Created a new SettingsHandler to encapsulate the store dependency
* Add ability to save RBAC enabled state into database
* RBAC service is switched to EnforcerService or NoOpService depending on enabled state
* RBAC can be enabled or disabled with cmd flag `-rbac-enabled=true/false`
* Fixed issues with missing or incorrect rbac-api-mappings.yml
* Started some acceptance tests for RBAC (probably to be finished outside this PR - its a lot of work)
* Added some tests for the content of the rbac-api-mappings.yml to alert us to any future change
* Endpoint Enforce now returns a PermissionDenied error
@codecov-commenter
Copy link

codecov-commenter commented Jun 16, 2020

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

Attention: Patch coverage is 65.66038% with 91 lines in your changes missing coverage. Please review.

Project coverage is 62.80%. Comparing base (532ac3f) to head (8c10fdd).
Report is 25 commits behind head on master.

Files with missing lines Patch % Lines
security/rbac/service.go 16.86% 67 Missing and 2 partials ⚠️
handlers/settings.go 66.66% 5 Missing and 4 partials ⚠️
handlers/auth.go 73.68% 3 Missing and 2 partials ⚠️
security/rbac/endpoint_enforcer.go 87.50% 3 Missing and 1 partial ⚠️
store/store.go 42.85% 3 Missing and 1 partial ⚠️

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #256      +/-   ##
==========================================
+ Coverage   62.33%   62.80%   +0.47%     
==========================================
  Files          46       51       +5     
  Lines        3746     4001     +255     
==========================================
+ Hits         2335     2513     +178     
- Misses       1027     1092      +65     
- Partials      384      396      +12     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@speza speza changed the title WIP: RBAC using Casbin RBAC using Casbin Jun 28, 2020
@speza speza marked this pull request as ready for review June 28, 2020 18:27

// SettingsStore is the interface that defines methods needed to save settings config into the store.
type SettingsStore interface {
SettingsPut(config *gaia.StoreConfig) error
Copy link
Member

Choose a reason for hiding this comment

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

why can't an RBACPut be a SettingsPut? Why are they two different functions? An interface shouldn't be explicit. It should be generic mostly. If there is a settingsRbacPut that should most likely be either a more specialised interface or a concrete implementation of settings put.

I see that it's putting / getting an RBACConfig instead of a StoreConfig, but that also means that it can't be a SettingsStore.

What I mean is, either make rbac use settingsput or have a separate interface. :)

server/server.go Outdated Show resolved Hide resolved
speza added 7 commits July 4, 2020 10:35
@Skarlso
Copy link
Member

Skarlso commented Jul 6, 2020

Testing was successful on the deployed instance! Well done @speza! Thanks for this awesome work and effort you've put into this and your perseverance. :) Fantastic work! 👍

@Skarlso Skarlso merged commit b419af3 into gaia-pipeline:master Jul 6, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants