enhancement: add allowed role validation to the go-playground validator

This commit is contained in:
Florian Schade
2024-08-21 14:08:29 +02:00
parent 56537e94fc
commit 4638280d21
7 changed files with 88 additions and 33 deletions
@@ -114,6 +114,9 @@ func DefaultConfig() *config.Config {
Cluster: "ocis-cluster", Cluster: "ocis-cluster",
EnableTLS: false, EnableTLS: false,
}, },
UnifiedRoles: config.UnifiedRoles{
AvailableRoles: nil, // will be populated with defaults in EnsureDefaults
},
} }
} }
@@ -73,13 +73,13 @@ const (
) )
// NewDriveItemPermissionsService creates a new DriveItemPermissionsService // NewDriveItemPermissionsService creates a new DriveItemPermissionsService
func NewDriveItemPermissionsService(logger log.Logger, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], identityCache identity.IdentityCache, config *config.Config) (DriveItemPermissionsService, error) { func NewDriveItemPermissionsService(logger log.Logger, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], identityCache identity.IdentityCache, c *config.Config) (DriveItemPermissionsService, error) {
return DriveItemPermissionsService{ return DriveItemPermissionsService{
BaseGraphService: BaseGraphService{ BaseGraphService: BaseGraphService{
logger: &log.Logger{Logger: logger.With().Str("graph api", "DrivesDriveItemService").Logger()}, logger: &log.Logger{Logger: logger.With().Str("graph api", "DrivesDriveItemService").Logger()},
gatewaySelector: gatewaySelector, gatewaySelector: gatewaySelector,
identityCache: identityCache, identityCache: identityCache,
config: config, config: c,
}, },
}, nil }, nil
} }
@@ -104,6 +104,11 @@ func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId *sto
unifiedRolePermissions := []*libregraph.UnifiedRolePermission{{AllowedResourceActions: invite.LibreGraphPermissionsActions}} unifiedRolePermissions := []*libregraph.UnifiedRolePermission{{AllowedResourceActions: invite.LibreGraphPermissionsActions}}
for _, roleID := range invite.GetRoles() { for _, roleID := range invite.GetRoles() {
// only allow roles that are enabled in the config
if !slices.Contains(s.config.UnifiedRoles.AvailableRoles, roleID) {
return libregraph.Permission{}, unifiedrole.ErrUnknownUnifiedRole
}
role, err := unifiedrole.NewUnifiedRoleFromID(roleID) role, err := unifiedrole.NewUnifiedRoleFromID(roleID)
if err != nil { if err != nil {
s.logger.Debug().Err(err).Interface("role", invite.GetRoles()[0]).Msg("unable to convert requested role") s.logger.Debug().Err(err).Interface("role", invite.GetRoles()[0]).Msg("unable to convert requested role")
@@ -544,18 +549,20 @@ func (s DriveItemPermissionsService) UpdateSpaceRootPermission(ctx context.Conte
return s.UpdatePermission(ctx, rootResourceID, permissionID, newPermission) return s.UpdatePermission(ctx, rootResourceID, permissionID, newPermission)
} }
// DriveItemPermissionsService is the api that registers the http endpoints which expose needed operation to the graph api. // DriveItemPermissionsApi is the api that registers the http endpoints which expose needed operation to the graph api.
// the business logic is delegated to the permissions service and further down to the cs3 client. // the business logic is delegated to the permissions service and further down to the cs3 client.
type DriveItemPermissionsApi struct { type DriveItemPermissionsApi struct {
logger log.Logger logger log.Logger
driveItemPermissionsService DriveItemPermissionsProvider driveItemPermissionsService DriveItemPermissionsProvider
config *config.Config
} }
// NewDriveItemPermissionsApi creates a new DriveItemPermissionsApi // NewDriveItemPermissionsApi creates a new DriveItemPermissionsApi
func NewDriveItemPermissionsApi(driveItemPermissionService DriveItemPermissionsProvider, logger log.Logger) (DriveItemPermissionsApi, error) { func NewDriveItemPermissionsApi(driveItemPermissionService DriveItemPermissionsProvider, logger log.Logger, c *config.Config) (DriveItemPermissionsApi, error) {
return DriveItemPermissionsApi{ return DriveItemPermissionsApi{
logger: log.Logger{Logger: logger.With().Str("graph api", "DrivesDriveItemApi").Logger()}, logger: log.Logger{Logger: logger.With().Str("graph api", "DrivesDriveItemApi").Logger()},
driveItemPermissionsService: driveItemPermissionService, driveItemPermissionsService: driveItemPermissionService,
config: c,
}, nil }, nil
} }
@@ -575,15 +582,14 @@ func (api DriveItemPermissionsApi) Invite(w http.ResponseWriter, r *http.Request
return return
} }
ctx := r.Context() ctx := validate.ContextWithAllowedRoleIDs(r.Context(), api.config.UnifiedRoles.AvailableRoles)
if err = validate.StructCtx(ctx, driveItemInvite); err != nil { if err = validate.StructCtx(ctx, driveItemInvite); err != nil {
api.logger.Debug().Err(err).Interface("Body", r.Body).Msg("invalid request body") api.logger.Debug().Err(err).Interface("Body", r.Body).Msg("invalid request body")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error()) errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return return
} }
permission, err := api.driveItemPermissionsService.Invite(ctx, &itemID, *driveItemInvite)
permission, err := api.driveItemPermissionsService.Invite(ctx, &itemID, *driveItemInvite)
if err != nil { if err != nil {
errorcode.RenderError(w, r, err) errorcode.RenderError(w, r, err)
return return
@@ -609,7 +615,7 @@ func (api DriveItemPermissionsApi) SpaceRootInvite(w http.ResponseWriter, r *htt
return return
} }
ctx := r.Context() ctx := validate.ContextWithAllowedRoleIDs(r.Context(), api.config.UnifiedRoles.AvailableRoles)
if err = validate.StructCtx(ctx, driveItemInvite); err != nil { if err = validate.StructCtx(ctx, driveItemInvite); err != nil {
api.logger.Debug().Err(err).Interface("Body", r.Body).Msg("invalid request body") api.logger.Debug().Err(err).Interface("Body", r.Body).Msg("invalid request body")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error()) errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
@@ -245,6 +245,7 @@ var _ = Describe("DriveItemPermissionsService", func() {
Expect(permission.GetLibreGraphPermissionsActions()).To(HaveLen(1)) Expect(permission.GetLibreGraphPermissionsActions()).To(HaveLen(1))
Expect(permission.GetLibreGraphPermissionsActions()[0]).To(Equal(unifiedrole.DriveItemContentRead)) Expect(permission.GetLibreGraphPermissionsActions()[0]).To(Equal(unifiedrole.DriveItemContentRead))
}) })
It("fails with a missing driveritem", func() { It("fails with a missing driveritem", func() {
statResponse.Status = status.NewNotFound(context.Background(), "not found") statResponse.Status = status.NewNotFound(context.Background(), "not found")
permission, err := driveItemPermissionsService.Invite(context.Background(), driveItemId, driveItemInvite) permission, err := driveItemPermissionsService.Invite(context.Background(), driveItemId, driveItemInvite)
@@ -252,6 +253,12 @@ var _ = Describe("DriveItemPermissionsService", func() {
Expect(err).To(MatchError(errorcode.New(errorcode.ItemNotFound, "not found").WithOrigin(errorcode.ErrorOriginCS3))) Expect(err).To(MatchError(errorcode.New(errorcode.ItemNotFound, "not found").WithOrigin(errorcode.ErrorOriginCS3)))
Expect(permission).To(BeZero()) Expect(permission).To(BeZero())
}) })
It("fails with unknown or disable role", func() {
driveItemInvite.Roles = []string{unifiedrole.UnifiedRoleViewerID, unifiedrole.UnifiedRoleSecureViewerID}
_, err := driveItemPermissionsService.Invite(context.Background(), driveItemId, driveItemInvite)
Expect(err).To(MatchError(unifiedrole.ErrUnknownUnifiedRole))
})
}) })
Describe("SpaceRootInvite", func() { Describe("SpaceRootInvite", func() {
var ( var (
@@ -1015,7 +1022,7 @@ var _ = Describe("DriveItemPermissionsApi", func() {
logger := log.NewLogger() logger := log.NewLogger()
mockProvider = mocks.NewDriveItemPermissionsProvider(GinkgoT()) mockProvider = mocks.NewDriveItemPermissionsProvider(GinkgoT())
api, err := svc.NewDriveItemPermissionsApi(mockProvider, logger) api, err := svc.NewDriveItemPermissionsApi(mockProvider, logger, defaults.FullDefaultConfig())
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
httpAPI = api httpAPI = api
@@ -1097,6 +1104,22 @@ var _ = Describe("DriveItemPermissionsApi", func() {
Expect(responseRecorder.Code).To(Equal(http.StatusOK)) Expect(responseRecorder.Code).To(Equal(http.StatusOK))
}) })
It("fails with unknown or disable role", func() {
rCTX.URLParams.Add("itemID", "1$2!3")
responseRecorder := httptest.NewRecorder()
invite.Roles = []string{unifiedrole.UnifiedRoleViewerID, unifiedrole.UnifiedRoleSecureViewerID}
inviteJson, err := json.Marshal(invite)
Expect(err).ToNot(HaveOccurred())
request := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(inviteJson)).
WithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, rCTX),
)
httpAPI.Invite(responseRecorder, request)
Expect(responseRecorder.Code).To(Equal(http.StatusBadRequest))
})
}) })
Describe("SpaceRootInvite", func() { Describe("SpaceRootInvite", func() {
It("call the Invite provider with the correct arguments", func() { It("call the Invite provider with the correct arguments", func() {
@@ -1132,6 +1155,21 @@ var _ = Describe("DriveItemPermissionsApi", func() {
Expect(responseRecorder.Code).To(Equal(http.StatusUnprocessableEntity)) Expect(responseRecorder.Code).To(Equal(http.StatusUnprocessableEntity))
}) })
It("fails with unknown or disable role", func() {
responseRecorder := httptest.NewRecorder()
invite.Roles = []string{unifiedrole.UnifiedRoleViewerID, unifiedrole.UnifiedRoleSecureViewerID}
inviteJson, err := json.Marshal(invite)
Expect(err).ToNot(HaveOccurred())
request := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(inviteJson)).
WithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, rCTX),
)
httpAPI.SpaceRootInvite(responseRecorder, request)
Expect(responseRecorder.Code).To(Equal(http.StatusBadRequest))
})
}) })
Describe("ListPermissions", func() { Describe("ListPermissions", func() {
It("calls the ListPermissions provider with the correct arguments", func() { It("calls the ListPermissions provider with the correct arguments", func() {
@@ -1,7 +1,6 @@
package svc package svc
import ( import (
"fmt"
"net/http" "net/http"
"net/url" "net/url"
@@ -16,8 +15,7 @@ import (
// GetRoleDefinitions a list of permission roles than can be used when sharing with users or groups // GetRoleDefinitions a list of permission roles than can be used when sharing with users or groups
func (g Graph) GetRoleDefinitions(w http.ResponseWriter, r *http.Request) { func (g Graph) GetRoleDefinitions(w http.ResponseWriter, r *http.Request) {
render.Status(r, http.StatusOK) render.Status(r, http.StatusOK)
// fixMe: should we consider all roles or only the ones that are enabled? render.JSON(w, r, unifiedrole.GetBuiltinRoleDefinitionList(unifiedrole.RoleFilterIDs(g.config.UnifiedRoles.AvailableRoles...)))
render.JSON(w, r, unifiedrole.GetBuiltinRoleDefinitionList())
} }
// GetRoleDefinition a permission role than can be used when sharing with users or groups // GetRoleDefinition a permission role than can be used when sharing with users or groups
@@ -29,7 +27,7 @@ func (g Graph) GetRoleDefinition(w http.ResponseWriter, r *http.Request) {
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping role id failed") errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping role id failed")
return return
} }
role, err := getRoleDefinition(roleID) role, err := getRoleDefinition(roleID, g.config.UnifiedRoles.AvailableRoles)
if err != nil { if err != nil {
logger.Debug().Str("roleID", roleID).Msg("could not get role: not found") logger.Debug().Str("roleID", roleID).Msg("could not get role: not found")
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, err.Error()) errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, err.Error())
@@ -39,13 +37,11 @@ func (g Graph) GetRoleDefinition(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, role) render.JSON(w, r, role)
} }
func getRoleDefinition(roleID string) (*libregraph.UnifiedRoleDefinition, error) { func getRoleDefinition(roleID string, availableRoles []string) (*libregraph.UnifiedRoleDefinition, error) {
// fixMe: should we consider all roles or only the ones that are enabled? for _, role := range unifiedrole.GetBuiltinRoleDefinitionList(unifiedrole.RoleFilterIDs(availableRoles...)) {
roleList := unifiedrole.GetBuiltinRoleDefinitionList()
for _, role := range roleList {
if role != nil && role.Id != nil && *role.Id == roleID { if role != nil && role.Id != nil && *role.Id == roleID {
return role, nil return role, nil
} }
} }
return nil, fmt.Errorf("role not found") return nil, unifiedrole.ErrUnknownUnifiedRole
} }
+1 -1
View File
@@ -214,7 +214,7 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
return svc, err return svc, err
} }
driveItemPermissionsApi, err := NewDriveItemPermissionsApi(driveItemPermissionsService, options.Logger) driveItemPermissionsApi, err := NewDriveItemPermissionsApi(driveItemPermissionsService, options.Logger, options.Config)
if err != nil { if err != nil {
return svc, err return svc, err
} }
+1 -1
View File
@@ -6,5 +6,5 @@ import (
var ( var (
// ErrUnknownUnifiedRole is returned when an unknown unified role is requested. // ErrUnknownUnifiedRole is returned when an unknown unified role is requested.
ErrUnknownUnifiedRole = errors.New("unknown unified role") ErrUnknownUnifiedRole = errors.New("unknown unified role, check if the role is enabled")
) )
+25 -13
View File
@@ -1,6 +1,7 @@
package validate package validate
import ( import (
"context"
"slices" "slices"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
@@ -9,6 +10,10 @@ import (
"github.com/owncloud/ocis/v2/services/graph/pkg/unifiedrole" "github.com/owncloud/ocis/v2/services/graph/pkg/unifiedrole"
) )
var (
_contextRoleIDsValueKey = "roleFilterIDs"
)
// initLibregraph initializes libregraph validation // initLibregraph initializes libregraph validation
func initLibregraph(v *validator.Validate) { func initLibregraph(v *validator.Validate) {
for _, f := range []func(*validator.Validate){ for _, f := range []func(*validator.Validate){
@@ -30,10 +35,9 @@ func libregraphDriveItemInvite(v *validator.Validate) {
"ExpirationDateTime": "omitnil,gt", "ExpirationDateTime": "omitnil,gt",
}, s) }, s)
v.RegisterStructValidation(func(sl validator.StructLevel) { v.RegisterStructValidationCtx(func(ctx context.Context, sl validator.StructLevel) {
driveItemInvite := sl.Current().Interface().(libregraph.DriveItemInvite) driveItemInvite := sl.Current().Interface().(libregraph.DriveItemInvite)
rolesAndActions(ctx, sl, driveItemInvite.Roles, driveItemInvite.LibreGraphPermissionsActions, false)
rolesAndActions(sl, driveItemInvite.Roles, driveItemInvite.LibreGraphPermissionsActions, false)
}, s) }, s)
} }
@@ -52,7 +56,7 @@ func libregraphPermission(v *validator.Validate) {
v.RegisterStructValidationMapRules(map[string]string{ v.RegisterStructValidationMapRules(map[string]string{
"Roles": "max=1", "Roles": "max=1",
}, s) }, s)
v.RegisterStructValidation(func(sl validator.StructLevel) { v.RegisterStructValidationCtx(func(ctx context.Context, sl validator.StructLevel) {
permission := sl.Current().Interface().(libregraph.Permission) permission := sl.Current().Interface().(libregraph.Permission)
if _, ok := permission.GetIdOk(); ok { if _, ok := permission.GetIdOk(); ok {
@@ -63,14 +67,13 @@ func libregraphPermission(v *validator.Validate) {
sl.ReportError(permission.HasPassword, "hasPassword", "HasPassword", "readonly", "") sl.ReportError(permission.HasPassword, "hasPassword", "HasPassword", "readonly", "")
} }
rolesAndActions(sl, permission.Roles, permission.LibreGraphPermissionsActions, true) rolesAndActions(ctx, sl, permission.Roles, permission.LibreGraphPermissionsActions, true)
}, s) }, s)
} }
func rolesAndActions(sl validator.StructLevel, roles, actions []string, allowEmpty bool) { func rolesAndActions(ctx context.Context, sl validator.StructLevel, roles, actions []string, allowEmpty bool) {
totalRoles := len(roles) totalRoles := len(roles)
totalActions := len(actions) totalActions := len(actions)
switch { switch {
case allowEmpty && totalRoles == 0 && totalActions == 0: case allowEmpty && totalRoles == 0 && totalActions == 0:
break break
@@ -83,12 +86,16 @@ func rolesAndActions(sl validator.StructLevel, roles, actions []string, allowEmp
var availableRoles []string var availableRoles []string
var availableActions []string var availableActions []string
for _, definition := range append( var definitions []*libregraph.UnifiedRoleDefinition
// fixMe: why twice!?
// fixMe: should we consider all roles or only the ones that are enabled? switch roles, ok := ctx.Value(_contextRoleIDsValueKey).([]string); {
unifiedrole.GetBuiltinRoleDefinitionList(), case ok:
unifiedrole.GetBuiltinRoleDefinitionList()..., definitions = unifiedrole.GetBuiltinRoleDefinitionList(unifiedrole.RoleFilterIDs(roles...))
) { default:
definitions = unifiedrole.GetBuiltinRoleDefinitionList()
}
for _, definition := range definitions {
if slices.Contains(availableRoles, definition.GetId()) { if slices.Contains(availableRoles, definition.GetId()) {
continue continue
} }
@@ -122,3 +129,8 @@ func rolesAndActions(sl validator.StructLevel, roles, actions []string, allowEmp
sl.ReportError(actions, "LibreGraphPermissionsActions", "LibreGraphPermissionsActions", "available_action", "") sl.ReportError(actions, "LibreGraphPermissionsActions", "LibreGraphPermissionsActions", "available_action", "")
} }
} }
// ContextWithAllowedRoleIDs returns a new context which includes the allowed role IDs.
func ContextWithAllowedRoleIDs(ctx context.Context, rolesIds []string) context.Context {
return context.WithValue(ctx, _contextRoleIDsValueKey, rolesIds)
}