Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
@@ -0,0 +1,139 @@
package validate
import (
"context"
"slices"
"github.com/go-playground/validator/v10"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/qsfera/server/services/graph/pkg/unifiedrole"
)
type contextKey int
const (
ContextKeyRoleIDsValueKey contextKey = iota
)
// initLibregraph initializes libregraph validation
func initLibregraph(v *validator.Validate) {
for _, f := range []func(*validator.Validate){
libregraphDriveItemInvite,
libregraphDriveRecipient,
libregraphPermission,
} {
f(v)
}
}
// libregraphDriveItemInvite validates libregraph.DriveItemInvite
func libregraphDriveItemInvite(v *validator.Validate) {
s := libregraph.DriveItemInvite{}
v.RegisterStructValidationMapRules(map[string]string{
"Recipients": "len=1,dive",
"Roles": "max=1",
"ExpirationDateTime": "omitnil,gt",
}, s)
v.RegisterStructValidationCtx(func(ctx context.Context, sl validator.StructLevel) {
driveItemInvite := sl.Current().Interface().(libregraph.DriveItemInvite)
rolesAndActions(ctx, sl, driveItemInvite.Roles, driveItemInvite.LibreGraphPermissionsActions, false)
}, s)
}
// libregraphDriveRecipient validates libregraph.DriveRecipient
func libregraphDriveRecipient(v *validator.Validate) {
v.RegisterStructValidationMapRules(map[string]string{
"ObjectId": "ne=",
"LibreGraphRecipientType": "oneof=user group",
}, libregraph.DriveRecipient{})
}
// libregraphPermission validates libregraph.Permission
func libregraphPermission(v *validator.Validate) {
s := libregraph.Permission{}
v.RegisterStructValidationMapRules(map[string]string{
"Roles": "max=1",
}, s)
v.RegisterStructValidationCtx(func(ctx context.Context, sl validator.StructLevel) {
permission := sl.Current().Interface().(libregraph.Permission)
if _, ok := permission.GetIdOk(); ok {
sl.ReportError(permission.Id, "Id", "Id", "readonly", "")
}
if _, ok := permission.GetHasPasswordOk(); ok {
sl.ReportError(permission.HasPassword, "hasPassword", "HasPassword", "readonly", "")
}
rolesAndActions(ctx, sl, permission.Roles, permission.LibreGraphPermissionsActions, true)
}, s)
}
func rolesAndActions(ctx context.Context, sl validator.StructLevel, roles, actions []string, allowEmpty bool) {
totalRoles := len(roles)
totalActions := len(actions)
switch {
case allowEmpty && totalRoles == 0 && totalActions == 0:
break
case totalRoles != 0 && totalActions != 0:
fallthrough
case totalRoles == totalActions:
sl.ReportError(roles, "Roles", "Roles", "one_or_another", "")
sl.ReportError(actions, "LibreGraphPermissionsActions", "LibreGraphPermissionsActions", "one_or_another", "")
}
var availableRoles []string
var availableActions []string
var definitions []*libregraph.UnifiedRoleDefinition
switch roles, ok := ctx.Value(ContextKeyRoleIDsValueKey).([]string); {
case ok:
definitions = unifiedrole.GetRoles(unifiedrole.RoleFilterIDs(roles...))
default:
// it the ctx does not contain the allowed role IDs, we need to fall back to all roles
definitions = unifiedrole.GetRoles(unifiedrole.RoleFilterAll())
}
for _, definition := range definitions {
if slices.Contains(availableRoles, definition.GetId()) {
continue
}
availableRoles = append(availableRoles, definition.GetId())
for _, permission := range definition.GetRolePermissions() {
for _, action := range permission.GetAllowedResourceActions() {
if slices.Contains(availableActions, action) {
continue
}
availableActions = append(availableActions, action)
}
}
}
for _, role := range roles {
if slices.Contains(availableRoles, role) {
continue
}
sl.ReportError(roles, "Roles", "Roles", "available_role", "")
}
for _, action := range actions {
if slices.Contains(availableActions, action) {
continue
}
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, ContextKeyRoleIDsValueKey, rolesIds)
}
@@ -0,0 +1,171 @@
package validate_test
import (
"context"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/qsfera/server/pkg/conversions"
"github.com/qsfera/server/services/graph/pkg/unifiedrole"
"github.com/qsfera/server/services/graph/pkg/validate"
)
type validatableFactory[T any] func() (T, bool)
var _ = Describe("libregraph", func() {
var ctx context.Context
var driveItemInvite libregraph.DriveItemInvite
var driveRecipient libregraph.DriveRecipient
BeforeEach(func() {
ctx = context.Background()
driveRecipient = libregraph.DriveRecipient{
ObjectId: conversions.ToPointer("1"),
LibreGraphRecipientType: conversions.ToPointer("user"),
}
driveItemInvite = libregraph.DriveItemInvite{
Recipients: []libregraph.DriveRecipient{driveRecipient},
Roles: []string{unifiedrole.UnifiedRoleEditorID},
LibreGraphPermissionsActions: []string{unifiedrole.DriveItemVersionsUpdate},
ExpirationDateTime: libregraph.PtrTime(time.Now().Add(time.Hour)),
}
})
DescribeTable("DriveItemInvite",
func(factories ...validatableFactory[libregraph.DriveItemInvite]) {
for _, factory := range factories {
s, pass := factory()
switch err := validate.StructCtx(ctx, s); pass {
case false:
Expect(err).To(HaveOccurred())
default:
Expect(err).ToNot(HaveOccurred())
}
}
},
Entry("succeed: roles only", func() (libregraph.DriveItemInvite, bool) {
driveItemInvite.LibreGraphPermissionsActions = nil
return driveItemInvite, true
}),
Entry("succeed: actions only", func() (libregraph.DriveItemInvite, bool) {
driveItemInvite.Roles = nil
return driveItemInvite, true
}),
Entry("succeed: no ExpirationDateTime", func() (libregraph.DriveItemInvite, bool) {
driveItemInvite.Roles = nil
driveItemInvite.ExpirationDateTime = nil
return driveItemInvite, true
}),
Entry("fail: multiple role assignment", func() (libregraph.DriveItemInvite, bool) {
driveItemInvite.Roles = []string{
unifiedrole.UnifiedRoleEditorID,
unifiedrole.UnifiedRoleManagerID,
}
driveItemInvite.LibreGraphPermissionsActions = nil
return driveItemInvite, false
}),
Entry("fail: unknown role", func() (libregraph.DriveItemInvite, bool) {
driveItemInvite.Roles = []string{"foo"}
driveItemInvite.LibreGraphPermissionsActions = nil
return driveItemInvite, false
}),
Entry("fail: disabled role", func() (libregraph.DriveItemInvite, bool) {
ctx = validate.ContextWithAllowedRoleIDs(ctx, []string{unifiedrole.UnifiedRoleEditorID})
driveItemInvite.Roles = []string{
unifiedrole.UnifiedRoleSecureViewerID,
}
driveItemInvite.LibreGraphPermissionsActions = nil
return driveItemInvite, false
}),
Entry("fail: unknown action", func() (libregraph.DriveItemInvite, bool) {
driveItemInvite.Roles = nil
driveItemInvite.LibreGraphPermissionsActions = []string{"foo"}
return driveItemInvite, false
}),
Entry("fail: missing roles or permission actions", func() (libregraph.DriveItemInvite, bool) {
driveItemInvite.Roles = nil
driveItemInvite.LibreGraphPermissionsActions = nil
return driveItemInvite, false
}),
Entry("fail: different number of roles and actions", func() (libregraph.DriveItemInvite, bool) {
driveItemInvite.LibreGraphPermissionsActions = []string{
unifiedrole.DriveItemVersionsUpdate,
unifiedrole.DriveItemChildrenCreate,
}
return driveItemInvite, false
}),
Entry("fail: missing recipients", func() (libregraph.DriveItemInvite, bool) {
driveItemInvite.Roles = nil
driveItemInvite.Recipients = nil
return driveItemInvite, false
}),
Entry("fail: dive recipients", func() (libregraph.DriveItemInvite, bool) {
driveItemInvite.Roles = nil
driveItemInvite.Recipients = []libregraph.DriveRecipient{
{ObjectId: libregraph.PtrString("1"), LibreGraphRecipientType: libregraph.PtrString("dive")},
}
return driveItemInvite, false
}),
Entry("fail: more than 1 recipient", func() (libregraph.DriveItemInvite, bool) {
driveItemInvite.Roles = nil
driveItemInvite.Recipients = []libregraph.DriveRecipient{
driveRecipient,
{ObjectId: libregraph.PtrString("2"), LibreGraphRecipientType: libregraph.PtrString("group")},
}
return driveItemInvite, false
}),
Entry("fail: expirationDateTime in the past", func() (libregraph.DriveItemInvite, bool) {
driveItemInvite.Roles = nil
driveItemInvite.ExpirationDateTime = libregraph.PtrTime(time.Now().Add(-time.Hour))
return driveItemInvite, false
}),
)
DescribeTable("DriveRecipient",
func(factories ...validatableFactory[libregraph.DriveRecipient]) {
for _, factory := range factories {
s, pass := factory()
switch err := validate.StructCtx(context.Background(), s); pass {
case false:
Expect(err).To(HaveOccurred())
default:
Expect(err).ToNot(HaveOccurred())
}
}
},
Entry("fail: invalid objectId",
func() (libregraph.DriveRecipient, bool) {
driveRecipient.ObjectId = nil
return driveRecipient, false
},
func() (libregraph.DriveRecipient, bool) {
driveRecipient.ObjectId = conversions.ToPointer("")
return driveRecipient, false
},
),
Entry("succeed: valid role",
func() (libregraph.DriveRecipient, bool) {
driveRecipient.LibreGraphRecipientType = conversions.ToPointer("user")
return driveRecipient, true
},
func() (libregraph.DriveRecipient, bool) {
driveRecipient.LibreGraphRecipientType = conversions.ToPointer("group")
return driveRecipient, true
},
),
Entry("fail: invalid role",
func() (libregraph.DriveRecipient, bool) {
driveRecipient.LibreGraphRecipientType = conversions.ToPointer("foo")
return driveRecipient, false
},
func() (libregraph.DriveRecipient, bool) {
driveRecipient.LibreGraphRecipientType = nil
return driveRecipient, false
},
),
)
})
@@ -0,0 +1,26 @@
package validate
import (
"context"
"sync/atomic"
"github.com/go-playground/validator/v10"
)
var defaultValidator atomic.Value
func init() {
v := validator.New()
initLibregraph(v)
defaultValidator.Store(v)
}
// Default returns the default validator.
func Default() *validator.Validate { return defaultValidator.Load().(*validator.Validate) }
// StructCtx validates a struct and returns the error.
func StructCtx(ctx context.Context, s any) error {
return Default().StructCtx(ctx, s)
}
@@ -0,0 +1,13 @@
package validate_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestGraph(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Validate Suite")
}