Files
ocis/services/graph/pkg/service/v0/graph.go
Julian Koberg 04b829e8a7 feat: expand mfa checks also to queries
Signed-off-by: Julian Koberg <julian.koberg@kiteworks.com>
2025-09-23 14:03:32 +02:00

192 lines
7.1 KiB
Go

package svc
import (
"context"
"errors"
"net/http"
"net/url"
"path"
"strings"
"github.com/CiscoM31/godata"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/go-chi/chi/v5"
"github.com/jellydator/ttlcache/v3"
"go-micro.dev/v4/client"
"go.opentelemetry.io/otel/trace"
"google.golang.org/protobuf/types/known/emptypb"
"github.com/owncloud/reva/v2/pkg/events"
"github.com/owncloud/reva/v2/pkg/storagespace"
"github.com/owncloud/ocis/v2/ocis-pkg/keycloak"
ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0"
searchsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/search/v0"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/identity"
)
// Permissions is the interface used to access the permissions service
type Permissions interface {
ListPermissions(ctx context.Context, req *settingssvc.ListPermissionsRequest, opts ...client.CallOption) (*settingssvc.ListPermissionsResponse, error)
GetPermissionByID(ctx context.Context, request *settingssvc.GetPermissionByIDRequest, opts ...client.CallOption) (*settingssvc.GetPermissionByIDResponse, error)
ListPermissionsByResource(ctx context.Context, in *settingssvc.ListPermissionsByResourceRequest, opts ...client.CallOption) (*settingssvc.ListPermissionsByResourceResponse, error)
}
// HTTPClient is the subset of the http.Client that is being used to interact with the download gateway
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
// GetGatewayServiceClientFunc is a callback used to pass in a mock during testing
type GetGatewayServiceClientFunc func() (gateway.GatewayAPIClient, error)
// RoleService is the interface used to access the role service
type RoleService interface {
ListRoles(ctx context.Context, in *settingssvc.ListBundlesRequest, opts ...client.CallOption) (*settingssvc.ListBundlesResponse, error)
ListRoleAssignments(ctx context.Context, in *settingssvc.ListRoleAssignmentsRequest, opts ...client.CallOption) (*settingssvc.ListRoleAssignmentsResponse, error)
ListRoleAssignmentsFiltered(ctx context.Context, in *settingssvc.ListRoleAssignmentsFilteredRequest, opts ...client.CallOption) (*settingssvc.ListRoleAssignmentsResponse, error)
AssignRoleToUser(ctx context.Context, in *settingssvc.AssignRoleToUserRequest, opts ...client.CallOption) (*settingssvc.AssignRoleToUserResponse, error)
RemoveRoleFromUser(ctx context.Context, in *settingssvc.RemoveRoleFromUserRequest, opts ...client.CallOption) (*emptypb.Empty, error)
}
// Graph defines implements the business logic for Service.
type Graph struct {
BaseGraphService
mux *chi.Mux
identityBackend identity.Backend
identityEducationBackend identity.EducationBackend
roleService RoleService
permissionsService Permissions
valueService settingssvc.ValueService
specialDriveItemsCache *ttlcache.Cache[string, interface{}]
eventsPublisher events.Publisher
eventsConsumer events.Consumer
searchService searchsvc.SearchProviderService
keycloakClient keycloak.Client
historyClient ehsvc.EventHistoryService
traceProvider trace.TracerProvider
}
// ServeHTTP implements the Service interface.
func (g Graph) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// There was a number of issues with the chi router and parameters with
// slashes/percentage/other characters that didn't get properly escaped.
// This is a workaround to fix this. Also, we're not the only ones who have
// tried to fix this, as seen in this issue:
// https://github.com/go-chi/chi/issues/641#issuecomment-883156692
r.URL.RawPath = r.URL.EscapedPath()
g.mux.ServeHTTP(w, r)
}
func (g Graph) publishEvent(ctx context.Context, ev interface{}) {
if g.eventsPublisher != nil {
if err := events.Publish(ctx, g.eventsPublisher, ev); err != nil {
g.logger.Error().
Err(err).
Msg("could not publish user created event")
}
}
}
func (g Graph) getWebDavBaseURL() (*url.URL, error) {
webDavBaseURL, err := url.Parse(g.config.Spaces.WebDavBase)
if err != nil {
return nil, err
}
webDavBaseURL.Path = path.Join(webDavBaseURL.Path, g.config.Spaces.WebDavPath)
return webDavBaseURL, nil
}
// ListResponse is used for proper marshalling of Graph list responses
type ListResponse struct {
Value interface{} `json:"value,omitempty"`
}
const (
// ReadmeSpecialFolderName for the drive specialFolder property
ReadmeSpecialFolderName = "readme"
// SpaceImageSpecialFolderName for the drive specialFolder property
SpaceImageSpecialFolderName = "image"
)
type APIVersion int
const (
// APIVersion_1 represents the first version of the API.
APIVersion_1 APIVersion = iota + 1
// APIVersion_1_Beta_1 refers to the beta version of the API.
// It is typically used for testing purposes and may have more
// inconsistencies and bugs than the stable version as it is
// still in the testing phase, use it with caution.
APIVersion_1_Beta_1
)
// TODO might be different for /education/users vs /users
func (g Graph) parseMemberRef(ref string) (string, string, error) {
memberURL, err := url.ParseRequestURI(ref)
if err != nil {
return "", "", err
}
segments := strings.Split(memberURL.Path, "/")
if len(segments) < 2 {
return "", "", errors.New("invalid member reference")
}
id := segments[len(segments)-1]
memberType := segments[len(segments)-2]
return memberType, id, nil
}
func parseIDParam(r *http.Request, param string) (storageprovider.ResourceId, error) {
driveID, err := url.PathUnescape(chi.URLParam(r, param))
if err != nil {
return storageprovider.ResourceId{}, errorcode.New(errorcode.InvalidRequest, err.Error())
}
id, err := storagespace.ParseID(driveID)
if err != nil {
return storageprovider.ResourceId{}, errorcode.New(errorcode.InvalidRequest, err.Error())
}
return id, nil
}
// regular users can only search for terms with a minimum length
func hasAcceptableSearch(query *godata.GoDataQuery, minSearchLength int) bool {
if query == nil || query.Search == nil {
return false
}
if strings.HasPrefix(query.Search.RawValue, "\"") {
// if search starts with double quotes then it must finish with double quotes
// add +2 to the minimum search length in this case
minSearchLength += 2
}
return len(query.Search.RawValue) >= minSearchLength
}
// regular users can only filter by userType
func hasAcceptableFilter(query *godata.GoDataQuery) bool {
switch {
case query == nil || query.Filter == nil:
return true
case query.Filter.Tree.Token.Type != godata.ExpressionTokenLogical:
return false
case query.Filter.Tree.Token.Value != "eq":
return false
case query.Filter.Tree.Children[0].Token.Value != "userType":
return false
}
return true
}
// regular users can only use basic queries without any expansions, computes or applies
func hasAcceptableQuery(query *godata.GoDataQuery) bool {
return query != nil && query.Apply == nil && query.Expand == nil && query.Compute == nil
}