enhancement: Evaluate policy resource information on single file shares (#6888)

* enhancement: Evaluate policy resource information on single file shares

* enhancement: switch to resource name evaluation for example rego put rule
This commit is contained in:
Florian Schade
2023-07-31 12:12:56 +02:00
committed by GitHub
parent 5b26de71d4
commit 90ba75e27e
8 changed files with 357 additions and 26 deletions

View File

@@ -0,0 +1,6 @@
Enhancement: Evaluate policy resource information on single file shares
The policy environment for single file shares now also includes information about the resource.
As a result, it is now possible to set up and check rules for them.
https://github.com/owncloud/ocis/pull/6888

View File

@@ -6,14 +6,28 @@ import data.utils
default granted := true
granted = false if {
print("PRINT MESSAGE EXAMPLE")
input.request.method == "PUT"
not startswith(input.request.path, "/ocs")
not utils.is_extension_allowed(input.request.path)
pathPrefixes := [
"/dav",
"/remote.php/webdav",
"/remote.php/dav",
"/webdav",
]
restricted := pathPrefixes[_]
startswith(input.request.path, restricted)
not utils.is_extension_allowed(input.resource.name)
}
granted = false if {
input.request.method == "POST"
startswith(input.request.path, "/remote.php")
pathPrefixes := [
"/data",
"/dav",
"/remote.php/webdav",
"/remote.php/dav",
"/webdav",
]
restricted := pathPrefixes[_]
startswith(input.request.path, restricted)
not utils.is_extension_allowed(input.resource.name)
}

View File

@@ -24,7 +24,8 @@ docs-generate: config-docs-generate
include ../../.make/generate.mk
.PHONY: ci-go-generate
ci-go-generate: # CI runs ci-node-generate automatically before this target
ci-go-generate: $(MOCKERY) # CI runs ci-node-generate automatically before this target
$(MOCKERY) --dir ../../protogen/gen/ocis/services/policies/v0 --case underscore --name PoliciesProviderService
.PHONY: ci-node-generate
ci-node-generate:

View File

@@ -0,0 +1,66 @@
// Code generated by mockery v2.22.1. DO NOT EDIT.
package mocks
import (
context "context"
client "go-micro.dev/v4/client"
mock "github.com/stretchr/testify/mock"
v0 "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/policies/v0"
)
// PoliciesProviderService is an autogenerated mock type for the PoliciesProviderService type
type PoliciesProviderService struct {
mock.Mock
}
// Evaluate provides a mock function with given fields: ctx, in, opts
func (_m *PoliciesProviderService) Evaluate(ctx context.Context, in *v0.EvaluateRequest, opts ...client.CallOption) (*v0.EvaluateResponse, error) {
_va := make([]interface{}, len(opts))
for _i := range opts {
_va[_i] = opts[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, in)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 *v0.EvaluateResponse
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *v0.EvaluateRequest, ...client.CallOption) (*v0.EvaluateResponse, error)); ok {
return rf(ctx, in, opts...)
}
if rf, ok := ret.Get(0).(func(context.Context, *v0.EvaluateRequest, ...client.CallOption) *v0.EvaluateResponse); ok {
r0 = rf(ctx, in, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v0.EvaluateResponse)
}
}
if rf, ok := ret.Get(1).(func(context.Context, *v0.EvaluateRequest, ...client.CallOption) error); ok {
r1 = rf(ctx, in, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
type mockConstructorTestingTNewPoliciesProviderService interface {
mock.TestingT
Cleanup(func())
}
// NewPoliciesProviderService creates a new instance of PoliciesProviderService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewPoliciesProviderService(t mockConstructorTestingTNewPoliciesProviderService) *PoliciesProviderService {
mock := &PoliciesProviderService{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -8,14 +8,19 @@ import (
"net/http"
"time"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/store"
"github.com/cs3org/reva/v2/pkg/token/manager/jwt"
"github.com/go-chi/chi/v5"
chimiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/render"
"github.com/justinas/alice"
"github.com/oklog/run"
"github.com/urfave/cli/v2"
microstore "go-micro.dev/v4/store"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel/trace"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/store"
"github.com/cs3org/reva/v2/pkg/token/manager/jwt"
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
pkgmiddleware "github.com/owncloud/ocis/v2/ocis-pkg/middleware"
@@ -24,6 +29,7 @@ import (
"github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
"github.com/owncloud/ocis/v2/ocis-pkg/tracing"
"github.com/owncloud/ocis/v2/ocis-pkg/version"
policiessvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/policies/v0"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
storesvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/store/v0"
"github.com/owncloud/ocis/v2/services/proxy/pkg/autoprovision"
@@ -38,10 +44,6 @@ import (
proxyHTTP "github.com/owncloud/ocis/v2/services/proxy/pkg/server/http"
"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend"
"github.com/owncloud/ocis/v2/services/proxy/pkg/userroles"
"github.com/urfave/cli/v2"
microstore "go-micro.dev/v4/store"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel/trace"
)
// Server is the entrypoint for the server command.
@@ -273,6 +275,7 @@ func (h *StaticRouteHandler) backchannelLogout(w http.ResponseWriter, r *http.Re
func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config, userInfoCache microstore.Store, traceProvider trace.TracerProvider) alice.Chain {
rolesClient := settingssvc.NewRoleService("com.owncloud.api.settings", cfg.GrpcClient)
policiesProviderClient := policiessvc.NewPoliciesProviderService("com.owncloud.api.policies", cfg.GrpcClient)
gatewaySelector, err := pool.GatewaySelector(cfg.Reva.Address, append(cfg.Reva.GetRevaOptions(), pool.WithRegistry(registry.GetRegistry()))...)
if err != nil {
logger.Fatal().Err(err).Msg("Failed to get gateway selector")
@@ -411,7 +414,12 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config,
middleware.Logger(logger),
middleware.PolicySelectorConfig(*cfg.PolicySelector),
),
middleware.Policies(logger, cfg.PoliciesMiddleware.Query, cfg.GrpcClient),
middleware.Policies(
cfg.PoliciesMiddleware.Query,
middleware.Logger(logger),
middleware.WithRevaGatewaySelector(gatewaySelector),
middleware.PoliciesProviderService(policiesProviderClient),
),
// finally, trigger home creation when a user logs in
middleware.CreateHome(
middleware.Logger(logger),

View File

@@ -5,16 +5,19 @@ import (
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
"go-micro.dev/v4/store"
"go.opentelemetry.io/otel/trace"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
policiessvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/policies/v0"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
storesvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/store/v0"
"github.com/owncloud/ocis/v2/services/proxy/pkg/config"
"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend"
"github.com/owncloud/ocis/v2/services/proxy/pkg/userroles"
store "go-micro.dev/v4/store"
"go.opentelemetry.io/otel/trace"
)
// Option defines a single option function.
@@ -36,6 +39,8 @@ type Options struct {
UserRoleAssigner userroles.UserRoleAssigner
// SettingsRoleService for the roles API in settings
SettingsRoleService settingssvc.RoleService
// PoliciesProviderService for policy evaluation
PoliciesProviderService policiessvc.PoliciesProviderService
// OIDCProviderFunc to lazily initialize an oidc provider, must be set for the oidc_auth middleware
OIDCClient oidc.OIDCClient
// OIDCIss is the oidcAuth-issuer
@@ -118,7 +123,14 @@ func SettingsRoleService(rc settingssvc.RoleService) Option {
}
}
// OIDCClient provides a function to set the the oidc client option.
// PoliciesProviderService provides a function to set the policies provider option.
func PoliciesProviderService(pps policiessvc.PoliciesProviderService) Option {
return func(o *Options) {
o.PoliciesProviderService = pps
}
}
// OIDCClient provides a function to set the oidc client option.
func OIDCClient(val oidc.OIDCClient) Option {
return func(o *Options) {
o.OIDCClient = val
@@ -139,7 +151,7 @@ func CredentialsByUserAgent(v map[string]string) Option {
}
}
// WithRevaGatewaySelector provides a function to set the the reva gateway service selector option.
// WithRevaGatewaySelector provides a function to set the reva gateway service selector option.
func WithRevaGatewaySelector(val pool.Selectable[gateway.GatewayAPIClient]) Option {
return func(o *Options) {
o.RevaGatewaySelector = val

View File

@@ -3,17 +3,23 @@ package middleware
import (
"net/http"
"path"
"path/filepath"
"strings"
"time"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/render"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
tusd "github.com/tus/tusd/pkg/handler"
"google.golang.org/grpc/metadata"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/cs3org/reva/v2/pkg/utils"
pMessage "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/policies/v0"
pService "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/policies/v0"
"github.com/owncloud/ocis/v2/services/webdav/pkg/net"
tusd "github.com/tus/tusd/pkg/handler"
"go-micro.dev/v4/client"
)
type (
@@ -34,8 +40,11 @@ type (
const DeniedMessage = "Operation denied due to security policies"
// Policies verifies if a request is granted or not.
func Policies(logger log.Logger, qs string, grpcClient client.Client) func(next http.Handler) http.Handler {
pClient := pService.NewPoliciesProviderService("com.owncloud.api.policies", grpcClient)
func Policies(qs string, opts ...Option) func(next http.Handler) http.Handler {
options := newOptions(opts...)
logger := options.Logger
gatewaySelector := options.RevaGatewaySelector
policiesProviderClient := options.PoliciesProviderService
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -55,11 +64,53 @@ func Policies(logger log.Logger, qs string, grpcClient client.Client) func(next
},
}
resource := &pMessage.Resource{}
// tus
meta := tusd.ParseMetadataHeader(r.Header.Get(net.HeaderUploadMetadata))
req.Environment.Resource = &pMessage.Resource{
Name: meta["filename"],
resource.Name = meta["filename"]
// name is part of the request path
if resource.Name == "" && filepath.Ext(r.URL.Path) != "" {
resource.Name = filepath.Base(r.URL.Path)
}
// no resource info in path, stat the resource and try to obtain the file information.
// this should only be used as last bastion, every request goes through the proxy and doing stats is expensive!
// needed for:
// - if a single resource is shared -> the url only contains the resourceID (spaceRef)
if resource.Name == "" && filepath.Ext(r.URL.Path) == "" && r.Method == http.MethodPut && strings.HasPrefix(r.URL.Path, "/remote.php/dav/spaces") {
client, err := gatewaySelector.Next()
if err != nil {
logger.Err(err).Msg("error selecting next gateway client")
RenderError(w, r, req, http.StatusForbidden, DeniedMessage)
return
}
resourceID, err := storagespace.ParseID(strings.TrimPrefix(r.URL.Path, "/remote.php/dav/spaces/"))
if err != nil {
logger.Debug().Err(err).Msg("error parsing the resourceId")
RenderError(w, r, req, http.StatusForbidden, DeniedMessage)
return
}
if resourceID.StorageId == "" && resourceID.SpaceId == utils.ShareStorageSpaceID {
resourceID.StorageId = utils.ShareStorageProviderID
}
token := r.Header.Get(revactx.TokenHeader)
ctx := metadata.AppendToOutgoingContext(r.Context(), revactx.TokenHeader, token)
sRes, err := client.Stat(ctx, &provider.StatRequest{
Ref: &provider.Reference{
ResourceId: &resourceID,
},
})
resource.Name = sRes.GetInfo().GetName()
}
req.Environment.Resource = resource
if user, ok := revactx.ContextGetUser(r.Context()); ok {
req.Environment.User = &pMessage.User{
Id: &pMessage.User_ID{
@@ -72,7 +123,7 @@ func Policies(logger log.Logger, qs string, grpcClient client.Client) func(next
}
}
rsp, err := pClient.Evaluate(r.Context(), req)
rsp, err := policiesProviderClient.Evaluate(r.Context(), req)
if err != nil {
logger.Err(err).Msg("error evaluating request")
w.WriteHeader(http.StatusInternalServerError)

View File

@@ -0,0 +1,173 @@
package middleware_test
import (
"context"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/mock"
"go-micro.dev/v4/client"
"google.golang.org/grpc"
cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks"
"github.com/owncloud/ocis/v2/services/proxy/mocks"
"github.com/owncloud/ocis/v2/services/webdav/pkg/net"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
pMessage "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/policies/v0"
policiesPG "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/policies/v0"
"github.com/owncloud/ocis/v2/services/proxy/pkg/middleware"
)
func TestPolicies_NoQuery_PassThrough(t *testing.T) {
var g = NewWithT(t)
policiesMiddleware, _, _ := prepare("")
responseRecorder := httptest.NewRecorder()
policiesMiddleware.ServeHTTP(responseRecorder, httptest.NewRequest(http.MethodGet, "/policies", nil))
g.Expect(responseRecorder.Code).To(Equal(http.StatusOK))
}
func TestPolicies_ErrorsOnEvaluationError(t *testing.T) {
var g = NewWithT(t)
policiesMiddleware, policiesProviderService, _ := prepare("any")
policiesProviderService.On("Evaluate", mock.Anything, mock.Anything).Return(
nil,
errors.New("any"),
)
responseRecorder := httptest.NewRecorder()
policiesMiddleware.ServeHTTP(responseRecorder, httptest.NewRequest(http.MethodGet, "/policies", nil))
g.Expect(responseRecorder.Code).To(Equal(http.StatusInternalServerError))
}
func TestPolicies_ErrorsOnDeny(t *testing.T) {
var g = NewWithT(t)
policiesMiddleware, policiesProviderService, _ := prepare("any")
policiesProviderService.On("Evaluate", mock.Anything, mock.Anything).Return(
&policiesPG.EvaluateResponse{},
nil,
)
responseRecorder := httptest.NewRecorder()
policiesMiddleware.ServeHTTP(responseRecorder, httptest.NewRequest(http.MethodGet, "/policies", nil))
result := responseRecorder.Result()
defer func() {
g.Expect(result.Body.Close()).ToNot(HaveOccurred())
}()
data, err := io.ReadAll(result.Body)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(data).To(ContainSubstring(middleware.DeniedMessage))
g.Expect(responseRecorder.Code).To(Equal(http.StatusForbidden))
}
func TestPolicies_EvaluationEnvironment_HTTPStage(t *testing.T) {
var g = NewWithT(t)
policiesMiddleware, policiesProviderService, _ := prepare("any")
policiesProviderService.On("Evaluate", mock.Anything, mock.Anything, mock.Anything).Return(
func(_ context.Context, in *policiesPG.EvaluateRequest, _ ...client.CallOption) (*policiesPG.EvaluateResponse, error) {
g.Expect(in.Environment.Stage).To(Equal(pMessage.Stage_STAGE_HTTP))
return &policiesPG.EvaluateResponse{Result: false}, nil
},
)
responseRecorder := httptest.NewRecorder()
policiesMiddleware.ServeHTTP(responseRecorder, httptest.NewRequest(http.MethodGet, "/policies", nil))
}
func TestPolicies_EvaluationEnvironment_Request(t *testing.T) {
var g = NewWithT(t)
policiesMiddleware, policiesProviderService, _ := prepare("any")
policiesProviderService.On("Evaluate", mock.Anything, mock.Anything, mock.Anything).Return(
func(_ context.Context, in *policiesPG.EvaluateRequest, _ ...client.CallOption) (*policiesPG.EvaluateResponse, error) {
g.Expect(in.Environment.Request.Method).To(Equal(http.MethodDelete))
g.Expect(in.Environment.Request.Path).To(Equal("/whatever"))
return &policiesPG.EvaluateResponse{Result: false}, nil
},
)
responseRecorder := httptest.NewRecorder()
policiesMiddleware.ServeHTTP(responseRecorder, httptest.NewRequest(http.MethodDelete, "/whatever", nil))
}
func TestPolicies_EvaluationEnvironment_Resource(t *testing.T) {
var g = NewWithT(t)
policiesMiddleware, policiesProviderService, _ := prepare("any")
// tus metadata
{
responseRecorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/remote.php/dav/spaces", nil)
request.Header.Set(net.HeaderUploadMetadata, fmt.Sprintf("filename %v", base64.StdEncoding.EncodeToString([]byte("tus-file-name.png"))))
policiesProviderService.On("Evaluate", mock.Anything, mock.Anything, mock.Anything).Return(
func(_ context.Context, in *policiesPG.EvaluateRequest, _ ...client.CallOption) (*policiesPG.EvaluateResponse, error) {
g.Expect(in.Environment.Resource.Name).To(Equal("tus-file-name.png"))
return &policiesPG.EvaluateResponse{Result: false}, nil
},
).Once()
policiesMiddleware.ServeHTTP(responseRecorder, request)
}
// url path
{
responseRecorder := httptest.NewRecorder()
policiesProviderService.On("Evaluate", mock.Anything, mock.Anything, mock.Anything).Return(
func(_ context.Context, in *policiesPG.EvaluateRequest, _ ...client.CallOption) (*policiesPG.EvaluateResponse, error) {
g.Expect(in.Environment.Resource.Name).To(Equal("simple-file-name.png"))
return &policiesPG.EvaluateResponse{Result: false}, nil
},
).Once()
policiesMiddleware.ServeHTTP(responseRecorder, httptest.NewRequest(http.MethodPut, "/remote.php/dav/spaces/simple-file-name.png", nil))
}
}
func prepare(q string) (http.Handler, *mocks.PoliciesProviderService, *cs3mocks.GatewayAPIClient) {
// mocked gatewaySelector
gatewayClient := &cs3mocks.GatewayAPIClient{}
gatewaySelector := pool.GetSelector[gateway.GatewayAPIClient](
"GatewaySelector",
"com.owncloud.api.gateway",
func(cc *grpc.ClientConn) gateway.GatewayAPIClient {
return gatewayClient
},
)
defer pool.RemoveSelector("GatewaySelector" + "com.owncloud.api.gateway")
// mocked policiesProviderService
policiesProviderService := &mocks.PoliciesProviderService{}
// spin up middleware
policiesMiddleware := middleware.Policies(
q,
middleware.WithRevaGatewaySelector(gatewaySelector),
middleware.PoliciesProviderService(policiesProviderService),
)(mockHandler{})
return policiesMiddleware, policiesProviderService, gatewayClient
}
type mockHandler struct{}
func (m mockHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {}