Merge pull request #48 from owncloud/filter-settings-by-permissions

Filter settings by permissions
This commit is contained in:
Benedikt Kulmann
2020-08-26 15:23:31 +02:00
committed by GitHub
19 changed files with 2477 additions and 804 deletions

View File

@@ -8,6 +8,10 @@ issues:
text: "SA1019:"
linters:
- staticcheck
# Exclude scopelint for tests files because of https://github.com/kyoh86/scopelint/issues/4
- path: _test\.go
linters:
- scopelint
linters:
enable:
- bodyclose

3
go.mod
View File

@@ -22,7 +22,7 @@ require (
github.com/mitchellh/reflectwalk v1.0.1 // indirect
github.com/oklog/run v1.0.0
github.com/openzipkin/zipkin-go v0.2.2
github.com/owncloud/ocis-pkg/v2 v2.2.2-0.20200812103920-db41b5a3d14d
github.com/owncloud/ocis-pkg/v2 v2.4.0
github.com/restic/calens v0.2.0
github.com/spf13/viper v1.6.3
github.com/stretchr/testify v1.6.1
@@ -34,6 +34,7 @@ require (
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884
google.golang.org/protobuf v1.23.0
gotest.tools v2.2.0+incompatible
)
replace google.golang.org/grpc => google.golang.org/grpc v1.26.0

6
go.sum
View File

@@ -534,6 +534,7 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/haya14busa/goverage v0.0.0-20180129164344-eec3514a20b5 h1:FdBGmSkD2QpQzRWup//SGObvWf2nq89zj9+ta9OvI3A=
github.com/haya14busa/goverage v0.0.0-20180129164344-eec3514a20b5/go.mod h1:0YZ2wQSuwviXXXGUiK6zXzskyBLAbLXhamxzcFHSLoM=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@@ -760,6 +761,10 @@ github.com/ory/x v0.0.85/go.mod h1:s44V8t3xyjWZREcU+mWlp4h302rTuM4aLXcW+y5FbQ8=
github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014/go.mod h1:joRatxRJaZBsY3JAOEMcoOp05CnZzsx4scTxi95DHyQ=
github.com/owncloud/ocis-pkg/v2 v2.2.2-0.20200812103920-db41b5a3d14d h1:eruHqxLfS3fiPO1ylg60T++wShVqtayI4LxUhwOEuN4=
github.com/owncloud/ocis-pkg/v2 v2.2.2-0.20200812103920-db41b5a3d14d/go.mod h1:FSzIvhx9HcZcq4jgNaDowNvM7PTX/XCyoMvyfzidUpE=
github.com/owncloud/ocis-pkg/v2 v2.3.1-0.20200825114153-bc31e3e4b1e0 h1:dz/I+K7+1t3RKrNafDtb6EWJCQe7+z6c4V9UF0bGxCI=
github.com/owncloud/ocis-pkg/v2 v2.3.1-0.20200825114153-bc31e3e4b1e0/go.mod h1:FSzIvhx9HcZcq4jgNaDowNvM7PTX/XCyoMvyfzidUpE=
github.com/owncloud/ocis-pkg/v2 v2.4.0 h1:/3ZOd4txtwjiNKJA9iLT9BjrJw5YgHSX13fQR4BYfGY=
github.com/owncloud/ocis-pkg/v2 v2.4.0/go.mod h1:FSzIvhx9HcZcq4jgNaDowNvM7PTX/XCyoMvyfzidUpE=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/parnurzeal/gorequest v0.2.15/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE=
@@ -1352,6 +1357,7 @@ gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

File diff suppressed because it is too large Load Diff

View File

@@ -567,3 +567,77 @@ func (h *roleServiceHandler) AssignRoleToUser(ctx context.Context, in *AssignRol
func (h *roleServiceHandler) RemoveRoleFromUser(ctx context.Context, in *RemoveRoleFromUserRequest, out *empty.Empty) error {
return h.RoleServiceHandler.RemoveRoleFromUser(ctx, in, out)
}
// Api Endpoints for PermissionService service
func NewPermissionServiceEndpoints() []*api.Endpoint {
return []*api.Endpoint{
&api.Endpoint{
Name: "PermissionService.ListPermissionsByResource",
Path: []string{"/api/v0/settings/permissions-list-by-resource"},
Method: []string{"POST"},
Body: "*",
Handler: "rpc",
},
}
}
// Client API for PermissionService service
type PermissionService interface {
ListPermissionsByResource(ctx context.Context, in *ListPermissionsByResourceRequest, opts ...client.CallOption) (*ListPermissionsByResourceResponse, error)
}
type permissionService struct {
c client.Client
name string
}
func NewPermissionService(name string, c client.Client) PermissionService {
return &permissionService{
c: c,
name: name,
}
}
func (c *permissionService) ListPermissionsByResource(ctx context.Context, in *ListPermissionsByResourceRequest, opts ...client.CallOption) (*ListPermissionsByResourceResponse, error) {
req := c.c.NewRequest(c.name, "PermissionService.ListPermissionsByResource", in)
out := new(ListPermissionsByResourceResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for PermissionService service
type PermissionServiceHandler interface {
ListPermissionsByResource(context.Context, *ListPermissionsByResourceRequest, *ListPermissionsByResourceResponse) error
}
func RegisterPermissionServiceHandler(s server.Server, hdlr PermissionServiceHandler, opts ...server.HandlerOption) error {
type permissionService interface {
ListPermissionsByResource(ctx context.Context, in *ListPermissionsByResourceRequest, out *ListPermissionsByResourceResponse) error
}
type PermissionService struct {
permissionService
}
h := &permissionServiceHandler{hdlr}
opts = append(opts, api.WithEndpoint(&api.Endpoint{
Name: "PermissionService.ListPermissionsByResource",
Path: []string{"/api/v0/settings/permissions-list-by-resource"},
Method: []string{"POST"},
Body: "*",
Handler: "rpc",
}))
return s.Handle(s.NewHandler(&PermissionService{h}, opts...))
}
type permissionServiceHandler struct {
PermissionServiceHandler
}
func (h *permissionServiceHandler) ListPermissionsByResource(ctx context.Context, in *ListPermissionsByResourceRequest, out *ListPermissionsByResourceResponse) error {
return h.PermissionServiceHandler.ListPermissionsByResource(ctx, in, out)
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -389,6 +389,48 @@ func RegisterRoleServiceWeb(r chi.Router, i RoleServiceHandler, middlewares ...f
r.MethodFunc("POST", "/api/v0/settings/assignments-remove", handler.RemoveRoleFromUser)
}
type webPermissionServiceHandler struct {
r chi.Router
h PermissionServiceHandler
}
func (h *webPermissionServiceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.r.ServeHTTP(w, r)
}
func (h *webPermissionServiceHandler) ListPermissionsByResource(w http.ResponseWriter, r *http.Request) {
req := &ListPermissionsByResourceRequest{}
resp := &ListPermissionsByResourceResponse{}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusPreconditionFailed)
return
}
if err := h.h.ListPermissionsByResource(
r.Context(),
req,
resp,
); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
render.Status(r, http.StatusCreated)
render.JSON(w, r, resp)
}
func RegisterPermissionServiceWeb(r chi.Router, i PermissionServiceHandler, middlewares ...func(http.Handler) http.Handler) {
handler := &webPermissionServiceHandler{
r: r,
h: i,
}
r.MethodFunc("POST", "/api/v0/settings/permissions-list-by-resource", handler.ListPermissionsByResource)
}
// SaveBundleRequestJSONMarshaler describes the default jsonpb.Marshaler used by all
// instances of SaveBundleRequest. This struct is safe to replace or modify but
// should not be done so concurrently.
@@ -1253,6 +1295,78 @@ func (m *UserRoleAssignment) UnmarshalJSON(b []byte) error {
var _ json.Unmarshaler = (*UserRoleAssignment)(nil)
// ListPermissionsByResourceRequestJSONMarshaler describes the default jsonpb.Marshaler used by all
// instances of ListPermissionsByResourceRequest. This struct is safe to replace or modify but
// should not be done so concurrently.
var ListPermissionsByResourceRequestJSONMarshaler = new(jsonpb.Marshaler)
// MarshalJSON satisfies the encoding/json Marshaler interface. This method
// uses the more correct jsonpb package to correctly marshal the message.
func (m *ListPermissionsByResourceRequest) MarshalJSON() ([]byte, error) {
if m == nil {
return json.Marshal(nil)
}
buf := &bytes.Buffer{}
if err := ListPermissionsByResourceRequestJSONMarshaler.Marshal(buf, m); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
var _ json.Marshaler = (*ListPermissionsByResourceRequest)(nil)
// ListPermissionsByResourceRequestJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all
// instances of ListPermissionsByResourceRequest. This struct is safe to replace or modify but
// should not be done so concurrently.
var ListPermissionsByResourceRequestJSONUnmarshaler = new(jsonpb.Unmarshaler)
// UnmarshalJSON satisfies the encoding/json Unmarshaler interface. This method
// uses the more correct jsonpb package to correctly unmarshal the message.
func (m *ListPermissionsByResourceRequest) UnmarshalJSON(b []byte) error {
return ListPermissionsByResourceRequestJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m)
}
var _ json.Unmarshaler = (*ListPermissionsByResourceRequest)(nil)
// ListPermissionsByResourceResponseJSONMarshaler describes the default jsonpb.Marshaler used by all
// instances of ListPermissionsByResourceResponse. This struct is safe to replace or modify but
// should not be done so concurrently.
var ListPermissionsByResourceResponseJSONMarshaler = new(jsonpb.Marshaler)
// MarshalJSON satisfies the encoding/json Marshaler interface. This method
// uses the more correct jsonpb package to correctly marshal the message.
func (m *ListPermissionsByResourceResponse) MarshalJSON() ([]byte, error) {
if m == nil {
return json.Marshal(nil)
}
buf := &bytes.Buffer{}
if err := ListPermissionsByResourceResponseJSONMarshaler.Marshal(buf, m); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
var _ json.Marshaler = (*ListPermissionsByResourceResponse)(nil)
// ListPermissionsByResourceResponseJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all
// instances of ListPermissionsByResourceResponse. This struct is safe to replace or modify but
// should not be done so concurrently.
var ListPermissionsByResourceResponseJSONUnmarshaler = new(jsonpb.Unmarshaler)
// UnmarshalJSON satisfies the encoding/json Unmarshaler interface. This method
// uses the more correct jsonpb package to correctly unmarshal the message.
func (m *ListPermissionsByResourceResponse) UnmarshalJSON(b []byte) error {
return ListPermissionsByResourceResponseJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m)
}
var _ json.Unmarshaler = (*ListPermissionsByResourceResponse)(nil)
// ResourceJSONMarshaler describes the default jsonpb.Marshaler used by all
// instances of Resource. This struct is safe to replace or modify but
// should not be done so concurrently.

View File

@@ -118,6 +118,15 @@ service RoleService {
}
}
service PermissionService {
rpc ListPermissionsByResource(ListPermissionsByResourceRequest) returns (ListPermissionsByResourceResponse) {
option (google.api.http) = {
post: "/api/v0/settings/permissions-list-by-resource",
body: "*"
};
}
}
// ---
// requests and responses for settings bundles
// ---
@@ -138,7 +147,6 @@ message GetBundleResponse {
}
message ListBundlesRequest {
string account_uuid = 1;
}
message ListBundlesResponse {
@@ -238,6 +246,19 @@ message UserRoleAssignment {
string role_id = 3;
}
// --
// requests and responses for permissions
// ---
message ListPermissionsByResourceRequest {
Resource resource = 1;
repeated string role_ids = 2;
}
message ListPermissionsByResourceResponse {
repeated Permission permissions = 1;
}
// ---
// resource payloads
// ---
@@ -334,6 +355,8 @@ message Permission {
OPERATION_READ = 2;
OPERATION_UPDATE = 3;
OPERATION_DELETE = 4;
OPERATION_WRITE = 5;// WRITE is a combination of CREATE and UPDATE
OPERATION_READWRITE = 6;// READWRITE is a combination of READ and WRITE
}
Operation operation = 1;
enum Constraint {

View File

@@ -280,6 +280,38 @@
]
}
},
"/api/v0/settings/permissions-list-by-resource": {
"post": {
"operationId": "PermissionService_ListPermissionsByResource",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/protoListPermissionsByResourceResponse"
}
},
"default": {
"description": "An unexpected error response",
"schema": {
"$ref": "#/definitions/runtimeError"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/protoListPermissionsByResourceRequest"
}
}
],
"tags": [
"PermissionService"
]
}
},
"/api/v0/settings/roles-list": {
"post": {
"operationId": "RoleService_ListRoles",
@@ -623,12 +655,7 @@
}
},
"protoListBundlesRequest": {
"type": "object",
"properties": {
"account_uuid": {
"type": "string"
}
}
"type": "object"
},
"protoListBundlesResponse": {
"type": "object",
@@ -668,6 +695,31 @@
}
}
},
"protoListPermissionsByResourceRequest": {
"type": "object",
"properties": {
"resource": {
"$ref": "#/definitions/protoResource"
},
"role_ids": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"protoListPermissionsByResourceResponse": {
"type": "object",
"properties": {
"permissions": {
"type": "array",
"items": {
"$ref": "#/definitions/protoPermission"
}
}
}
},
"protoListRoleAssignmentsRequest": {
"type": "object",
"properties": {
@@ -749,7 +801,9 @@
"OPERATION_CREATE",
"OPERATION_READ",
"OPERATION_UPDATE",
"OPERATION_DELETE"
"OPERATION_DELETE",
"OPERATION_WRITE",
"OPERATION_READWRITE"
],
"default": "OPERATION_UNKNOWN"
},

View File

@@ -0,0 +1,57 @@
package svc
import "github.com/owncloud/ocis-settings/pkg/proto/v0"
func (g Service) hasPermission(
roleIDs []string,
resource *proto.Resource,
operations []proto.Permission_Operation,
constraint proto.Permission_Constraint,
) bool {
permissions, err := g.manager.ListPermissionsByResource(resource, roleIDs)
if err != nil {
g.logger.Debug().Err(err).
Str("resource-type", resource.Type.String()).
Str("resource-id", resource.Id).
Msg("permissions could not be loaded for resource")
return false
}
permissions = getFilteredPermissionsByOperations(permissions, operations)
return isConstraintFulfilled(permissions, constraint)
}
// filterPermissionsByOperations returns the subset of the given permissions, where at least one of the given operations is fulfilled.
func getFilteredPermissionsByOperations(permissions []*proto.Permission, operations []proto.Permission_Operation) []*proto.Permission {
var filteredPermissions []*proto.Permission
for _, permission := range permissions {
if isAnyOperationFulfilled(permission, operations) {
filteredPermissions = append(filteredPermissions, permission)
}
}
return filteredPermissions
}
// isAnyOperationFulfilled checks if the permissions is about any of the operations
func isAnyOperationFulfilled(permission *proto.Permission, operations []proto.Permission_Operation) bool {
for _, operation := range operations {
if operation == permission.Operation {
return true
}
}
return false
}
// isConstraintFulfilled checks if one of the permissions has the same or a parent of the constraint.
// this is only a comparison on ENUM level. More sophisticated checks cannot happen here...
func isConstraintFulfilled(permissions []*proto.Permission, constraint proto.Permission_Constraint) bool {
for _, permission := range permissions {
// comparing enum by order is not a feasible solution, because `SHARED` is not a superset of `OWN`.
if permission.Constraint == proto.Permission_CONSTRAINT_ALL {
return true
}
if permission.Constraint != proto.Permission_CONSTRAINT_UNKNOWN && permission.Constraint == constraint {
return true
}
}
return false
}

View File

@@ -2,9 +2,11 @@ package svc
import (
"context"
"fmt"
"github.com/golang/protobuf/ptypes/empty"
merrors "github.com/micro/go-micro/v2/errors"
"github.com/micro/go-micro/v2/metadata"
"github.com/owncloud/ocis-pkg/v2/log"
"github.com/owncloud/ocis-pkg/v2/middleware"
"github.com/owncloud/ocis-settings/pkg/config"
@@ -27,23 +29,28 @@ func NewService(cfg *config.Config, logger log.Logger) Service {
logger: logger,
manager: store.New(cfg),
}
// FIXME: we're writing default roles per service start (i.e. twice at the moment, for http and grpc server).
service.RegisterDefaultRoles()
return service
}
// RegisterDefaultRoles composes default roles and saves them. Skipped if the roles already exist.
func (g Service) RegisterDefaultRoles() {
// FIXME: we're writing default roles per service start (i.e. twice at the moment, for http and grpc server). has to happen only once.
for _, role := range generateBundlesDefaultRoles() {
bundleID := role.Extension + "." + role.Id
// check if the role already exists
bundle, _ := service.manager.ReadBundle(role.Id)
bundle, _ := g.manager.ReadBundle(role.Id)
if bundle != nil {
logger.Debug().Msgf("Settings bundle %v already exists. Skipping.", bundleID)
g.logger.Debug().Str("bundleID", bundleID).Msg("bundle already exists. skipping.")
continue
}
// create the role
_, err := service.manager.WriteBundle(role)
_, err := g.manager.WriteBundle(role)
if err != nil {
logger.Error().Err(err).Msgf("Failed to register settings bundle %v", bundleID)
g.logger.Error().Err(err).Str("bundleID", bundleID).Msg("failed to register bundle")
}
logger.Debug().Msgf("Successfully registered settings bundle %v", bundleID)
g.logger.Debug().Str("bundleID", bundleID).Msg("successfully registered bundle")
}
return service
}
// TODO: check permissions on every request
@@ -64,21 +71,28 @@ func (g Service) SaveBundle(c context.Context, req *proto.SaveBundleRequest, res
// GetBundle implements the BundleServiceHandler interface
func (g Service) GetBundle(c context.Context, req *proto.GetBundleRequest, res *proto.GetBundleResponse) error {
accountUUID := getValidatedAccountUUID(c, "me")
if validationError := validateGetBundle(req); validationError != nil {
return merrors.BadRequest("ocis-settings", "%s", validationError)
}
r, err := g.manager.ReadBundle(req.BundleId)
bundle, err := g.manager.ReadBundle(req.BundleId)
if err != nil {
return merrors.NotFound("ocis-settings", "%s", err)
}
res.Bundle = r
roleIDs := g.getRoleIDs(c, accountUUID)
filteredBundle := g.getFilteredBundle(roleIDs, bundle)
if len(filteredBundle.Settings) == 0 {
err = fmt.Errorf("could not read bundle: %s", req.BundleId)
return merrors.NotFound("ocis-settings", "%s", err)
}
res.Bundle = filteredBundle
return nil
}
// ListBundles implements the BundleServiceHandler interface
func (g Service) ListBundles(c context.Context, req *proto.ListBundlesRequest, res *proto.ListBundlesResponse) error {
// fetch all bundles
req.AccountUuid = getValidatedAccountUUID(c, req.AccountUuid)
accountUUID := getValidatedAccountUUID(c, "me")
if validationError := validateListBundles(req); validationError != nil {
return merrors.BadRequest("ocis-settings", "%s", validationError)
}
@@ -86,10 +100,56 @@ func (g Service) ListBundles(c context.Context, req *proto.ListBundlesRequest, r
if err != nil {
return merrors.NotFound("ocis-settings", "%s", err)
}
res.Bundles = bundles
roleIDs := g.getRoleIDs(c, accountUUID)
// filter settings in bundles that are allowed according to roles
var filteredBundles []*proto.Bundle
for _, bundle := range bundles {
filteredBundle := g.getFilteredBundle(roleIDs, bundle)
if len(filteredBundle.Settings) > 0 {
filteredBundles = append(filteredBundles, filteredBundle)
}
}
res.Bundles = filteredBundles
return nil
}
func (g Service) getFilteredBundle(roleIDs []string, bundle *proto.Bundle) *proto.Bundle {
// check if full bundle is whitelisted
bundleResource := &proto.Resource{
Type: proto.Resource_TYPE_BUNDLE,
Id: bundle.Id,
}
if g.hasPermission(
roleIDs,
bundleResource,
[]proto.Permission_Operation{proto.Permission_OPERATION_READ, proto.Permission_OPERATION_READWRITE},
proto.Permission_CONSTRAINT_OWN,
) {
return bundle
}
// filter settings based on permissions
var filteredSettings []*proto.Setting
for _, setting := range bundle.Settings {
settingResource := &proto.Resource{
Type: proto.Resource_TYPE_SETTING,
Id: setting.Id,
}
if g.hasPermission(
roleIDs,
settingResource,
[]proto.Permission_Operation{proto.Permission_OPERATION_READ, proto.Permission_OPERATION_READWRITE},
proto.Permission_CONSTRAINT_OWN,
) {
filteredSettings = append(filteredSettings, setting)
}
}
bundle.Settings = filteredSettings
return bundle
}
// AddSettingToBundle implements the BundleServiceHandler interface
func (g Service) AddSettingToBundle(c context.Context, req *proto.AddSettingToBundleRequest, res *proto.AddSettingToBundleResponse) error {
cleanUpResource(c, req.Setting.Resource)
@@ -194,7 +254,7 @@ func (g Service) ListValues(c context.Context, req *proto.ListValuesRequest, res
// ListRoles implements the RoleServiceHandler interface
func (g Service) ListRoles(c context.Context, req *proto.ListBundlesRequest, res *proto.ListBundlesResponse) error {
req.AccountUuid = getValidatedAccountUUID(c, req.AccountUuid)
//accountUUID := getValidatedAccountUUID(c, "me")
if validationError := validateListRoles(req); validationError != nil {
return merrors.BadRequest("ocis-settings", "%s", validationError)
}
@@ -202,6 +262,7 @@ func (g Service) ListRoles(c context.Context, req *proto.ListBundlesRequest, res
if err != nil {
return merrors.NotFound("ocis-settings", "%s", err)
}
// TODO: only allow to list roles when user has account management permissions
res.Bundles = r
return nil
}
@@ -245,6 +306,19 @@ func (g Service) RemoveRoleFromUser(c context.Context, req *proto.RemoveRoleFrom
return nil
}
// ListPermissionsByResource implements the PermissionServiceHandler interface
func (g Service) ListPermissionsByResource(c context.Context, req *proto.ListPermissionsByResourceRequest, res *proto.ListPermissionsByResourceResponse) error {
if validationError := validateListPermissionsByResource(req); validationError != nil {
return merrors.BadRequest("ocis-settings", "%s", validationError)
}
permissions, err := g.manager.ListPermissionsByResource(req.Resource, req.RoleIds)
if err != nil {
return merrors.BadRequest("ocis-settings", "%s", err)
}
res.Permissions = permissions
return nil
}
// cleanUpResource makes sure that the account uuid of the authenticated user is injected if needed.
func cleanUpResource(c context.Context, resource *proto.Resource) {
if resource != nil && resource.Type == proto.Resource_TYPE_USER {
@@ -256,13 +330,35 @@ func cleanUpResource(c context.Context, resource *proto.Resource) {
// the result of this function will always be a valid lower-case UUID or an empty string.
func getValidatedAccountUUID(c context.Context, accountUUID string) string {
if accountUUID == "me" {
if ownAccountUUID, ok := c.Value(middleware.UUIDKey).(string); ok {
if ownAccountUUID, ok := metadata.Get(c, middleware.AccountID); ok {
accountUUID = ownAccountUUID
}
}
if accountUUID == "me" {
// no matter what happens above, an accountUUID of `me` must not be passed on. Clear it instead.
accountUUID = ""
}
return accountUUID
}
// getRoleIDs loads the role assignments for the given accountUUID
// TODO: this should work on the context in the future, as roles are supposed to be sent within the context.
func (g Service) getRoleIDs(c context.Context, accountUUID string) []string {
// TODO: replace this with role ids from the context
// WIP PR: https://github.com/owncloud/ocis-proxy/pull/70
rolesResponse := &proto.ListRoleAssignmentsResponse{}
err := g.ListRoleAssignments(c, &proto.ListRoleAssignmentsRequest{AccountUuid: accountUUID}, rolesResponse)
if err != nil {
g.logger.Err(err).Str("accountUUID", accountUUID).Msg("failed to list role assignments")
return []string{}
}
roleIDs := make([]string, 0)
for _, assignment := range rolesResponse.Assignments {
roleIDs = append(roleIDs, assignment.RoleId)
}
return roleIDs
}
func (g Service) getValueWithIdentifier(value *proto.Value) (*proto.ValueWithIdentifier, error) {
bundle, err := g.manager.ReadBundle(value.BundleId)
if err != nil {

View File

@@ -4,13 +4,14 @@ import (
"context"
"testing"
"github.com/micro/go-micro/v2/metadata"
"github.com/owncloud/ocis-pkg/v2/middleware"
"github.com/stretchr/testify/assert"
)
var (
ctxWithUUID = context.WithValue(context.Background(), middleware.UUIDKey, "61445573-4dbe-4d56-88dc-88ab47aceba7")
ctxWithEmptyUUID = context.WithValue(context.Background(), middleware.UUIDKey, "")
ctxWithUUID = metadata.Set(context.Background(), middleware.AccountID, "61445573-4dbe-4d56-88dc-88ab47aceba7")
ctxWithEmptyUUID = metadata.Set(context.Background(), middleware.AccountID, "")
emptyCtx = context.Background()
scenarios = []struct {
@@ -23,11 +24,17 @@ var (
name: "context with UUID; identifier = 'me'",
ctx: ctxWithUUID,
accountUUID: "me",
expect: ctxWithUUID.Value(middleware.UUIDKey).(string),
expect: "61445573-4dbe-4d56-88dc-88ab47aceba7",
},
{
name: "context with empty UUID; identifier = 'me'",
ctx: ctxWithEmptyUUID,
accountUUID: "me",
expect: "",
},
{
name: "context without UUID; identifier = 'me'",
ctx: ctxWithEmptyUUID,
ctx: emptyCtx,
accountUUID: "me",
expect: "",
},

View File

@@ -46,11 +46,11 @@ func validateSaveBundle(req *proto.SaveBundleRequest) error {
}
func validateGetBundle(req *proto.GetBundleRequest) error {
return validation.Validate(&req.BundleId, requireAccountID...)
return validation.Validate(&req.BundleId, is.UUID)
}
func validateListBundles(req *proto.ListBundlesRequest) error {
return validation.Validate(&req.AccountUuid, requireAccountID...)
return nil
}
func validateAddSettingToBundle(req *proto.AddSettingToBundleRequest) error {
@@ -100,7 +100,7 @@ func validateListValues(req *proto.ListValuesRequest) error {
}
func validateListRoles(req *proto.ListBundlesRequest) error {
return validation.Validate(&req.AccountUuid, requireAccountID...)
return nil
}
func validateListRoleAssignments(req *proto.ListRoleAssignmentsRequest) error {
@@ -122,6 +122,16 @@ func validateRemoveRoleFromUser(req *proto.RemoveRoleFromUserRequest) error {
)
}
func validateListPermissionsByResource(req *proto.ListPermissionsByResourceRequest) error {
if err := validateResource(req.Resource); err != nil {
return err
}
return validation.ValidateStruct(
req,
validation.Field(&req.RoleIds, validation.Each(requireAlphanumeric...)),
)
}
// validateResource is an internal helper for validating the content of a resource.
func validateResource(resource *proto.Resource) error {
if err := validation.Validate(&resource, validation.Required); err != nil {

View File

@@ -18,6 +18,7 @@ type Manager interface {
BundleManager
ValueManager
RoleAssignmentManager
PermissionManager
}
// BundleManager is a bundle service interface for abstraction of storage implementations
@@ -44,3 +45,8 @@ type RoleAssignmentManager interface {
WriteRoleAssignment(accountUUID, roleID string) (*proto.UserRoleAssignment, error)
RemoveRoleAssignment(assignmentID string) error
}
// PermissionManager is a permissions service interface for abstraction of storage implementations
type PermissionManager interface {
ListPermissionsByResource(resource *proto.Resource, roleIDs []string) ([]*proto.Permission, error)
}

View File

@@ -16,7 +16,6 @@ func (s Store) ListRoleAssignments(accountUUID string) ([]*proto.UserRoleAssignm
assignmentsFolder := s.buildFolderPathForRoleAssignments(false)
assignmentFiles, err := ioutil.ReadDir(assignmentsFolder)
if err != nil {
s.Logger.Error().Err(err).Str("assignmentFiles", assignmentsFolder).Msg("error reading assignment file")
return records, nil
}
@@ -35,7 +34,7 @@ func (s Store) ListRoleAssignments(accountUUID string) ([]*proto.UserRoleAssignm
// WriteRoleAssignment appends the given role assignment to the existing assignments of the respective account.
func (s Store) WriteRoleAssignment(accountUUID, roleID string) (*proto.UserRoleAssignment, error) {
// as per https://jira.owncloud.com/browse/OCIS-117 "Each user can have exactly one role"
// as per https://github.com/owncloud/product/issues/103 "Each user can have exactly one role"
list, err := s.ListRoleAssignments(accountUUID)
if err != nil {
return nil, err

View File

@@ -73,7 +73,7 @@ func (s Store) ReadSetting(settingID string) (*proto.Setting, error) {
}
}
}
return nil, fmt.Errorf(settingID, fmt.Sprintf("could not read setting: %v", settingID))
return nil, fmt.Errorf("could not read setting: %v", settingID)
}
// WriteBundle writes the given record into a file within the dataPath.

View File

@@ -0,0 +1,34 @@
package store
import (
"github.com/owncloud/ocis-settings/pkg/proto/v0"
"github.com/owncloud/ocis-settings/pkg/util"
)
// ListPermissionsByResource collects all permissions from the provided roleIDs that match the requested resource
func (s Store) ListPermissionsByResource(resource *proto.Resource, roleIDs []string) ([]*proto.Permission, error) {
records := make([]*proto.Permission, 0)
for _, roleID := range roleIDs {
role, err := s.ReadBundle(roleID)
if err != nil {
s.Logger.Debug().Str("roleID", roleID).Msg("role not found, skipping")
continue
}
records = append(records, extractPermissionsByResource(resource, role)...)
}
return records, nil
}
// extractPermissionsByResource collects all permissions from the provided role that match the requested resource
func extractPermissionsByResource(resource *proto.Resource, role *proto.Bundle) []*proto.Permission {
permissions := make([]*proto.Permission, 0)
for _, setting := range role.Settings {
if _, ok := setting.Value.(*proto.Setting_PermissionValue); ok {
value := setting.Value.(*proto.Setting_PermissionValue).PermissionValue
if util.IsResourceMatched(setting.Resource, resource) {
permissions = append(permissions, value)
}
}
}
return permissions
}

View File

@@ -0,0 +1,16 @@
package util
import "github.com/owncloud/ocis-settings/pkg/proto/v0"
const (
// ResourceIDAll declares on a resource that it matches any id
ResourceIDAll = "all"
)
// IsResourceMatched checks if the `example` resource is an exact match or a subset of `definition`
func IsResourceMatched(definition, example *proto.Resource) bool {
if definition.Type != example.Type {
return false
}
return definition.Id == ResourceIDAll || definition.Id == example.Id
}

View File

@@ -0,0 +1,91 @@
package util
import (
"testing"
"github.com/owncloud/ocis-settings/pkg/proto/v0"
"gotest.tools/assert"
)
func TestIsResourceMatched(t *testing.T) {
scenarios := []struct {
name string
definition *proto.Resource
example *proto.Resource
matched bool
}{
{
"same resource types without ids match",
&proto.Resource{
Type: proto.Resource_TYPE_SYSTEM,
},
&proto.Resource{
Type: proto.Resource_TYPE_SYSTEM,
},
true,
},
{
"different resource types without ids don't match",
&proto.Resource{
Type: proto.Resource_TYPE_SYSTEM,
},
&proto.Resource{
Type: proto.Resource_TYPE_USER,
},
false,
},
{
"same resource types with different ids don't match",
&proto.Resource{
Type: proto.Resource_TYPE_USER,
Id: "einstein",
},
&proto.Resource{
Type: proto.Resource_TYPE_USER,
Id: "marie",
},
false,
},
{
"same resource types with same ids match",
&proto.Resource{
Type: proto.Resource_TYPE_USER,
Id: "einstein",
},
&proto.Resource{
Type: proto.Resource_TYPE_USER,
Id: "einstein",
},
true,
},
{
"same resource types with definition = ALL and without id in example is a match",
&proto.Resource{
Type: proto.Resource_TYPE_USER,
Id: ResourceIDAll,
},
&proto.Resource{
Type: proto.Resource_TYPE_USER,
},
true,
},
{
"same resource types with definition.id = ALL and with some id in example is a match",
&proto.Resource{
Type: proto.Resource_TYPE_USER,
Id: ResourceIDAll,
},
&proto.Resource{
Type: proto.Resource_TYPE_USER,
Id: "einstein",
},
true,
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
assert.Equal(t, scenario.matched, IsResourceMatched(scenario.definition, scenario.example))
})
}
}