Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/controlplane/cmd/wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,15 @@ func wireApp(*conf.Bootstrap, credentials.ReaderWriter, log.Logger, sdk.Availabl
newCASServerOptions,
newAuthAllowList,
newJWTConfig,
authzConfig,
),
)
}

func authzConfig() *authz.Config {
return &authz.Config{ManagedResources: authz.ManagedResources, RolesMap: authz.RolesMap}
}

func newJWTConfig(conf *conf.Auth) *biz.APITokenJWTConfig {
return &biz.APITokenJWTConfig{
SymmetricHmacKey: conf.GeneratedJwsHmacSecret,
Expand Down
7 changes: 6 additions & 1 deletion app/controlplane/cmd/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

253 changes: 24 additions & 229 deletions app/controlplane/pkg/authz/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,11 @@
// Authorization package
package authz

import (
"context"
_ "embed"
"errors"
"fmt"

psqlwatcher "github.com/IguteChung/casbin-psql-watcher"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/model"
"github.com/casbin/casbin/v2/persist"
fileadapter "github.com/casbin/casbin/v2/persist/file-adapter"
entadapter "github.com/casbin/ent-adapter"
config "github.com/chainloop-dev/chainloop/app/controlplane/pkg/conf/controlplane/config/v1"
)
// resource, action tuple
type Policy struct {
Resource string
Action string
}

type Role string

Expand All @@ -55,9 +46,7 @@ const (
ResourceRobotAccount = "robot_account"
ResourceWorkflowRun = "workflow_run"
ResourceWorkflow = "workflow"
UserMembership = "membership_user"
Organization = "organization"
ResourceProject = "project"
ResourceGroup = "group"
ResourceGroupMembership = "group_membership"

Expand All @@ -83,10 +72,22 @@ const (
RoleProjectViewer Role = "role:project:viewer"
)

// resource, action tuple
type Policy struct {
Resource string
Action string
// ManagedResources are the resources that are managed by Chainloop, considered during permissions sync
var ManagedResources = []string{
ResourceWorkflowContract,
ResourceCASArtifact,
ResourceCASBackend,
ResourceReferrer,
ResourceAvailableIntegration,
ResourceRegisteredIntegration,
ResourceAttachedIntegration,
ResourceOrgMetric,
ResourceRobotAccount,
ResourceWorkflowRun,
ResourceWorkflow,
Organization,
ResourceGroup,
ResourceGroupMembership,
}

var (
Expand Down Expand Up @@ -142,10 +143,10 @@ var (
PolicyGroupListMemberships = &Policy{ResourceGroupMembership, ActionList}
)

// List of policies for each role
// NOTE: roles are hierarchical, this means that the Admin Role can inherit all the policies from the Viewer Role
// RolesMap The default list of policies for each role
// NOTE: roles are not necessarily hierarchical, however the Admin Role inherits all the policies from the Viewer Role
// so we do not need to add them as well.
var rolesMap = map[Role][]*Policy{
var RolesMap = map[Role][]*Policy{
// RoleViewer is an org-scoped role that provides read-only access to all resources
RoleViewer: {
// Referrer
Expand Down Expand Up @@ -327,212 +328,6 @@ var ServerOperationsMap = map[string][]*Policy{
"/controlplane.v1.GroupService/ListMembers": {PolicyGroupListMemberships},
}

type SubjectAPIToken struct {
ID string
}

func (t *SubjectAPIToken) String() string {
return fmt.Sprintf("api-token:%s", t.ID)
}

//go:embed model.conf
var modelFile []byte

type Enforcer struct {
*casbin.Enforcer
}

func (e *Enforcer) AddPolicies(sub *SubjectAPIToken, policies ...*Policy) error {
if len(policies) == 0 {
return errors.New("no policies to add")
}

if sub == nil {
return errors.New("no subject provided")
}

for _, p := range policies {
casbinPolicy := []string{sub.String(), p.Resource, p.Action}
// Add policies one by one to skip existing ones.
// This is because the bulk method AddPoliciesEx does not work well with the ent adapter
if _, err := e.AddPolicy(casbinPolicy); err != nil {
return fmt.Errorf("failed to add policy: %w", err)
}
}

return nil
}

func (e *Enforcer) Enforce(sub string, p *Policy) (bool, error) {
return e.Enforcer.Enforce(sub, p.Resource, p.Action)
}

// Remove all the policies for the given subject
func (e *Enforcer) ClearPolicies(sub *SubjectAPIToken) error {
if sub == nil {
return errors.New("no subject provided")
}

// Get all the policies for the subject
policies, err := e.GetFilteredPolicy(0, sub.String())
if err != nil {
return fmt.Errorf("failed to get policies: %w", err)
}

if _, err := e.Enforcer.RemovePolicies(policies); err != nil {
return fmt.Errorf("failed to remove policies: %w", err)
}

return nil
}

// NewDatabaseEnforcer creates a new casbin authorization enforcer
// based on a database backend as policies storage backend
func NewDatabaseEnforcer(c *config.DatabaseConfig) (*Enforcer, error) {
// policy storage in database
a, err := entadapter.NewAdapter(c.Driver, c.Source)
if err != nil {
return nil, fmt.Errorf("failed to create adapter: %w", err)
}

e, err := newEnforcer(a)
if err != nil {
return nil, fmt.Errorf("failed to create enforcer: %w", err)
}

// watch for policy changes in database and update enforcer
w, err := psqlwatcher.NewWatcherWithConnString(context.Background(), c.Source, psqlwatcher.Option{})
if err != nil {
return nil, fmt.Errorf("failed to create watcher: %w", err)
}

if err = e.SetWatcher(w); err != nil {
return nil, fmt.Errorf("failed to set watcher: %w", err)
}

if err = w.SetUpdateCallback(func(string) {
// When there is a change in the policy, we load the in-memory policy for the current enforcer
if err := e.LoadPolicy(); err != nil {
fmt.Printf("failed to load policy: %v", err)
}
}); err != nil {
return nil, fmt.Errorf("failed to set update callback: %w", err)
}

return e, nil
}

// NewFileAdapter creates a new casbin authorization enforcer
// based on a CSV file as policies storage backend
func NewFiletypeEnforcer(path string) (*Enforcer, error) {
// policy storage in filesystem
a := fileadapter.NewAdapter(path)
e, err := newEnforcer(a)
if err != nil {
return nil, fmt.Errorf("failed to create enforcer: %w", err)
}

return e, nil
}

// NewEnforcer creates a new casbin authorization enforcer for the policies stored
// in the database and the model defined in model.conf
func newEnforcer(a persist.Adapter) (*Enforcer, error) {
// load model defined in model.conf
m, err := model.NewModelFromString(string(modelFile))
if err != nil {
return nil, fmt.Errorf("failed to create model: %w", err)
}

// create enforcer for authorization
enforcer, err := casbin.NewEnforcer(m, a)
if err != nil {
return nil, fmt.Errorf("failed to create enforcer: %w", err)
}

// Initialize the enforcer with the roles map
if err := syncRBACRoles(&Enforcer{enforcer}); err != nil {
return nil, fmt.Errorf("failed to sync roles: %w", err)
}

return &Enforcer{enforcer}, nil
}

// Load the roles map into the enforcer
// This is done by adding all the policies defined in the roles map
// and removing all the policies that are not
func syncRBACRoles(e *Enforcer) error {
return doSync(e, rolesMap)
}

func doSync(e *Enforcer, rolesMap map[Role][]*Policy) error {
// Add all the defined policies if they don't exist
for role, policies := range rolesMap {
for _, p := range policies {
// Add policies one by one to skip existing ones.
// This is because the bulk method AddPoliciesEx does not work well with the ent adapter
casbinPolicy := []string{string(role), p.Resource, p.Action}
_, err := e.AddPolicy(casbinPolicy)
if err != nil {
return fmt.Errorf("failed to add policy: %w", err)
}
}
}

// Delete all the policies that are not in the roles map
// 1 - load the policies from the enforcer DB
policies, err := e.GetPolicy()
if err != nil {
return fmt.Errorf("failed to get policies: %w", err)
}

for _, gotPolicies := range policies {
role := gotPolicies[0]
policy := &Policy{Resource: gotPolicies[1], Action: gotPolicies[2]}

wantPolicies, ok := rolesMap[Role(role)]
// if the role does not exist in the map, we can delete the policy
if !ok {
_, err := e.RemovePolicy(role, policy.Resource, policy.Action)
if err != nil {
return fmt.Errorf("failed to remove policy: %w", err)
}
continue
}

// We have the role in the map, so we now compare the policies
found := false
for _, p := range wantPolicies {
if p.Resource == policy.Resource && p.Action == policy.Action {
found = true
break
}
}

// If the policy is not in the map, we remove it
if !found {
_, err := e.RemovePolicy(gotPolicies)
if err != nil {
return fmt.Errorf("failed to remove policy: %w", err)
}
}
}

// To finish we make sure that the admin role inherit all the policies from the viewer role
_, err = e.AddGroupingPolicy(string(RoleAdmin), string(RoleViewer))
if err != nil {
return fmt.Errorf("failed to add grouping policy: %w", err)
}

// same for the owner
_, err = e.AddGroupingPolicy(string(RoleOwner), string(RoleAdmin))
if err != nil {
return fmt.Errorf("failed to add grouping policy: %w", err)
}

return nil
}

// Implements https://pkg.go.dev/entgo.io/ent/schema/field#EnumValues
// so they can be added to the database schema
func (Role) Values() (roles []string) {
Expand Down
4 changes: 2 additions & 2 deletions app/controlplane/pkg/authz/authz_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ func TestMultiReplicaPropagation(t *testing.T) {
db := testhelpers.NewTestDatabase(t)
defer db.Close(t)

enforcerA, err := authz.NewDatabaseEnforcer(testhelpers.NewDataConfig(testhelpers.NewConfData(db, t)))
enforcerA, err := authz.NewDatabaseEnforcer(testhelpers.NewDataConfig(testhelpers.NewConfData(db, t)), &authz.Config{})
require.NoError(t, err)
enforcerB, err := authz.NewDatabaseEnforcer(testhelpers.NewDataConfig(testhelpers.NewConfData(db, t)))
enforcerB, err := authz.NewDatabaseEnforcer(testhelpers.NewDataConfig(testhelpers.NewConfData(db, t)), &authz.Config{})
require.NoError(t, err)

// Subject and policies to add
Expand Down
Loading
Loading