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
2 changes: 2 additions & 0 deletions app/controlplane/configs/config.devel.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ enable_profiler: true
# operation_authorization_provider:
# enabled: true
# url: http://localhost:8002/v1/authorize
# operations:
# - /controlplane.v1.WorkflowRunService/Create

ui_dashboard_url: http://localhost:3000

Expand Down
18 changes: 15 additions & 3 deletions app/controlplane/internal/conf/controlplane/config/v1/conf.pb.go

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

Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ message OperationAuthorizationProvider {
string url = 1 [(buf.validate.field).string.uri = true];
// Whether to enable the operation authorization
bool enabled = 2;
// authorized operations list
repeated string operations = 3;
}

message FederatedAuthentication {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ import (
"encoding/json"
"fmt"
"net/http"
"slices"
"time"

conf "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf/controlplane/config/v1"
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities"
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz"
errorsAPI "github.com/go-kratos/kratos/v2/errors"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/middleware"
Expand Down Expand Up @@ -61,7 +61,7 @@ func WithOperationAuthorizationMiddleware(conf *conf.OperationAuthorizationProvi
}

operation := info.Operation()
if !authz.RequiresExternalAuthz(operation) {
if !slices.Contains(conf.GetOperations(), operation) {
Comment thread
jiparis marked this conversation as resolved.
return handler(ctx, req)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (
"testing"

conf "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf/controlplane/config/v1"
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/transport"
"github.com/stretchr/testify/assert"
Expand All @@ -33,14 +32,6 @@ import (

const testExternalAuthzOp = "/test.v1.ExternalAuthzTestService/Target"

func registerTestExternalAuthzOp(t *testing.T) {
t.Helper()
authz.ServerOperationsMap[testExternalAuthzOp] = &authz.OperationPolicy{ExternalAuthz: true}
t.Cleanup(func() {
delete(authz.ServerOperationsMap, testExternalAuthzOp)
})
}

// fakeTransport implements transport.Transporter for testing middleware operation matching.
type fakeTransport struct {
operation string
Expand Down Expand Up @@ -106,7 +97,6 @@ func TestWithOperationAuthorizationMiddleware(t *testing.T) {
})

t.Run("target operation allowed", func(t *testing.T) {
registerTestExternalAuthzOp(t)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req operationAuthRequest
require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
Expand All @@ -117,7 +107,7 @@ func TestWithOperationAuthorizationMiddleware(t *testing.T) {
}))
defer srv.Close()

cfg := &conf.OperationAuthorizationProvider{Enabled: true, Url: srv.URL}
cfg := &conf.OperationAuthorizationProvider{Enabled: true, Url: srv.URL, Operations: []string{testExternalAuthzOp}}
m := WithOperationAuthorizationMiddleware(cfg, logHelper)

ctx := ctxWithOperation(context.Background(), testExternalAuthzOp)
Expand All @@ -128,14 +118,13 @@ func TestWithOperationAuthorizationMiddleware(t *testing.T) {
})

t.Run("target operation denied", func(t *testing.T) {
registerTestExternalAuthzOp(t)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(operationAuthResponse{Allowed: false, Reason: "org limit reached"}) //nolint:errcheck
}))
defer srv.Close()

cfg := &conf.OperationAuthorizationProvider{Enabled: true, Url: srv.URL}
cfg := &conf.OperationAuthorizationProvider{Enabled: true, Url: srv.URL, Operations: []string{testExternalAuthzOp}}
m := WithOperationAuthorizationMiddleware(cfg, logHelper)

ctx := ctxWithOperation(context.Background(), testExternalAuthzOp)
Expand All @@ -147,8 +136,7 @@ func TestWithOperationAuthorizationMiddleware(t *testing.T) {
})

t.Run("provider unreachable is fail-closed", func(t *testing.T) {
registerTestExternalAuthzOp(t)
cfg := &conf.OperationAuthorizationProvider{Enabled: true, Url: "http://127.0.0.1:1"}
cfg := &conf.OperationAuthorizationProvider{Enabled: true, Url: "http://127.0.0.1:1", Operations: []string{testExternalAuthzOp}}
m := WithOperationAuthorizationMiddleware(cfg, logHelper)

ctx := ctxWithOperation(context.Background(), testExternalAuthzOp)
Expand All @@ -160,13 +148,12 @@ func TestWithOperationAuthorizationMiddleware(t *testing.T) {
})

t.Run("provider returns 500 is fail-closed", func(t *testing.T) {
registerTestExternalAuthzOp(t)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer srv.Close()

cfg := &conf.OperationAuthorizationProvider{Enabled: true, Url: srv.URL}
cfg := &conf.OperationAuthorizationProvider{Enabled: true, Url: srv.URL, Operations: []string{testExternalAuthzOp}}
m := WithOperationAuthorizationMiddleware(cfg, logHelper)

ctx := ctxWithOperation(context.Background(), testExternalAuthzOp)
Expand All @@ -178,7 +165,6 @@ func TestWithOperationAuthorizationMiddleware(t *testing.T) {
})

t.Run("bearer token is forwarded", func(t *testing.T) {
registerTestExternalAuthzOp(t)
var gotAuth string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotAuth = r.Header.Get("Authorization")
Expand All @@ -187,7 +173,7 @@ func TestWithOperationAuthorizationMiddleware(t *testing.T) {
}))
defer srv.Close()

cfg := &conf.OperationAuthorizationProvider{Enabled: true, Url: srv.URL}
cfg := &conf.OperationAuthorizationProvider{Enabled: true, Url: srv.URL, Operations: []string{testExternalAuthzOp}}
m := WithOperationAuthorizationMiddleware(cfg, logHelper)

ft := &fakeTransport{
Expand Down
14 changes: 2 additions & 12 deletions app/controlplane/pkg/authz/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,8 @@ type Policy struct {
}

// OperationPolicy describes both the local Casbin policies required for an operation
// and whether the operation also requires external authorization.
type OperationPolicy struct {
Policies []*Policy
ExternalAuthz bool
Policies []*Policy
}

type Role string
Expand Down Expand Up @@ -375,7 +373,7 @@ var ServerOperationsMap = map[string]*OperationPolicy{
// CAS Backend listing
"/controlplane.v1.CASBackendService/List": {Policies: []*Policy{PolicyCASBackendList}},
"/controlplane.v1.CASBackendService/Revalidate": {Policies: []*Policy{PolicyCASBackendUpdate}},
"/controlplane.v1.CASBackendService/Create": {Policies: []*Policy{PolicyCASBackendCreate}, ExternalAuthz: true},
"/controlplane.v1.CASBackendService/Create": {Policies: []*Policy{PolicyCASBackendCreate}},
// Available integrations
"/controlplane.v1.IntegrationsService/ListAvailable": {Policies: []*Policy{PolicyAvailableIntegrationList, PolicyAvailableIntegrationRead}},
// Registered integrations
Expand Down Expand Up @@ -459,14 +457,6 @@ var ServerOperationsMap = map[string]*OperationPolicy{
"/controlplane.v1.OrgInvitationService/Create": {Policies: []*Policy{PolicyOrganizationInvitationsCreate}},
}

// RequiresExternalAuthz returns whether the given operation requires external authorization.
func RequiresExternalAuthz(operation string) bool {
if entry, ok := ServerOperationsMap[operation]; ok {
return entry.ExternalAuthz
}
return false
}

// 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
30 changes: 0 additions & 30 deletions app/controlplane/pkg/authz/authz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,36 +129,6 @@ func TestDoSync(t *testing.T) {
assert.Equal(t, "delete", got[0][2])
}

func TestRequiresExternalAuthz(t *testing.T) {
testCases := []struct {
name string
operation string
want bool
}{
{
name: "CAS backend creation is forwarded to the external authorizer",
operation: "/controlplane.v1.CASBackendService/Create",
want: true,
},
{
name: "operations without external authz flag are not forwarded",
operation: "/controlplane.v1.WorkflowService/List",
want: false,
},
{
name: "unknown operations are not forwarded",
operation: "/controlplane.v1.UnknownService/Unknown",
want: false,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, RequiresExternalAuthz(tc.operation))
})
}
}

func testEnforcer(t *testing.T) (*CasbinEnforcer, io.Closer) {
f, err := os.CreateTemp(t.TempDir(), "policy*.csv")
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion deployment/chainloop/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: Chainloop is an open source software supply chain control plane, a

type: application
# Bump the patch (not minor, not major) version on each change in the Chart Source code
version: 1.385.0
version: 1.385.1
# Do not update appVersion, this is handled automatically by the release process
appVersion: v1.98.4

Expand Down
2 changes: 2 additions & 0 deletions deployment/chainloop/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,11 @@ controlplane:
## @extra controlplane.operationAuthorizationProvider Enable external operation authorization
## @param controlplane.operationAuthorizationProvider.enabled Enable operation authorization
## @param controlplane.operationAuthorizationProvider.url URL of the authorization endpoint
## @param controlplane.operationAuthorizationProvider.operations List of gRPC operations that require external authorization (e.g. "/controlplane.v1.WorkflowRunService/Create")
operationAuthorizationProvider:
enabled: false
url: ""
operations: []

## @skip controlplane.restrictOrgCreation Restrict organization creation to instance admins
restrictOrgCreation: false
Expand Down
Loading