package svc import ( "context" "errors" "net/http" "net/url" "path" "strings" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/v2/pkg/events" "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/go-chi/chi/v5" "github.com/jellydator/ttlcache/v3" "go-micro.dev/v4/client" mevents "go-micro.dev/v4/events" "go.opentelemetry.io/otel/trace" "google.golang.org/protobuf/types/known/emptypb" "github.com/owncloud/ocis/v2/ocis-pkg/keycloak" "github.com/owncloud/ocis/v2/ocis-pkg/log" 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/config" "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" "github.com/owncloud/ocis/v2/services/graph/pkg/identity" ) //go:generate make -C ../../.. generate // Publisher is the interface for events publisher type Publisher interface { Publish(string, interface{}, ...mevents.PublishOption) error } // 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) 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 { config *config.Config mux *chi.Mux logger *log.Logger identityBackend identity.Backend identityEducationBackend identity.EducationBackend gatewaySelector pool.Selectable[gateway.GatewayAPIClient] roleService RoleService permissionsService Permissions valueService settingssvc.ValueService specialDriveItemsCache *ttlcache.Cache[string, interface{}] identityCache identity.IdentityCache eventsPublisher events.Publisher 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" ) // 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 }