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
+9
View File
@@ -0,0 +1,9 @@
# This is the official list of REVA authors for copyright purposes.
# This file is distinct from the CONTRIBUTORS files.
# See the latter for an explanation.
# Names should be added to this file as:
# Name or Organization <email address>
# The email address is not required for organizations.
CERN
+19
View File
@@ -0,0 +1,19 @@
- Alex Unger <zyxancf@gmail.com>
- Andrew Schleifer <me@andrewschleifer.name>
- Aritz Brosa <aritz.brosa.iartza@cern.ch>
- Diogo Castro <diogo.castro@cern.ch>
- Felix Hillingshaeuser <felix@mxcore.de>
- Giuseppe Lo Presti <giuseppe.lopresti@cern.ch>
- Hannah von Reth <vonreth@kde.org>
- Hugo Gonzalez Labrador <github@hugo.labkode.com>
- Ilja Neumann <ineumann@owncloud.com>
- Ilja Neumann <node512@gmail.com>
- Ishank Arora <ishank011@gmail.com>
- Jörn Friedrich Dreyer <jfd@butonic.de>
- LovisaLugnegard <lovisa.lugnegard@gmail.com>
- Michael D'Silva <md@aarnet.edu.au>
- Michael D'Silva <michael.dsilva@aarnet.edu.au>
- Mohitty <mohitt@iitk.ac.in>
- Thomas Boerger <thomas@webhippie.de>
- Miroslav Bauer <bauer@cesnet.cz>
- Daniel Mueller <daniel.mueller@uni-muenster.de>
+176
View File
@@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
@@ -0,0 +1,401 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package grace
import (
"fmt"
"net"
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/pkg/errors"
"github.com/rs/zerolog"
)
// Watcher watches a process for a graceful restart
// preserving open network sockets to avoid packet loss.
type Watcher struct {
log zerolog.Logger
graceful bool
ppid int
lns map[string]net.Listener
ss map[string]Server
pidFile string
childPIDs []int
gracefulShutdownTimeout int
}
// Option represent an option.
type Option func(w *Watcher)
// WithLogger adds a logger to the Watcher.
func WithLogger(l zerolog.Logger) Option {
return func(w *Watcher) {
w.log = l
}
}
// WithPIDFile specifies the pid file to use.
func WithPIDFile(fn string) Option {
return func(w *Watcher) {
w.pidFile = fn
}
}
func WithGracefuleShutdownTimeout(seconds int) Option {
return func(w *Watcher) {
w.gracefulShutdownTimeout = seconds
}
}
// NewWatcher creates a Watcher.
func NewWatcher(opts ...Option) *Watcher {
w := &Watcher{
log: zerolog.Nop(),
graceful: os.Getenv("GRACEFUL") == "true",
ppid: os.Getppid(),
ss: map[string]Server{},
}
for _, opt := range opts {
opt(w)
}
return w
}
// Exit exits the current process cleaning up
// existing pid files.
func (w *Watcher) Exit(errc int) {
w.Clean()
os.Exit(errc)
}
// Clean removes the pid file.
func (w *Watcher) Clean() {
err := w.clean()
if err != nil {
w.log.Warn().Err(err).Msg("error removing pid file")
} else {
w.log.Info().Msgf("pid file %q got removed", w.pidFile)
}
}
func (w *Watcher) clean() error {
// only remove PID file if the PID has been written by us
filePID, err := w.readPID()
if err != nil {
return err
}
if filePID != os.Getpid() {
// the pidfile may have been changed by a forked child
// TODO(labkode): is there a way to list children pids for current process?
return fmt.Errorf("pid:%d in pidfile is different from pid:%d, it can be a leftover from a hard shutdown or that a reload was triggered", filePID, os.Getpid())
}
return os.Remove(w.pidFile)
}
func (w *Watcher) readPID() (int, error) {
piddata, err := os.ReadFile(w.pidFile)
if err != nil {
return 0, err
}
// Convert the file contents to an integer.
pid, err := strconv.Atoi(string(piddata))
if err != nil {
return 0, err
}
return pid, nil
}
// GetProcessFromFile reads the pidfile and returns the running process or error if the process or file
// are not available.
func GetProcessFromFile(pfile string) (*os.Process, error) {
data, err := os.ReadFile(pfile)
if err != nil {
return nil, err
}
pid, err := strconv.Atoi(string(data))
if err != nil {
return nil, err
}
process, err := os.FindProcess(pid)
if err != nil {
return nil, err
}
return process, nil
}
// WritePID writes the pid to the configured pid file.
func (w *Watcher) WritePID() error {
// Read in the pid file as a slice of bytes.
if piddata, err := os.ReadFile(w.pidFile); err == nil {
// Convert the file contents to an integer.
if pid, err := strconv.Atoi(string(piddata)); err == nil {
// Look for the pid in the process list.
if process, err := os.FindProcess(pid); err == nil {
// Send the process a signal zero kill.
if err := process.Signal(syscall.Signal(0)); err == nil {
if !w.graceful {
return fmt.Errorf("pid already running: %d", pid)
}
if pid != w.ppid { // overwrite only if parent pid is pidfile
// We only get an error if the pid isn't running, or it's not ours.
return fmt.Errorf("pid %d is not this process parent", pid)
}
} else {
w.log.Warn().Err(err).Msg("error sending zero kill signal to current process")
}
} else {
w.log.Warn().Msgf("pid:%d not found", pid)
}
} else {
w.log.Warn().Msg("error casting contents of pidfile to pid(int)")
}
} // else {
// w.log.Info().Msg("error reading pidfile")
//}
// If we get here, then the pidfile didn't exist or we are are in graceful reload and thus we overwrite
// or the pid in it doesn't belong to the user running this app.
err := os.WriteFile(w.pidFile, []byte(fmt.Sprintf("%d", os.Getpid())), 0664)
if err != nil {
return err
}
w.log.Info().Msgf("pidfile saved at: %s", w.pidFile)
return nil
}
func newListener(network, addr string) (net.Listener, error) {
return net.Listen(network, addr)
}
// GetListeners return grpc listener first and http listener second.
func (w *Watcher) GetListeners(servers map[string]Server) (map[string]net.Listener, error) {
w.ss = servers
lns := map[string]net.Listener{}
if w.graceful {
w.log.Info().Msg("graceful restart, inheriting parent ln fds for grpc and http")
count := 3
for k, s := range servers {
network, addr := s.Network(), s.Address()
fd := os.NewFile(uintptr(count), "") // 3 because ExtraFile passed to new process
count++
ln, err := net.FileListener(fd)
if err != nil {
w.log.Error().Err(err).Msg("error creating net.Listener from fd")
// create new fd
ln, err := newListener(network, addr)
if err != nil {
return nil, err
}
lns[k] = ln
} else {
lns[k] = ln
}
}
// kill parent
// TODO(labkode): maybe race condition here?
// What do we do if we cannot kill the parent but we have valid fds?
// Do we abort running the forked child? Probably yes, as if the parent cannot be
// killed that means we run two version of the code indefinitely.
w.log.Info().Msgf("killing parent pid gracefully with SIGQUIT: %d", w.ppid)
p, err := os.FindProcess(w.ppid)
if err != nil {
w.log.Error().Err(err).Msgf("error finding parent process with ppid:%d", w.ppid)
err = errors.Wrap(err, "error finding parent process")
return nil, err
}
err = p.Kill()
if err != nil {
w.log.Error().Err(err).Msgf("error killing parent process with ppid:%d", w.ppid)
err = errors.Wrap(err, "error killing parent process")
return nil, err
}
w.lns = lns
return lns, nil
}
// create two listeners for grpc and http
for k, s := range servers {
network, addr := s.Network(), s.Address()
ln, err := newListener(network, addr)
if err != nil {
return nil, err
}
lns[k] = ln
}
w.lns = lns
return lns, nil
}
// Server is the interface that servers like HTTP or gRPC
// servers need to implement.
type Server interface {
Stop() error
GracefulStop() error
Network() string
Address() string
}
// TrapSignals captures the OS signal.
func (w *Watcher) TrapSignals() {
signalCh := make(chan os.Signal, 1024)
signal.Notify(signalCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
for {
s := <-signalCh
w.log.Info().Msgf("%v signal received", s)
switch s {
case syscall.SIGHUP:
w.log.Info().Msg("preparing for a hot-reload, forking child process...")
// Fork a child process.
listeners := w.lns
p, err := forkChild(listeners)
if err != nil {
w.log.Error().Err(err).Msgf("unable to fork child process")
} else {
w.log.Info().Msgf("child forked with new pid %d", p.Pid)
w.childPIDs = append(w.childPIDs, p.Pid)
}
case syscall.SIGQUIT, syscall.SIGINT, syscall.SIGTERM:
gracefulShutdown(w)
return
}
}
}
func gracefulShutdown(w *Watcher) {
defer w.Clean()
w.log.Info().Int("Timeout", w.gracefulShutdownTimeout).Msg("preparing for a graceful shutdown with deadline")
wg := sync.WaitGroup{}
for _, s := range w.ss {
wg.Add(1)
go func() {
defer wg.Done()
w.log.Info().Str("network.transport", s.Network()).Str("network.local.address", s.Address()).Msg("fd gracefully closed")
err := s.GracefulStop()
if err != nil {
w.log.Error().Err(err).Msg("error stopping server")
}
}()
}
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case <-time.After(time.Duration(w.gracefulShutdownTimeout) * time.Second):
w.log.Info().Msg("graceful shutdown timeout reached. running hard shutdown")
for _, s := range w.ss {
w.log.Info().Str("network.transport", s.Network()).Str("network.local.address", s.Address()).Msg("fd abruptly closed")
err := s.Stop()
if err != nil {
w.log.Error().Err(err).Msg("error stopping server")
}
}
return
case <-done:
w.log.Info().Msg("all servers gracefully stopped")
return
}
}
func getListenerFile(ln net.Listener) (*os.File, error) {
switch t := ln.(type) {
case *net.TCPListener:
return t.File()
case *net.UnixListener:
return t.File()
}
return nil, fmt.Errorf("unsupported listener: %T", ln)
}
func forkChild(lns map[string]net.Listener) (*os.Process, error) {
// Get the file descriptor for the listener and marshal the metadata to pass
// to the child in the environment.
fds := map[string]*os.File{}
for k, ln := range lns {
fd, err := getListenerFile(ln)
if err != nil {
return nil, err
}
fds[k] = fd
}
// Pass stdin, stdout, and stderr along with the listener file to the child
files := []*os.File{
os.Stdin,
os.Stdout,
os.Stderr,
}
// Get current environment and add in the listener to it.
environment := append(os.Environ(), "GRACEFUL=true")
var counter = 3
for k, fd := range fds {
k = strings.ToUpper(k)
environment = append(environment, k+"FD="+fmt.Sprintf("%d", counter))
files = append(files, fd)
counter++
}
// Get current process name and directory.
execName, err := os.Executable()
if err != nil {
return nil, err
}
execDir := filepath.Dir(execName)
// Spawn child process.
p, err := os.StartProcess(execName, os.Args, &os.ProcAttr{
Dir: execDir,
Env: environment,
Files: files,
Sys: &syscall.SysProcAttr{},
})
// TODO(labkode): if the process dies (because config changed and is wrong
// we need to return an error
if err != nil {
return nil, err
}
return p, nil
}
@@ -0,0 +1,186 @@
package runtime
import (
"errors"
"fmt"
"net"
"net/http"
"os"
"time"
"github.com/opencloud-eu/reva/v2/pkg/registry"
"github.com/rs/zerolog"
)
const (
HTTP = iota
GRPC
)
// RevaDrivenServer is an interface that defines the methods for starting and stopping reva HTTP/GRPC services.
type RevaDrivenServer interface {
Start() error
Stop() error
}
// revaServer is an interface that defines the methods for starting and stopping a reva server.
type revaServer interface {
Start(ln net.Listener) error
Stop() error
GracefulStop() error
Network() string
Address() string
}
// sever represents a generic reva server that implements the RevaDrivenServer interface.
type server struct {
srv revaServer
log *zerolog.Logger
gracefulShutdownTimeout time.Duration
protocol string
}
// NewDrivenHTTPServerWithOptions runs a revad server w/o watcher with the given config file and options.
// Use it in cases where you want to run a revad server without the need for a watcher and the os signal handling as a part of another runtime.
// Returns nil if no http server is configured in the config file.
// The GracefulShutdownTimeout set to default 20 seconds and can be overridden in the core config.
// Logging a fatal error and exit with code 1 if the http server cannot be created.
func NewDrivenHTTPServerWithOptions(mainConf map[string]interface{}, opts ...Option) *server {
if !isEnabledHTTP(mainConf) {
return nil
}
options := newOptions(opts...)
var srv *server
var err error
if srv, err = newServer(HTTP, mainConf, options); err != nil {
options.Logger.Fatal().Err(err).Msg("failed to create http server")
}
return srv
}
// NewDrivenGRPCServerWithOptions runs a revad server w/o watcher with the given config file and options.
// Use it in cases where you want to run a revad server without the need for a watcher and the os signal handling as a part of another runtime.
// Returns nil if no grpc server is configured in the config file.
// The GracefulShutdownTimeout set to default 20 seconds and can be overridden in the core config.
// Logging a fatal error and exit with code 1 if the grpc server cannot be created.
func NewDrivenGRPCServerWithOptions(mainConf map[string]interface{}, opts ...Option) *server {
if !isEnabledGRPC(mainConf) {
return nil
}
options := newOptions(opts...)
var srv *server
var err error
if srv, err = newServer(GRPC, mainConf, options); err != nil {
options.Logger.Fatal().Err(err).Msg("failed to create grpc server")
}
return srv
}
// Start starts the reva server, listening on the configured address and network.
func (s *server) Start() error {
if s.srv == nil {
return fmt.Errorf("reva %s server not initialized", s.protocol)
}
ln, err := net.Listen(s.srv.Network(), s.srv.Address())
if err != nil {
return err
}
if err = s.srv.Start(ln); err != nil {
if !errors.Is(err, http.ErrServerClosed) {
s.log.Error().Err(err).Msg("reva server error")
}
return err
}
// update logger with transport and address
logger := s.log.With().Str("network.transport", s.srv.Network()).Str("network.local.address", s.srv.Address()).Logger()
s.log = &logger
return nil
}
// Stop gracefully stops the reva server, waiting for the graceful shutdown timeout.
func (s *server) Stop() error {
if s.srv == nil {
return nil
}
done := make(chan struct{})
go func() {
s.log.Info().Msg("gracefully stopping reva server")
if err := s.srv.GracefulStop(); err != nil {
s.log.Error().Err(err).Msg("error gracefully stopping reva server")
err := s.srv.Stop()
if err != nil {
s.log.Error().Err(err).Msg("error stopping reva server")
}
}
close(done)
}()
select {
case <-time.After(s.gracefulShutdownTimeout):
s.log.Info().Msg("graceful shutdown timeout reached. running hard shutdown")
err := s.srv.Stop()
if err != nil {
s.log.Error().Err(err).Msg("error stopping reva server")
}
return nil
case <-done:
s.log.Info().Msg("reva server gracefully stopped")
return nil
}
}
// newServer runs a revad server w/o watcher with the given config file and options.
func newServer(protocol int, mainConf map[string]interface{}, options Options) (*server, error) {
parseSharedConfOrDie(mainConf["shared"])
coreConf := parseCoreConfOrDie(mainConf["core"])
if err := registry.Init(options.Registry); err != nil {
return nil, err
}
srv := &server{}
// update logger with hostname
host, _ := os.Hostname()
logger := options.Logger.With().Str("host.name", host).Logger()
srv.log = &logger
// Only initialize tracing if we didn't get a tracer provider.
if options.TraceProvider == nil {
srv.log.Debug().Msg("no pre-existing tracer given, initializing tracing")
options.TraceProvider = initTracing(coreConf)
}
initCPUCount(coreConf, srv.log)
srv.gracefulShutdownTimeout = 20 * time.Second
if coreConf.GracefulShutdownTimeout > 0 {
srv.gracefulShutdownTimeout = time.Duration(coreConf.GracefulShutdownTimeout) * time.Second
}
switch protocol {
case HTTP:
s, err := getHTTPServer(mainConf["http"], srv.log, options.TraceProvider)
if err != nil {
return nil, err
}
srv.srv = s
srv.protocol = "http"
// update logger with protocol
logger := srv.log.With().Str("protocol", "http").Logger()
srv.log = &logger
return srv, nil
case GRPC:
s, err := getGRPCServer(mainConf["grpc"], srv.log, options.TraceProvider)
if err != nil {
return nil, err
}
srv.srv = s
srv.protocol = "grpc"
// update logger with protocol
logger := srv.log.With().Str("protocol", "grpc").Logger()
srv.log = &logger
return srv, nil
}
return nil, fmt.Errorf("unknown protocol: %d", protocol)
}
@@ -0,0 +1,52 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package runtime
import (
// These are all the extensions points for REVA
_ "github.com/opencloud-eu/reva/v2/internal/grpc/interceptors/loader"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/loader"
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/credential/loader"
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/token/loader"
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/tokenwriter/loader"
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/loader"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/loader"
_ "github.com/opencloud-eu/reva/v2/pkg/app/provider/loader"
_ "github.com/opencloud-eu/reva/v2/pkg/app/registry/loader"
_ "github.com/opencloud-eu/reva/v2/pkg/appauth/manager/loader"
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/loader"
_ "github.com/opencloud-eu/reva/v2/pkg/auth/registry/loader"
_ "github.com/opencloud-eu/reva/v2/pkg/datatx/manager/loader"
_ "github.com/opencloud-eu/reva/v2/pkg/group/manager/loader"
_ "github.com/opencloud-eu/reva/v2/pkg/metrics/driver/loader"
_ "github.com/opencloud-eu/reva/v2/pkg/ocm/invite/repository/loader"
_ "github.com/opencloud-eu/reva/v2/pkg/ocm/provider/authorizer/loader"
_ "github.com/opencloud-eu/reva/v2/pkg/ocm/share/repository/loader"
_ "github.com/opencloud-eu/reva/v2/pkg/permission/manager/loader"
_ "github.com/opencloud-eu/reva/v2/pkg/preferences/loader"
_ "github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/loader"
_ "github.com/opencloud-eu/reva/v2/pkg/rhttp/datatx/manager/loader"
_ "github.com/opencloud-eu/reva/v2/pkg/share/cache/warmup/loader"
_ "github.com/opencloud-eu/reva/v2/pkg/share/manager/loader"
_ "github.com/opencloud-eu/reva/v2/pkg/storage/fs/loader"
_ "github.com/opencloud-eu/reva/v2/pkg/storage/registry/loader"
_ "github.com/opencloud-eu/reva/v2/pkg/tenant/manager/loader"
_ "github.com/opencloud-eu/reva/v2/pkg/token/manager/loader"
_ "github.com/opencloud-eu/reva/v2/pkg/user/manager/loader"
)
@@ -0,0 +1,67 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package runtime
import (
"github.com/rs/zerolog"
"go-micro.dev/v4/registry"
"go.opentelemetry.io/otel/trace"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger *zerolog.Logger
Registry registry.Registry
TraceProvider trace.TracerProvider
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// WithLogger provides a function to set the logger option.
func WithLogger(logger *zerolog.Logger) Option {
return func(o *Options) {
o.Logger = logger
}
}
// WithRegistry provides a function to set the registry.
func WithRegistry(r registry.Registry) Option {
return func(o *Options) {
o.Registry = r
}
}
// WithTraceProvider provides a function to set the trace provider.
func WithTraceProvider(tp trace.TracerProvider) Option {
return func(o *Options) {
o.TraceProvider = tp
}
}
@@ -0,0 +1,301 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package runtime
import (
"fmt"
"log"
"net"
"os"
"runtime"
"strconv"
"strings"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/cmd/revad/internal/grace"
"github.com/opencloud-eu/reva/v2/pkg/logger"
"github.com/opencloud-eu/reva/v2/pkg/registry"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
rtrace "github.com/opencloud-eu/reva/v2/pkg/trace"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/trace"
)
// Run runs a reva server with the given config file and pid file.
func Run(mainConf map[string]interface{}, pidFile, logLevel string) {
log := logger.InitLoggerOrDie(mainConf["log"], logLevel)
RunWithOptions(mainConf, pidFile, WithLogger(log))
}
// RunWithOptions runs a reva server with the given config file, pid file and options.
func RunWithOptions(mainConf map[string]interface{}, pidFile string, opts ...Option) {
options := newOptions(opts...)
parseSharedConfOrDie(mainConf["shared"])
coreConf := parseCoreConfOrDie(mainConf["core"])
if err := registry.Init(options.Registry); err != nil {
options.Logger.Fatal().Err(err).Msg("failed to initialize registry client")
return
}
run(mainConf, coreConf, options.Logger, options.TraceProvider, pidFile)
}
type coreConf struct {
MaxCPUs string `mapstructure:"max_cpus"`
TracesExporter string `mapstructure:"traces_exporter"`
TracingServiceName string `mapstructure:"tracing_service_name"`
GracefulShutdownTimeout int `mapstructure:"graceful_shutdown_timeout"`
}
func run(
mainConf map[string]interface{},
coreConf *coreConf,
logger *zerolog.Logger,
tp trace.TracerProvider,
filename string,
) {
host, _ := os.Hostname()
logger.Info().Msgf("host info: %s", host)
// Only initialise tracing if we didn't get a tracer provider.
if tp == nil {
logger.Debug().Msg("No pre-existing tracer given, initializing tracing")
tp = initTracing(coreConf)
}
initCPUCount(coreConf, logger)
servers := initServers(mainConf, logger, tp)
watcher, err := initWatcher(logger, filename, coreConf.GracefulShutdownTimeout)
if err != nil {
log.Panic(err)
}
listeners := initListeners(watcher, servers, logger)
start(mainConf, servers, listeners, logger, watcher)
}
func initListeners(watcher *grace.Watcher, servers map[string]grace.Server, log *zerolog.Logger) map[string]net.Listener {
listeners, err := watcher.GetListeners(servers)
if err != nil {
log.Error().Err(err).Msg("error getting sockets")
watcher.Exit(1)
}
return listeners
}
func initWatcher(log *zerolog.Logger, filename string, gracefulShutdownTimeout int) (*grace.Watcher, error) {
watcher, err := handlePIDFlag(log, filename, gracefulShutdownTimeout)
// TODO(labkode): maybe pidfile can be created later on? like once a server is going to be created?
if err != nil {
log.Error().Err(err).Msg("error creating grace watcher")
os.Exit(1)
}
return watcher, err
}
func initServers(mainConf map[string]interface{}, log *zerolog.Logger, tp trace.TracerProvider) map[string]grace.Server {
servers := map[string]grace.Server{}
if isEnabledHTTP(mainConf) {
s, err := getHTTPServer(mainConf["http"], log, tp)
if err != nil {
log.Error().Err(err).Msg("error creating http server")
os.Exit(1)
}
servers["http"] = s
}
if isEnabledGRPC(mainConf) {
s, err := getGRPCServer(mainConf["grpc"], log, tp)
if err != nil {
log.Error().Err(err).Msg("error creating grpc server")
os.Exit(1)
}
servers["grpc"] = s
}
if len(servers) == 0 {
log.Info().Msg("nothing to do, no grpc/http enabled_services declared in config")
os.Exit(1)
}
return servers
}
func initTracing(conf *coreConf) trace.TracerProvider {
if conf.TracesExporter != "none" && conf.TracesExporter != "" {
tp := rtrace.NewTracerProvider(conf.TracingServiceName, conf.TracesExporter)
rtrace.SetDefaultTracerProvider(tp)
return tp
}
return rtrace.DefaultProvider()
}
func initCPUCount(conf *coreConf, log *zerolog.Logger) {
ncpus, err := adjustCPU(conf.MaxCPUs)
if err != nil {
log.Error().Err(err).Msg("error adjusting number of cpus")
os.Exit(1)
}
// log.Info().Msgf("%s", getVersionString())
log.Info().Msgf("running on %d cpus", ncpus)
}
func handlePIDFlag(l *zerolog.Logger, pidFile string, gracefulShutdownTimeout int) (*grace.Watcher, error) {
w := grace.NewWatcher(grace.WithPIDFile(pidFile),
grace.WithLogger(l.With().Str("pkg", "grace").Logger()),
grace.WithGracefuleShutdownTimeout(gracefulShutdownTimeout),
)
err := w.WritePID()
if err != nil {
return nil, err
}
return w, nil
}
func start(mainConf map[string]interface{}, servers map[string]grace.Server, listeners map[string]net.Listener, log *zerolog.Logger, watcher *grace.Watcher) {
if isEnabledHTTP(mainConf) {
go func() {
if err := servers["http"].(*rhttp.Server).Start(listeners["http"]); err != nil {
log.Error().Err(err).Msg("error starting the http server")
watcher.Exit(1)
}
}()
}
if isEnabledGRPC(mainConf) {
go func() {
if err := servers["grpc"].(*rgrpc.Server).Start(listeners["grpc"]); err != nil {
log.Error().Err(err).Msg("error starting the grpc server")
watcher.Exit(1)
}
}()
}
watcher.TrapSignals()
}
func getGRPCServer(conf interface{}, l *zerolog.Logger, tp trace.TracerProvider) (*rgrpc.Server, error) {
sub := l.With().Str("pkg", "rgrpc").Logger()
s, err := rgrpc.NewServer(conf, sub, tp)
if err != nil {
err = errors.Wrap(err, "main: error creating grpc server")
return nil, err
}
return s, nil
}
func getHTTPServer(conf interface{}, l *zerolog.Logger, tp trace.TracerProvider) (*rhttp.Server, error) {
sub := l.With().Str("pkg", "rhttp").Logger()
s, err := rhttp.New(conf, sub, tp)
if err != nil {
err = errors.Wrap(err, "main: error creating http server")
return nil, err
}
return s, nil
}
// adjustCPU parses string cpu and sets GOMAXPROCS
// according to its value. It accepts either
// a number (e.g. 3) or a percent (e.g. 50%).
// Default is to use all available cores.
func adjustCPU(cpu string) (int, error) {
var numCPU int
availCPU := runtime.NumCPU()
if cpu != "" {
if strings.HasSuffix(cpu, "%") {
// Percent
var percent float32
pctStr := cpu[:len(cpu)-1]
pctInt, err := strconv.Atoi(pctStr)
if err != nil || pctInt < 1 || pctInt > 100 {
return 0, fmt.Errorf("invalid CPU value: percentage must be between 1-100")
}
percent = float32(pctInt) / 100
numCPU = int(float32(availCPU) * percent)
} else {
// Number
num, err := strconv.Atoi(cpu)
if err != nil || num < 1 {
return 0, fmt.Errorf("invalid CPU value: provide a number or percent greater than 0")
}
numCPU = num
}
} else {
numCPU = availCPU
}
if numCPU > availCPU || numCPU == 0 {
numCPU = availCPU
}
runtime.GOMAXPROCS(numCPU)
return numCPU, nil
}
func parseCoreConfOrDie(v interface{}) *coreConf {
c := &coreConf{}
if err := mapstructure.Decode(v, c); err != nil {
fmt.Fprintf(os.Stderr, "error decoding core config: %s\n", err.Error())
os.Exit(1)
}
// tracing defaults to enabled if not explicitly configured
if v == nil {
c.TracesExporter = "console"
} else if _, ok := v.(map[string]interface{})["traces_exporter"]; !ok {
c.TracesExporter = "console"
}
return c
}
func parseSharedConfOrDie(v interface{}) {
if err := sharedconf.Decode(v); err != nil {
fmt.Fprintf(os.Stderr, "error decoding shared config: %s\n", err.Error())
os.Exit(1)
}
}
func isEnabledHTTP(conf map[string]interface{}) bool {
return isEnabled("http", conf)
}
func isEnabledGRPC(conf map[string]interface{}) bool {
return isEnabled("grpc", conf)
}
func isEnabled(key string, conf map[string]interface{}) bool {
if a, ok := conf[key]; ok {
if b, ok := a.(map[string]interface{}); ok {
if c, ok := b["services"]; ok {
if d, ok := c.(map[string]interface{}); ok {
if len(d) > 0 {
return true
}
}
}
}
}
return false
}
@@ -0,0 +1,94 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package appctx
import (
"context"
"runtime"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/rs/zerolog"
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc"
)
// name is the Tracer name used to identify this instrumentation library.
const tracerName = "appctx"
// NewUnary returns a new unary interceptor that creates the application context.
func NewUnary(log zerolog.Logger, tp trace.TracerProvider) grpc.UnaryServerInterceptor {
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
span := trace.SpanFromContext(ctx)
defer span.End()
if !span.SpanContext().HasTraceID() {
ctx, span = tp.Tracer(tracerName).Start(ctx, "grpc unary")
}
_, file, _, ok := runtime.Caller(1)
if ok {
span.SetAttributes(semconv.CodeFilePathKey.String(file))
}
sub := log.With().Str("traceid", span.SpanContext().TraceID().String()).Logger()
ctx = appctx.WithLogger(ctx, &sub)
ctx = appctx.WithTracerProvider(ctx, tp)
res, err := handler(ctx, req)
return res, err
}
return interceptor
}
// NewStream returns a new server stream interceptor
// that creates the application context.
func NewStream(log zerolog.Logger, tp trace.TracerProvider) grpc.StreamServerInterceptor {
interceptor := func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
ctx := ss.Context()
span := trace.SpanFromContext(ctx)
defer span.End()
if !span.SpanContext().HasTraceID() {
ctx, span = tp.Tracer(tracerName).Start(ctx, "grpc stream")
}
_, file, _, ok := runtime.Caller(1)
if ok {
span.SetAttributes(semconv.CodeFilePathKey.String(file))
}
sub := log.With().Str("traceid", span.SpanContext().TraceID().String()).Logger()
ctx = appctx.WithLogger(ctx, &sub)
wrapped := newWrappedServerStream(ctx, ss)
err := handler(srv, wrapped)
return err
}
return interceptor
}
func newWrappedServerStream(ctx context.Context, ss grpc.ServerStream) *wrappedServerStream {
return &wrappedServerStream{ServerStream: ss, newCtx: ctx}
}
type wrappedServerStream struct {
grpc.ServerStream
newCtx context.Context
}
func (ss *wrappedServerStream) Context() context.Context {
return ss.newCtx
}
@@ -0,0 +1,320 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package auth
import (
"context"
"sync"
"time"
"github.com/bluele/gcache"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/token"
tokenmgr "github.com/opencloud-eu/reva/v2/pkg/token/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/pkg/errors"
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// name is the Tracer name used to identify this instrumentation library.
const tracerName = "auth"
var (
userGroupsCache gcache.Cache
scopeExpansionCache gcache.Cache
cacheOnce sync.Once
)
type config struct {
// TODO(labkode): access a map is more performant as uri as fixed in length
// for SkipMethods.
TokenManager string `mapstructure:"token_manager"`
TokenManagers map[string]map[string]interface{} `mapstructure:"token_managers"`
GatewayAddr string `mapstructure:"gateway_addr"`
UserGroupsCacheSize int `mapstructure:"usergroups_cache_size"`
ScopeExpansionCacheSize int `mapstructure:"scope_expansion_cache_size"`
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "auth: error decoding conf")
return nil, err
}
return c, nil
}
// NewUnary returns a new unary interceptor that adds
// trace information for the request.
func NewUnary(m map[string]interface{}, unprotected []string, tp trace.TracerProvider) (grpc.UnaryServerInterceptor, error) {
conf, err := parseConfig(m)
if err != nil {
err = errors.Wrap(err, "auth: error parsing config")
return nil, err
}
if conf.TokenManager == "" {
conf.TokenManager = "jwt"
}
conf.GatewayAddr = sharedconf.GetGatewaySVC(conf.GatewayAddr)
if conf.UserGroupsCacheSize == 0 {
conf.UserGroupsCacheSize = 5000
}
if conf.ScopeExpansionCacheSize == 0 {
conf.ScopeExpansionCacheSize = 5000
}
cacheOnce.Do(func() {
userGroupsCache = gcache.New(conf.UserGroupsCacheSize).LFU().Build()
scopeExpansionCache = gcache.New(conf.ScopeExpansionCacheSize).LFU().Build()
})
h, ok := tokenmgr.NewFuncs[conf.TokenManager]
if !ok {
return nil, errtypes.NotFound("auth: token manager does not exist: " + conf.TokenManager)
}
tokenManager, err := h(conf.TokenManagers[conf.TokenManager])
if err != nil {
return nil, errors.Wrap(err, "auth: error creating token manager")
}
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
log := appctx.GetLogger(ctx)
span := trace.SpanFromContext(ctx)
defer span.End()
if !span.SpanContext().HasTraceID() {
ctx, span = tp.Tracer(tracerName).Start(ctx, "grpc auth unary")
}
if utils.Skip(info.FullMethod, unprotected) {
log.Debug().Str("method", info.FullMethod).Msg("skipping auth")
// If a token is present, set it anyway, as we might need the user info
// to decide the storage provider.
tkn, ok := ctxpkg.ContextGetToken(ctx)
if ok {
u, tokenScope, err := dismantleToken(ctx, tkn, req, tokenManager, conf.GatewayAddr)
if err == nil {
// store user and scopes in context
ctx = ctxpkg.ContextSetUser(ctx, u)
ctx = ctxpkg.ContextSetScopes(ctx, tokenScope)
span.SetAttributes(semconv.EnduserIDKey.String(u.Id.OpaqueId))
}
}
return handler(ctx, req)
}
tkn, ok := ctxpkg.ContextGetToken(ctx)
if !ok || tkn == "" {
log.Warn().Msg("access token not found or empty")
return nil, status.Errorf(codes.Unauthenticated, "auth: core access token not found")
}
// validate the token and ensure access to the resource is allowed
u, tokenScope, err := dismantleToken(ctx, tkn, req, tokenManager, conf.GatewayAddr)
if err != nil {
log.Warn().Err(err).Msg("access token is invalid")
return nil, status.Errorf(codes.PermissionDenied, "auth: core access token is invalid")
}
if sharedconf.MultiTenantEnabled() && u.GetId().GetType() != userpb.UserType_USER_TYPE_SERVICE && u.GetId().GetTenantId() == "" {
log.Warn().Msg("user has no tenant id, rejecting request")
return nil, status.Errorf(codes.PermissionDenied, "auth: user has no tenant id, rejecting request")
}
// store user and scopes in context
ctx = ctxpkg.ContextSetUser(ctx, u)
ctx = ctxpkg.ContextSetScopes(ctx, tokenScope)
span.SetAttributes(semconv.EnduserIDKey.String(u.Id.OpaqueId))
return handler(ctx, req)
}
return interceptor, nil
}
// NewStream returns a new server stream interceptor
// that adds trace information to the request.
func NewStream(m map[string]interface{}, unprotected []string, tp trace.TracerProvider) (grpc.StreamServerInterceptor, error) {
conf, err := parseConfig(m)
if err != nil {
return nil, err
}
if conf.TokenManager == "" {
conf.TokenManager = "jwt"
}
if conf.UserGroupsCacheSize == 0 {
conf.UserGroupsCacheSize = 10000
}
if conf.ScopeExpansionCacheSize == 0 {
conf.ScopeExpansionCacheSize = 10000
}
cacheOnce.Do(func() {
userGroupsCache = gcache.New(conf.UserGroupsCacheSize).LFU().Build()
scopeExpansionCache = gcache.New(conf.ScopeExpansionCacheSize).LFU().Build()
})
h, ok := tokenmgr.NewFuncs[conf.TokenManager]
if !ok {
return nil, errtypes.NotFound("auth: token manager not found: " + conf.TokenManager)
}
tokenManager, err := h(conf.TokenManagers[conf.TokenManager])
if err != nil {
return nil, errtypes.NotFound("auth: token manager not found: " + conf.TokenManager)
}
interceptor := func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
ctx := ss.Context()
log := appctx.GetLogger(ctx)
span := trace.SpanFromContext(ctx)
defer span.End()
if !span.SpanContext().HasTraceID() {
ctx, span = tp.Tracer(tracerName).Start(ctx, "grpc auth new stream")
}
if utils.Skip(info.FullMethod, unprotected) {
log.Debug().Str("method", info.FullMethod).Msg("skipping auth")
// If a token is present, set it anyway, as we might need the user info
// to decide the storage provider.
tkn, ok := ctxpkg.ContextGetToken(ctx)
if ok {
u, tokenScope, err := dismantleToken(ctx, tkn, ss, tokenManager, conf.GatewayAddr)
if err == nil {
// store user and scopes in context
ctx = ctxpkg.ContextSetUser(ctx, u)
ctx = ctxpkg.ContextSetScopes(ctx, tokenScope)
ss = newWrappedServerStream(ctx, ss)
span.SetAttributes(semconv.EnduserIDKey.String(u.Id.OpaqueId))
}
}
return handler(srv, ss)
}
tkn, ok := ctxpkg.ContextGetToken(ctx)
if !ok || tkn == "" {
log.Warn().Msg("access token not found")
return status.Errorf(codes.Unauthenticated, "auth: core access token not found")
}
// validate the token and ensure access to the resource is allowed
u, tokenScope, err := dismantleToken(ctx, tkn, ss, tokenManager, conf.GatewayAddr)
if err != nil {
log.Warn().Err(err).Msg("access token is invalid")
return status.Errorf(codes.PermissionDenied, "auth: core access token is invalid")
}
// store user and scopes in context
ctx = ctxpkg.ContextSetUser(ctx, u)
ctx = ctxpkg.ContextSetScopes(ctx, tokenScope)
wrapped := newWrappedServerStream(ctx, ss)
span.SetAttributes(semconv.EnduserIDKey.String(u.Id.OpaqueId))
return handler(srv, wrapped)
}
return interceptor, nil
}
func newWrappedServerStream(ctx context.Context, ss grpc.ServerStream) *wrappedServerStream {
return &wrappedServerStream{ServerStream: ss, newCtx: ctx}
}
type wrappedServerStream struct {
grpc.ServerStream
newCtx context.Context
}
func (ss *wrappedServerStream) Context() context.Context {
return ss.newCtx
}
// dismantleToken extracts the user and scopes from the reva access token
func dismantleToken(ctx context.Context, tkn string, req interface{}, mgr token.Manager, gatewayAddr string) (*userpb.User, map[string]*authpb.Scope, error) {
u, tokenScope, err := mgr.DismantleToken(ctx, tkn)
if err != nil {
return nil, nil, err
}
if sharedconf.SkipUserGroupsInToken() {
client, err := pool.GetGatewayServiceClient(gatewayAddr)
if err != nil {
return nil, nil, err
}
groups, err := getUserGroups(ctx, u, client)
if err != nil {
return nil, nil, err
}
u.Groups = groups
}
// Check if access to the resource is in the scope of the token
ok, err := scope.VerifyScope(ctx, tokenScope, req)
if err != nil {
return nil, nil, errtypes.InternalError("error verifying scope of access token")
}
if ok {
return u, tokenScope, nil
}
if err = expandAndVerifyScope(ctx, req, tokenScope, u, gatewayAddr, mgr); err != nil {
return nil, nil, err
}
return u, tokenScope, nil
}
func getUserGroups(ctx context.Context, u *userpb.User, client gatewayv1beta1.GatewayAPIClient) ([]string, error) {
if groupsIf, err := userGroupsCache.Get(u.Id.OpaqueId); err == nil {
log := appctx.GetLogger(ctx)
log.Info().Str("userid", u.Id.OpaqueId).Msg("user groups found in cache")
return groupsIf.([]string), nil
}
res, err := client.GetUserGroups(ctx, &userpb.GetUserGroupsRequest{UserId: u.Id})
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetUserGroups")
}
_ = userGroupsCache.SetWithExpire(u.Id.OpaqueId, res.Groups, 3600*time.Second)
return res.Groups, nil
}
@@ -0,0 +1,464 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package auth
import (
"context"
"fmt"
"path/filepath"
"strings"
"time"
appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
ocmv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
statuspkg "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/token"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"google.golang.org/grpc/metadata"
)
const (
scopeDelimiter = "#"
scopeCacheExpiration = 3600
)
func expandAndVerifyScope(ctx context.Context, req interface{}, tokenScope map[string]*authpb.Scope, user *userpb.User, gatewayAddr string, mgr token.Manager) error {
log := appctx.GetLogger(ctx)
client, err := pool.GetGatewayServiceClient(gatewayAddr)
if err != nil {
return err
}
if ref, ok := extractRef(req, tokenScope); ok {
// The request is for a storage reference. This can be the case for multiple scenarios:
// - If the path is not empty, the request might be coming from a share where the accessor is
// trying to impersonate the owner, since the share manager doesn't know the
// share path.
// - If the ID not empty, the request might be coming from
// - a resource present inside a shared folder, or
// - a share created for a lightweight account after the token was minted.
log.Info().Msgf("resolving storage reference to check token scope %s", ref.String())
for k := range tokenScope {
switch {
case strings.HasPrefix(k, "publicshare"):
if err = resolvePublicShare(ctx, ref, tokenScope[k], client, mgr); err == nil {
return nil
}
case strings.HasPrefix(k, "lightweight"):
if err = resolveLightweightScope(ctx, ref, tokenScope[k], user, client, mgr); err == nil {
return nil
}
case strings.HasPrefix(k, "ocmshare"):
if err = resolveOCMShare(ctx, ref, tokenScope[k], client, mgr); err == nil {
return nil
}
}
log.Err(err).Interface("ref", ref).Interface("scope", k).Msg("error resolving reference under scope")
}
} else if ref, ok := extractShareRef(req); ok {
// It's a share ref
// The request might be coming from a share created for a lightweight account
// after the token was minted.
log.Info().Msgf("resolving share reference against received shares to verify token scope %+v", ref.String())
for k := range tokenScope {
if strings.HasPrefix(k, "lightweight") {
// Check if this ID is cached
key := "lw:" + user.Id.OpaqueId + scopeDelimiter + ref.GetId().OpaqueId
if _, err := scopeExpansionCache.Get(key); err == nil {
return nil
}
shares, err := client.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{})
if err != nil || shares.Status.Code != rpc.Code_CODE_OK {
log.Warn().Err(err).Msg("error listing received shares")
continue
}
for _, s := range shares.Shares {
shareKey := "lw:" + user.Id.OpaqueId + scopeDelimiter + s.Share.Id.OpaqueId
_ = scopeExpansionCache.SetWithExpire(shareKey, nil, scopeCacheExpiration*time.Second)
if ref.GetId() != nil && ref.GetId().OpaqueId == s.Share.Id.OpaqueId {
return nil
}
if key := ref.GetKey(); key != nil && (utils.UserEqual(key.Owner, s.Share.Owner) || utils.UserEqual(key.Owner, s.Share.Creator)) &&
utils.ResourceIDEqual(key.ResourceId, s.Share.ResourceId) && utils.GranteeEqual(key.Grantee, s.Share.Grantee) {
return nil
}
}
}
}
}
return errtypes.PermissionDenied(fmt.Sprintf("access to resource %+v not allowed within the assigned scope", req))
}
func resolveLightweightScope(ctx context.Context, ref *provider.Reference, scope *authpb.Scope, user *userpb.User, client gateway.GatewayAPIClient, mgr token.Manager) error {
refString, err := storagespace.FormatReference(ref)
if err != nil {
// cannot format reference, so cannot be valid
return errtypes.PermissionDenied("invalid reference")
}
// Check if this ref is cached
key := "lw:" + user.Id.OpaqueId + scopeDelimiter + refString
if _, err := scopeExpansionCache.Get(key); err == nil {
return nil
}
shares, err := client.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{})
if err != nil || shares.Status.Code != rpc.Code_CODE_OK {
return errtypes.InternalError("error listing received shares")
}
for _, share := range shares.Shares {
shareKey := "lw:" + user.Id.OpaqueId + scopeDelimiter + storagespace.FormatResourceID(share.Share.ResourceId)
_ = scopeExpansionCache.SetWithExpire(shareKey, nil, scopeCacheExpiration*time.Second)
if ref.ResourceId != nil && utils.ResourceIDEqual(share.Share.ResourceId, ref.ResourceId) {
return nil
}
if ok, err := checkIfNestedResource(ctx, ref, share.Share.ResourceId, client, mgr); err == nil && ok {
_ = scopeExpansionCache.SetWithExpire(key, nil, scopeCacheExpiration*time.Second)
return nil
}
}
return errtypes.PermissionDenied("request is not for a nested resource")
}
func resolvePublicShare(ctx context.Context, ref *provider.Reference, scope *authpb.Scope, client gateway.GatewayAPIClient, mgr token.Manager) error {
var share link.PublicShare
err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &share)
if err != nil {
return err
}
return checkCacheForNestedResource(ctx, ref, share.ResourceId, client, mgr)
}
func resolveOCMShare(ctx context.Context, ref *provider.Reference, scope *authpb.Scope, client gateway.GatewayAPIClient, mgr token.Manager) error {
var share ocmv1beta1.Share
if err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &share); err != nil {
return err
}
// for ListOCMSharesRequest, the ref resource id is empty and we set path to . to indicate the root of the share
if ref.GetResourceId() == nil && ref.Path == "." {
ref.ResourceId = share.GetResourceId()
}
return checkCacheForNestedResource(ctx, ref, share.ResourceId, client, mgr)
}
func checkCacheForNestedResource(ctx context.Context, ref *provider.Reference, resource *provider.ResourceId, client gateway.GatewayAPIClient, mgr token.Manager) error {
refString, err := storagespace.FormatReference(ref)
if err != nil {
// cannot format reference, so cannot be valid
return errtypes.PermissionDenied("invalid reference")
}
// Check if this ref is cached
key := storagespace.FormatResourceID(resource) + scopeDelimiter + refString
if _, err := scopeExpansionCache.Get(key); err == nil {
return nil
}
if ok, err := checkIfNestedResource(ctx, ref, resource, client, mgr); err == nil && ok {
_ = scopeExpansionCache.SetWithExpire(key, nil, scopeCacheExpiration*time.Second)
return nil
}
return errtypes.PermissionDenied("request is not for a nested resource")
}
func checkIfNestedResource(ctx context.Context, ref *provider.Reference, parent *provider.ResourceId, client gateway.GatewayAPIClient, mgr token.Manager) (bool, error) {
// Since the resource ID is obtained from the scope, the current token
// has access to it.
statResponse, err := client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: parent}})
if err != nil {
return false, err
}
if statResponse.GetStatus().GetCode() != rpc.Code_CODE_OK {
return false, statuspkg.NewErrorFromCode(statResponse.Status.Code, "auth interceptor")
}
parentInfo := statResponse.GetInfo()
// We need to find out if the requested resource is child of the `parent` (coming from token scope)
// We mint a token as the owner of the public share and try to stat the reference
// TODO(ishank011): We need to find a better alternative to this
// NOTE: did somebody say service accounts? ...
var user *userpb.User
if parentInfo.GetOwner().GetType() == userpb.UserType_USER_TYPE_SPACE_OWNER {
// fake a space owner user
user = &userpb.User{
Id: parentInfo.GetOwner(),
}
} else {
userResp, err := client.GetUser(ctx, &userpb.GetUserRequest{UserId: parentInfo.GetOwner(), SkipFetchingUserGroups: true})
if err != nil || userResp.Status.Code != rpc.Code_CODE_OK {
return false, err
}
user = userResp.User
}
scope, err := scope.AddOwnerScope(map[string]*authpb.Scope{})
if err != nil {
return false, err
}
token, err := mgr.MintToken(ctx, user, scope)
if err != nil {
return false, err
}
ctx = metadata.AppendToOutgoingContext(context.Background(), ctxpkg.TokenHeader, token)
childStat, err := client.Stat(ctx, &provider.StatRequest{Ref: ref})
if err != nil {
return false, err
}
if childStat.GetStatus().GetCode() == rpc.Code_CODE_NOT_FOUND && ref.GetPath() != "" && ref.GetPath() != "." {
// The resource does not seem to exist (yet?). We might be part of an initiate upload request.
// Stat the parent to get its path and check that against the root path.
childStat, err = client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: ref.GetResourceId()}})
if err != nil {
return false, err
}
}
if childStat.GetStatus().GetCode() != rpc.Code_CODE_OK {
return false, statuspkg.NewErrorFromCode(childStat.Status.Code, "auth interceptor")
}
childInfo := childStat.GetInfo()
// child can only be a nested resource if it is within the same space as parent
if childInfo.GetId().GetStorageId() != parentInfo.GetId().GetStorageId() ||
childInfo.GetId().GetSpaceId() != parentInfo.GetId().GetSpaceId() {
return false, nil
}
// Both resources are in the same space, now check paths
pathResp, err := client.GetPath(ctx, &provider.GetPathRequest{ResourceId: statResponse.GetInfo().GetId()})
if err != nil {
return false, err
}
if pathResp.Status.Code != rpc.Code_CODE_OK {
return false, statuspkg.NewErrorFromCode(pathResp.Status.Code, "auth interceptor")
}
parentPath := pathResp.Path
pathResp, err = client.GetPath(ctx, &provider.GetPathRequest{ResourceId: childStat.GetInfo().GetId()})
if err != nil {
return false, err
}
if pathResp.GetStatus().GetCode() != rpc.Code_CODE_OK {
return false, statuspkg.NewErrorFromCode(pathResp.Status.Code, "auth interceptor")
}
childPath := pathResp.Path
rel, err := filepath.Rel(parentPath, childPath)
if err != nil {
return false, err
}
return !strings.HasPrefix(rel, ".."), nil
}
func extractRefFromListProvidersReq(v *registry.ListStorageProvidersRequest) (*provider.Reference, bool) {
ref := &provider.Reference{}
if v.Opaque != nil && v.Opaque.Map != nil {
if e, ok := v.Opaque.Map["storage_id"]; ok {
if ref.ResourceId == nil {
ref.ResourceId = &provider.ResourceId{}
}
ref.ResourceId.StorageId = string(e.Value)
}
if e, ok := v.Opaque.Map["space_id"]; ok {
if ref.ResourceId == nil {
ref.ResourceId = &provider.ResourceId{}
}
ref.ResourceId.SpaceId = string(e.Value)
}
if e, ok := v.Opaque.Map["opaque_id"]; ok {
if ref.ResourceId == nil {
ref.ResourceId = &provider.ResourceId{}
}
ref.ResourceId.OpaqueId = string(e.Value)
}
if e, ok := v.Opaque.Map["path"]; ok {
ref.Path = string(e.Value)
}
}
return ref, true
}
func extractRefForReaderRole(req interface{}) (*provider.Reference, bool) {
switch v := req.(type) {
// Read requests
case *registry.GetStorageProvidersRequest:
return v.GetRef(), true
case *registry.ListStorageProvidersRequest:
return extractRefFromListProvidersReq(v)
case *provider.StatRequest:
return v.GetRef(), true
case *provider.ListContainerRequest:
return v.GetRef(), true
case *provider.InitiateFileDownloadRequest:
return v.GetRef(), true
// App provider requests
case *appregistry.GetAppProvidersRequest:
return &provider.Reference{ResourceId: v.ResourceInfo.Id}, true
case *appprovider.OpenInAppRequest:
return &provider.Reference{ResourceId: v.ResourceInfo.Id}, true
case *gateway.OpenInAppRequest:
return v.GetRef(), true
// Locking
case *provider.GetLockRequest:
return v.GetRef(), true
case *provider.SetLockRequest:
return v.GetRef(), true
case *provider.RefreshLockRequest:
return v.GetRef(), true
case *provider.UnlockRequest:
return v.GetRef(), true
// OCM shares
case *ocmv1beta1.ListReceivedOCMSharesRequest:
return &provider.Reference{Path: "."}, true // we will try to stat the shared node
}
return nil, false
}
func extractRefForUploaderRole(req interface{}) (*provider.Reference, bool) {
switch v := req.(type) {
// Write Requests
case *registry.GetStorageProvidersRequest:
return v.GetRef(), true
case *registry.ListStorageProvidersRequest:
return extractRefFromListProvidersReq(v)
case *provider.StatRequest:
return v.GetRef(), true
case *provider.CreateContainerRequest:
return v.GetRef(), true
case *provider.TouchFileRequest:
return v.GetRef(), true
case *provider.InitiateFileUploadRequest:
return v.GetRef(), true
// App provider requests
case *appregistry.GetAppProvidersRequest:
return &provider.Reference{ResourceId: v.ResourceInfo.Id}, true
case *appprovider.OpenInAppRequest:
return &provider.Reference{ResourceId: v.ResourceInfo.Id}, true
case *gateway.OpenInAppRequest:
return v.GetRef(), true
// Locking
case *provider.GetLockRequest:
return v.GetRef(), true
case *provider.SetLockRequest:
return v.GetRef(), true
case *provider.RefreshLockRequest:
return v.GetRef(), true
case *provider.UnlockRequest:
return v.GetRef(), true
}
return nil, false
}
func extractRefForEditorRole(req interface{}) (*provider.Reference, bool) {
switch v := req.(type) {
// Remaining edit Requests
case *provider.DeleteRequest:
return v.GetRef(), true
case *provider.MoveRequest:
return v.GetSource(), true
case *provider.SetArbitraryMetadataRequest:
return v.GetRef(), true
case *provider.UnsetArbitraryMetadataRequest:
return v.GetRef(), true
}
return nil, false
}
func extractRef(req interface{}, tokenScope map[string]*authpb.Scope) (*provider.Reference, bool) {
var readPerm, uploadPerm, editPerm bool
for _, v := range tokenScope {
if v.Role == authpb.Role_ROLE_OWNER || v.Role == authpb.Role_ROLE_EDITOR || v.Role == authpb.Role_ROLE_VIEWER {
readPerm = true
}
if v.Role == authpb.Role_ROLE_OWNER || v.Role == authpb.Role_ROLE_EDITOR || v.Role == authpb.Role_ROLE_UPLOADER {
uploadPerm = true
}
if v.Role == authpb.Role_ROLE_OWNER || v.Role == authpb.Role_ROLE_EDITOR {
editPerm = true
}
}
if readPerm {
ref, ok := extractRefForReaderRole(req)
if ok {
return ref, true
}
}
if uploadPerm {
ref, ok := extractRefForUploaderRole(req)
if ok {
return ref, true
}
}
if editPerm {
ref, ok := extractRefForEditorRole(req)
if ok {
return ref, true
}
}
return nil, false
}
func extractShareRef(req interface{}) (*collaboration.ShareReference, bool) {
switch v := req.(type) {
case *collaboration.GetReceivedShareRequest:
return v.GetRef(), true
case *collaboration.UpdateReceivedShareRequest:
return &collaboration.ShareReference{Spec: &collaboration.ShareReference_Id{Id: v.GetShare().GetShare().GetId()}}, true
}
return nil, false
}
@@ -0,0 +1,496 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package eventsmiddleware
import (
"strings"
"time"
group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
// ContainerCreated converts the response to an event
func ContainerCreated(r *provider.CreateContainerResponse, req *provider.CreateContainerRequest, spaceOwner *user.UserId, executant *user.User) events.ContainerCreated {
return events.ContainerCreated{
SpaceOwner: spaceOwner,
Executant: executant.GetId(),
Ref: req.Ref,
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// ShareCreated converts the response to an event
func ShareCreated(r *collaboration.CreateShareResponse, executant *user.User) events.ShareCreated {
return events.ShareCreated{
ShareID: r.Share.GetId(),
Executant: executant.GetId(),
Sharer: r.Share.Creator,
GranteeUserID: r.Share.GetGrantee().GetUserId(),
GranteeGroupID: r.Share.GetGrantee().GetGroupId(),
ItemID: r.Share.ResourceId,
ResourceName: utils.ReadPlainFromOpaque(r.Opaque, "resourcename"),
CTime: r.Share.Ctime,
Permissions: r.Share.Permissions,
}
}
// ShareRemoved converts the response to an event
func ShareRemoved(r *collaboration.RemoveShareResponse, req *collaboration.RemoveShareRequest, executant *user.User) events.ShareRemoved {
var (
userid *user.UserId
groupid *group.GroupId
rid *provider.ResourceId
)
_ = utils.ReadJSONFromOpaque(r.Opaque, "granteeuserid", &userid)
_ = utils.ReadJSONFromOpaque(r.Opaque, "granteegroupid", &userid)
_ = utils.ReadJSONFromOpaque(r.Opaque, "resourceid", &rid)
return events.ShareRemoved{
Executant: executant.GetId(),
ShareID: req.Ref.GetId(),
ShareKey: req.Ref.GetKey(),
GranteeUserID: userid,
GranteeGroupID: groupid,
ItemID: rid,
ResourceName: utils.ReadPlainFromOpaque(r.Opaque, "resourcename"),
Timestamp: time.Now(),
}
}
// ShareUpdated converts the response to an event
func ShareUpdated(r *collaboration.UpdateShareResponse, req *collaboration.UpdateShareRequest, executant *user.User) events.ShareUpdated {
return events.ShareUpdated{
Executant: executant.GetId(),
ShareID: r.Share.Id,
ItemID: r.Share.ResourceId,
ResourceName: utils.ReadPlainFromOpaque(r.Opaque, "resourcename"),
Permissions: r.Share.Permissions,
GranteeUserID: r.Share.GetGrantee().GetUserId(),
GranteeGroupID: r.Share.GetGrantee().GetGroupId(),
Sharer: r.Share.Creator,
MTime: r.Share.Mtime,
UpdateMask: req.GetUpdateMask().GetPaths(),
}
}
// ReceivedShareUpdated converts the response to an event
func ReceivedShareUpdated(r *collaboration.UpdateReceivedShareResponse, executant *user.User) events.ReceivedShareUpdated {
return events.ReceivedShareUpdated{
Executant: executant.GetId(),
ShareID: r.Share.Share.Id,
ItemID: r.Share.Share.ResourceId,
Permissions: r.Share.Share.Permissions,
GranteeUserID: r.Share.Share.GetGrantee().GetUserId(),
GranteeGroupID: r.Share.Share.GetGrantee().GetGroupId(),
Sharer: r.Share.Share.Creator,
MTime: r.Share.Share.Mtime,
State: collaboration.ShareState_name[int32(r.Share.State)],
}
}
// LinkCreated converts the response to an event
func LinkCreated(r *link.CreatePublicShareResponse, executant *user.User) events.LinkCreated {
return events.LinkCreated{
Executant: executant.GetId(),
ShareID: r.Share.Id,
Sharer: r.Share.Creator,
ItemID: r.Share.ResourceId,
ResourceName: utils.ReadPlainFromOpaque(r.Opaque, "resourcename"),
Permissions: r.Share.Permissions,
DisplayName: r.Share.DisplayName,
Expiration: r.Share.Expiration,
PasswordProtected: r.Share.PasswordProtected,
CTime: r.Share.Ctime,
Token: r.Share.Token,
}
}
// LinkUpdated converts the response to an event
func LinkUpdated(r *link.UpdatePublicShareResponse, req *link.UpdatePublicShareRequest, executant *user.User) events.LinkUpdated {
return events.LinkUpdated{
Executant: executant.GetId(),
ShareID: r.Share.Id,
Sharer: r.Share.Creator,
ItemID: r.Share.ResourceId,
ResourceName: utils.ReadPlainFromOpaque(r.Opaque, "resourcename"),
Permissions: r.Share.Permissions,
DisplayName: r.Share.DisplayName,
Expiration: r.Share.Expiration,
PasswordProtected: r.Share.PasswordProtected,
MTime: r.Share.Mtime,
Token: r.Share.Token,
FieldUpdated: link.UpdatePublicShareRequest_Update_Type_name[int32(req.Update.GetType())],
}
}
// LinkAccessed converts the response to an event
func LinkAccessed(r *link.GetPublicShareByTokenResponse, executant *user.User) events.LinkAccessed {
return events.LinkAccessed{
Executant: executant.GetId(),
ShareID: r.Share.Id,
Sharer: r.Share.Creator,
ItemID: r.Share.ResourceId,
Permissions: r.Share.Permissions,
DisplayName: r.Share.DisplayName,
Expiration: r.Share.Expiration,
PasswordProtected: r.Share.PasswordProtected,
CTime: r.Share.Ctime,
Token: r.Share.Token,
}
}
// LinkAccessFailed converts the response to an event
func LinkAccessFailed(r *link.GetPublicShareByTokenResponse, req *link.GetPublicShareByTokenRequest, executant *user.User) events.LinkAccessFailed {
e := events.LinkAccessFailed{
Executant: executant.GetId(),
Status: r.Status.Code,
Message: r.Status.Message,
Timestamp: utils.TSNow(),
Token: req.Token,
}
if r.Share != nil {
e.ShareID = r.Share.Id
e.Token = r.Share.Token
}
return e
}
// LinkRemoved converts the response to an event
func LinkRemoved(r *link.RemovePublicShareResponse, req *link.RemovePublicShareRequest, executant *user.User) events.LinkRemoved {
var rid *provider.ResourceId
_ = utils.ReadJSONFromOpaque(r.Opaque, "resourceid", &rid)
return events.LinkRemoved{
Executant: executant.GetId(),
ShareID: req.Ref.GetId(),
ShareToken: req.Ref.GetToken(),
Timestamp: utils.TSNow(),
ItemID: rid,
ResourceName: utils.ReadPlainFromOpaque(r.Opaque, "resourcename"),
}
}
func OCMCoreShareCreated(r *ocmcore.CreateOCMCoreShareResponse, req *ocmcore.CreateOCMCoreShareRequest, executant *user.User) events.OCMCoreShareCreated {
var permissions *provider.ResourcePermissions
for _, p := range req.GetProtocols() {
if p.GetWebdavOptions() != nil {
permissions = p.GetWebdavOptions().GetPermissions().GetPermissions()
break
}
}
return events.OCMCoreShareCreated{
ShareID: r.GetId(),
Executant: executant.GetId(),
Sharer: req.GetSender(),
GranteeUserID: req.GetShareWith(),
ItemID: req.GetResourceId(),
ResourceName: req.GetName(),
CTime: r.GetCreated(),
Permissions: permissions,
}
}
// FileTouched converts the response to an event
func FileTouched(r *provider.TouchFileResponse, req *provider.TouchFileRequest, spaceOwner *user.UserId, executant *user.User) events.FileTouched {
return events.FileTouched{
SpaceOwner: spaceOwner,
Executant: executant.GetId(),
Ref: req.Ref,
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// FileUploaded converts the response to an event
func FileUploaded(r *provider.InitiateFileUploadResponse, req *provider.InitiateFileUploadRequest, spaceOwner *user.UserId, executant *user.User) events.FileUploaded {
return events.FileUploaded{
SpaceOwner: spaceOwner,
Executant: executant.GetId(),
Ref: req.Ref,
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// FileDownloaded converts the response to an event
func FileDownloaded(r *provider.InitiateFileDownloadResponse, req *provider.InitiateFileDownloadRequest, executant *user.User) events.FileDownloaded {
return events.FileDownloaded{
Executant: executant.GetId(),
Ref: req.Ref,
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// FileLocked converts the response to an events
func FileLocked(r *provider.SetLockResponse, req *provider.SetLockRequest, owner *user.UserId, executant *user.User) events.FileLocked {
return events.FileLocked{
Executant: executant.GetId(),
Ref: req.Ref,
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// FileUnlocked converts the response to an event
func FileUnlocked(r *provider.UnlockResponse, req *provider.UnlockRequest, owner *user.UserId, executant *user.User) events.FileUnlocked {
return events.FileUnlocked{
Executant: executant.GetId(),
Ref: req.Ref,
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// ItemTrashed converts the response to an event
func ItemTrashed(r *provider.DeleteResponse, req *provider.DeleteRequest, spaceOwner *user.UserId, executant *user.User) events.ItemTrashed {
opaqueID := utils.ReadPlainFromOpaque(r.Opaque, "opaque_id")
return events.ItemTrashed{
SpaceOwner: spaceOwner,
Executant: executant.GetId(),
Ref: req.Ref,
ID: &provider.ResourceId{
StorageId: req.Ref.GetResourceId().GetStorageId(),
SpaceId: req.Ref.GetResourceId().GetSpaceId(),
OpaqueId: opaqueID,
},
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// ItemMoved converts the response to an event
func ItemMoved(r *provider.MoveResponse, req *provider.MoveRequest, spaceOwner *user.UserId, executant *user.User) events.ItemMoved {
return events.ItemMoved{
SpaceOwner: spaceOwner,
Executant: executant.GetId(),
Ref: req.Destination,
OldReference: req.Source,
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// TrashbinPurged converts the response to an event
func TrashbinPurged(r *provider.PurgeRecycleResponse, req *provider.PurgeRecycleRequest, executant *user.User) events.TrashbinPurged {
return events.TrashbinPurged{
Executant: executant.GetId(),
Ref: &provider.Reference{
ResourceId: &provider.ResourceId{
StorageId: req.Ref.GetResourceId().GetStorageId(),
SpaceId: req.Ref.GetResourceId().GetSpaceId(),
OpaqueId: req.Ref.GetResourceId().GetSpaceId(),
},
},
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// ItemPurged converts the response to an event
func ItemPurged(r *provider.PurgeRecycleResponse, req *provider.PurgeRecycleRequest, executant *user.User) events.ItemPurged {
root, relativePath := router.ShiftPath(req.Key)
if relativePath == "/" && !strings.HasSuffix(req.Key, "/") {
relativePath = ""
}
if root == "" {
// if there is no key this is about purging the whole trashbin
root = req.Ref.GetResourceId().GetSpaceId()
}
ref := &provider.Reference{
ResourceId: &provider.ResourceId{
StorageId: req.Ref.GetResourceId().GetStorageId(),
SpaceId: req.Ref.GetResourceId().GetSpaceId(),
OpaqueId: root,
},
Path: relativePath,
}
return events.ItemPurged{
Executant: executant.GetId(),
Ref: ref,
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// ItemRestored converts the response to an event
func ItemRestored(r *provider.RestoreRecycleItemResponse, req *provider.RestoreRecycleItemRequest, spaceOwner *user.UserId, executant *user.User) events.ItemRestored {
ref := req.Ref
if req.RestoreRef != nil {
ref = req.RestoreRef
}
return events.ItemRestored{
SpaceOwner: spaceOwner,
Executant: executant.GetId(),
Ref: ref,
OldReference: req.Ref,
Key: req.Key,
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// FileVersionRestored converts the response to an event
func FileVersionRestored(r *provider.RestoreFileVersionResponse, req *provider.RestoreFileVersionRequest, spaceOwner *user.UserId, executant *user.User) events.FileVersionRestored {
return events.FileVersionRestored{
SpaceOwner: spaceOwner,
Executant: executant.GetId(),
Ref: req.Ref,
Key: req.Key,
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// SpaceCreated converts the response to an event
func SpaceCreated(r *provider.CreateStorageSpaceResponse, executant *user.User) events.SpaceCreated {
return events.SpaceCreated{
Executant: executant.GetId(),
ID: r.StorageSpace.Id,
Owner: extractOwner(r.StorageSpace.Owner),
Root: r.StorageSpace.Root,
Name: r.StorageSpace.Name,
Type: r.StorageSpace.SpaceType,
Quota: r.StorageSpace.Quota,
MTime: r.StorageSpace.Mtime,
}
}
// SpaceRenamed converts the response to an event
func SpaceRenamed(r *provider.UpdateStorageSpaceResponse, req *provider.UpdateStorageSpaceRequest, executant *user.User) events.SpaceRenamed {
return events.SpaceRenamed{
Executant: executant.GetId(),
ID: r.StorageSpace.Id,
Owner: extractOwner(r.StorageSpace.Owner),
Name: r.StorageSpace.Name,
Timestamp: utils.TSNow(),
}
}
// SpaceUpdated converts the response to an event
func SpaceUpdated(r *provider.UpdateStorageSpaceResponse, req *provider.UpdateStorageSpaceRequest, executant *user.User) events.SpaceUpdated {
return events.SpaceUpdated{
Executant: executant.GetId(),
ID: r.StorageSpace.Id,
Space: r.StorageSpace,
Timestamp: utils.TSNow(),
}
}
// SpaceEnabled converts the response to an event
func SpaceEnabled(r *provider.UpdateStorageSpaceResponse, req *provider.UpdateStorageSpaceRequest, executant *user.User) events.SpaceEnabled {
return events.SpaceEnabled{
Executant: executant.GetId(),
ID: r.StorageSpace.Id,
Owner: extractOwner(r.StorageSpace.Owner),
Timestamp: utils.TSNow(),
}
}
// SpaceShared converts the response to an event
func SpaceShared(r *collaboration.CreateShareResponse, executant *user.User) events.SpaceShared {
id := storagespace.FormatStorageID(r.GetShare().GetResourceId().GetStorageId(), r.GetShare().GetResourceId().GetSpaceId())
return events.SpaceShared{
Executant: executant.GetId(),
Creator: r.Share.GetCreator(),
GranteeUserID: r.Share.GetGrantee().GetUserId(),
GranteeGroupID: r.Share.GetGrantee().GetGroupId(),
ID: &provider.StorageSpaceId{OpaqueId: id},
Timestamp: time.Now(),
}
}
// SpaceShareUpdated converts the response to an events
func SpaceShareUpdated(r *collaboration.UpdateShareResponse, executant *user.User) events.SpaceShareUpdated {
id := storagespace.FormatStorageID(r.GetShare().GetResourceId().GetStorageId(), r.GetShare().GetResourceId().GetSpaceId())
return events.SpaceShareUpdated{
Executant: executant.GetId(),
GranteeUserID: r.GetShare().GetGrantee().GetUserId(),
GranteeGroupID: r.GetShare().GetGrantee().GetGroupId(),
ID: &provider.StorageSpaceId{OpaqueId: id},
Timestamp: time.Now(),
}
}
// SpaceUnshared converts the response to an event
func SpaceUnshared(r *collaboration.RemoveShareResponse, req *collaboration.RemoveShareRequest, executant *user.User) events.SpaceUnshared {
var (
userid *user.UserId
groupid *group.GroupId
rid *provider.ResourceId
)
_ = utils.ReadJSONFromOpaque(r.Opaque, "granteeuserid", &userid)
_ = utils.ReadJSONFromOpaque(r.Opaque, "granteegroupid", &groupid)
_ = utils.ReadJSONFromOpaque(r.Opaque, "resourceid", &rid)
id := storagespace.FormatStorageID(rid.StorageId, rid.SpaceId)
return events.SpaceUnshared{
Executant: executant.GetId(),
GranteeUserID: userid,
GranteeGroupID: groupid,
ID: &provider.StorageSpaceId{OpaqueId: id},
Timestamp: time.Now(),
}
}
// SpaceDisabled converts the response to an event
func SpaceDisabled(r *provider.DeleteStorageSpaceResponse, req *provider.DeleteStorageSpaceRequest, executant *user.User) events.SpaceDisabled {
return events.SpaceDisabled{
Executant: executant.GetId(),
ID: req.Id,
Timestamp: time.Now(),
}
}
// SpaceDeleted converts the response to an event
func SpaceDeleted(r *provider.DeleteStorageSpaceResponse, req *provider.DeleteStorageSpaceRequest, executant *user.User) events.SpaceDeleted {
var final map[string]provider.ResourcePermissions
_ = utils.ReadJSONFromOpaque(r.GetOpaque(), "grants", &final)
return events.SpaceDeleted{
Executant: executant.GetId(),
ID: req.Id,
SpaceName: utils.ReadPlainFromOpaque(r.GetOpaque(), "spacename"),
FinalMembers: final,
Timestamp: time.Now(),
}
}
func extractOwner(u *user.User) *user.UserId {
if u != nil {
return u.Id
}
return nil
}
func extractImpersonator(u *user.User) *user.User {
var impersonator user.User
if err := utils.ReadJSONFromOpaque(u.Opaque, "impersonating-user", &impersonator); err != nil {
return nil
}
return &impersonator
}
@@ -0,0 +1,246 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package eventsmiddleware
import (
"context"
"fmt"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/events/stream"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"google.golang.org/grpc"
)
const (
defaultPriority = 200
)
func init() {
rgrpc.RegisterUnaryInterceptor("eventsmiddleware", NewUnary)
}
// NewUnary returns a new unary interceptor that emits events when needed
// no lint because of the switch statement that should be extendable
//
//nolint:gocritic
func NewUnary(m map[string]interface{}) (grpc.UnaryServerInterceptor, int, error) {
publisher, err := publisherFromConfig(m)
if err != nil {
return nil, 0, err
}
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// Register a channel in the context to receive the space owner id from the handler(s) further down the stack
var ownerID *user.UserId
sendOwnerChan := make(chan *user.UserId)
ctx = storagespace.ContextRegisterSendOwnerChan(ctx, sendOwnerChan)
res, err := handler(ctx, req)
if err != nil {
return res, err
}
// Read the space owner id from the channel
select {
case ownerID = <-sendOwnerChan:
default:
}
executant, _ := revactx.ContextGetUser(ctx)
// The MoveResponse event is moved to the decomposedfs
var ev interface{}
switch v := res.(type) {
case *collaboration.CreateShareResponse:
if isSuccess(v) {
if utils.ExistsInOpaque(v.Opaque, "noevent") {
break
}
if utils.ExistsInOpaque(v.Opaque, "spacegrant") {
ev = SpaceShared(v, executant)
} else {
ev = ShareCreated(v, executant)
}
}
case *collaboration.RemoveShareResponse:
if isSuccess(v) {
if utils.ExistsInOpaque(v.Opaque, "spacegrant") {
ev = SpaceUnshared(v, req.(*collaboration.RemoveShareRequest), executant)
} else {
ev = ShareRemoved(v, req.(*collaboration.RemoveShareRequest), executant)
}
}
case *collaboration.UpdateShareResponse:
if isSuccess(v) {
if utils.ExistsInOpaque(v.Opaque, "spacegrant") {
ev = SpaceShareUpdated(v, executant)
} else {
ev = ShareUpdated(v, req.(*collaboration.UpdateShareRequest), executant)
}
}
case *collaboration.UpdateReceivedShareResponse:
if isSuccess(v) {
ev = ReceivedShareUpdated(v, executant)
}
case *link.CreatePublicShareResponse:
if isSuccess(v) {
ev = LinkCreated(v, executant)
}
case *link.UpdatePublicShareResponse:
if isSuccess(v) {
ev = LinkUpdated(v, req.(*link.UpdatePublicShareRequest), executant)
}
case *link.RemovePublicShareResponse:
if isSuccess(v) {
ev = LinkRemoved(v, req.(*link.RemovePublicShareRequest), executant)
}
case *link.GetPublicShareByTokenResponse:
if isSuccess(v) {
ev = LinkAccessed(v, executant)
} else {
ev = LinkAccessFailed(v, req.(*link.GetPublicShareByTokenRequest), executant)
}
case *ocmcore.CreateOCMCoreShareResponse:
if isSuccess(v) {
ev = OCMCoreShareCreated(v, req.(*ocmcore.CreateOCMCoreShareRequest), executant)
}
case *provider.CreateContainerResponse:
if isSuccess(v) {
ev = ContainerCreated(v, req.(*provider.CreateContainerRequest), ownerID, executant)
}
case *provider.InitiateFileDownloadResponse:
if isSuccess(v) {
ev = FileDownloaded(v, req.(*provider.InitiateFileDownloadRequest), executant)
}
case *provider.DeleteResponse:
if isSuccess(v) {
ev = ItemTrashed(v, req.(*provider.DeleteRequest), ownerID, executant)
}
case *provider.PurgeRecycleResponse:
if isSuccess(v) {
id := req.(*provider.PurgeRecycleRequest).GetRef().GetResourceId()
if id != nil && id.OpaqueId == id.SpaceId && req.(*provider.PurgeRecycleRequest).Key == "" {
// this is a purge of the whole trashbin, NOT the whole space
ev = TrashbinPurged(v, req.(*provider.PurgeRecycleRequest), executant)
} else {
ev = ItemPurged(v, req.(*provider.PurgeRecycleRequest), executant)
}
}
case *provider.RestoreRecycleItemResponse:
if isSuccess(v) {
ev = ItemRestored(v, req.(*provider.RestoreRecycleItemRequest), ownerID, executant)
}
case *provider.RestoreFileVersionResponse:
if isSuccess(v) {
ev = FileVersionRestored(v, req.(*provider.RestoreFileVersionRequest), ownerID, executant)
}
case *provider.CreateStorageSpaceResponse:
if isSuccess(v) && v.StorageSpace != nil { // TODO: Why are there CreateStorageSpaceResponses with nil StorageSpace?
ev = SpaceCreated(v, executant)
}
case *provider.UpdateStorageSpaceResponse:
if isSuccess(v) {
r := req.(*provider.UpdateStorageSpaceRequest)
if r.StorageSpace.Name != "" {
ev = SpaceRenamed(v, r, executant)
} else if utils.ExistsInOpaque(r.Opaque, "restore") {
ev = SpaceEnabled(v, r, executant)
} else {
ev = SpaceUpdated(v, r, executant)
}
}
case *provider.DeleteStorageSpaceResponse:
if isSuccess(v) {
r := req.(*provider.DeleteStorageSpaceRequest)
if utils.ExistsInOpaque(r.Opaque, "purge") {
ev = SpaceDeleted(v, r, executant)
} else {
ev = SpaceDisabled(v, r, executant)
}
}
case *provider.TouchFileResponse:
if isSuccess(v) {
ev = FileTouched(v, req.(*provider.TouchFileRequest), ownerID, executant)
}
case *provider.SetLockResponse:
if isSuccess(v) {
ev = FileLocked(v, req.(*provider.SetLockRequest), ownerID, executant)
}
case *provider.UnlockResponse:
if isSuccess(v) {
ev = FileUnlocked(v, req.(*provider.UnlockRequest), ownerID, executant)
}
}
if ev != nil {
if err := events.Publish(ctx, publisher, ev); err != nil {
appctx.GetLogger(ctx).Error().Err(err).Interface("event", ev).Msg("publishing event failed")
}
}
return res, nil
}
return interceptor, defaultPriority, nil
}
// NewStream returns a new server stream interceptor
// that creates the application context.
func NewStream() grpc.StreamServerInterceptor {
interceptor := func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
// TODO: Use ss.RecvMsg() and ss.SendMsg() to send events from a stream
return handler(srv, ss)
}
return interceptor
}
// common interface to all responses
type su interface {
GetStatus() *rpc.Status
}
func isSuccess(res su) bool {
return res.GetStatus().Code == rpc.Code_CODE_OK
}
func publisherFromConfig(m map[string]interface{}) (events.Publisher, error) {
typ := m["type"].(string)
switch typ {
default:
return nil, fmt.Errorf("stream type '%s' not supported", typ)
case "nats":
var cfg stream.NatsConfig
if err := mapstructure.Decode(m, &cfg); err != nil {
return nil, err
}
name, _ := m["name"].(string)
return stream.NatsFromConfig(name, false, cfg)
}
}
@@ -0,0 +1,27 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package loader
import (
// Load core GRPC services
_ "github.com/opencloud-eu/reva/v2/internal/grpc/interceptors/eventsmiddleware"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/interceptors/prometheus"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/interceptors/readonly"
// Add your own service here
)
@@ -0,0 +1,117 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package log
import (
"context"
"time"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/rs/zerolog"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
)
// NewUnary returns a new unary interceptor
// that logs grpc calls.
func NewUnary() grpc.UnaryServerInterceptor {
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
start := time.Now()
res, err := handler(ctx, req)
code := status.Code(err)
end := time.Now()
diff := end.Sub(start).Nanoseconds()
var fromAddress string
if p, ok := peer.FromContext(ctx); ok {
fromAddress = p.Addr.Network() + "://" + p.Addr.String()
}
userAgent, ok := ctxpkg.ContextGetUserAgentString(ctx)
if !ok {
userAgent = ""
}
log := appctx.GetLogger(ctx)
var event *zerolog.Event
var msg string
if code != codes.OK {
event = log.Error()
msg = err.Error()
} else {
event = log.Debug()
msg = "unary"
}
event.Str("user-agent", userAgent).
Str("from", fromAddress).
Str("uri", info.FullMethod).
Str("start", start.Format("02/Jan/2006:15:04:05 -0700")).
Str("end", end.Format("02/Jan/2006:15:04:05 -0700")).Int("time_ns", int(diff)).
Str("code", code.String()).
Msg(msg)
return res, err
}
return interceptor
}
// NewStream returns a new server stream interceptor
// that adds trace information to the request.
func NewStream() grpc.StreamServerInterceptor {
interceptor := func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
ctx := ss.Context()
start := time.Now()
err := handler(srv, ss)
end := time.Now()
code := status.Code(err)
diff := end.Sub(start).Nanoseconds()
var fromAddress string
if p, ok := peer.FromContext(ss.Context()); ok {
fromAddress = p.Addr.Network() + "://" + p.Addr.String()
}
userAgent, ok := ctxpkg.ContextGetUserAgentString(ctx)
if !ok {
userAgent = ""
}
log := appctx.GetLogger(ss.Context())
var event *zerolog.Event
var msg string
if code != codes.OK {
event = log.Error()
msg = err.Error()
} else {
event = log.Debug()
msg = "stream"
}
event.Str("user-agent", userAgent).
Str("from", fromAddress).
Str("uri", info.FullMethod).
Str("start", start.Format("02/Jan/2006:15:04:05 -0700")).
Str("end", end.Format("02/Jan/2006:15:04:05 -0700")).Int("time_ns", int(diff)).
Str("code", code.String()).
Msg(msg)
return err
}
return interceptor
}
@@ -0,0 +1,77 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package prometheus
import (
"context"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"google.golang.org/grpc"
)
const (
defaultPriority = 100
)
func init() {
rgrpc.RegisterUnaryInterceptor("prometheus", NewUnary)
}
// NewUnary returns a new unary interceptor
// that counts grpc calls.
func NewUnary(m map[string]interface{}) (grpc.UnaryServerInterceptor, int, error) {
interceptor, err := interceptorFromConfig(m)
if err != nil {
return nil, 0, err
}
return interceptor, defaultPriority, nil
}
// NewStream returns a new server stream interceptor
// that counts grpc calls.
func NewStream() grpc.StreamServerInterceptor {
interceptor := func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
err := handler(srv, ss)
// TODO count res codes & errors
return err
}
return interceptor
}
func interceptorFromConfig(m map[string]interface{}) (grpc.UnaryServerInterceptor, error) {
namespace := m["namespace"].(string)
if namespace == "" {
namespace = "reva"
}
subsystem := m["subsystem"].(string)
reqProcessed := promauto.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "grpc_requests_total",
Help: "The total number of processed " + subsystem + " GRPC requests for " + namespace,
})
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
res, err := handler(ctx, req)
reqProcessed.Inc()
return res, err
}
return interceptor, nil
}
@@ -0,0 +1,150 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package readonly
import (
"context"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
rstatus "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
defaultPriority = 200
)
func init() {
rgrpc.RegisterUnaryInterceptor("readonly", NewUnary)
}
// NewUnary returns a new unary interceptor
// that checks grpc calls and blocks write requests.
func NewUnary(map[string]interface{}) (grpc.UnaryServerInterceptor, int, error) {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
log := appctx.GetLogger(ctx)
switch req.(type) {
// handle known non-write request types
case *provider.GetHomeRequest,
*provider.GetPathRequest,
*provider.GetQuotaRequest,
*registry.GetStorageProvidersRequest,
*provider.InitiateFileDownloadRequest,
*provider.ListFileVersionsRequest,
*provider.ListGrantsRequest,
*provider.ListRecycleRequest:
return handler(ctx, req)
case *provider.ListContainerRequest:
resp, err := handler(ctx, req)
if listResp, ok := resp.(*provider.ListContainerResponse); ok && listResp.Infos != nil {
for _, info := range listResp.Infos {
// use the existing PermissionsSet and change the writes to false
if info.PermissionSet != nil {
info.PermissionSet.AddGrant = false
info.PermissionSet.CreateContainer = false
info.PermissionSet.Delete = false
info.PermissionSet.InitiateFileUpload = false
info.PermissionSet.Move = false
info.PermissionSet.RemoveGrant = false
info.PermissionSet.PurgeRecycle = false
info.PermissionSet.RestoreFileVersion = false
info.PermissionSet.RestoreRecycleItem = false
info.PermissionSet.UpdateGrant = false
}
}
}
return resp, err
case *provider.StatRequest:
resp, err := handler(ctx, req)
if statResp, ok := resp.(*provider.StatResponse); ok && statResp.Info != nil && statResp.Info.PermissionSet != nil {
// use the existing PermissionsSet and change the writes to false
statResp.Info.PermissionSet.AddGrant = false
statResp.Info.PermissionSet.CreateContainer = false
statResp.Info.PermissionSet.Delete = false
statResp.Info.PermissionSet.InitiateFileUpload = false
statResp.Info.PermissionSet.Move = false
statResp.Info.PermissionSet.RemoveGrant = false
statResp.Info.PermissionSet.PurgeRecycle = false
statResp.Info.PermissionSet.RestoreFileVersion = false
statResp.Info.PermissionSet.RestoreRecycleItem = false
statResp.Info.PermissionSet.UpdateGrant = false
}
return resp, err
// Don't allow the following requests types
case *provider.AddGrantRequest:
return &provider.AddGrantResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to add grant on readonly storage"),
}, nil
case *provider.CreateContainerRequest:
return &provider.CreateContainerResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to create resource on read-only storage"),
}, nil
case *provider.TouchFileRequest:
return &provider.TouchFileResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to create resource on read-only storage"),
}, nil
case *provider.CreateHomeRequest:
return &provider.CreateHomeResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to create home on readonly storage"),
}, nil
case *provider.DeleteRequest:
return &provider.DeleteResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to delete resource on readonly storage"),
}, nil
case *provider.InitiateFileUploadRequest:
return &provider.InitiateFileUploadResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to upload resource on readonly storage"),
}, nil
case *provider.MoveRequest:
return &provider.MoveResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to move resource on readonly storage"),
}, nil
case *provider.PurgeRecycleRequest:
return &provider.PurgeRecycleResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to purge recycle on readonly storage"),
}, nil
case *provider.RemoveGrantRequest:
return &provider.RemoveGrantResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to remove grant on readonly storage"),
}, nil
case *provider.RestoreRecycleItemRequest:
return &provider.RestoreRecycleItemResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to restore recycle item on readonly storage"),
}, nil
case *provider.SetArbitraryMetadataRequest:
return &provider.SetArbitraryMetadataResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to set arbitrary metadata on readonly storage"),
}, nil
case *provider.UnsetArbitraryMetadataRequest:
return &provider.UnsetArbitraryMetadataResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to unset arbitrary metadata on readonly storage"),
}, nil
// block unknown request types and return error
default:
log.Debug().Msg("storage is readonly")
return nil, status.Errorf(codes.PermissionDenied, "permission denied: tried to execute an unknown operation: %T!", req)
}
}, defaultPriority, nil
}
@@ -0,0 +1,52 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package recovery
import (
"context"
"runtime/debug"
grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// NewUnary returns a server interceptor that adds telemetry to
// grpc calls.
func NewUnary() grpc.UnaryServerInterceptor {
interceptor := grpc_recovery.UnaryServerInterceptor(grpc_recovery.WithRecoveryHandlerContext(recoveryFunc))
return interceptor
}
// NewStream returns a streaming server interceptor that adds telemetry to
// streaming grpc calls.
func NewStream() grpc.StreamServerInterceptor {
interceptor := grpc_recovery.StreamServerInterceptor(grpc_recovery.WithRecoveryHandlerContext(recoveryFunc))
return interceptor
}
func recoveryFunc(ctx context.Context, p interface{}) (err error) {
debug.PrintStack()
log := appctx.GetLogger(ctx)
log.Error().Msgf("%+v; stack: %s", p, debug.Stack())
return status.Errorf(codes.Internal, "%s", p)
}
@@ -0,0 +1,99 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package token
import (
"context"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
// NewUnary returns a new unary interceptor that adds
// the token to the context.
func NewUnary() grpc.UnaryServerInterceptor {
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
md, ok := metadata.FromIncomingContext(ctx)
if ok && md != nil {
if val, ok := md[ctxpkg.TokenHeader]; ok {
if len(val) > 0 && val[0] != "" {
tkn := val[0]
ctx = ctxpkg.ContextSetToken(ctx, tkn)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, tkn)
}
}
if val, ok := md[ctxpkg.InitiatorHeader]; ok {
if len(val) > 0 && val[0] != "" {
initiatorID := val[0]
ctx = ctxpkg.ContextSetInitiator(ctx, initiatorID)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.InitiatorHeader, initiatorID)
}
}
}
return handler(ctx, req)
}
return interceptor
}
// NewStream returns a new server stream interceptor
// that adds trace information to the request.
func NewStream() grpc.StreamServerInterceptor {
interceptor := func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
ctx := ss.Context()
md, ok := metadata.FromIncomingContext(ss.Context())
if ok && md != nil {
if val, ok := md[ctxpkg.TokenHeader]; ok {
if len(val) > 0 && val[0] != "" {
tkn := val[0]
ctx = ctxpkg.ContextSetToken(ctx, tkn)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, tkn)
}
}
if val, ok := md[ctxpkg.InitiatorHeader]; ok {
if len(val) > 0 && val[0] != "" {
initiatorID := val[0]
ctx = ctxpkg.ContextSetInitiator(ctx, initiatorID)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.InitiatorHeader, initiatorID)
}
}
}
wrapped := newWrappedServerStream(ctx, ss)
return handler(srv, wrapped)
}
return interceptor
}
func newWrappedServerStream(ctx context.Context, ss grpc.ServerStream) *wrappedServerStream {
return &wrappedServerStream{ServerStream: ss, newCtx: ctx}
}
type wrappedServerStream struct {
grpc.ServerStream
newCtx context.Context
}
func (ss *wrappedServerStream) Context() context.Context {
return ss.newCtx
}
@@ -0,0 +1,70 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package useragent
import (
"context"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
// NewUnary returns a new unary interceptor that adds
// the useragent to the context.
func NewUnary() grpc.UnaryServerInterceptor {
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if md, ok := metadata.FromIncomingContext(ctx); ok {
if lst, ok := md[ctxpkg.UserAgentHeader]; ok && len(lst) != 0 {
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.UserAgentHeader, lst[0])
}
}
return handler(ctx, req)
}
return interceptor
}
// NewStream returns a new server stream interceptor
// that adds the user agent to the context.
func NewStream() grpc.StreamServerInterceptor {
interceptor := func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
ctx := ss.Context()
if md, ok := metadata.FromIncomingContext(ctx); ok {
if lst, ok := md[ctxpkg.UserAgentHeader]; ok && len(lst) != 0 {
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.UserAgentHeader, lst[0])
}
}
wrapped := newWrappedServerStream(ctx, ss)
return handler(srv, wrapped)
}
return interceptor
}
func newWrappedServerStream(ctx context.Context, ss grpc.ServerStream) *wrappedServerStream {
return &wrappedServerStream{ServerStream: ss, newCtx: ctx}
}
type wrappedServerStream struct {
grpc.ServerStream
newCtx context.Context
}
func (ss *wrappedServerStream) Context() context.Context {
return ss.newCtx
}
@@ -0,0 +1,162 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package applicationauth
import (
"context"
appauthpb "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appauth"
"github.com/opencloud-eu/reva/v2/pkg/appauth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("applicationauth", New)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
}
type service struct {
conf *config
am appauth.Manager
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "json"
}
}
func (s *service) Register(ss *grpc.Server) {
appauthpb.RegisterApplicationsAPIServer(ss, s)
}
func getAppAuthManager(c *config) (appauth.Manager, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound("driver not found: " + c.Driver)
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
return c, nil
}
// New creates a app auth provider svc
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
am, err := getAppAuthManager(c)
if err != nil {
return nil, err
}
service := &service{
conf: c,
am: am,
}
return service, nil
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{"/cs3.auth.applications.v1beta1.ApplicationsAPI/GetAppPassword"}
}
func (s *service) GenerateAppPassword(ctx context.Context, req *appauthpb.GenerateAppPasswordRequest) (*appauthpb.GenerateAppPasswordResponse, error) {
logger := appctx.GetLogger(ctx)
pwd, err := s.am.GenerateAppPassword(ctx, req.TokenScope, req.Label, req.Expiration)
if err != nil {
logger.Debug().Err(err).Msg("error generating app password")
return &appauthpb.GenerateAppPasswordResponse{
Status: status.NewInternal(ctx, "error generating app password"),
}, nil
}
return &appauthpb.GenerateAppPasswordResponse{
Status: status.NewOK(ctx),
AppPassword: pwd,
}, nil
}
func (s *service) ListAppPasswords(ctx context.Context, req *appauthpb.ListAppPasswordsRequest) (*appauthpb.ListAppPasswordsResponse, error) {
pwds, err := s.am.ListAppPasswords(ctx)
if err != nil {
return &appauthpb.ListAppPasswordsResponse{
Status: status.NewInternal(ctx, "error listing app passwords"),
}, nil
}
return &appauthpb.ListAppPasswordsResponse{
Status: status.NewOK(ctx),
AppPasswords: pwds,
}, nil
}
func (s *service) InvalidateAppPassword(ctx context.Context, req *appauthpb.InvalidateAppPasswordRequest) (*appauthpb.InvalidateAppPasswordResponse, error) {
err := s.am.InvalidateAppPassword(ctx, req.Password)
if err != nil {
return &appauthpb.InvalidateAppPasswordResponse{
Status: status.NewInternal(ctx, "error invalidating app password"),
}, nil
}
return &appauthpb.InvalidateAppPasswordResponse{
Status: status.NewOK(ctx),
}, nil
}
func (s *service) GetAppPassword(ctx context.Context, req *appauthpb.GetAppPasswordRequest) (*appauthpb.GetAppPasswordResponse, error) {
pwd, err := s.am.GetAppPassword(ctx, req.User, req.Password)
if err != nil {
return &appauthpb.GetAppPasswordResponse{
Status: status.NewStatusFromErrType(ctx, "error getting app password via username/password", err),
}, nil
}
return &appauthpb.GetAppPasswordResponse{
Status: status.NewOK(ctx),
AppPassword: pwd,
}, nil
}
@@ -0,0 +1,208 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package appprovider
import (
"context"
"os"
"strconv"
"time"
providerpb "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
registrypb "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/juliangruber/go-intersect"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/app"
"github.com/opencloud-eu/reva/v2/pkg/app/provider/registry"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/logger"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("appprovider", New)
}
type service struct {
provider app.Provider
conf *config
context context.Context
cancelFunc context.CancelFunc
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
AppProviderURL string `mapstructure:"app_provider_url"`
GatewaySvc string `mapstructure:"gatewaysvc"`
MimeTypes []string `mapstructure:"mime_types"`
Priority uint64 `mapstructure:"priority"`
RefreshTime time.Duration `mapstructure:"refreshtime"`
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "demo"
}
if c.RefreshTime < 1 {
c.RefreshTime = time.Second * 20
}
c.AppProviderURL = sharedconf.GetGatewaySVC(c.AppProviderURL)
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
c.init()
return c, nil
}
// New creates a new AppProviderService
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
provider, err := getProvider(c)
if err != nil {
return nil, err
}
ctx, cancelFunc := context.WithCancel(context.Background())
service := &service{
conf: c,
provider: provider,
context: ctx,
cancelFunc: cancelFunc,
}
t := time.NewTicker(c.RefreshTime)
go func() {
for {
select {
case <-t.C:
service.registerProvider()
case <-service.context.Done():
t.Stop()
}
}
}()
return service, nil
}
func (s *service) registerProvider() {
// Give the appregistry service time to come up
time.Sleep(2 * time.Second)
ctx := context.Background()
log := logger.New().With().Int("pid", os.Getpid()).Logger()
pInfo, err := s.provider.GetAppProviderInfo(ctx)
if err != nil {
log.Error().Err(err).Msg("error registering app provider: could not get provider info")
return
}
pInfo.Address = s.conf.AppProviderURL
if len(s.conf.MimeTypes) != 0 {
mimeTypesIf := intersect.Simple(pInfo.MimeTypes, s.conf.MimeTypes)
var mimeTypes []string
for _, m := range mimeTypesIf {
mimeTypes = append(mimeTypes, m.(string))
}
pInfo.MimeTypes = mimeTypes
}
client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc)
if err != nil {
log.Error().Err(err).Msg("error registering app provider: could not get gateway client")
return
}
req := &registrypb.AddAppProviderRequest{Provider: pInfo}
if s.conf.Priority != 0 {
req.Opaque = &types.Opaque{
Map: map[string]*types.OpaqueEntry{
"priority": {
Decoder: "plain",
Value: []byte(strconv.FormatUint(s.conf.Priority, 10)),
},
},
}
}
res, err := client.AddAppProvider(ctx, req)
if err != nil {
log.Error().Err(err).Msg("error registering app provider: error calling add app provider")
return
}
if res.Status.Code != rpc.Code_CODE_OK {
err = status.NewErrorFromCode(res.Status.Code, "appprovider")
log.Error().Err(err).Msg("error registering app provider: add app provider returned error")
return
}
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{}
}
func (s *service) Register(ss *grpc.Server) {
providerpb.RegisterProviderAPIServer(ss, s)
}
func getProvider(c *config) (app.Provider, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound("driver not found: " + c.Driver)
}
func (s *service) OpenInApp(ctx context.Context, req *providerpb.OpenInAppRequest) (*providerpb.OpenInAppResponse, error) {
appURL, err := s.provider.GetAppURL(ctx, req.ResourceInfo, req.ViewMode, req.AccessToken, utils.ReadPlainFromOpaque(req.Opaque, "lang"))
if err != nil {
res := &providerpb.OpenInAppResponse{
Status: status.NewInternal(ctx, err.Error()),
}
return res, nil
}
res := &providerpb.OpenInAppResponse{
Status: status.NewOK(ctx),
AppUrl: appURL,
}
return res, nil
}
@@ -0,0 +1,196 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package appregistry
import (
"context"
"google.golang.org/grpc"
registrypb "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/app"
"github.com/opencloud-eu/reva/v2/pkg/app/registry/registry"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/rs/zerolog"
)
func init() {
rgrpc.Register("appregistry", New)
}
type svc struct {
reg app.Registry
}
func (s *svc) Close() error {
return nil
}
func (s *svc) UnprotectedEndpoints() []string {
return []string{"/cs3.app.registry.v1beta1.RegistryAPI/AddAppProvider", "/cs3.app.registry.v1beta1.RegistryAPI/ListSupportedMimeTypes"}
}
func (s *svc) Register(ss *grpc.Server) {
registrypb.RegisterRegistryAPIServer(ss, s)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "static"
}
}
// New creates a new StorageRegistryService
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
reg, err := getRegistry(c)
if err != nil {
return nil, err
}
svc := &svc{
reg: reg,
}
return svc, nil
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
c.init()
return c, nil
}
func getRegistry(c *config) (app.Registry, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound("appregistrysvc: driver not found: " + c.Driver)
}
func (s *svc) GetAppProviders(ctx context.Context, req *registrypb.GetAppProvidersRequest) (*registrypb.GetAppProvidersResponse, error) {
p, err := s.reg.FindProviders(ctx, req.ResourceInfo.MimeType)
if err != nil {
return &registrypb.GetAppProvidersResponse{
Status: status.NewInternal(ctx, "error looking for the app provider"),
}, nil
}
res := &registrypb.GetAppProvidersResponse{
Status: status.NewOK(ctx),
Providers: p,
}
return res, nil
}
func (s *svc) AddAppProvider(ctx context.Context, req *registrypb.AddAppProviderRequest) (*registrypb.AddAppProviderResponse, error) {
err := s.reg.AddProvider(ctx, req.Provider)
if err != nil {
return &registrypb.AddAppProviderResponse{
Status: status.NewInternal(ctx, "error adding the app provider"),
}, nil
}
res := &registrypb.AddAppProviderResponse{
Status: status.NewOK(ctx),
}
return res, nil
}
func (s *svc) ListAppProviders(ctx context.Context, req *registrypb.ListAppProvidersRequest) (*registrypb.ListAppProvidersResponse, error) {
providers, err := s.reg.ListProviders(ctx)
if err != nil {
return &registrypb.ListAppProvidersResponse{
Status: status.NewInternal(ctx, "error listing the app providers"),
}, nil
}
res := &registrypb.ListAppProvidersResponse{
Status: status.NewOK(ctx),
Providers: providers,
}
return res, nil
}
func (s *svc) ListSupportedMimeTypes(ctx context.Context, req *registrypb.ListSupportedMimeTypesRequest) (*registrypb.ListSupportedMimeTypesResponse, error) {
mimeTypes, err := s.reg.ListSupportedMimeTypes(ctx)
if err != nil {
return &registrypb.ListSupportedMimeTypesResponse{
Status: status.NewInternal(ctx, "error listing the supported mime types"),
}, nil
}
// hide mimetypes for app providers
for _, mime := range mimeTypes {
for _, app := range mime.AppProviders {
app.MimeTypes = nil
}
}
res := &registrypb.ListSupportedMimeTypesResponse{
Status: status.NewOK(ctx),
MimeTypes: mimeTypes,
}
return res, nil
}
func (s *svc) GetDefaultAppProviderForMimeType(ctx context.Context, req *registrypb.GetDefaultAppProviderForMimeTypeRequest) (*registrypb.GetDefaultAppProviderForMimeTypeResponse, error) {
provider, err := s.reg.GetDefaultProviderForMimeType(ctx, req.MimeType)
if err != nil {
return &registrypb.GetDefaultAppProviderForMimeTypeResponse{
Status: status.NewInternal(ctx, "error getting the default app provider for the mimetype"),
}, nil
}
res := &registrypb.GetDefaultAppProviderForMimeTypeResponse{
Status: status.NewOK(ctx),
Provider: provider,
}
return res, nil
}
func (s *svc) SetDefaultAppProviderForMimeType(ctx context.Context, req *registrypb.SetDefaultAppProviderForMimeTypeRequest) (*registrypb.SetDefaultAppProviderForMimeTypeResponse, error) {
err := s.reg.SetDefaultProviderForMimeType(ctx, req.MimeType, req.Provider)
if err != nil {
return &registrypb.SetDefaultAppProviderForMimeTypeResponse{
Status: status.NewInternal(ctx, "error setting the default app provider for the mimetype"),
}, nil
}
res := &registrypb.SetDefaultAppProviderForMimeTypeResponse{
Status: status.NewOK(ctx),
}
return res, nil
}
@@ -0,0 +1,152 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package authprovider
import (
"context"
"fmt"
"path/filepath"
provider "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/plugin"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("authprovider", New)
}
type config struct {
AuthManager string `mapstructure:"auth_manager"`
AuthManagers map[string]map[string]interface{} `mapstructure:"auth_managers"`
}
func (c *config) init() {
if c.AuthManager == "" {
c.AuthManager = "json"
}
}
type service struct {
authmgr auth.Manager
conf *config
plugin *plugin.RevaPlugin
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
c.init()
return c, nil
}
func getAuthManager(manager string, m map[string]map[string]interface{}) (auth.Manager, *plugin.RevaPlugin, error) {
if manager == "" {
return nil, nil, errtypes.InternalError("authsvc: driver not configured for auth manager")
}
p, err := plugin.Load("authprovider", manager)
if err == nil {
authManager, ok := p.Plugin.(auth.Manager)
if !ok {
return nil, nil, fmt.Errorf("could not assert the loaded plugin")
}
pluginConfig := filepath.Base(manager)
err = authManager.Configure(m[pluginConfig])
if err != nil {
return nil, nil, err
}
return authManager, p, nil
} else if _, ok := err.(errtypes.NotFound); ok {
if f, ok := registry.NewFuncs[manager]; ok {
authmgr, err := f(m[manager])
return authmgr, nil, err
}
} else {
return nil, nil, err
}
return nil, nil, errtypes.NotFound(fmt.Sprintf("authsvc: driver %s not found for auth manager", manager))
}
// New returns a new AuthProviderServiceServer.
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
authManager, plug, err := getAuthManager(c.AuthManager, c.AuthManagers)
if err != nil {
return nil, err
}
svc := &service{
conf: c,
authmgr: authManager,
plugin: plug,
}
return svc, nil
}
func (s *service) Close() error {
if s.plugin != nil {
s.plugin.Kill()
}
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{"/cs3.auth.provider.v1beta1.ProviderAPI/Authenticate"}
}
func (s *service) Register(ss *grpc.Server) {
provider.RegisterProviderAPIServer(ss, s)
}
func (s *service) Authenticate(ctx context.Context, req *provider.AuthenticateRequest) (*provider.AuthenticateResponse, error) {
log := appctx.GetLogger(ctx)
username := req.ClientId
password := req.ClientSecret
u, scope, err := s.authmgr.Authenticate(ctx, username, password)
if err != nil {
log.Debug().Str("client_id", username).Err(err).Msg("authsvc: error in Authenticate")
return &provider.AuthenticateResponse{
Status: status.NewStatusFromErrType(ctx, "authsvc: error in Authenticate", err),
}, nil
}
log.Info().Msgf("user %s authenticated", u.Id)
return &provider.AuthenticateResponse{
Status: status.NewOK(ctx),
User: u,
TokenScope: scope,
}, nil
}
@@ -0,0 +1,133 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package authregistry
import (
"context"
registrypb "github.com/cs3org/go-cs3apis/cs3/auth/registry/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/registry/registry"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("authregistry", New)
}
type service struct {
reg auth.Registry
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{
"/cs3.auth.registry.v1beta1.RegistryAPI/GetAuthProvider",
"/cs3.auth.registry.v1beta1.RegistryAPI/ListAuthProviders",
}
}
func (s *service) Register(ss *grpc.Server) {
registrypb.RegisterRegistryAPIServer(ss, s)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "static"
}
}
// New creates a new AuthRegistry
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
reg, err := getRegistry(c)
if err != nil {
return nil, err
}
service := &service{
reg: reg,
}
return service, nil
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
return c, nil
}
func getRegistry(c *config) (auth.Registry, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound("authregistrysvc: driver not found: " + c.Driver)
}
func (s *service) ListAuthProviders(ctx context.Context, req *registrypb.ListAuthProvidersRequest) (*registrypb.ListAuthProvidersResponse, error) {
pinfos, err := s.reg.ListProviders(ctx)
if err != nil {
return &registrypb.ListAuthProvidersResponse{
Status: status.NewInternal(ctx, "error getting list of auth providers"),
}, nil
}
res := &registrypb.ListAuthProvidersResponse{
Status: status.NewOK(ctx),
Providers: pinfos,
}
return res, nil
}
func (s *service) GetAuthProviders(ctx context.Context, req *registrypb.GetAuthProvidersRequest) (*registrypb.GetAuthProvidersResponse, error) {
pinfo, err := s.reg.GetProvider(ctx, req.Type)
if err != nil {
return &registrypb.GetAuthProvidersResponse{
Status: status.NewInternal(ctx, "error getting auth provider for type: "+req.Type),
}, nil
}
res := &registrypb.GetAuthProvidersResponse{
Status: status.NewOK(ctx),
Providers: []*registrypb.ProviderInfo{pinfo},
}
return res, nil
}
@@ -0,0 +1,381 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package datatx
import (
"context"
"encoding/json"
"fmt"
"io"
"net/url"
"os"
"sync"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
datatx "github.com/cs3org/go-cs3apis/cs3/tx/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/mitchellh/mapstructure"
txdriver "github.com/opencloud-eu/reva/v2/pkg/datatx"
txregistry "github.com/opencloud-eu/reva/v2/pkg/datatx/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("datatx", New)
}
type config struct {
// transfer driver
TxDriver string `mapstructure:"txdriver"`
TxDrivers map[string]map[string]interface{} `mapstructure:"txdrivers"`
// storage driver to persist share/transfer relation
StorageDriver string `mapstructure:"storage_driver"`
StorageDrivers map[string]map[string]interface{} `mapstructure:"storage_drivers"`
TxSharesFile string `mapstructure:"tx_shares_file"`
DataTransfersFolder string `mapstructure:"data_transfers_folder"`
}
type service struct {
conf *config
txManager txdriver.Manager
txShareDriver *txShareDriver
}
type txShareDriver struct {
sync.Mutex // concurrent access to the file
model *txShareModel
}
type txShareModel struct {
File string
TxShares map[string]*txShare `json:"shares"`
}
type txShare struct {
TxID string
SrcTargetURI string
DestTargetURI string
Opaque *types.Opaque `json:"opaque"`
}
type webdavEndpoint struct {
filePath string
endpoint string
endpointScheme string
token string
}
func (c *config) init() {
if c.TxDriver == "" {
c.TxDriver = "rclone"
}
if c.TxSharesFile == "" {
c.TxSharesFile = "/var/tmp/reva/datatx-shares.json"
}
if c.DataTransfersFolder == "" {
c.DataTransfersFolder = "/home/DataTransfers"
}
}
func (s *service) Register(ss *grpc.Server) {
datatx.RegisterTxAPIServer(ss, s)
}
func getDatatxManager(c *config) (txdriver.Manager, error) {
if f, ok := txregistry.NewFuncs[c.TxDriver]; ok {
return f(c.TxDrivers[c.TxDriver])
}
return nil, errtypes.NotFound("datatx service: driver not found: " + c.TxDriver)
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "datatx service: error decoding conf")
return nil, err
}
return c, nil
}
// New creates a new datatx svc
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
txManager, err := getDatatxManager(c)
if err != nil {
return nil, err
}
model, err := loadOrCreate(c.TxSharesFile)
if err != nil {
err = errors.Wrap(err, "datatx service: error loading the file containing the transfer shares")
return nil, err
}
txShareDriver := &txShareDriver{
model: model,
}
service := &service{
conf: c,
txManager: txManager,
txShareDriver: txShareDriver,
}
return service, nil
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{}
}
func (s *service) CreateTransfer(ctx context.Context, req *datatx.CreateTransferRequest) (*datatx.CreateTransferResponse, error) {
srcEp, err := s.extractEndpointInfo(ctx, req.SrcTargetUri)
if err != nil {
return nil, err
}
srcRemote := fmt.Sprintf("%s://%s", srcEp.endpointScheme, srcEp.endpoint)
srcPath := srcEp.filePath
srcToken := srcEp.token
destEp, err := s.extractEndpointInfo(ctx, req.DestTargetUri)
if err != nil {
return nil, err
}
dstRemote := fmt.Sprintf("%s://%s", destEp.endpointScheme, destEp.endpoint)
dstPath := destEp.filePath
dstToken := destEp.token
txInfo, startTransferErr := s.txManager.StartTransfer(ctx, srcRemote, srcPath, srcToken, dstRemote, dstPath, dstToken)
// we always save the transfer regardless of start transfer outcome
// only then, if starting fails, can we try to restart it
txShare := &txShare{
TxID: txInfo.GetId().OpaqueId,
SrcTargetURI: req.SrcTargetUri,
DestTargetURI: req.DestTargetUri,
Opaque: req.Opaque,
}
s.txShareDriver.Lock()
defer s.txShareDriver.Unlock()
s.txShareDriver.model.TxShares[txInfo.GetId().OpaqueId] = txShare
if err := s.txShareDriver.model.saveTxShare(); err != nil {
err = errors.Wrap(err, "datatx service: error saving transfer share: "+datatx.Status_STATUS_INVALID.String())
return &datatx.CreateTransferResponse{
Status: status.NewInvalid(ctx, "error pulling transfer"),
}, err
}
// now check start transfer outcome
if startTransferErr != nil {
startTransferErr = errors.Wrap(startTransferErr, "datatx service: error starting transfer job")
return &datatx.CreateTransferResponse{
Status: status.NewInvalid(ctx, "datatx service: error pulling transfer"),
TxInfo: txInfo,
}, startTransferErr
}
return &datatx.CreateTransferResponse{
Status: status.NewOK(ctx),
TxInfo: txInfo,
}, err
}
func (s *service) GetTransferStatus(ctx context.Context, req *datatx.GetTransferStatusRequest) (*datatx.GetTransferStatusResponse, error) {
txShare, ok := s.txShareDriver.model.TxShares[req.GetTxId().GetOpaqueId()]
if !ok {
return nil, errtypes.InternalError("datatx service: transfer not found")
}
txInfo, err := s.txManager.GetTransferStatus(ctx, req.GetTxId().OpaqueId)
if err != nil {
return &datatx.GetTransferStatusResponse{
Status: status.NewInternal(ctx, "datatx service: error getting transfer status"),
TxInfo: txInfo,
}, err
}
txInfo.ShareId = &ocm.ShareId{OpaqueId: string(txShare.Opaque.Map["shareId"].Value)}
return &datatx.GetTransferStatusResponse{
Status: status.NewOK(ctx),
TxInfo: txInfo,
}, nil
}
func (s *service) CancelTransfer(ctx context.Context, req *datatx.CancelTransferRequest) (*datatx.CancelTransferResponse, error) {
txShare, ok := s.txShareDriver.model.TxShares[req.GetTxId().GetOpaqueId()]
if !ok {
return nil, errtypes.InternalError("datatx service: transfer not found")
}
txInfo, err := s.txManager.CancelTransfer(ctx, req.GetTxId().OpaqueId)
if err != nil {
txInfo.ShareId = &ocm.ShareId{OpaqueId: string(txShare.Opaque.Map["shareId"].Value)}
return &datatx.CancelTransferResponse{
Status: status.NewInternal(ctx, "error cancelling transfer"),
TxInfo: txInfo,
}, err
}
txInfo.ShareId = &ocm.ShareId{OpaqueId: string(txShare.Opaque.Map["shareId"].Value)}
return &datatx.CancelTransferResponse{
Status: status.NewOK(ctx),
TxInfo: txInfo,
}, nil
}
func (s *service) ListTransfers(ctx context.Context, req *datatx.ListTransfersRequest) (*datatx.ListTransfersResponse, error) {
filters := req.Filters
var txInfos []*datatx.TxInfo
for _, txShare := range s.txShareDriver.model.TxShares {
if len(filters) == 0 {
txInfos = append(txInfos, &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: txShare.TxID},
ShareId: &ocm.ShareId{OpaqueId: string(txShare.Opaque.Map["shareId"].Value)},
})
} else {
for _, f := range filters {
if f.Type == datatx.ListTransfersRequest_Filter_TYPE_SHARE_ID {
if f.GetShareId().GetOpaqueId() == string(txShare.Opaque.Map["shareId"].Value) {
txInfos = append(txInfos, &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: txShare.TxID},
ShareId: &ocm.ShareId{OpaqueId: string(txShare.Opaque.Map["shareId"].Value)},
})
}
}
}
}
}
return &datatx.ListTransfersResponse{
Status: status.NewOK(ctx),
Transfers: txInfos,
}, nil
}
func (s *service) RetryTransfer(ctx context.Context, req *datatx.RetryTransferRequest) (*datatx.RetryTransferResponse, error) {
txShare, ok := s.txShareDriver.model.TxShares[req.GetTxId().GetOpaqueId()]
if !ok {
return nil, errtypes.InternalError("datatx service: transfer not found")
}
txInfo, err := s.txManager.RetryTransfer(ctx, req.GetTxId().OpaqueId)
if err != nil {
return &datatx.RetryTransferResponse{
Status: status.NewInternal(ctx, "error retrying transfer"),
TxInfo: txInfo,
}, err
}
txInfo.ShareId = &ocm.ShareId{OpaqueId: string(txShare.Opaque.Map["shareId"].Value)}
return &datatx.RetryTransferResponse{
Status: status.NewOK(ctx),
TxInfo: txInfo,
}, nil
}
func (s *service) extractEndpointInfo(ctx context.Context, targetURL string) (*webdavEndpoint, error) {
if targetURL == "" {
return nil, errtypes.BadRequest("datatx service: ref target is an empty uri")
}
uri, err := url.Parse(targetURL)
if err != nil {
return nil, errors.Wrap(err, "datatx service: error parsing target uri: "+targetURL)
}
m, err := url.ParseQuery(uri.RawQuery)
if err != nil {
return nil, errors.Wrap(err, "datatx service: error parsing target resource name")
}
return &webdavEndpoint{
filePath: m["name"][0],
endpoint: uri.Host + uri.Path,
endpointScheme: uri.Scheme,
token: uri.User.String(),
}, nil
}
func loadOrCreate(file string) (*txShareModel, error) {
_, err := os.Stat(file)
if os.IsNotExist(err) {
if err := os.WriteFile(file, []byte("{}"), 0700); err != nil {
err = errors.Wrap(err, "datatx service: error creating the transfer shares storage file: "+file)
return nil, err
}
}
fd, err := os.OpenFile(file, os.O_CREATE, 0644)
if err != nil {
err = errors.Wrap(err, "datatx service: error opening the transfer shares storage file: "+file)
return nil, err
}
defer fd.Close()
data, err := io.ReadAll(fd)
if err != nil {
err = errors.Wrap(err, "datatx service: error reading the data")
return nil, err
}
model := &txShareModel{}
if err := json.Unmarshal(data, model); err != nil {
err = errors.Wrap(err, "datatx service: error decoding transfer shares data to json")
return nil, err
}
if model.TxShares == nil {
model.TxShares = make(map[string]*txShare)
}
model.File = file
return model, nil
}
func (m *txShareModel) saveTxShare() error {
data, err := json.Marshal(m)
if err != nil {
err = errors.Wrap(err, "datatx service: error encoding transfer share data to json")
return err
}
if err := os.WriteFile(m.File, data, 0644); err != nil {
err = errors.Wrap(err, "datatx service: error writing transfer share data to file: "+m.File)
return err
}
return nil
}
@@ -0,0 +1,92 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
appauthpb "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (s *svc) GenerateAppPassword(ctx context.Context, req *appauthpb.GenerateAppPasswordRequest) (*appauthpb.GenerateAppPasswordResponse, error) {
c, err := pool.GetAppAuthProviderServiceClient(s.c.ApplicationAuthEndpoint)
if err != nil {
return &appauthpb.GenerateAppPasswordResponse{
Status: status.NewInternal(ctx, "error getting app auth provider client"),
}, nil
}
res, err := c.GenerateAppPassword(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GenerateAppPassword")
}
return res, nil
}
func (s *svc) ListAppPasswords(ctx context.Context, req *appauthpb.ListAppPasswordsRequest) (*appauthpb.ListAppPasswordsResponse, error) {
c, err := pool.GetAppAuthProviderServiceClient(s.c.ApplicationAuthEndpoint)
if err != nil {
return &appauthpb.ListAppPasswordsResponse{
Status: status.NewInternal(ctx, "error getting app auth provider client"),
}, nil
}
res, err := c.ListAppPasswords(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ListAppPasswords")
}
return res, nil
}
func (s *svc) InvalidateAppPassword(ctx context.Context, req *appauthpb.InvalidateAppPasswordRequest) (*appauthpb.InvalidateAppPasswordResponse, error) {
c, err := pool.GetAppAuthProviderServiceClient(s.c.ApplicationAuthEndpoint)
if err != nil {
return &appauthpb.InvalidateAppPasswordResponse{
Status: status.NewInternal(ctx, "error getting app auth provider client"),
}, nil
}
res, err := c.InvalidateAppPassword(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling InvalidateAppPassword")
}
return res, nil
}
func (s *svc) GetAppPassword(ctx context.Context, req *appauthpb.GetAppPasswordRequest) (*appauthpb.GetAppPasswordResponse, error) {
c, err := pool.GetAppAuthProviderServiceClient(s.c.ApplicationAuthEndpoint)
if err != nil {
return &appauthpb.GetAppPasswordResponse{
Status: status.NewInternal(ctx, "error getting app auth provider client"),
}, nil
}
res, err := c.GetAppPassword(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetAppPassword")
}
return res, nil
}
@@ -0,0 +1,340 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
"crypto/tls"
"fmt"
"net/url"
"strings"
providerpb "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
registry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/token"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/pkg/errors"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
)
func (s *svc) OpenInApp(ctx context.Context, req *gateway.OpenInAppRequest) (*providerpb.OpenInAppResponse, error) {
statRes, err := s.Stat(ctx, &storageprovider.StatRequest{
Ref: req.Ref,
})
if err != nil {
return &providerpb.OpenInAppResponse{
Status: status.NewInternal(ctx, "gateway: error calling Stat on the resource path for the app provider: "+req.Ref.GetPath()),
}, nil
}
if statRes.Status.Code != rpc.Code_CODE_OK {
return &providerpb.OpenInAppResponse{
Status: statRes.Status,
}, nil
}
fileInfo := statRes.Info
// The file is a share
if fileInfo.Type == storageprovider.ResourceType_RESOURCE_TYPE_REFERENCE {
uri, err := url.Parse(fileInfo.Target)
if err != nil {
return &providerpb.OpenInAppResponse{
Status: status.NewInternal(ctx, "gateway: error parsing target uri: "+fileInfo.Target),
}, nil
}
if uri.Scheme == "webdav" {
insecure, skipVerify := getGRPCConfig(req.Opaque)
return s.openFederatedShares(ctx, fileInfo.Target, req.ViewMode, req.App, insecure, skipVerify, "")
}
res, err := s.Stat(ctx, &storageprovider.StatRequest{
Ref: req.Ref,
})
if err != nil {
return &providerpb.OpenInAppResponse{
Status: status.NewInternal(ctx, "gateway: error calling Stat on the resource path for the app provider: "+req.Ref.GetPath()),
}, nil
}
if res.Status.Code != rpc.Code_CODE_OK {
return &providerpb.OpenInAppResponse{
Status: status.NewInternal(ctx, "Stat failed on the resource path for the app provider: "+req.Ref.GetPath()),
}, nil
}
fileInfo = res.Info
}
return s.openLocalResources(ctx, fileInfo, req.ViewMode, req.App, req.Opaque)
}
func (s *svc) openFederatedShares(ctx context.Context, targetURL string, vm gateway.OpenInAppRequest_ViewMode, app string,
insecure, skipVerify bool, nameQueries ...string) (*providerpb.OpenInAppResponse, error) {
log := appctx.GetLogger(ctx)
targetURL, err := appendNameQuery(targetURL, nameQueries...)
if err != nil {
return nil, err
}
ep, err := s.extractEndpointInfo(ctx, targetURL)
if err != nil {
return nil, err
}
ref := &storageprovider.Reference{Path: ep.filePath}
appProviderReq := &gateway.OpenInAppRequest{
Ref: ref,
ViewMode: vm,
App: app,
}
meshProvider, err := s.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{
Domain: ep.endpoint,
})
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetInfoByDomain")
}
var gatewayEP string
for _, s := range meshProvider.ProviderInfo.Services {
if strings.ToLower(s.Endpoint.Type.Name) == "gateway" {
gatewayEP = s.Endpoint.Path
}
}
log.Debug().Msgf("Forwarding OpenInApp request to: %s", gatewayEP)
conn, err := getConn(gatewayEP, insecure, skipVerify)
if err != nil {
log.Err(err).Msg("error connecting to remote reva")
return nil, errors.Wrap(err, "gateway: error connecting to remote reva")
}
gatewayClient := gateway.NewGatewayAPIClient(conn)
remoteCtx := ctxpkg.ContextSetToken(context.Background(), ep.token)
remoteCtx = metadata.AppendToOutgoingContext(remoteCtx, ctxpkg.TokenHeader, ep.token)
res, err := gatewayClient.OpenInApp(remoteCtx, appProviderReq)
if err != nil {
log.Err(err).Msg("error returned by remote OpenInApp call")
return nil, errors.Wrap(err, "gateway: error calling OpenInApp")
}
return res, nil
}
func (s *svc) openLocalResources(ctx context.Context, ri *storageprovider.ResourceInfo,
vm gateway.OpenInAppRequest_ViewMode, app string, opaque *typespb.Opaque) (*providerpb.OpenInAppResponse, error) {
accessToken, ok := ctxpkg.ContextGetToken(ctx)
if !ok || accessToken == "" {
return &providerpb.OpenInAppResponse{
Status: status.NewUnauthenticated(ctx, errtypes.InvalidCredentials("Access token is invalid or empty"), ""),
}, nil
}
provider, err := s.findAppProvider(ctx, ri, app)
if err != nil {
err = errors.Wrap(err, "gateway: error calling findAppProvider")
if _, ok := err.(errtypes.IsNotFound); ok {
return &providerpb.OpenInAppResponse{
Status: status.NewNotFound(ctx, "Could not find the requested app provider"),
}, nil
}
return nil, err
}
appProviderClient, err := pool.GetAppProviderClient(provider.Address)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetAppProviderClient")
}
appProviderReq, err := buildOpenInAppRequest(ctx, ri, vm, s.tokenmgr, accessToken, opaque)
if err != nil {
return nil, errors.Wrap(err, "gateway: error building OpenInApp request")
}
res, err := appProviderClient.OpenInApp(ctx, appProviderReq)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling OpenInApp")
}
return res, nil
}
func buildOpenInAppRequest(ctx context.Context, ri *storageprovider.ResourceInfo, vm gateway.OpenInAppRequest_ViewMode, tokenmgr token.Manager, accessToken string, opaque *typespb.Opaque) (*providerpb.OpenInAppRequest, error) {
// in case of a view only mode and a stat permission we need to create a view only token
if vm == gateway.OpenInAppRequest_VIEW_MODE_VIEW_ONLY && ri.GetPermissionSet().GetStat() {
// Limit scope to the resource
scope, err := scope.AddResourceInfoScope(ri, providerv1beta1.Role_ROLE_VIEWER, nil)
if err != nil {
return nil, err
}
// build a fake user object for the token
currentuser, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
return nil, errors.New("user not found in context")
}
scopedUser := &userpb.User{
Id: ri.GetOwner(), // the owner of the resource is always set, right?
DisplayName: "View Only user for " + currentuser.GetUsername(),
}
// mint a view only token
viewOnlyToken, err := tokenmgr.MintToken(ctx, scopedUser, scope)
if err != nil {
return nil, err
}
// TODO we should not append the token to the opaque, we should have a dedicated field in the request
opaque = utils.AppendPlainToOpaque(opaque, "viewOnlyToken", viewOnlyToken)
}
return &providerpb.OpenInAppRequest{
ResourceInfo: ri,
ViewMode: providerpb.ViewMode(vm),
AccessToken: accessToken,
// ViewOnlyToken: viewOnlyToken // scoped to the shared resource if the stat response hase a ViewOnly permission
Opaque: opaque,
}, nil
}
func (s *svc) findAppProvider(ctx context.Context, ri *storageprovider.ResourceInfo, app string) (*registry.ProviderInfo, error) {
c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint)
if err != nil {
err = errors.Wrap(err, "gateway: error getting appregistry client")
return nil, err
}
// when app is empty it means the user assumes a default behaviour.
// From a web perspective, means the user click on the file itself.
// Normally the file will get downloaded but if a suitable application exists
// the behaviour will change from download to open the file with the app.
if app == "" {
// If app is empty means that we need to rely on "default" behaviour.
// We currently do not have user preferences implemented so the only default
// we can currently enforce is one configured by the system admins, the
// "system default".
// If a default is not set we raise an error rather that giving the user the first provider in the list
// as the list is built on init time and is not deterministic, giving the user different results on service
// reload.
res, err := c.GetDefaultAppProviderForMimeType(ctx, &registry.GetDefaultAppProviderForMimeTypeRequest{
MimeType: ri.MimeType,
})
if err != nil {
err = errors.Wrap(err, "gateway: error calling GetDefaultAppProviderForMimeType")
return nil, err
}
// we've found a provider
if res.Status.Code == rpc.Code_CODE_OK && res.Provider != nil {
return res.Provider, nil
}
// we did not find a default provider
if res.Status.Code == rpc.Code_CODE_NOT_FOUND {
err := errtypes.NotFound(fmt.Sprintf("gateway: default app provider for mime type:%s not found", ri.MimeType))
return nil, err
}
// response code is something else, bubble up error
// if a default is not set we abort as returning the first application available is not
// deterministic for the end-user as it depends on initialization order of the app approviders with the registry.
// It also provides a good hint to the system admin to configure the defaults accordingly.
err = errtypes.InternalError(fmt.Sprintf("gateway: unexpected grpc response status:%s when calling GetDefaultAppProviderForMimeType", res.Status))
return nil, err
}
// app has been forced and is set, we try to get an app provider that can satisfy it
// Note that we ask for the list of all available providers for a given resource
// even though we're only interested into the one set by the "app" parameter.
// A better call will be to issue a (to be added) GetAppProviderByName(app) method
// to just get what we ask for.
res, err := c.GetAppProviders(ctx, &registry.GetAppProvidersRequest{
ResourceInfo: ri,
})
if err != nil {
err = errors.Wrap(err, "gateway: error calling GetAppProviders")
return nil, err
}
// if the list of app providers is empty means we expect a CODE_NOT_FOUND in the response
if res.Status.Code != rpc.Code_CODE_OK {
if res.Status.Code == rpc.Code_CODE_NOT_FOUND {
return nil, errtypes.NotFound("gateway: app provider not found for resource: " + ri.String())
}
return nil, errtypes.InternalError("gateway: error finding app providers")
}
// as long as the above mentioned GetAppProviderByName(app) method is not available
// we need to apply a manual filter
filteredProviders := []*registry.ProviderInfo{}
for _, p := range res.Providers {
if p.Name == app {
filteredProviders = append(filteredProviders, p)
}
}
res.Providers = filteredProviders
if len(res.Providers) == 0 {
return nil, errtypes.NotFound(fmt.Sprintf("app '%s' not found", app))
}
if len(res.Providers) == 1 {
return res.Providers[0], nil
}
// we should never arrive to the point of having more than one
// provider for the given "app" parameters sent by the user
return nil, errtypes.InternalError(fmt.Sprintf("gateway: user requested app %q and we provided %d applications", app, len(res.Providers)))
}
func getGRPCConfig(opaque *typespb.Opaque) (bool, bool) {
if opaque == nil {
return false, false
}
_, insecure := opaque.Map["insecure"]
_, skipVerify := opaque.Map["skip-verify"]
return insecure, skipVerify
}
func getConn(host string, ins, skipverify bool) (*grpc.ClientConn, error) {
if ins {
return grpc.NewClient(host, grpc.WithTransportCredentials(insecure.NewCredentials()))
}
// TODO(labkode): if in the future we want client-side certificate validation,
// we need to load the client cert here
tlsconf := &tls.Config{InsecureSkipVerify: skipverify}
creds := credentials.NewTLS(tlsconf)
return grpc.NewClient(host, grpc.WithTransportCredentials(creds))
}
@@ -0,0 +1,124 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
registry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (s *svc) GetAppProviders(ctx context.Context, req *registry.GetAppProvidersRequest) (*registry.GetAppProvidersResponse, error) {
c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint)
if err != nil {
return &registry.GetAppProvidersResponse{
Status: status.NewInternal(ctx, "error getting app registry client"),
}, nil
}
res, err := c.GetAppProviders(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetAppProviders")
}
return res, nil
}
func (s *svc) AddAppProvider(ctx context.Context, req *registry.AddAppProviderRequest) (*registry.AddAppProviderResponse, error) {
c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint)
if err != nil {
return &registry.AddAppProviderResponse{
Status: status.NewInternal(ctx, "error getting app registry client"),
}, nil
}
res, err := c.AddAppProvider(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling AddAppProvider")
}
return res, nil
}
func (s *svc) ListAppProviders(ctx context.Context, req *registry.ListAppProvidersRequest) (*registry.ListAppProvidersResponse, error) {
c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint)
if err != nil {
return &registry.ListAppProvidersResponse{
Status: status.NewInternal(ctx, "error getting app registry client"),
}, nil
}
res, err := c.ListAppProviders(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ListAppProviders")
}
return res, nil
}
func (s *svc) ListSupportedMimeTypes(ctx context.Context, req *registry.ListSupportedMimeTypesRequest) (*registry.ListSupportedMimeTypesResponse, error) {
c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint)
if err != nil {
return &registry.ListSupportedMimeTypesResponse{
Status: status.NewInternal(ctx, "error getting app registry client"),
}, nil
}
res, err := c.ListSupportedMimeTypes(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ListSupportedMimeTypes")
}
return res, nil
}
func (s *svc) GetDefaultAppProviderForMimeType(ctx context.Context, req *registry.GetDefaultAppProviderForMimeTypeRequest) (*registry.GetDefaultAppProviderForMimeTypeResponse, error) {
c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint)
if err != nil {
return &registry.GetDefaultAppProviderForMimeTypeResponse{
Status: status.NewInternal(ctx, "error getting app registry client"),
}, nil
}
res, err := c.GetDefaultAppProviderForMimeType(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetDefaultAppProviderForMimeType")
}
return res, nil
}
func (s *svc) SetDefaultAppProviderForMimeType(ctx context.Context, req *registry.SetDefaultAppProviderForMimeTypeRequest) (*registry.SetDefaultAppProviderForMimeTypeResponse, error) {
c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint)
if err != nil {
return &registry.SetDefaultAppProviderForMimeTypeResponse{
Status: status.NewInternal(ctx, "error getting app registry client"),
}, nil
}
res, err := c.SetDefaultAppProviderForMimeType(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling SetDefaultAppProviderForMimeType")
}
return res, nil
}
@@ -0,0 +1,206 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
"fmt"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
registry "github.com/cs3org/go-cs3apis/cs3/auth/registry/v1beta1"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/pkg/errors"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/proto"
)
func (s *svc) Authenticate(ctx context.Context, req *gateway.AuthenticateRequest) (*gateway.AuthenticateResponse, error) {
log := appctx.GetLogger(ctx)
// find auth provider
c, err := s.findAuthProvider(ctx, req.Type)
if err != nil {
log.Err(err).Str("type", req.Type).Msg("error getting auth provider client")
return &gateway.AuthenticateResponse{
Status: status.NewInternal(ctx, "error getting auth provider client"),
}, nil
}
authProviderReq := &authpb.AuthenticateRequest{
ClientId: req.ClientId,
ClientSecret: req.ClientSecret,
}
res, err := c.Authenticate(ctx, authProviderReq)
switch {
case err != nil:
return &gateway.AuthenticateResponse{
Status: status.NewInternal(ctx, fmt.Sprintf("gateway: error calling Authenticate for type: %s", req.Type)),
}, nil
case res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED:
fallthrough
case res.Status.Code == rpc.Code_CODE_UNAUTHENTICATED:
fallthrough
case res.Status.Code == rpc.Code_CODE_NOT_FOUND:
// normal failures, no need to log
return &gateway.AuthenticateResponse{
Status: res.Status,
}, nil
case res.Status.Code != rpc.Code_CODE_OK:
return &gateway.AuthenticateResponse{
Status: status.NewInternal(ctx, fmt.Sprintf("error authenticating credentials to auth provider for type: %s", req.Type)),
}, nil
}
// validate valid userId
if res.User == nil {
err := errtypes.NotFound("gateway: user after Authenticate is nil")
log.Err(err).Msg("user is nil")
return &gateway.AuthenticateResponse{
Status: status.NewInternal(ctx, "user is nil"),
}, nil
}
if res.User.Id == nil {
err := errtypes.NotFound("gateway: uid after Authenticate is nil")
log.Err(err).Msg("user id is nil")
return &gateway.AuthenticateResponse{
Status: status.NewInternal(ctx, "user id is nil"),
}, nil
}
u := proto.Clone(res.User).(*userpb.User)
if sharedconf.SkipUserGroupsInToken() {
u.Groups = []string{}
}
token, err := s.tokenmgr.MintToken(ctx, u, res.TokenScope)
if err != nil {
err = errors.Wrap(err, "authsvc: error in MintToken")
res := &gateway.AuthenticateResponse{
Status: status.NewUnauthenticated(ctx, err, "error creating access token"),
}
return res, nil
}
if scope, ok := res.TokenScope["user"]; s.c.DisableHomeCreationOnLogin || !ok || scope.Role != authpb.Role_ROLE_OWNER || res.User.Id.Type == userpb.UserType_USER_TYPE_FEDERATED {
gwRes := &gateway.AuthenticateResponse{
Status: status.NewOK(ctx),
User: res.User,
Token: token,
}
return gwRes, nil
}
// we need to pass the token to authenticate the CreateHome request.
// TODO(labkode): appending to existing context will not pass the token.
ctx = ctxpkg.ContextSetToken(ctx, token)
ctx = ctxpkg.ContextSetUser(ctx, res.User)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, token) // TODO(jfd): hardcoded metadata key. use PerRPCCredentials?
// create home directory
createHomeRes, err := s.CreateHome(ctx, &storageprovider.CreateHomeRequest{})
if err != nil {
log.Err(err).Msg("error calling CreateHome")
return &gateway.AuthenticateResponse{
Status: status.NewInternal(ctx, "error creating user home"),
}, nil
}
if createHomeRes.Status.Code != rpc.Code_CODE_OK && createHomeRes.Status.Code != rpc.Code_CODE_ALREADY_EXISTS {
err := status.NewErrorFromCode(createHomeRes.Status.Code, "gateway")
log.Err(err).Msg("error calling Createhome")
return &gateway.AuthenticateResponse{
Status: status.NewInternal(ctx, "error creating user home"),
}, nil
}
gwRes := &gateway.AuthenticateResponse{
Status: status.NewOK(ctx),
User: res.User,
Token: token,
}
return gwRes, nil
}
func (s *svc) WhoAmI(ctx context.Context, req *gateway.WhoAmIRequest) (*gateway.WhoAmIResponse, error) {
u, _, err := s.tokenmgr.DismantleToken(ctx, req.Token)
if err != nil {
err = errors.Wrap(err, "gateway: error getting user from token")
return &gateway.WhoAmIResponse{
Status: status.NewUnauthenticated(ctx, err, "error dismantling token"),
}, nil
}
if sharedconf.SkipUserGroupsInToken() {
groupsRes, err := s.GetUserGroups(ctx, &userpb.GetUserGroupsRequest{UserId: u.Id})
if err != nil {
return nil, err
}
u.Groups = groupsRes.Groups
}
res := &gateway.WhoAmIResponse{
Status: status.NewOK(ctx),
User: u,
}
return res, nil
}
func (s *svc) findAuthProvider(ctx context.Context, authType string) (authpb.ProviderAPIClient, error) {
c, err := pool.GetAuthRegistryServiceClient(s.c.AuthRegistryEndpoint)
if err != nil {
err = errors.Wrap(err, "gateway: error getting auth registry client")
return nil, err
}
res, err := c.GetAuthProviders(ctx, &registry.GetAuthProvidersRequest{
Type: authType,
})
if err != nil {
err = errors.Wrap(err, "gateway: error calling GetAuthProvider")
return nil, err
}
if res.Status.Code == rpc.Code_CODE_OK && res.Providers != nil && len(res.Providers) > 0 {
// TODO(labkode): check for capabilities here
c, err := pool.GetAuthProviderServiceClient(res.Providers[0].Address)
if err != nil {
err = errors.Wrap(err, "gateway: error getting an auth provider client")
return nil, err
}
return c, nil
}
if res.Status.Code == rpc.Code_CODE_NOT_FOUND {
return nil, errtypes.NotFound("gateway: auth provider not found for type:" + authType)
}
return nil, errtypes.InternalError("gateway: error finding an auth provider for type: " + authType)
}
@@ -0,0 +1,64 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
registry "github.com/cs3org/go-cs3apis/cs3/auth/registry/v1beta1"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
)
func (s *svc) ListAuthProviders(ctx context.Context, req *registry.ListAuthProvidersRequest) (*gateway.ListAuthProvidersResponse, error) {
c, err := pool.GetAuthRegistryServiceClient(s.c.AuthRegistryEndpoint)
if err != nil {
return &gateway.ListAuthProvidersResponse{
Status: status.NewInternal(ctx, "gateway"),
}, nil
}
res, err := c.ListAuthProviders(ctx, req)
if err != nil {
return &gateway.ListAuthProvidersResponse{
Status: status.NewInternal(ctx, "gateway"),
}, nil
}
if res.Status.Code != rpc.Code_CODE_OK {
return &gateway.ListAuthProvidersResponse{
Status: status.NewInternal(ctx, "gateway"),
}, nil
}
types := make([]string, len(res.Providers))
for i, p := range res.Providers {
types[i] = p.ProviderType
}
gwRes := &gateway.ListAuthProvidersResponse{
Status: res.Status,
Opaque: res.Opaque,
Types: types,
}
return gwRes, nil
}
@@ -0,0 +1,108 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
datatx "github.com/cs3org/go-cs3apis/cs3/tx/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (s *svc) CreateTransfer(ctx context.Context, req *datatx.CreateTransferRequest) (*datatx.CreateTransferResponse, error) {
c, err := pool.GetDataTxClient(s.c.DataTxEndpoint)
if err != nil {
return &datatx.CreateTransferResponse{
Status: status.NewInternal(ctx, "error getting data transfer client"),
}, nil
}
res, err := c.CreateTransfer(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling PullTransfer")
}
return res, nil
}
func (s *svc) GetTransferStatus(ctx context.Context, req *datatx.GetTransferStatusRequest) (*datatx.GetTransferStatusResponse, error) {
c, err := pool.GetDataTxClient(s.c.DataTxEndpoint)
if err != nil {
return &datatx.GetTransferStatusResponse{
Status: status.NewInternal(ctx, "error getting data transfer client"),
}, nil
}
res, err := c.GetTransferStatus(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetTransferStatus")
}
return res, nil
}
func (s *svc) CancelTransfer(ctx context.Context, req *datatx.CancelTransferRequest) (*datatx.CancelTransferResponse, error) {
c, err := pool.GetDataTxClient(s.c.DataTxEndpoint)
if err != nil {
return &datatx.CancelTransferResponse{
Status: status.NewInternal(ctx, "error getting data transfer client"),
}, nil
}
res, err := c.CancelTransfer(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling CancelTransfer")
}
return res, nil
}
func (s *svc) ListTransfers(ctx context.Context, req *datatx.ListTransfersRequest) (*datatx.ListTransfersResponse, error) {
c, err := pool.GetDataTxClient(s.c.DataTxEndpoint)
if err != nil {
return &datatx.ListTransfersResponse{
Status: status.NewInternal(ctx, "error getting data transfer client"),
}, nil
}
res, err := c.ListTransfers(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ListTransfers")
}
return res, nil
}
func (s *svc) RetryTransfer(ctx context.Context, req *datatx.RetryTransferRequest) (*datatx.RetryTransferResponse, error) {
c, err := pool.GetDataTxClient(s.c.DataTxEndpoint)
if err != nil {
return &datatx.RetryTransferResponse{
Status: status.NewInternal(ctx, "error getting data transfer client"),
}, nil
}
res, err := c.RetryTransfer(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling RetryTransfer")
}
return res, nil
}
@@ -0,0 +1,213 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"fmt"
"net/url"
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/storage/cache"
"github.com/opencloud-eu/reva/v2/pkg/token"
"github.com/opencloud-eu/reva/v2/pkg/token/manager/registry"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("gateway", New)
}
type config struct {
AuthRegistryEndpoint string `mapstructure:"authregistrysvc"`
ApplicationAuthEndpoint string `mapstructure:"applicationauthsvc"`
StorageRegistryEndpoint string `mapstructure:"storageregistrysvc"`
AppRegistryEndpoint string `mapstructure:"appregistrysvc"`
PreferencesEndpoint string `mapstructure:"preferencessvc"`
UserShareProviderEndpoint string `mapstructure:"usershareprovidersvc"`
PublicShareProviderEndpoint string `mapstructure:"publicshareprovidersvc"`
OCMShareProviderEndpoint string `mapstructure:"ocmshareprovidersvc"`
OCMInviteManagerEndpoint string `mapstructure:"ocminvitemanagersvc"`
OCMProviderAuthorizerEndpoint string `mapstructure:"ocmproviderauthorizersvc"`
OCMCoreEndpoint string `mapstructure:"ocmcoresvc"`
UserProviderEndpoint string `mapstructure:"userprovidersvc"`
GroupProviderEndpoint string `mapstructure:"groupprovidersvc"`
TenantProviderEndpoint string `mapstructure:"tenantprovidersvc"`
DataTxEndpoint string `mapstructure:"datatx"`
DataGatewayEndpoint string `mapstructure:"datagateway"`
PermissionsEndpoint string `mapstructure:"permissionssvc"`
CommitShareToStorageGrant bool `mapstructure:"commit_share_to_storage_grant"`
DisableHomeCreationOnLogin bool `mapstructure:"disable_home_creation_on_login"`
TransferSharedSecret string `mapstructure:"transfer_shared_secret"`
TransferExpires int64 `mapstructure:"transfer_expires"`
TokenManager string `mapstructure:"token_manager"`
// ShareFolder is the location where to create shares in the recipient's storage provider.
// FIXME get rid of ShareFolder, there are findByPath calls in the ocmshareporvider.go and usershareprovider.go
ShareFolder string `mapstructure:"share_folder"`
DataTransfersFolder string `mapstructure:"data_transfers_folder"`
TokenManagers map[string]map[string]interface{} `mapstructure:"token_managers"`
AllowedUserAgents map[string][]string `mapstructure:"allowed_user_agents"` // map[path][]user-agent
CreatePersonalSpaceCacheConfig cache.Config `mapstructure:"create_personal_space_cache_config"`
ProviderCacheConfig cache.Config `mapstructure:"provider_cache_config"`
}
// sets defaults
func (c *config) init() {
if c.ShareFolder == "" {
c.ShareFolder = "MyShares"
}
c.ShareFolder = strings.Trim(c.ShareFolder, "/")
if c.DataTransfersFolder == "" {
c.DataTransfersFolder = "DataTransfers"
}
if c.TokenManager == "" {
c.TokenManager = "jwt"
}
// if services address are not specified we used the shared conf
// for the gatewaysvc to have dev setups very quickly.
c.AuthRegistryEndpoint = sharedconf.GetGatewaySVC(c.AuthRegistryEndpoint)
c.ApplicationAuthEndpoint = sharedconf.GetGatewaySVC(c.ApplicationAuthEndpoint)
c.StorageRegistryEndpoint = sharedconf.GetGatewaySVC(c.StorageRegistryEndpoint)
c.AppRegistryEndpoint = sharedconf.GetGatewaySVC(c.AppRegistryEndpoint)
c.PreferencesEndpoint = sharedconf.GetGatewaySVC(c.PreferencesEndpoint)
c.UserShareProviderEndpoint = sharedconf.GetGatewaySVC(c.UserShareProviderEndpoint)
c.PublicShareProviderEndpoint = sharedconf.GetGatewaySVC(c.PublicShareProviderEndpoint)
c.OCMShareProviderEndpoint = sharedconf.GetGatewaySVC(c.OCMShareProviderEndpoint)
c.OCMInviteManagerEndpoint = sharedconf.GetGatewaySVC(c.OCMInviteManagerEndpoint)
c.OCMProviderAuthorizerEndpoint = sharedconf.GetGatewaySVC(c.OCMProviderAuthorizerEndpoint)
c.OCMCoreEndpoint = sharedconf.GetGatewaySVC(c.OCMCoreEndpoint)
c.UserProviderEndpoint = sharedconf.GetGatewaySVC(c.UserProviderEndpoint)
c.GroupProviderEndpoint = sharedconf.GetGatewaySVC(c.GroupProviderEndpoint)
// Fall back to userprovidersvc when no dedicated tenant provider is configured.
if c.TenantProviderEndpoint == "" {
c.TenantProviderEndpoint = c.UserProviderEndpoint
} else {
c.TenantProviderEndpoint = sharedconf.GetGatewaySVC(c.TenantProviderEndpoint)
}
c.DataTxEndpoint = sharedconf.GetGatewaySVC(c.DataTxEndpoint)
c.DataGatewayEndpoint = sharedconf.GetDataGateway(c.DataGatewayEndpoint)
// use shared secret if not set
c.TransferSharedSecret = sharedconf.GetJWTSecret(c.TransferSharedSecret)
// lifetime for the transfer token (TUS upload)
if c.TransferExpires == 0 {
c.TransferExpires = 100 * 60 // seconds
}
// caching needs to be explicitly enabled
if c.ProviderCacheConfig.Store == "" {
c.ProviderCacheConfig.Store = "noop"
}
if c.ProviderCacheConfig.Database == "" {
c.ProviderCacheConfig.Database = "reva"
}
if c.CreatePersonalSpaceCacheConfig.Store == "" {
c.CreatePersonalSpaceCacheConfig.Store = "memory"
}
if c.CreatePersonalSpaceCacheConfig.Database == "" {
c.CreatePersonalSpaceCacheConfig.Database = "reva"
}
}
type svc struct {
c *config
dataGatewayURL url.URL
tokenmgr token.Manager
providerCache cache.ProviderCache
createPersonalSpaceCache cache.CreatePersonalSpaceCache
}
// New creates a new gateway svc that acts as a proxy for any grpc operation.
// The gateway is responsible for high-level controls: rate-limiting, coordination between svcs
// like sharing and storage acls, asynchronous transactions, ...
func New(m map[string]interface{}, _ *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
// ensure DataGatewayEndpoint is a valid URI
u, err := url.Parse(c.DataGatewayEndpoint)
if err != nil {
return nil, err
}
tokenManager, err := getTokenManager(c.TokenManager, c.TokenManagers)
if err != nil {
return nil, err
}
s := &svc{
c: c,
dataGatewayURL: *u,
tokenmgr: tokenManager,
providerCache: cache.GetProviderCache(c.ProviderCacheConfig),
createPersonalSpaceCache: cache.GetCreatePersonalSpaceCache(c.CreatePersonalSpaceCacheConfig),
}
return s, nil
}
func (s *svc) Register(ss *grpc.Server) {
gateway.RegisterGatewayAPIServer(ss, s)
}
func (s *svc) Close() error {
s.providerCache.Close()
s.createPersonalSpaceCache.Close()
return nil
}
func (s *svc) UnprotectedEndpoints() []string {
return []string{"/cs3.gateway.v1beta1.GatewayAPI"}
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "gateway: error decoding conf")
return nil, err
}
return c, nil
}
func getTokenManager(manager string, m map[string]map[string]interface{}) (token.Manager, error) {
if f, ok := registry.NewFuncs[manager]; ok {
return f(m[manager])
}
return nil, errtypes.NotFound(fmt.Sprintf("driver %s not found for token manager", manager))
}
@@ -0,0 +1,108 @@
// Copyright 2018-2020 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (s *svc) GetGroup(ctx context.Context, req *group.GetGroupRequest) (*group.GetGroupResponse, error) {
c, err := pool.GetGroupProviderServiceClient(s.c.GroupProviderEndpoint)
if err != nil {
return &group.GetGroupResponse{
Status: status.NewInternal(ctx, "error getting auth client"),
}, nil
}
res, err := c.GetGroup(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetGroup")
}
return res, nil
}
func (s *svc) GetGroupByClaim(ctx context.Context, req *group.GetGroupByClaimRequest) (*group.GetGroupByClaimResponse, error) {
c, err := pool.GetGroupProviderServiceClient(s.c.GroupProviderEndpoint)
if err != nil {
return &group.GetGroupByClaimResponse{
Status: status.NewInternal(ctx, "error getting auth client"),
}, nil
}
res, err := c.GetGroupByClaim(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetGroupByClaim")
}
return res, nil
}
func (s *svc) FindGroups(ctx context.Context, req *group.FindGroupsRequest) (*group.FindGroupsResponse, error) {
c, err := pool.GetGroupProviderServiceClient(s.c.GroupProviderEndpoint)
if err != nil {
return &group.FindGroupsResponse{
Status: status.NewInternal(ctx, "error getting auth client"),
}, nil
}
res, err := c.FindGroups(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling FindGroups")
}
return res, nil
}
func (s *svc) GetMembers(ctx context.Context, req *group.GetMembersRequest) (*group.GetMembersResponse, error) {
c, err := pool.GetGroupProviderServiceClient(s.c.GroupProviderEndpoint)
if err != nil {
return &group.GetMembersResponse{
Status: status.NewInternal(ctx, "error getting auth client"),
}, nil
}
res, err := c.GetMembers(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetMembers")
}
return res, nil
}
func (s *svc) HasMember(ctx context.Context, req *group.HasMemberRequest) (*group.HasMemberResponse, error) {
c, err := pool.GetGroupProviderServiceClient(s.c.GroupProviderEndpoint)
if err != nil {
return &group.HasMemberResponse{
Status: status.NewInternal(ctx, "error getting auth client"),
}, nil
}
res, err := c.HasMember(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling HasMember")
}
return res, nil
}
@@ -0,0 +1,76 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (s *svc) CreateOCMCoreShare(ctx context.Context, req *ocmcore.CreateOCMCoreShareRequest) (*ocmcore.CreateOCMCoreShareResponse, error) {
c, err := pool.GetOCMCoreClient(s.c.OCMCoreEndpoint)
if err != nil {
return &ocmcore.CreateOCMCoreShareResponse{
Status: status.NewInternal(ctx, "error getting ocm core client"),
}, nil
}
res, err := c.CreateOCMCoreShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling CreateOCMCoreShare")
}
return res, nil
}
func (s *svc) UpdateOCMCoreShare(ctx context.Context, req *ocmcore.UpdateOCMCoreShareRequest) (*ocmcore.UpdateOCMCoreShareResponse, error) {
c, err := pool.GetOCMCoreClient(s.c.OCMCoreEndpoint)
if err != nil {
return &ocmcore.UpdateOCMCoreShareResponse{
Status: status.NewInternal(ctx, "error getting ocm core client"),
}, nil
}
res, err := c.UpdateOCMCoreShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling UpdateOCMCoreShare")
}
return res, nil
}
func (s *svc) DeleteOCMCoreShare(ctx context.Context, req *ocmcore.DeleteOCMCoreShareRequest) (*ocmcore.DeleteOCMCoreShareResponse, error) {
c, err := pool.GetOCMCoreClient(s.c.OCMCoreEndpoint)
if err != nil {
return &ocmcore.DeleteOCMCoreShareResponse{
Status: status.NewInternal(ctx, "error getting ocm core client"),
}, nil
}
res, err := c.DeleteOCMCoreShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling UpdateOCMCoreShare")
}
return res, nil
}
@@ -0,0 +1,140 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (s *svc) GenerateInviteToken(ctx context.Context, req *invitepb.GenerateInviteTokenRequest) (*invitepb.GenerateInviteTokenResponse, error) {
c, err := pool.GetOCMInviteManagerClient(s.c.OCMInviteManagerEndpoint)
if err != nil {
return &invitepb.GenerateInviteTokenResponse{
Status: status.NewInternal(ctx, "error getting user invite provider client"),
}, nil
}
res, err := c.GenerateInviteToken(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GenerateInviteToken")
}
return res, nil
}
func (s *svc) ListInviteTokens(ctx context.Context, req *invitepb.ListInviteTokensRequest) (*invitepb.ListInviteTokensResponse, error) {
c, err := pool.GetOCMInviteManagerClient(s.c.OCMInviteManagerEndpoint)
if err != nil {
return &invitepb.ListInviteTokensResponse{
Status: status.NewInternal(ctx, "error getting user invite provider client"),
}, nil
}
res, err := c.ListInviteTokens(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ListInviteTokens")
}
return res, nil
}
func (s *svc) ForwardInvite(ctx context.Context, req *invitepb.ForwardInviteRequest) (*invitepb.ForwardInviteResponse, error) {
c, err := pool.GetOCMInviteManagerClient(s.c.OCMInviteManagerEndpoint)
if err != nil {
return &invitepb.ForwardInviteResponse{
Status: status.NewInternal(ctx, "error getting user invite provider client"),
}, nil
}
res, err := c.ForwardInvite(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ForwardInvite")
}
return res, nil
}
func (s *svc) AcceptInvite(ctx context.Context, req *invitepb.AcceptInviteRequest) (*invitepb.AcceptInviteResponse, error) {
c, err := pool.GetOCMInviteManagerClient(s.c.OCMInviteManagerEndpoint)
if err != nil {
return &invitepb.AcceptInviteResponse{
Status: status.NewInternal(ctx, "error getting user invite provider client"),
}, nil
}
res, err := c.AcceptInvite(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling AcceptInvite")
}
return res, nil
}
func (s *svc) GetAcceptedUser(ctx context.Context, req *invitepb.GetAcceptedUserRequest) (*invitepb.GetAcceptedUserResponse, error) {
c, err := pool.GetOCMInviteManagerClient(s.c.OCMInviteManagerEndpoint)
if err != nil {
return &invitepb.GetAcceptedUserResponse{
Status: status.NewInternal(ctx, "error getting user invite provider client"),
}, nil
}
res, err := c.GetAcceptedUser(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetAcceptedUser")
}
return res, nil
}
func (s *svc) FindAcceptedUsers(ctx context.Context, req *invitepb.FindAcceptedUsersRequest) (*invitepb.FindAcceptedUsersResponse, error) {
c, err := pool.GetOCMInviteManagerClient(s.c.OCMInviteManagerEndpoint)
if err != nil {
return &invitepb.FindAcceptedUsersResponse{
Status: status.NewInternal(ctx, "error getting user invite provider client"),
}, nil
}
res, err := c.FindAcceptedUsers(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling FindAcceptedUsers")
}
return res, nil
}
func (s *svc) DeleteAcceptedUser(ctx context.Context, req *invitepb.DeleteAcceptedUserRequest) (*invitepb.DeleteAcceptedUserResponse, error) {
c, err := pool.GetOCMInviteManagerClient(s.c.OCMInviteManagerEndpoint)
if err != nil {
return &invitepb.DeleteAcceptedUserResponse{
Status: status.NewInternal(ctx, "error getting user invite provider client"),
}, nil
}
res, err := c.DeleteAcceptedUser(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling FindAcceptedUsers")
}
return res, nil
}
@@ -0,0 +1,76 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (s *svc) IsProviderAllowed(ctx context.Context, req *ocmprovider.IsProviderAllowedRequest) (*ocmprovider.IsProviderAllowedResponse, error) {
c, err := pool.GetOCMProviderAuthorizerClient(s.c.OCMProviderAuthorizerEndpoint)
if err != nil {
return &ocmprovider.IsProviderAllowedResponse{
Status: status.NewInternal(ctx, "error getting ocm authorizer provider client"),
}, nil
}
res, err := c.IsProviderAllowed(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling IsProviderAllowed")
}
return res, nil
}
func (s *svc) GetInfoByDomain(ctx context.Context, req *ocmprovider.GetInfoByDomainRequest) (*ocmprovider.GetInfoByDomainResponse, error) {
c, err := pool.GetOCMProviderAuthorizerClient(s.c.OCMProviderAuthorizerEndpoint)
if err != nil {
return &ocmprovider.GetInfoByDomainResponse{
Status: status.NewInternal(ctx, "error getting ocm authorizer provider client"),
}, nil
}
res, err := c.GetInfoByDomain(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetInfoByDomain")
}
return res, nil
}
func (s *svc) ListAllProviders(ctx context.Context, req *ocmprovider.ListAllProvidersRequest) (*ocmprovider.ListAllProvidersResponse, error) {
c, err := pool.GetOCMProviderAuthorizerClient(s.c.OCMProviderAuthorizerEndpoint)
if err != nil {
return &ocmprovider.ListAllProvidersResponse{
Status: status.NewInternal(ctx, "error getting ocm authorizer provider client"),
}, nil
}
res, err := c.ListAllProviders(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ListAllProviders")
}
return res, nil
}
@@ -0,0 +1,465 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
"fmt"
"net/url"
"path"
"slices"
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
incoming "github.com/cs3org/go-cs3apis/cs3/ocm/incoming/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
datatx "github.com/cs3org/go-cs3apis/cs3/tx/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
// TODO(labkode): add multi-phase commit logic when commit share or commit ref is enabled.
func (s *svc) CreateOCMShare(ctx context.Context, req *ocm.CreateOCMShareRequest) (*ocm.CreateOCMShareResponse, error) {
if len(req.AccessMethods) == 0 {
return &ocm.CreateOCMShareResponse{
Status: status.NewInvalidArg(ctx, "access methods cannot be empty"),
}, nil
}
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
return &ocm.CreateOCMShareResponse{
Status: status.NewInternal(ctx, "error getting user share provider client"),
}, nil
}
// persist the OCM share in the ocm share provider
res, err := c.CreateOCMShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling CreateOCMShare")
}
// add a grant to the storage provider so the share can efficiently be listed
// the grant does not grant any permissions. access is granted by the OCM link token
// that is used by the public storage provider to impersonate the resource owner
status, err := s.addGrant(ctx, req.ResourceId, req.Grantee, req.AccessMethods[0].GetWebdavOptions().Permissions, req.Expiration, nil)
switch {
case err != nil:
appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg(err.Error())
return nil, errors.Wrap(err, "gateway: error adding grant to storage")
case status.Code == rpc.Code_CODE_UNIMPLEMENTED:
appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg("storing grants not supported, ignoring")
case status.Code != rpc.Code_CODE_OK:
appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg("storing grants is not successful")
return &ocm.CreateOCMShareResponse{
Status: status,
}, nil
}
return res, nil
}
func (s *svc) RemoveOCMShare(ctx context.Context, req *ocm.RemoveOCMShareRequest) (*ocm.RemoveOCMShareResponse, error) {
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
return &ocm.RemoveOCMShareResponse{
Status: status.NewInternal(ctx, "error getting user share provider client"),
}, nil
}
getShareRes, err := c.GetOCMShare(ctx, &ocm.GetOCMShareRequest{
Ref: req.Ref,
})
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetOCMShare")
}
if getShareRes.Status.Code != rpc.Code_CODE_OK {
res := &ocm.RemoveOCMShareResponse{
Status: status.NewInternal(ctx,
"error getting ocm share when committing to the storage"),
}
return res, nil
}
share := getShareRes.Share
res, err := c.RemoveOCMShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling RemoveOCMShare")
}
// remove the grant from the storage provider
status, err := s.removeGrant(ctx, share.GetResourceId(), share.GetGrantee(), share.GetAccessMethods()[0].GetWebdavOptions().GetPermissions(), nil)
if err != nil {
return nil, errors.Wrap(err, "gateway: error removing grant from storage")
}
if status.Code != rpc.Code_CODE_OK {
return &ocm.RemoveOCMShareResponse{
Status: status,
}, err
}
return res, nil
}
// TODO(labkode): we need to validate share state vs storage grant and storage ref
// If there are any inconsistencies, the share needs to be flag as invalid and a background process
// or active fix needs to be performed.
func (s *svc) GetOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest) (*ocm.GetOCMShareResponse, error) {
return s.getOCMShare(ctx, req)
}
func (s *svc) getOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest) (*ocm.GetOCMShareResponse, error) {
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient")
return &ocm.GetOCMShareResponse{
Status: status.NewInternal(ctx, "error getting user share provider client"),
}, nil
}
res, err := c.GetOCMShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetOCMShare")
}
return res, nil
}
func (s *svc) GetOCMShareByToken(ctx context.Context, req *ocm.GetOCMShareByTokenRequest) (*ocm.GetOCMShareByTokenResponse, error) {
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetOCMShareProviderClient")
}
res, err := c.GetOCMShareByToken(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetOCMShareByToken")
}
return res, nil
}
// TODO(labkode): read GetShare comment.
func (s *svc) ListOCMShares(ctx context.Context, req *ocm.ListOCMSharesRequest) (*ocm.ListOCMSharesResponse, error) {
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient")
return &ocm.ListOCMSharesResponse{
Status: status.NewInternal(ctx, "error getting user share provider client"),
}, nil
}
res, err := c.ListOCMShares(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ListOCMShares")
}
return res, nil
}
func (s *svc) UpdateOCMShare(ctx context.Context, req *ocm.UpdateOCMShareRequest) (*ocm.UpdateOCMShareResponse, error) {
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient")
return &ocm.UpdateOCMShareResponse{
Status: status.NewInternal(ctx, "error getting share provider client"),
}, nil
}
res, err := c.UpdateOCMShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling UpdateOCMShare")
}
return res, nil
}
func (s *svc) ListReceivedOCMShares(ctx context.Context, req *ocm.ListReceivedOCMSharesRequest) (*ocm.ListReceivedOCMSharesResponse, error) {
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient")
return &ocm.ListReceivedOCMSharesResponse{
Status: status.NewInternal(ctx, "error getting share provider client"),
}, nil
}
res, err := c.ListReceivedOCMShares(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ListReceivedOCMShares")
}
return res, nil
}
func (s *svc) UpdateReceivedOCMShare(ctx context.Context, req *ocm.UpdateReceivedOCMShareRequest) (*ocm.UpdateReceivedOCMShareResponse, error) {
log := appctx.GetLogger(ctx)
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient")
return &ocm.UpdateReceivedOCMShareResponse{
Status: status.NewInternal(ctx, "error getting share provider client"),
}, nil
}
// retrieve the current received share
getShareReq := &ocm.GetReceivedOCMShareRequest{
Ref: &ocm.ShareReference{
Spec: &ocm.ShareReference_Id{
Id: req.Share.Id,
},
},
}
getShareRes, err := s.GetReceivedOCMShare(ctx, getShareReq)
if err != nil {
log.Error().Err(err).Msg("gateway: error calling GetReceivedOCMShare")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
},
}, nil
}
if getShareRes.Status.Code != rpc.Code_CODE_OK {
log.Error().Msg("gateway: error calling GetReceivedOCMShare")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
Message: "gateway: error calling GetReceivedOCMShare",
},
}, nil
}
share := getShareRes.Share
if share == nil {
log.Error().Err(err).Msg("gateway: got a nil share from GetReceivedOCMShare")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
Message: "gateway: got a nil share from GetReceivedOCMShare",
},
}, nil
}
res, err := c.UpdateReceivedOCMShare(ctx, req)
if err != nil {
log.Err(err).Msg("gateway: error calling UpdateReceivedOCMShare")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
},
}, nil
}
for i := range req.UpdateMask.Paths {
switch req.UpdateMask.Paths[i] {
case "state":
switch req.GetShare().GetState() {
case ocm.ShareState_SHARE_STATE_ACCEPTED:
// for a transfer this is handled elsewhere
case ocm.ShareState_SHARE_STATE_PENDING:
// currently no consequences
case ocm.ShareState_SHARE_STATE_REJECTED:
// TODO
return res, nil
}
case "mount_point":
// TODO(labkode): implementing updating mount point
err = errtypes.NotSupported("gateway: update of mount point is not yet implemented")
return &ocm.UpdateReceivedOCMShareResponse{
Status: status.NewUnimplemented(ctx, err, "error updating received share"),
}, nil
default:
return nil, errtypes.NotSupported("updating " + req.UpdateMask.Paths[i] + " is not supported")
}
}
// handle transfer in case it has not already been accepted
if s.isTransferShare(share) && req.GetShare().State == ocm.ShareState_SHARE_STATE_ACCEPTED {
if share.State == ocm.ShareState_SHARE_STATE_ACCEPTED {
log.Err(err).Msg("gateway: error calling UpdateReceivedOCMShare, share already accepted.")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_FAILED_PRECONDITION,
Message: "Share already accepted.",
},
}, err
}
// get provided destination path
transferDestinationPath, err := s.getTransferDestinationPath(ctx, req)
if err != nil {
if err != nil {
log.Err(err).Msg("gateway: error calling UpdateReceivedOCMShare")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
},
}, err
}
}
error := s.handleTransfer(ctx, share, transferDestinationPath)
if error != nil {
log.Err(error).Msg("gateway: error handling transfer in UpdateReceivedOCMShare")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
},
}, error
}
}
return res, nil
}
func (s *svc) handleTransfer(ctx context.Context, share *ocm.ReceivedShare, transferDestinationPath string) error {
log := appctx.GetLogger(ctx)
protocol, ok := s.getTransferProtocol(share)
if !ok {
return errors.New("gateway: unable to retrieve transfer protocol")
}
sourceURI := protocol.Uri
// get the webdav endpoint of the grantee's idp
var granteeIdp string
if share.GetGrantee().Type == provider.GranteeType_GRANTEE_TYPE_USER {
granteeIdp = share.GetGrantee().GetUserId().Idp
}
if share.GetGrantee().Type == provider.GranteeType_GRANTEE_TYPE_GROUP {
granteeIdp = share.GetGrantee().GetGroupId().Idp
}
destWebdavEndpoint, err := s.getWebdavEndpoint(ctx, granteeIdp)
if err != nil {
log.Err(err).Msg("gateway: error calling UpdateReceivedOCMShare")
return err
}
destWebdavEndpointURL, err := url.Parse(destWebdavEndpoint)
if err != nil {
log.Err(err).Msg("gateway: error calling UpdateReceivedOCMShare: unable to parse webdav endpoint \"" + destWebdavEndpoint + "\" into URL structure")
return err
}
destWebdavHost, err := s.getWebdavHost(ctx, granteeIdp)
if err != nil {
log.Err(err).Msg("gateway: error calling UpdateReceivedOCMShare")
return err
}
var dstWebdavURLString string
if strings.Contains(destWebdavHost, "://") {
dstWebdavURLString = destWebdavHost
} else {
dstWebdavURLString = "http://" + destWebdavHost
}
dstWebdavHostURL, err := url.Parse(dstWebdavURLString)
if err != nil {
log.Err(err).Msg("gateway: error calling UpdateReceivedOCMShare: unable to parse webdav service host \"" + dstWebdavURLString + "\" into URL structure")
return err
}
destServiceHost := dstWebdavHostURL.Host + dstWebdavHostURL.Path
// optional prefix must only appear in target url path:
// http://...token...@reva.eu/prefix/?name=remote.php/webdav/home/...
destEndpointPath := strings.TrimPrefix(destWebdavEndpointURL.Path, dstWebdavHostURL.Path)
destEndpointScheme := destWebdavEndpointURL.Scheme
destToken := ctxpkg.ContextMustGetToken(ctx)
destPath := path.Join(destEndpointPath, transferDestinationPath, path.Base(share.Name))
destTargetURI := fmt.Sprintf("%s://%s@%s?name=%s", destEndpointScheme, destToken, destServiceHost, destPath)
// var destUri string
req := &datatx.CreateTransferRequest{
SrcTargetUri: sourceURI,
DestTargetUri: destTargetURI,
ShareId: share.Id,
}
res, err := s.CreateTransfer(ctx, req)
if err != nil {
return err
}
log.Info().Msgf("gateway: CreateTransfer: %v", res.TxInfo)
return nil
}
func (s *svc) isTransferShare(share *ocm.ReceivedShare) bool {
_, ok := s.getTransferProtocol(share)
return ok
}
func (s *svc) getTransferDestinationPath(ctx context.Context, req *ocm.UpdateReceivedOCMShareRequest) (string, error) {
log := appctx.GetLogger(ctx)
// the destination path is not part of any protocol, but an opaque field
destPathOpaque, ok := req.GetOpaque().GetMap()["transfer_destination_path"]
if ok {
switch destPathOpaque.Decoder {
case "plain":
if string(destPathOpaque.Value) != "" {
return string(destPathOpaque.Value), nil
}
default:
return "", errtypes.NotSupported("decoder of opaque entry 'transfer_destination_path' not recognized: " + destPathOpaque.Decoder)
}
}
log.Info().Msg("destination path not provided, trying default transfer destination folder")
if s.c.DataTransfersFolder == "" {
return "", errtypes.NotSupported("no destination path provided and default transfer destination folder is not set")
}
return s.c.DataTransfersFolder, nil
}
func (s *svc) GetReceivedOCMShare(ctx context.Context, req *ocm.GetReceivedOCMShareRequest) (*ocm.GetReceivedOCMShareResponse, error) {
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient")
return &ocm.GetReceivedOCMShareResponse{
Status: status.NewInternal(ctx, "error getting share provider client"),
}, nil
}
res, err := c.GetReceivedOCMShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetReceivedOCMShare")
}
return res, nil
}
func (s *svc) getTransferProtocol(share *ocm.ReceivedShare) (*ocm.WebDAVProtocol, bool) {
for _, p := range share.Protocols {
if d, ok := p.Term.(*ocm.Protocol_WebdavOptions); ok {
if slices.Contains(d.WebdavOptions.AccessTypes, ocm.AccessType_ACCESS_TYPE_DATATX) {
return d.WebdavOptions, true
}
}
}
return nil, false
}
func (s *svc) CreateOCMIncomingShare(context.Context, *incoming.CreateOCMIncomingShareRequest) (*incoming.CreateOCMIncomingShareResponse, error) {
return nil, errtypes.NotSupported("gateway: CreateOCMIncomingShare is not supported")
}
func (s *svc) UpdateOCMIncomingShare(context.Context, *incoming.UpdateOCMIncomingShareRequest) (*incoming.UpdateOCMIncomingShareResponse, error) {
return nil, errtypes.NotSupported("gateway: UpdateOCMIncomingShare is not supported")
}
func (s *svc) DeleteOCMIncomingShare(context.Context, *incoming.DeleteOCMIncomingShareRequest) (*incoming.DeleteOCMIncomingShareResponse, error) {
return nil, errtypes.NotSupported("gateway: DeleteOCMIncomingShare is not supported")
}
func (s *svc) ListExistingOCMShares(context.Context, *ocm.ListOCMSharesRequest) (*gateway.ListExistingOCMSharesResponse, error) {
return nil, errtypes.NotSupported("gateway: ListExistingOCMShares is not supported")
}
@@ -0,0 +1,37 @@
// Copyright 2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
permissions "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
)
func (s *svc) CheckPermission(ctx context.Context, req *permissions.CheckPermissionRequest) (*permissions.CheckPermissionResponse, error) {
c, err := pool.GetPermissionsClient(s.c.PermissionsEndpoint)
if err != nil {
return &permissions.CheckPermissionResponse{
Status: status.NewInternal(ctx, "error getting permissions client"),
}, nil
}
return c.CheckPermission(ctx, req)
}
@@ -0,0 +1,60 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
preferences "github.com/cs3org/go-cs3apis/cs3/preferences/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (s *svc) SetKey(ctx context.Context, req *preferences.SetKeyRequest) (*preferences.SetKeyResponse, error) {
c, err := pool.GetPreferencesClient(s.c.PreferencesEndpoint)
if err != nil {
return &preferences.SetKeyResponse{
Status: status.NewInternal(ctx, "error getting preferences client"),
}, nil
}
res, err := c.SetKey(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling SetKey")
}
return res, nil
}
func (s *svc) GetKey(ctx context.Context, req *preferences.GetKeyRequest) (*preferences.GetKeyResponse, error) {
c, err := pool.GetPreferencesClient(s.c.PreferencesEndpoint)
if err != nil {
return &preferences.GetKeyResponse{
Status: status.NewInternal(ctx, "error getting preferences client"),
}, nil
}
res, err := c.GetKey(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetKey")
}
return res, nil
}
@@ -0,0 +1,131 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (s *svc) CreatePublicShare(ctx context.Context, req *link.CreatePublicShareRequest) (*link.CreatePublicShareResponse, error) {
log := appctx.GetLogger(ctx)
log.Info().Msg("create public share")
c, err := pool.GetPublicShareProviderClient(s.c.PublicShareProviderEndpoint)
if err != nil {
return nil, err
}
return c.CreatePublicShare(ctx, req)
}
func (s *svc) RemovePublicShare(ctx context.Context, req *link.RemovePublicShareRequest) (*link.RemovePublicShareResponse, error) {
log := appctx.GetLogger(ctx)
log.Info().Msg("remove public share")
driver, err := pool.GetPublicShareProviderClient(s.c.PublicShareProviderEndpoint)
if err != nil {
return nil, err
}
return driver.RemovePublicShare(ctx, req)
}
func (s *svc) GetPublicShareByToken(ctx context.Context, req *link.GetPublicShareByTokenRequest) (*link.GetPublicShareByTokenResponse, error) {
log := appctx.GetLogger(ctx)
log.Info().Msg("get public share by token")
driver, err := pool.GetPublicShareProviderClient(s.c.PublicShareProviderEndpoint)
if err != nil {
return nil, err
}
res, err := driver.GetPublicShareByToken(ctx, req)
if err != nil {
return nil, err
}
return res, nil
}
func (s *svc) GetPublicShare(ctx context.Context, req *link.GetPublicShareRequest) (*link.GetPublicShareResponse, error) {
log := appctx.GetLogger(ctx)
log.Info().Msg("get public share")
pClient, err := pool.GetPublicShareProviderClient(s.c.PublicShareProviderEndpoint)
if err != nil {
log.Err(err).Msg("error connecting to a public share provider")
return &link.GetPublicShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
},
}, nil
}
return pClient.GetPublicShare(ctx, req)
}
func (s *svc) ListPublicShares(ctx context.Context, req *link.ListPublicSharesRequest) (*link.ListPublicSharesResponse, error) {
log := appctx.GetLogger(ctx)
log.Info().Msg("listing public shares")
pClient, err := pool.GetPublicShareProviderClient(s.c.PublicShareProviderEndpoint)
if err != nil {
log.Err(err).Msg("error connecting to a public share provider")
return &link.ListPublicSharesResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
},
}, nil
}
res, err := pClient.ListPublicShares(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "error listing shares")
}
return res, nil
}
func (s *svc) ListExistingPublicShares(_ context.Context, _ *link.ListPublicSharesRequest) (*gateway.ListExistingPublicSharesResponse, error) {
return nil, errtypes.NotSupported("method ListExistingPublicShares not implemented")
}
func (s *svc) UpdatePublicShare(ctx context.Context, req *link.UpdatePublicShareRequest) (*link.UpdatePublicShareResponse, error) {
log := appctx.GetLogger(ctx)
log.Info().Msg("update public share")
pClient, err := pool.GetPublicShareProviderClient(s.c.PublicShareProviderEndpoint)
if err != nil {
log.Err(err).Msg("error connecting to a public share provider")
return &link.UpdatePublicShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
},
}, nil
}
return pClient.UpdatePublicShare(ctx, req)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,272 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
sdk "github.com/opencloud-eu/reva/v2/pkg/sdk/common"
"github.com/opencloud-eu/reva/v2/pkg/storage/cache"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/pkg/errors"
"google.golang.org/grpc"
)
/*
Cached Registry
*/
type cachedRegistryClient struct {
c registry.RegistryAPIClient
cache cache.ProviderCache
}
func (c *cachedRegistryClient) ListStorageProviders(ctx context.Context, in *registry.ListStorageProvidersRequest, opts ...grpc.CallOption) (*registry.ListStorageProvidersResponse, error) {
spaceID := sdk.DecodeOpaqueMap(in.Opaque)["space_id"]
u, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
return nil, errors.New("user not found in context")
}
key := c.cache.GetKey(u.GetId(), spaceID)
if key != "" {
s := &registry.ListStorageProvidersResponse{}
if err := c.cache.PullFromCache(key, s); err == nil {
return s, nil
}
}
resp, err := c.c.ListStorageProviders(ctx, in, opts...)
switch {
case err != nil:
return nil, err
case resp.Status.Code != rpc.Code_CODE_OK:
return resp, nil
case spaceID == "":
return resp, nil
case spaceID == utils.ShareStorageSpaceID: // TODO do we need to compare providerid and spaceid separately?
return resp, nil
default:
return resp, c.cache.PushToCache(key, resp)
}
}
// not cached
func (c *cachedRegistryClient) GetStorageProviders(ctx context.Context, in *registry.GetStorageProvidersRequest, opts ...grpc.CallOption) (*registry.GetStorageProvidersResponse, error) {
return c.c.GetStorageProviders(ctx, in, opts...)
}
func (c *cachedRegistryClient) GetHome(ctx context.Context, in *registry.GetHomeRequest, opts ...grpc.CallOption) (*registry.GetHomeResponse, error) {
return c.c.GetHome(ctx, in, opts...)
}
/*
Cached Spaces Provider
*/
type cachedSpacesAPIClient struct {
c provider.SpacesAPIClient
createPersonalSpaceCache cache.CreatePersonalSpaceCache
}
// CreateStorageSpace creates a storage space
func (c *cachedSpacesAPIClient) CreateStorageSpace(ctx context.Context, in *provider.CreateStorageSpaceRequest, opts ...grpc.CallOption) (*provider.CreateStorageSpaceResponse, error) {
if in.Type == "personal" {
u, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
return nil, errors.New("user not found in context")
}
key := c.createPersonalSpaceCache.GetKey(u.GetId())
if key != "" {
s := &provider.CreateStorageSpaceResponse{}
if err := c.createPersonalSpaceCache.PullFromCache(key, s); err == nil {
return s, nil
}
}
resp, err := c.c.CreateStorageSpace(ctx, in, opts...)
switch {
case err != nil:
return nil, err
case resp.Status.Code != rpc.Code_CODE_OK && resp.Status.Code != rpc.Code_CODE_ALREADY_EXISTS:
return resp, nil
case key == "":
return resp, nil
default:
return resp, c.createPersonalSpaceCache.PushToCache(key, resp)
}
}
return c.c.CreateStorageSpace(ctx, in, opts...)
}
func (c *cachedSpacesAPIClient) ListStorageSpaces(ctx context.Context, in *provider.ListStorageSpacesRequest, opts ...grpc.CallOption) (*provider.ListStorageSpacesResponse, error) {
return c.c.ListStorageSpaces(ctx, in, opts...)
}
func (c *cachedSpacesAPIClient) UpdateStorageSpace(ctx context.Context, in *provider.UpdateStorageSpaceRequest, opts ...grpc.CallOption) (*provider.UpdateStorageSpaceResponse, error) {
return c.c.UpdateStorageSpace(ctx, in, opts...)
}
func (c *cachedSpacesAPIClient) DeleteStorageSpace(ctx context.Context, in *provider.DeleteStorageSpaceRequest, opts ...grpc.CallOption) (*provider.DeleteStorageSpaceResponse, error) {
return c.c.DeleteStorageSpace(ctx, in, opts...)
}
/*
Cached Storage Provider
*/
type cachedAPIClient struct {
c provider.ProviderAPIClient
createPersonalSpaceCache cache.CreatePersonalSpaceCache
}
// CreateHome caches calls to CreateHome locally - anyways they only need to be called once per user
func (c *cachedAPIClient) CreateHome(ctx context.Context, in *provider.CreateHomeRequest, opts ...grpc.CallOption) (*provider.CreateHomeResponse, error) {
u, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
return nil, errors.New("user not found in context")
}
key := c.createPersonalSpaceCache.GetKey(u.GetId())
if key != "" {
s := &provider.CreateHomeResponse{}
if err := c.createPersonalSpaceCache.PullFromCache(key, s); err == nil {
return s, nil
}
}
resp, err := c.c.CreateHome(ctx, in, opts...)
switch {
case err != nil:
return nil, err
case resp.Status.Code != rpc.Code_CODE_OK && resp.Status.Code != rpc.Code_CODE_ALREADY_EXISTS:
return resp, nil
case key == "":
return resp, nil
default:
return resp, c.createPersonalSpaceCache.PushToCache(key, resp)
}
}
// methods below here are not cached, they just call the client directly
// Stat returns the Resoure info for a given resource
func (c *cachedAPIClient) Stat(ctx context.Context, in *provider.StatRequest, opts ...grpc.CallOption) (*provider.StatResponse, error) {
return c.c.Stat(ctx, in, opts...)
}
func (c *cachedAPIClient) AddGrant(ctx context.Context, in *provider.AddGrantRequest, opts ...grpc.CallOption) (*provider.AddGrantResponse, error) {
return c.c.AddGrant(ctx, in, opts...)
}
func (c *cachedAPIClient) CreateContainer(ctx context.Context, in *provider.CreateContainerRequest, opts ...grpc.CallOption) (*provider.CreateContainerResponse, error) {
return c.c.CreateContainer(ctx, in, opts...)
}
func (c *cachedAPIClient) Delete(ctx context.Context, in *provider.DeleteRequest, opts ...grpc.CallOption) (*provider.DeleteResponse, error) {
return c.c.Delete(ctx, in, opts...)
}
func (c *cachedAPIClient) DenyGrant(ctx context.Context, in *provider.DenyGrantRequest, opts ...grpc.CallOption) (*provider.DenyGrantResponse, error) {
return c.c.DenyGrant(ctx, in, opts...)
}
func (c *cachedAPIClient) GetPath(ctx context.Context, in *provider.GetPathRequest, opts ...grpc.CallOption) (*provider.GetPathResponse, error) {
return c.c.GetPath(ctx, in, opts...)
}
func (c *cachedAPIClient) GetQuota(ctx context.Context, in *provider.GetQuotaRequest, opts ...grpc.CallOption) (*provider.GetQuotaResponse, error) {
return c.c.GetQuota(ctx, in, opts...)
}
func (c *cachedAPIClient) InitiateFileDownload(ctx context.Context, in *provider.InitiateFileDownloadRequest, opts ...grpc.CallOption) (*provider.InitiateFileDownloadResponse, error) {
return c.c.InitiateFileDownload(ctx, in, opts...)
}
func (c *cachedAPIClient) InitiateFileUpload(ctx context.Context, in *provider.InitiateFileUploadRequest, opts ...grpc.CallOption) (*provider.InitiateFileUploadResponse, error) {
return c.c.InitiateFileUpload(ctx, in, opts...)
}
func (c *cachedAPIClient) ListGrants(ctx context.Context, in *provider.ListGrantsRequest, opts ...grpc.CallOption) (*provider.ListGrantsResponse, error) {
return c.c.ListGrants(ctx, in, opts...)
}
func (c *cachedAPIClient) ListContainerStream(ctx context.Context, in *provider.ListContainerStreamRequest, opts ...grpc.CallOption) (provider.ProviderAPI_ListContainerStreamClient, error) {
return c.c.ListContainerStream(ctx, in, opts...)
}
func (c *cachedAPIClient) ListContainer(ctx context.Context, in *provider.ListContainerRequest, opts ...grpc.CallOption) (*provider.ListContainerResponse, error) {
return c.c.ListContainer(ctx, in, opts...)
}
func (c *cachedAPIClient) ListFileVersions(ctx context.Context, in *provider.ListFileVersionsRequest, opts ...grpc.CallOption) (*provider.ListFileVersionsResponse, error) {
return c.c.ListFileVersions(ctx, in, opts...)
}
func (c *cachedAPIClient) ListRecycleStream(ctx context.Context, in *provider.ListRecycleStreamRequest, opts ...grpc.CallOption) (provider.ProviderAPI_ListRecycleStreamClient, error) {
return c.c.ListRecycleStream(ctx, in, opts...)
}
func (c *cachedAPIClient) ListRecycle(ctx context.Context, in *provider.ListRecycleRequest, opts ...grpc.CallOption) (*provider.ListRecycleResponse, error) {
return c.c.ListRecycle(ctx, in, opts...)
}
func (c *cachedAPIClient) Move(ctx context.Context, in *provider.MoveRequest, opts ...grpc.CallOption) (*provider.MoveResponse, error) {
return c.c.Move(ctx, in, opts...)
}
func (c *cachedAPIClient) RemoveGrant(ctx context.Context, in *provider.RemoveGrantRequest, opts ...grpc.CallOption) (*provider.RemoveGrantResponse, error) {
return c.c.RemoveGrant(ctx, in, opts...)
}
func (c *cachedAPIClient) PurgeRecycle(ctx context.Context, in *provider.PurgeRecycleRequest, opts ...grpc.CallOption) (*provider.PurgeRecycleResponse, error) {
return c.c.PurgeRecycle(ctx, in, opts...)
}
func (c *cachedAPIClient) RestoreFileVersion(ctx context.Context, in *provider.RestoreFileVersionRequest, opts ...grpc.CallOption) (*provider.RestoreFileVersionResponse, error) {
return c.c.RestoreFileVersion(ctx, in, opts...)
}
func (c *cachedAPIClient) RestoreRecycleItem(ctx context.Context, in *provider.RestoreRecycleItemRequest, opts ...grpc.CallOption) (*provider.RestoreRecycleItemResponse, error) {
return c.c.RestoreRecycleItem(ctx, in, opts...)
}
func (c *cachedAPIClient) UpdateGrant(ctx context.Context, in *provider.UpdateGrantRequest, opts ...grpc.CallOption) (*provider.UpdateGrantResponse, error) {
return c.c.UpdateGrant(ctx, in, opts...)
}
func (c *cachedAPIClient) CreateSymlink(ctx context.Context, in *provider.CreateSymlinkRequest, opts ...grpc.CallOption) (*provider.CreateSymlinkResponse, error) {
return c.c.CreateSymlink(ctx, in, opts...)
}
func (c *cachedAPIClient) CreateReference(ctx context.Context, in *provider.CreateReferenceRequest, opts ...grpc.CallOption) (*provider.CreateReferenceResponse, error) {
return c.c.CreateReference(ctx, in, opts...)
}
func (c *cachedAPIClient) SetArbitraryMetadata(ctx context.Context, in *provider.SetArbitraryMetadataRequest, opts ...grpc.CallOption) (*provider.SetArbitraryMetadataResponse, error) {
return c.c.SetArbitraryMetadata(ctx, in, opts...)
}
func (c *cachedAPIClient) UnsetArbitraryMetadata(ctx context.Context, in *provider.UnsetArbitraryMetadataRequest, opts ...grpc.CallOption) (*provider.UnsetArbitraryMetadataResponse, error) {
return c.c.UnsetArbitraryMetadata(ctx, in, opts...)
}
func (c *cachedAPIClient) SetLock(ctx context.Context, in *provider.SetLockRequest, opts ...grpc.CallOption) (*provider.SetLockResponse, error) {
return c.c.SetLock(ctx, in, opts...)
}
func (c *cachedAPIClient) GetLock(ctx context.Context, in *provider.GetLockRequest, opts ...grpc.CallOption) (*provider.GetLockResponse, error) {
return c.c.GetLock(ctx, in, opts...)
}
func (c *cachedAPIClient) RefreshLock(ctx context.Context, in *provider.RefreshLockRequest, opts ...grpc.CallOption) (*provider.RefreshLockResponse, error) {
return c.c.RefreshLock(ctx, in, opts...)
}
func (c *cachedAPIClient) Unlock(ctx context.Context, in *provider.UnlockRequest, opts ...grpc.CallOption) (*provider.UnlockResponse, error) {
return c.c.Unlock(ctx, in, opts...)
}
func (c *cachedAPIClient) GetHome(ctx context.Context, in *provider.GetHomeRequest, opts ...grpc.CallOption) (*provider.GetHomeResponse, error) {
return c.c.GetHome(ctx, in, opts...)
}
func (c *cachedAPIClient) TouchFile(ctx context.Context, in *provider.TouchFileRequest, opts ...grpc.CallOption) (*provider.TouchFileResponse, error) {
return c.c.TouchFile(ctx, in, opts...)
}
func (c *cachedAPIClient) AddLabel(ctx context.Context, in *provider.AddLabelRequest, opts ...grpc.CallOption) (*provider.AddLabelResponse, error) {
return c.c.AddLabel(ctx, in, opts...)
}
func (c *cachedAPIClient) RemoveLabel(ctx context.Context, in *provider.RemoveLabelRequest, opts ...grpc.CallOption) (*provider.RemoveLabelResponse, error) {
return c.c.RemoveLabel(ctx, in, opts...)
}
@@ -0,0 +1,61 @@
// Copyright 2018-2021 CERN
// Copyright 2026 OpenCloud GmbH
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
tenant "github.com/cs3org/go-cs3apis/cs3/identity/tenant/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (s *svc) GetTenant(ctx context.Context, req *tenant.GetTenantRequest) (*tenant.GetTenantResponse, error) {
c, err := pool.GetTenantProviderServiceClient(s.c.TenantProviderEndpoint)
if err != nil {
return &tenant.GetTenantResponse{
Status: status.NewInternal(ctx, "error getting tenant service client"),
}, nil
}
res, err := c.GetTenant(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetTenant")
}
return res, nil
}
func (s *svc) GetTenantByClaim(ctx context.Context, req *tenant.GetTenantByClaimRequest) (*tenant.GetTenantByClaimResponse, error) {
c, err := pool.GetTenantProviderServiceClient(s.c.TenantProviderEndpoint)
if err != nil {
return &tenant.GetTenantByClaimResponse{
Status: status.NewInternal(ctx, "error getting tenant service client"),
}, nil
}
res, err := c.GetTenantByClaim(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetTenantByClaim")
}
return res, nil
}
@@ -0,0 +1,92 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (s *svc) GetUser(ctx context.Context, req *user.GetUserRequest) (*user.GetUserResponse, error) {
c, err := pool.GetUserProviderServiceClient(s.c.UserProviderEndpoint)
if err != nil {
return &user.GetUserResponse{
Status: status.NewInternal(ctx, "error getting auth client"),
}, nil
}
res, err := c.GetUser(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetUser")
}
return res, nil
}
func (s *svc) GetUserByClaim(ctx context.Context, req *user.GetUserByClaimRequest) (*user.GetUserByClaimResponse, error) {
c, err := pool.GetUserProviderServiceClient(s.c.UserProviderEndpoint)
if err != nil {
return &user.GetUserByClaimResponse{
Status: status.NewInternal(ctx, "error getting auth client"),
}, nil
}
res, err := c.GetUserByClaim(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetUserByClaim")
}
return res, nil
}
func (s *svc) FindUsers(ctx context.Context, req *user.FindUsersRequest) (*user.FindUsersResponse, error) {
c, err := pool.GetUserProviderServiceClient(s.c.UserProviderEndpoint)
if err != nil {
return &user.FindUsersResponse{
Status: status.NewInternal(ctx, "error getting auth client"),
}, nil
}
res, err := c.FindUsers(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling FindUsers")
}
return res, nil
}
func (s *svc) GetUserGroups(ctx context.Context, req *user.GetUserGroupsRequest) (*user.GetUserGroupsResponse, error) {
c, err := pool.GetUserProviderServiceClient(s.c.UserProviderEndpoint)
if err != nil {
return &user.GetUserGroupsResponse{
Status: status.NewInternal(ctx, "error getting auth client"),
}, nil
}
res, err := c.GetUserGroups(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetUserGroups")
}
return res, nil
}
@@ -0,0 +1,644 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/grants"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/pkg/errors"
)
// TODO(labkode): add multi-phase commit logic when commit share or commit ref is enabled.
func (s *svc) CreateShare(ctx context.Context, req *collaboration.CreateShareRequest) (*collaboration.CreateShareResponse, error) {
return s.addShare(ctx, req)
}
func (s *svc) RemoveShare(ctx context.Context, req *collaboration.RemoveShareRequest) (*collaboration.RemoveShareResponse, error) {
return s.removeShare(ctx, req)
}
func (s *svc) UpdateShare(ctx context.Context, req *collaboration.UpdateShareRequest) (*collaboration.UpdateShareResponse, error) {
return s.updateShare(ctx, req)
}
// TODO(labkode): we need to validate share state vs storage grant and storage ref
// If there are any inconsistencies, the share needs to be flag as invalid and a background process
// or active fix needs to be performed.
func (s *svc) GetShare(ctx context.Context, req *collaboration.GetShareRequest) (*collaboration.GetShareResponse, error) {
return s.getShare(ctx, req)
}
func (s *svc) getShare(ctx context.Context, req *collaboration.GetShareRequest) (*collaboration.GetShareResponse, error) {
c, err := pool.GetUserShareProviderClient(s.c.UserShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Msg("getShare: failed to get user share provider")
return &collaboration.GetShareResponse{
Status: status.NewInternal(ctx, "error getting user share provider client"),
}, nil
}
res, err := c.GetShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetShare")
}
return res, nil
}
// TODO(labkode): read GetShare comment.
func (s *svc) ListShares(ctx context.Context, req *collaboration.ListSharesRequest) (*collaboration.ListSharesResponse, error) {
c, err := pool.GetUserShareProviderClient(s.c.UserShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Msg("ListShares: failed to get user share provider")
return &collaboration.ListSharesResponse{
Status: status.NewInternal(ctx, "error getting user share provider client"),
}, nil
}
res, err := c.ListShares(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ListShares")
}
return res, nil
}
func (s *svc) ListExistingShares(_ context.Context, _ *collaboration.ListSharesRequest) (*gateway.ListExistingSharesResponse, error) {
return nil, errtypes.NotSupported("method ListExistingShares not implemented")
}
func (s *svc) updateShare(ctx context.Context, req *collaboration.UpdateShareRequest) (*collaboration.UpdateShareResponse, error) {
// TODO: update wopi server
// FIXME This is a workaround that should prevent removing or changing the share permissions when the file is locked.
// https://github.com/owncloud/ocis/issues/8474
if status, err := s.checkLock(ctx, req.GetShare().GetId()); err != nil {
return &collaboration.UpdateShareResponse{
Status: status,
}, nil
}
c, err := pool.GetUserShareProviderClient(s.c.UserShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Msg("UpdateShare: failed to get user share provider")
return &collaboration.UpdateShareResponse{
Status: status.NewInternal(ctx, "error getting share provider client"),
}, nil
}
res, err := c.UpdateShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling UpdateShare")
}
if res.GetStatus().GetCode() != rpc.Code_CODE_OK {
return res, nil
}
if s.c.CommitShareToStorageGrant {
creator, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
return nil, errors.New("user not found in context")
}
grant := &provider.Grant{
Grantee: res.GetShare().GetGrantee(),
Permissions: res.GetShare().GetPermissions().GetPermissions(),
Expiration: res.GetShare().GetExpiration(),
Creator: creator.GetId(),
}
var opaque *typesv1beta1.Opaque
if refIsSpaceRoot(res.GetShare().GetResourceId()) {
opaque = utils.SpaceGrantOpaque()
utils.AppendPlainToOpaque(opaque, "spacetype", utils.ReadPlainFromOpaque(req.GetOpaque(), "spacetype"))
}
updateGrantStatus, err := s.updateGrant(ctx, res.GetShare().GetResourceId(), grant, opaque)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling updateGrant")
}
if updateGrantStatus.Code != rpc.Code_CODE_OK {
return &collaboration.UpdateShareResponse{
Status: updateGrantStatus,
Share: res.GetShare(),
}, nil
}
}
return res, nil
}
// TODO(labkode): listing received shares just goes to the user share manager and gets the list of
// received shares. The display name of the shares should be the a friendly name, like the basename
// of the original file.
func (s *svc) ListReceivedShares(ctx context.Context, req *collaboration.ListReceivedSharesRequest) (*collaboration.ListReceivedSharesResponse, error) {
logger := appctx.GetLogger(ctx)
c, err := pool.GetUserShareProviderClient(s.c.UserShareProviderEndpoint)
if err != nil {
logger.Error().
Err(err).
Msg("ListReceivedShares: failed to get user share provider")
return &collaboration.ListReceivedSharesResponse{
Status: status.NewInternal(ctx, "error getting share provider client"),
}, nil
}
res, err := c.ListReceivedShares(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ListReceivedShares")
}
return res, nil
}
func (s *svc) GetReceivedShare(ctx context.Context, req *collaboration.GetReceivedShareRequest) (*collaboration.GetReceivedShareResponse, error) {
c, err := pool.GetUserShareProviderClient(s.c.UserShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Msg("GetReceivedShare: failed to get user share provider")
return &collaboration.GetReceivedShareResponse{
Status: status.NewInternal(ctx, "error getting received share"),
}, nil
}
res, err := c.GetReceivedShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetReceivedShare")
}
return res, nil
}
// When updating a received share:
// if the update contains update for displayName:
// 1. if received share is mounted: we also do a rename in the storage
// 2. if received share is not mounted: we only rename in user share provider.
func (s *svc) UpdateReceivedShare(ctx context.Context, req *collaboration.UpdateReceivedShareRequest) (*collaboration.UpdateReceivedShareResponse, error) {
ctx, span := appctx.GetTracerProvider(ctx).Tracer("gateway").Start(ctx, "Gateway.UpdateReceivedShare")
defer span.End()
// sanity checks
switch {
case req.GetShare() == nil:
return &collaboration.UpdateReceivedShareResponse{
Status: status.NewInvalid(ctx, "updating requires a received share object"),
}, nil
case req.GetShare().GetShare() == nil:
return &collaboration.UpdateReceivedShareResponse{
Status: status.NewInvalid(ctx, "share missing"),
}, nil
case req.GetShare().GetShare().GetId() == nil:
return &collaboration.UpdateReceivedShareResponse{
Status: status.NewInvalid(ctx, "share id missing"),
}, nil
case req.GetShare().GetShare().GetId().GetOpaqueId() == "":
return &collaboration.UpdateReceivedShareResponse{
Status: status.NewInvalid(ctx, "share id empty"),
}, nil
}
c, err := pool.GetUserShareProviderClient(s.c.UserShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Msg("UpdateReceivedShare: failed to get user share provider")
return &collaboration.UpdateReceivedShareResponse{
Status: status.NewInternal(ctx, "error getting share provider client"),
}, nil
}
return c.UpdateReceivedShare(ctx, req)
/*
TODO: Leftover from master merge. Do we need this?
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Msg("UpdateReceivedShare: failed to get user share provider")
return &collaboration.UpdateReceivedShareResponse{
Status: status.NewInternal(ctx, "error getting share provider client"),
}, nil
}
// check if we have a resource id in the update response that we can use to update references
if res.GetShare().GetShare().GetResourceId() == nil {
log.Err(err).Msg("gateway: UpdateReceivedShare must return a ResourceId")
return &collaboration.UpdateReceivedShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
},
}, nil
}
// properties are updated in the order they appear in the field mask
// when an error occurs the request ends and no further fields are updated
for i := range req.UpdateMask.Paths {
switch req.UpdateMask.Paths[i] {
case "state":
switch req.GetShare().GetState() {
case collaboration.ShareState_SHARE_STATE_ACCEPTED:
rpcStatus := s.createReference(ctx, res.GetShare().GetShare().GetResourceId())
if rpcStatus.Code != rpc.Code_CODE_OK {
return &collaboration.UpdateReceivedShareResponse{Status: rpcStatus}, nil
}
case collaboration.ShareState_SHARE_STATE_REJECTED:
rpcStatus := s.removeReference(ctx, res.GetShare().GetShare().ResourceId)
if rpcStatus.Code != rpc.Code_CODE_OK && rpcStatus.Code != rpc.Code_CODE_NOT_FOUND {
return &collaboration.UpdateReceivedShareResponse{Status: rpcStatus}, nil
}
}
case "mount_point":
// TODO(labkode): implementing updating mount point
err = errtypes.NotSupported("gateway: update of mount point is not yet implemented")
return &collaboration.UpdateReceivedShareResponse{
Status: status.NewUnimplemented(ctx, err, "error updating received share"),
}, nil
default:
return nil, errtypes.NotSupported("updating " + req.UpdateMask.Paths[i] + " is not supported")
}
}
return res, nil
*/
}
func (s *svc) ListExistingReceivedShares(_ context.Context, _ *collaboration.ListReceivedSharesRequest) (*gateway.ListExistingReceivedSharesResponse, error) {
return nil, errtypes.NotSupported("Unimplemented")
}
func (s *svc) denyGrant(ctx context.Context, id *provider.ResourceId, g *provider.Grantee, opaque *typesv1beta1.Opaque) (*rpc.Status, error) {
ref := &provider.Reference{
ResourceId: id,
}
grantReq := &provider.DenyGrantRequest{
Ref: ref,
Grantee: g,
Opaque: opaque,
// TODO add creator
}
c, _, err := s.find(ctx, ref)
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Interface("reference", ref).
Msg("denyGrant: failed to get storage provider")
if _, ok := err.(errtypes.IsNotFound); ok {
return status.NewNotFound(ctx, "storage provider not found"), nil
}
return status.NewInternal(ctx, "error finding storage provider"), nil
}
grantRes, err := c.DenyGrant(ctx, grantReq)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling DenyGrant")
}
return grantRes.Status, nil
}
func (s *svc) addGrant(ctx context.Context, id *provider.ResourceId, g *provider.Grantee, p *provider.ResourcePermissions, expiration *typesv1beta1.Timestamp, opaque *typesv1beta1.Opaque) (*rpc.Status, error) {
ref := &provider.Reference{
ResourceId: id,
}
creator, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
return nil, errors.New("user not found in context")
}
grantReq := &provider.AddGrantRequest{
Ref: ref,
Grant: &provider.Grant{
Grantee: g,
Permissions: p,
Creator: creator.GetId(),
Expiration: expiration,
},
Opaque: opaque,
}
c, _, err := s.find(ctx, ref)
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Interface("reference", ref).
Msg("addGrant: failed to get storage provider")
if _, ok := err.(errtypes.IsNotFound); ok {
return status.NewNotFound(ctx, "storage provider not found"), nil
}
return status.NewInternal(ctx, "error finding storage provider"), nil
}
grantRes, err := c.AddGrant(ctx, grantReq)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling AddGrant")
}
return grantRes.Status, nil
}
func (s *svc) updateGrant(ctx context.Context, id *provider.ResourceId, grant *provider.Grant, opaque *typesv1beta1.Opaque) (*rpc.Status, error) {
ref := &provider.Reference{
ResourceId: id,
}
grantReq := &provider.UpdateGrantRequest{
Opaque: opaque,
Ref: ref,
Grant: grant,
}
c, _, err := s.find(ctx, ref)
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Interface("reference", ref).
Msg("updateGrant: failed to get storage provider")
if _, ok := err.(errtypes.IsNotFound); ok {
return status.NewNotFound(ctx, "storage provider not found"), nil
}
return status.NewInternal(ctx, "error finding storage provider"), nil
}
grantRes, err := c.UpdateGrant(ctx, grantReq)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling UpdateGrant")
}
if grantRes.Status.Code != rpc.Code_CODE_OK {
return status.NewInternal(ctx,
"error committing share to storage grant"), nil
}
return status.NewOK(ctx), nil
}
func (s *svc) removeGrant(ctx context.Context, id *provider.ResourceId, g *provider.Grantee, p *provider.ResourcePermissions, opaque *typesv1beta1.Opaque) (*rpc.Status, error) {
ref := &provider.Reference{
ResourceId: id,
}
grantReq := &provider.RemoveGrantRequest{
Ref: ref,
Grant: &provider.Grant{
Grantee: g,
Permissions: p,
},
Opaque: opaque,
}
c, _, err := s.find(ctx, ref)
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Interface("reference", ref).
Msg("removeGrant: failed to get storage provider")
if _, ok := err.(errtypes.IsNotFound); ok {
return status.NewNotFound(ctx, "storage provider not found"), nil
}
return status.NewInternal(ctx, "error finding storage provider"), nil
}
grantRes, err := c.RemoveGrant(ctx, grantReq)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling RemoveGrant")
}
if grantRes.Status.Code != rpc.Code_CODE_OK {
return grantRes.GetStatus(), nil
}
return status.NewOK(ctx), nil
}
func (s *svc) addShare(ctx context.Context, req *collaboration.CreateShareRequest) (*collaboration.CreateShareResponse, error) {
c, err := pool.GetUserShareProviderClient(s.c.UserShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Msg("CreateShare: failed to get user share provider")
return &collaboration.CreateShareResponse{
Status: status.NewInternal(ctx, "error getting user share provider client"),
}, nil
}
// TODO the user share manager needs to be able to decide if the current user is allowed to create that share (and not eg. incerase permissions)
// jfd: AFAICT this can only be determined by a storage driver - either the storage provider is queried first or the share manager needs to access the storage using a storage driver
res, err := c.CreateShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling CreateShare")
}
if res.Status.Code != rpc.Code_CODE_OK {
return res, nil
}
rollBackFn := func(status *rpc.Status) {
rmvReq := &collaboration.RemoveShareRequest{
Ref: &collaboration.ShareReference{
Spec: &collaboration.ShareReference_Key{
Key: &collaboration.ShareKey{
ResourceId: req.ResourceInfo.Id,
Grantee: req.Grant.Grantee,
},
},
},
}
appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg("rollback the CreateShare attempt")
if resp, err := s.removeShare(ctx, rmvReq); err != nil {
appctx.GetLogger(ctx).Debug().Interface("status", resp.GetStatus()).Interface("req", req).Msg(err.Error())
}
}
if s.c.CommitShareToStorageGrant {
// If the share is a denial we call denyGrant instead.
var status *rpc.Status
var opaque *typesv1beta1.Opaque
if refIsSpaceRoot(req.ResourceInfo.Id) {
opaque = utils.SpaceGrantOpaque()
utils.AppendPlainToOpaque(opaque, "spacetype", req.ResourceInfo.GetSpace().GetSpaceType())
}
if grants.PermissionsEqual(req.Grant.Permissions.Permissions, &provider.ResourcePermissions{}) {
status, err = s.denyGrant(ctx, req.ResourceInfo.Id, req.Grant.Grantee, opaque)
if err != nil {
return nil, errors.Wrap(err, "gateway: error denying grant in storage")
}
} else {
status, err = s.addGrant(ctx, req.ResourceInfo.Id, req.Grant.Grantee, req.Grant.Permissions.Permissions, req.Grant.Expiration, opaque)
if err != nil {
appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg(err.Error())
rollBackFn(status)
return nil, errors.Wrap(err, "gateway: error adding grant to storage")
}
}
switch status.Code {
case rpc.Code_CODE_OK, rpc.Code_CODE_ALREADY_EXISTS:
// ok
case rpc.Code_CODE_UNIMPLEMENTED:
appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg("storing grants not supported, ignoring")
rollBackFn(status)
default:
appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg("storing grants is not successful")
rollBackFn(status)
return &collaboration.CreateShareResponse{
Status: status,
}, err
}
}
return res, nil
}
func (s *svc) removeShare(ctx context.Context, req *collaboration.RemoveShareRequest) (*collaboration.RemoveShareResponse, error) {
c, err := pool.GetUserShareProviderClient(s.c.UserShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Msg("RemoveShare: failed to get user share provider")
return &collaboration.RemoveShareResponse{
Status: status.NewInternal(ctx, "error getting user share provider client"),
}, nil
}
// if we need to commit the share, we need the resource it points to.
var share *collaboration.Share
// FIXME: I will cause a panic as share will be nil when I'm false
if s.c.CommitShareToStorageGrant {
getShareReq := &collaboration.GetShareRequest{
Ref: req.Ref,
}
getShareRes, err := c.GetShare(ctx, getShareReq)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetShare")
}
if getShareRes.Status.Code != rpc.Code_CODE_OK {
res := &collaboration.RemoveShareResponse{
Status: status.NewInternal(ctx,
"error getting share when committing to the storage"),
}
return res, nil
}
share = getShareRes.Share
}
// TODO: update wopi server
// FIXME This is a workaround that should prevent removing or changing the share permissions when the file is locked.
// https://github.com/owncloud/ocis/issues/8474
if status, err := s.checkShareLock(ctx, share); err != nil {
return &collaboration.RemoveShareResponse{
Status: status,
}, nil
}
res, err := c.RemoveShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling RemoveShare")
}
if res.Status.Code != rpc.Code_CODE_OK {
return res, nil
}
if s.c.CommitShareToStorageGrant {
var opaque *typesv1beta1.Opaque
if refIsSpaceRoot(share.ResourceId) {
opaque = utils.SpaceGrantOpaque()
}
removeGrantStatus, err := s.removeGrant(ctx, share.ResourceId, share.Grantee, share.Permissions.Permissions, opaque)
if err != nil {
return nil, errors.Wrap(err, "gateway: error removing grant from storage")
}
if removeGrantStatus.Code != rpc.Code_CODE_OK {
return &collaboration.RemoveShareResponse{
Status: removeGrantStatus,
}, err
}
}
return res, nil
}
func (s *svc) checkLock(ctx context.Context, shareId *collaboration.ShareId) (*rpc.Status, error) {
logger := appctx.GetLogger(ctx)
getShareRes, err := s.GetShare(ctx, &collaboration.GetShareRequest{
Ref: &collaboration.ShareReference{
Spec: &collaboration.ShareReference_Id{Id: shareId},
},
})
if err != nil {
msg := "gateway: error calling GetShare"
logger.Err(err).Interface("share_id", shareId).Msg(msg)
return status.NewInternal(ctx, msg), errors.Wrap(err, msg)
}
if getShareRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
msg := "can not get share stat " + getShareRes.GetStatus().GetMessage()
logger.Debug().Interface("share", shareId).Msg(msg)
if getShareRes.GetStatus().GetCode() != rpc.Code_CODE_NOT_FOUND {
return status.NewNotFound(ctx, msg), errors.New(msg)
}
return status.NewInternal(ctx, msg), errors.New(msg)
}
return s.checkShareLock(ctx, getShareRes.Share)
}
func (s *svc) checkShareLock(ctx context.Context, share *collaboration.Share) (*rpc.Status, error) {
logger := appctx.GetLogger(ctx)
sRes, err := s.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: share.GetResourceId()},
ArbitraryMetadataKeys: []string{"lockdiscovery"}})
if err != nil {
msg := "failed to stat shared resource"
logger.Err(err).Interface("resource_id", share.GetResourceId()).Msg(msg)
return status.NewInternal(ctx, msg), errors.Wrap(err, msg)
}
if sRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
msg := "can not get share stat " + sRes.GetStatus().GetMessage()
logger.Debug().Interface("lock", sRes.GetInfo().GetLock()).Msg(msg)
if sRes.GetStatus().GetCode() != rpc.Code_CODE_NOT_FOUND {
return status.NewNotFound(ctx, msg), errors.New(msg)
}
return status.NewInternal(ctx, msg), errors.New(msg)
}
if sRes.GetInfo().GetLock() != nil {
msg := "can not change grants, the shared resource is locked"
logger.Debug().Interface("lock", sRes.GetInfo().GetLock()).Msg(msg)
return status.NewLocked(ctx, msg), errors.New(msg)
}
return nil, nil
}
func refIsSpaceRoot(ref *provider.ResourceId) bool {
if ref == nil {
return false
}
if ref.SpaceId == "" || ref.OpaqueId == "" {
return false
}
return ref.SpaceId == ref.OpaqueId
}
@@ -0,0 +1,106 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
"net/url"
"path"
"strings"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/pkg/errors"
)
type webdavEndpoint struct {
filePath string
endpoint string
token string
}
func (s *svc) extractEndpointInfo(ctx context.Context, targetURL string) (*webdavEndpoint, error) {
if targetURL == "" {
return nil, errtypes.BadRequest("gateway: ref target is an empty uri")
}
uri, err := url.Parse(targetURL)
if err != nil {
return nil, errors.Wrap(err, "gateway: error parsing target uri: "+targetURL)
}
if uri.Scheme != "webdav" {
return nil, errtypes.NotSupported("ref target does not have the webdav scheme")
}
m, err := url.ParseQuery(uri.RawQuery)
if err != nil {
return nil, errors.Wrap(err, "gateway: error parsing target resource name")
}
return &webdavEndpoint{
filePath: m["name"][0],
endpoint: uri.Host,
token: uri.User.String(),
}, nil
}
func (s *svc) getWebdavEndpoint(ctx context.Context, domain string) (string, error) {
meshProvider, err := s.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{
Domain: domain,
})
if err != nil {
return "", errors.Wrap(err, "gateway: error calling GetInfoByDomain")
}
for _, s := range meshProvider.ProviderInfo.Services {
if strings.ToLower(s.Endpoint.Type.Name) == "webdav" {
return s.Endpoint.Path, nil
}
}
return "", errtypes.NotFound(domain)
}
func (s *svc) getWebdavHost(ctx context.Context, domain string) (string, error) {
meshProvider, err := s.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{
Domain: domain,
})
if err != nil {
return "", errors.Wrap(err, "gateway: error calling GetInfoByDomain")
}
for _, s := range meshProvider.ProviderInfo.Services {
if strings.ToLower(s.Endpoint.Type.Name) == "webdav" {
return s.Host, nil
}
}
return "", errtypes.NotFound(domain)
}
func appendNameQuery(targetURL string, nameQueries ...string) (string, error) {
uri, err := url.Parse(targetURL)
if err != nil {
return "", err
}
q, err := url.ParseQuery(uri.RawQuery)
if err != nil {
return "", err
}
name := append([]string{q["name"][0]}, nameQueries...)
q.Set("name", path.Join(name...))
uri.RawQuery = q.Encode()
return uri.String(), nil
}
@@ -0,0 +1,207 @@
// Copyright 2018-2020 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package groupprovider
import (
"context"
"fmt"
"sort"
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/group"
"github.com/opencloud-eu/reva/v2/pkg/group/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("groupprovider", New)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "json"
}
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
c.init()
return c, nil
}
func getDriver(c *config) (group.Manager, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound(fmt.Sprintf("driver %s not found for group manager", c.Driver))
}
// New returns a new GroupProviderServiceServer.
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
groupManager, err := getDriver(c)
if err != nil {
return nil, err
}
svc := &service{groupmgr: groupManager}
return svc, nil
}
type service struct {
groupmgr group.Manager
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{}
}
func (s *service) Register(ss *grpc.Server) {
grouppb.RegisterGroupAPIServer(ss, s)
}
func (s *service) GetGroup(ctx context.Context, req *grouppb.GetGroupRequest) (*grouppb.GetGroupResponse, error) {
if req.GroupId == nil {
res := &grouppb.GetGroupResponse{
Status: status.NewInvalid(ctx, "groupid missing"),
}
return res, nil
}
group, err := s.groupmgr.GetGroup(ctx, req.GroupId, req.SkipFetchingMembers)
if err != nil {
res := &grouppb.GetGroupResponse{}
if _, ok := err.(errtypes.NotFound); ok {
res.Status = status.NewNotFound(ctx, "group not found")
} else {
res.Status = status.NewInternal(ctx, "error getting group")
}
return res, nil
}
return &grouppb.GetGroupResponse{
Status: status.NewOK(ctx),
Group: group,
}, nil
}
func (s *service) GetGroupByClaim(ctx context.Context, req *grouppb.GetGroupByClaimRequest) (*grouppb.GetGroupByClaimResponse, error) {
group, err := s.groupmgr.GetGroupByClaim(ctx, req.Claim, req.Value, req.SkipFetchingMembers)
if err != nil {
res := &grouppb.GetGroupByClaimResponse{}
if _, ok := err.(errtypes.NotFound); ok {
res.Status = status.NewNotFound(ctx, fmt.Sprintf("group not found %s %s", req.Claim, req.Value))
} else {
res.Status = status.NewInternal(ctx, "error getting group by claim")
}
return res, nil
}
return &grouppb.GetGroupByClaimResponse{
Status: status.NewOK(ctx),
Group: group,
}, nil
}
func (s *service) FindGroups(ctx context.Context, req *grouppb.FindGroupsRequest) (*grouppb.FindGroupsResponse, error) {
if len(req.Filters) > 1 || req.Filters[0].GetType() != grouppb.Filter_TYPE_QUERY {
return nil, fmt.Errorf("only one query filter supported")
}
groups, err := s.groupmgr.FindGroups(ctx, req.Filters[0].GetQuery(), req.SkipFetchingMembers)
if err != nil {
return &grouppb.FindGroupsResponse{
Status: status.NewInternal(ctx, "error finding groups"),
}, nil
}
// sort group by groupname
sort.Slice(groups, func(i, j int) bool {
return groups[i].GroupName <= groups[j].GroupName
})
return &grouppb.FindGroupsResponse{
Status: status.NewOK(ctx),
Groups: groups,
}, nil
}
func (s *service) GetMembers(ctx context.Context, req *grouppb.GetMembersRequest) (*grouppb.GetMembersResponse, error) {
if req.GroupId == nil {
res := &grouppb.GetMembersResponse{
Status: status.NewInvalid(ctx, "groupid missing"),
}
return res, nil
}
members, err := s.groupmgr.GetMembers(ctx, req.GroupId)
if err != nil {
return &grouppb.GetMembersResponse{
Status: status.NewInternal(ctx, "error getting group members"),
}, nil
}
return &grouppb.GetMembersResponse{
Status: status.NewOK(ctx),
Members: members,
}, nil
}
func (s *service) HasMember(ctx context.Context, req *grouppb.HasMemberRequest) (*grouppb.HasMemberResponse, error) {
if req.GroupId == nil || req.UserId == nil {
res := &grouppb.HasMemberResponse{
Status: status.NewInvalid(ctx, "groupid or userid missing"),
}
return res, nil
}
ok, err := s.groupmgr.HasMember(ctx, req.GroupId, req.UserId)
if err != nil {
return &grouppb.HasMemberResponse{
Status: status.NewInternal(ctx, "error checking for group member"),
}, nil
}
return &grouppb.HasMemberResponse{
Status: status.NewOK(ctx),
Ok: ok,
}, nil
}
@@ -0,0 +1,82 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package helloworld
import (
"context"
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/internal/grpc/services/helloworld/proto"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("helloworld", New)
}
type conf struct {
Message string `mapstructure:"message"`
}
type service struct {
conf *conf
}
// New returns a new PreferencesServiceServer
// It can be tested like this:
// prototool grpc --address 0.0.0.0:9999 --method 'revad.helloworld.HelloWorldService/Hello' --data '{"name": "Alice"}'
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c := &conf{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "helloworld: error decoding conf")
return nil, err
}
if c.Message == "" {
c.Message = "Hello"
}
service := &service{conf: c}
return service, nil
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{}
}
func (s *service) Register(ss *grpc.Server) {
proto.RegisterHelloWorldServiceServer(ss, s)
}
func (s *service) Hello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloResponse, error) {
if req.Name == "" {
req.Name = "Mr. Nobody"
}
message := fmt.Sprintf("%s %s", s.conf.Message, req.Name)
res := &proto.HelloResponse{
Message: message,
}
return res, nil
}
@@ -0,0 +1,224 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: helloworld.proto
package proto
import (
context "context"
fmt "fmt"
math "math"
proto "github.com/golang/protobuf/proto"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
type HelloRequest struct {
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *HelloRequest) Reset() { *m = HelloRequest{} }
func (m *HelloRequest) String() string { return proto.CompactTextString(m) }
func (*HelloRequest) ProtoMessage() {}
func (*HelloRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_f5f9b8b18f6fb9a6, []int{0}
}
func (m *HelloRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_HelloRequest.Unmarshal(m, b)
}
func (m *HelloRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_HelloRequest.Marshal(b, m, deterministic)
}
func (m *HelloRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_HelloRequest.Merge(m, src)
}
func (m *HelloRequest) XXX_Size() int {
return xxx_messageInfo_HelloRequest.Size(m)
}
func (m *HelloRequest) XXX_DiscardUnknown() {
xxx_messageInfo_HelloRequest.DiscardUnknown(m)
}
var xxx_messageInfo_HelloRequest proto.InternalMessageInfo
func (m *HelloRequest) GetName() string {
if m != nil {
return m.Name
}
return ""
}
type HelloResponse struct {
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *HelloResponse) Reset() { *m = HelloResponse{} }
func (m *HelloResponse) String() string { return proto.CompactTextString(m) }
func (*HelloResponse) ProtoMessage() {}
func (*HelloResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_f5f9b8b18f6fb9a6, []int{1}
}
func (m *HelloResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_HelloResponse.Unmarshal(m, b)
}
func (m *HelloResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_HelloResponse.Marshal(b, m, deterministic)
}
func (m *HelloResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_HelloResponse.Merge(m, src)
}
func (m *HelloResponse) XXX_Size() int {
return xxx_messageInfo_HelloResponse.Size(m)
}
func (m *HelloResponse) XXX_DiscardUnknown() {
xxx_messageInfo_HelloResponse.DiscardUnknown(m)
}
var xxx_messageInfo_HelloResponse proto.InternalMessageInfo
func (m *HelloResponse) GetMessage() string {
if m != nil {
return m.Message
}
return ""
}
func init() {
proto.RegisterType((*HelloRequest)(nil), "revad.helloworld.HelloRequest")
proto.RegisterType((*HelloResponse)(nil), "revad.helloworld.HelloResponse")
}
func init() { proto.RegisterFile("helloworld.proto", fileDescriptor_f5f9b8b18f6fb9a6) }
var fileDescriptor_f5f9b8b18f6fb9a6 = []byte{
// 162 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0xce, 0x48, 0xcd, 0xc9,
0xc9, 0x2f, 0xcf, 0x2f, 0xca, 0x49, 0x29, 0x2e, 0x4b, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17,
0x12, 0x28, 0x4a, 0x2d, 0x4b, 0x4c, 0xd1, 0x43, 0x48, 0x29, 0x29, 0x71, 0xf1, 0x78, 0x80, 0x78,
0x41, 0xa9, 0x85, 0xa5, 0xa9, 0xc5, 0x25, 0x42, 0x42, 0x5c, 0x2c, 0x79, 0x89, 0xb9, 0xa9, 0x12,
0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x60, 0xb6, 0x92, 0x26, 0x17, 0x2f, 0x54, 0x4d, 0x71, 0x41,
0x7e, 0x5e, 0x71, 0xaa, 0x90, 0x04, 0x17, 0x7b, 0x6e, 0x6a, 0x71, 0x71, 0x62, 0x7a, 0xaa, 0x04,
0x13, 0x58, 0x1d, 0x8c, 0x6b, 0x14, 0xcb, 0x25, 0x08, 0x56, 0x1a, 0x0e, 0x32, 0x3c, 0x38, 0xb5,
0xa8, 0x2c, 0x33, 0x39, 0x55, 0xc8, 0x83, 0x8b, 0x15, 0x2c, 0x28, 0x24, 0xa7, 0x87, 0x6e, 0xbf,
0x1e, 0xb2, 0xe5, 0x52, 0xf2, 0x38, 0xe5, 0x21, 0x16, 0x3b, 0xb1, 0x47, 0xb1, 0x82, 0x3d, 0x92,
0xc4, 0x06, 0xa6, 0x8c, 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0xe7, 0xab, 0x90, 0x7f, 0xe6, 0x00,
0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// HelloWorldServiceClient is the client API for HelloWorldService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type HelloWorldServiceClient interface {
Hello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error)
}
type helloWorldServiceClient struct {
cc *grpc.ClientConn
}
func NewHelloWorldServiceClient(cc *grpc.ClientConn) HelloWorldServiceClient {
return &helloWorldServiceClient{cc}
}
func (c *helloWorldServiceClient) Hello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) {
out := new(HelloResponse)
err := c.cc.Invoke(ctx, "/revad.helloworld.HelloWorldService/Hello", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// HelloWorldServiceServer is the server API for HelloWorldService service.
type HelloWorldServiceServer interface {
Hello(context.Context, *HelloRequest) (*HelloResponse, error)
}
// UnimplementedHelloWorldServiceServer can be embedded to have forward compatible implementations.
type UnimplementedHelloWorldServiceServer struct {
}
func (*UnimplementedHelloWorldServiceServer) Hello(ctx context.Context, req *HelloRequest) (*HelloResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Hello not implemented")
}
func RegisterHelloWorldServiceServer(s *grpc.Server, srv HelloWorldServiceServer) {
s.RegisterService(&_HelloWorldService_serviceDesc, srv)
}
func _HelloWorldService_Hello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(HelloRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HelloWorldServiceServer).Hello(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/revad.helloworld.HelloWorldService/Hello",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HelloWorldServiceServer).Hello(ctx, req.(*HelloRequest))
}
return interceptor(ctx, in, info, handler)
}
var _HelloWorldService_serviceDesc = grpc.ServiceDesc{
ServiceName: "revad.helloworld.HelloWorldService",
HandlerType: (*HelloWorldServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Hello",
Handler: _HelloWorldService_Hello_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "helloworld.proto",
}
@@ -0,0 +1,35 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
syntax = "proto3";
package revad.helloworld;
option go_package = "proto";
service HelloWorldService {
rpc Hello(HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 2;
}
@@ -0,0 +1,8 @@
generate:
go_options:
import_path: github.com/cs3org/reva/cmd/revad/svcs/grpcsvcs/helloworldsvc/proto
plugins:
- name : go
type: go
flags: plugins=grpc
output: ./
@@ -0,0 +1,46 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package loader
import (
// Load core gRPC services.
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/applicationauth"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/appprovider"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/appregistry"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/authprovider"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/authregistry"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/datatx"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/gateway"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/groupprovider"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/helloworld"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/ocmcore"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/ocminvitemanager"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/ocmproviderauthorizer"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/ocmshareprovider"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/permissions"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/preferences"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/publicshareprovider"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/publicstorageprovider"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/sharesstorageprovider"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/storageprovider"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/storageregistry"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/userprovider"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/usershareprovider"
// Add your own service here
)
@@ -0,0 +1,148 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package ocmcore
import (
"context"
"fmt"
"time"
ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/ocm/share"
"github.com/opencloud-eu/reva/v2/pkg/ocm/share/repository/registry"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/utils/cfg"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("ocmcore", New)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
}
type service struct {
conf *config
repo share.Repository
}
func (c *config) ApplyDefaults() {
if c.Driver == "" {
c.Driver = "json"
}
}
func (s *service) Register(ss *grpc.Server) {
ocmcore.RegisterOcmCoreAPIServer(ss, s)
}
func getShareRepository(c *config) (share.Repository, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound(fmt.Sprintf("driver not found: %s", c.Driver))
}
// New creates a new ocm core svc.
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
repo, err := getShareRepository(&c)
if err != nil {
return nil, err
}
service := &service{
conf: &c,
repo: repo,
}
return service, nil
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{"/cs3.ocm.core.v1beta1.OcmCoreAPI/CreateOCMCoreShare"}
}
// CreateOCMCoreShare is called when an OCM request comes into this reva instance from.
func (s *service) CreateOCMCoreShare(ctx context.Context, req *ocmcore.CreateOCMCoreShareRequest) (*ocmcore.CreateOCMCoreShareResponse, error) {
if req.ShareType != ocm.ShareType_SHARE_TYPE_USER {
return nil, errtypes.NotSupported("share type not supported")
}
now := &typesv1beta1.Timestamp{
Seconds: uint64(time.Now().Unix()),
}
share, err := s.repo.StoreReceivedShare(ctx, &ocm.ReceivedShare{
RemoteShareId: req.ResourceId,
Name: req.Name,
Grantee: &providerpb.Grantee{
Type: providerpb.GranteeType_GRANTEE_TYPE_USER,
Id: &providerpb.Grantee_UserId{
UserId: req.ShareWith,
},
},
ResourceType: req.ResourceType,
ShareType: req.ShareType,
Owner: req.Owner,
Creator: req.Sender,
Protocols: req.Protocols,
Ctime: now,
Mtime: now,
Expiration: req.Expiration,
State: ocm.ShareState_SHARE_STATE_PENDING,
})
if err != nil {
// TODO: identify errors
return &ocmcore.CreateOCMCoreShareResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
return &ocmcore.CreateOCMCoreShareResponse{
Status: status.NewOK(ctx),
Id: share.Id.OpaqueId,
Created: share.Ctime,
}, nil
}
func (s *service) UpdateOCMCoreShare(ctx context.Context, req *ocmcore.UpdateOCMCoreShareRequest) (*ocmcore.UpdateOCMCoreShareResponse, error) {
return nil, errtypes.NotSupported("not implemented")
}
func (s *service) DeleteOCMCoreShare(ctx context.Context, req *ocmcore.DeleteOCMCoreShareRequest) (*ocmcore.DeleteOCMCoreShareResponse, error) {
return nil, errtypes.NotSupported("not implemented")
}
@@ -0,0 +1,392 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package ocminvitemanager
import (
"context"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/ocm/client"
"github.com/opencloud-eu/reva/v2/pkg/ocm/invite"
"github.com/opencloud-eu/reva/v2/pkg/ocm/invite/repository/registry"
ocmuser "github.com/opencloud-eu/reva/v2/pkg/ocm/user"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/opencloud-eu/reva/v2/pkg/utils/cfg"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("ocminvitemanager", New)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
TokenExpiration string `mapstructure:"token_expiration"`
OCMClientTimeout int `mapstructure:"ocm_timeout"`
OCMClientInsecure bool `mapstructure:"ocm_insecure"`
GatewaySVC string `mapstructure:"gatewaysvc" validate:"required"`
ProviderDomain string `mapstructure:"provider_domain" validate:"required" docs:"The same domain registered in the provider authorizer"`
tokenExpiration time.Duration
}
type service struct {
conf *config
repo invite.Repository
ocmClient *client.OCMClient
gatewaySelector *pool.Selector[gateway.GatewayAPIClient]
}
func (c *config) ApplyDefaults() {
if c.Driver == "" {
c.Driver = "json"
}
if c.TokenExpiration == "" {
c.TokenExpiration = "24h"
}
c.GatewaySVC = sharedconf.GetGatewaySVC(c.GatewaySVC)
}
func (s *service) Register(ss *grpc.Server) {
invitepb.RegisterInviteAPIServer(ss, s)
}
func getInviteRepository(c *config) (invite.Repository, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound("driver not found: " + c.Driver)
}
// New creates a new OCM invite manager svc.
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
p, err := time.ParseDuration(c.TokenExpiration)
if err != nil {
return nil, err
}
c.tokenExpiration = p
repo, err := getInviteRepository(&c)
if err != nil {
return nil, err
}
gatewaySelector, err := pool.GatewaySelector(c.GatewaySVC)
if err != nil {
return nil, err
}
service := &service{
conf: &c,
repo: repo,
ocmClient: client.New(&client.Config{
Timeout: time.Duration(c.OCMClientTimeout) * time.Second,
Insecure: c.OCMClientInsecure,
}),
gatewaySelector: gatewaySelector,
}
return service, nil
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{"/cs3.ocm.invite.v1beta1.InviteAPI/AcceptInvite", "/cs3.ocm.invite.v1beta1.InviteAPI/GetAcceptedUser"}
}
func (s *service) GenerateInviteToken(ctx context.Context, req *invitepb.GenerateInviteTokenRequest) (*invitepb.GenerateInviteTokenResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
token := CreateToken(s.conf.tokenExpiration, user.GetId(), req.Description)
if err := s.repo.AddToken(ctx, token); err != nil {
return &invitepb.GenerateInviteTokenResponse{
Status: status.NewInternal(ctx, "error generating invite token"),
}, nil
}
return &invitepb.GenerateInviteTokenResponse{
Status: status.NewOK(ctx),
InviteToken: token,
}, nil
}
func (s *service) ListInviteTokens(ctx context.Context, req *invitepb.ListInviteTokensRequest) (*invitepb.ListInviteTokensResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
tokens, err := s.repo.ListTokens(ctx, user.Id)
if err != nil {
return &invitepb.ListInviteTokensResponse{
Status: status.NewInternal(ctx, "error listing tokens"),
}, nil
}
return &invitepb.ListInviteTokensResponse{
Status: status.NewOK(ctx),
InviteTokens: tokens,
}, nil
}
func (s *service) ForwardInvite(ctx context.Context, req *invitepb.ForwardInviteRequest) (*invitepb.ForwardInviteResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
ocmEndpoint, err := getOCMEndpoint(req.GetOriginSystemProvider())
if err != nil {
return nil, err
}
// Accept the invitation on the remote OCM provider
remoteUser, err := s.ocmClient.InviteAccepted(ctx, ocmEndpoint, &client.InviteAcceptedRequest{
Token: req.InviteToken.GetToken(),
RecipientProvider: s.conf.ProviderDomain,
// The UserID is only a string here. To not lose the IDP information we use the LocalUserFederatedID encoding
// i.e. UserID@IDP
UserID: ocmuser.FormatOCMUser(user.GetId()),
Email: user.GetMail(),
Name: user.GetDisplayName(),
})
if err != nil {
switch {
case errors.Is(err, client.ErrTokenInvalid):
return &invitepb.ForwardInviteResponse{
Status: status.NewInvalid(ctx, "token not valid"),
}, nil
case errors.Is(err, client.ErrTokenNotFound):
return &invitepb.ForwardInviteResponse{
Status: status.NewNotFound(ctx, "token not found"),
}, nil
case errors.Is(err, client.ErrUserAlreadyAccepted):
return &invitepb.ForwardInviteResponse{
Status: status.NewAlreadyExists(ctx, err, err.Error()),
}, nil
case errors.Is(err, client.ErrServiceNotTrusted):
return &invitepb.ForwardInviteResponse{
Status: status.NewPermissionDenied(ctx, err, err.Error()),
}, nil
default:
return &invitepb.ForwardInviteResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
}
// create a link between the user that accepted the share (in ctx)
// and the remote one (the initiator), so at the end of the invitation workflow they
// know each other
// remoteUser.UserID is the federated ID (just a string), to get a unique CS3 userid
// we're using the provider domain as the IDP part of the ID
remoteUserID := &userpb.UserId{
Type: userpb.UserType_USER_TYPE_FEDERATED,
Idp: req.GetOriginSystemProvider().Domain,
OpaqueId: remoteUser.UserID,
}
// we need to use a unique identifier for federated users
remoteUserID = ocmuser.LocalUserFederatedID(remoteUserID, "")
if err := s.repo.AddRemoteUser(ctx, user.Id, &userpb.User{
Id: remoteUserID,
Mail: remoteUser.Email,
DisplayName: remoteUser.Name,
}); err != nil {
if errors.Is(err, invite.ErrUserAlreadyAccepted) {
return &invitepb.ForwardInviteResponse{
Status: status.NewAlreadyExists(ctx, err, err.Error()),
}, nil
}
return &invitepb.ForwardInviteResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
return &invitepb.ForwardInviteResponse{
Status: status.NewOK(ctx),
UserId: remoteUserID,
Email: remoteUser.Email,
DisplayName: remoteUser.Name,
}, nil
}
func getOCMEndpoint(originProvider *ocmprovider.ProviderInfo) (string, error) {
for _, s := range originProvider.Services {
if s.Endpoint.Type.Name == "OCM" {
return s.Endpoint.Path, nil
}
}
return "", errors.New("ocm endpoint not specified for mesh provider")
}
func (s *service) AcceptInvite(ctx context.Context, req *invitepb.AcceptInviteRequest) (*invitepb.AcceptInviteResponse, error) {
token, err := s.repo.GetToken(ctx, req.InviteToken.Token)
if err != nil {
if errors.Is(err, invite.ErrTokenNotFound) {
return &invitepb.AcceptInviteResponse{
Status: status.NewNotFound(ctx, "token not found"),
}, nil
}
return &invitepb.AcceptInviteResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
if !isTokenValid(token) {
return &invitepb.AcceptInviteResponse{
Status: status.NewInvalid(ctx, "token is not valid"),
}, nil
}
initiator, err := s.getUserInfo(ctx, token.UserId)
if err != nil {
return &invitepb.AcceptInviteResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
remoteUser := req.GetRemoteUser()
// we need to use a unique identifier for federated users
remoteUser.Id = ocmuser.LocalUserFederatedID(remoteUser.Id, "")
if err := s.repo.AddRemoteUser(ctx, token.GetUserId(), remoteUser); err != nil {
if !errors.Is(err, invite.ErrUserAlreadyAccepted) {
// skip error if user was already accepted
return &invitepb.AcceptInviteResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
}
return &invitepb.AcceptInviteResponse{
Status: status.NewOK(ctx),
UserId: initiator.GetId(),
Email: initiator.Mail,
DisplayName: initiator.DisplayName,
}, nil
}
func (s *service) getUserInfo(ctx context.Context, id *userpb.UserId) (*userpb.User, error) {
gw, err := s.gatewaySelector.Next()
if err != nil {
return nil, nil
}
res, err := gw.GetUser(ctx, &userpb.GetUserRequest{
UserId: id,
})
if err != nil {
return nil, err
}
if res.Status.Code != rpcv1beta1.Code_CODE_OK {
return nil, errors.New(res.Status.Message)
}
return res.User, nil
}
func isTokenValid(token *invitepb.InviteToken) bool {
return time.Now().Unix() < int64(token.Expiration.Seconds)
}
func (s *service) GetAcceptedUser(ctx context.Context, req *invitepb.GetAcceptedUserRequest) (*invitepb.GetAcceptedUserResponse, error) {
user, ok := getUserFilter(ctx, req)
if !ok {
return &invitepb.GetAcceptedUserResponse{
Status: status.NewInvalidArg(ctx, "user not found"),
}, nil
}
remoteUser, err := s.repo.GetRemoteUser(ctx, user.GetId(), req.GetRemoteUserId())
if err != nil {
return &invitepb.GetAcceptedUserResponse{
Status: status.NewInternal(ctx, "error fetching remote user details"),
}, nil
}
return &invitepb.GetAcceptedUserResponse{
Status: status.NewOK(ctx),
RemoteUser: remoteUser,
}, nil
}
func getUserFilter(ctx context.Context, req *invitepb.GetAcceptedUserRequest) (*userpb.User, bool) {
user, ok := ctxpkg.ContextGetUser(ctx)
if ok {
return user, true
}
if req.Opaque == nil || req.Opaque.Map == nil {
return nil, false
}
v, ok := req.Opaque.Map["user-filter"]
if !ok {
return nil, false
}
var u userpb.UserId
if err := utils.UnmarshalJSONToProtoV1(v.Value, &u); err != nil {
return nil, false
}
return &userpb.User{Id: &u}, true
}
func (s *service) FindAcceptedUsers(ctx context.Context, req *invitepb.FindAcceptedUsersRequest) (*invitepb.FindAcceptedUsersResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
acceptedUsers, err := s.repo.FindRemoteUsers(ctx, user.GetId(), req.GetFilter())
if err != nil {
return &invitepb.FindAcceptedUsersResponse{
Status: status.NewInternal(ctx, "error finding remote users: "+err.Error()),
}, nil
}
return &invitepb.FindAcceptedUsersResponse{
Status: status.NewOK(ctx),
AcceptedUsers: acceptedUsers,
}, nil
}
func (s *service) DeleteAcceptedUser(ctx context.Context, req *invitepb.DeleteAcceptedUserRequest) (*invitepb.DeleteAcceptedUserResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
if err := s.repo.DeleteRemoteUser(ctx, user.Id, req.RemoteUserId); err != nil {
return &invitepb.DeleteAcceptedUserResponse{
Status: status.NewInternal(ctx, "error deleting remote users: "+err.Error()),
}, nil
}
return &invitepb.DeleteAcceptedUserResponse{
Status: status.NewOK(ctx),
}, nil
}
@@ -0,0 +1,45 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package ocminvitemanager
import (
"time"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/google/uuid"
)
// CreateToken creates a InviteToken object for the userID indicated by userID.
func CreateToken(expiration time.Duration, userID *userpb.UserId, description string) *invitepb.InviteToken {
tokenID := uuid.New().String()
now := time.Now()
expirationTime := now.Add(expiration)
return &invitepb.InviteToken{
Token: tokenID,
UserId: userID,
Expiration: &typesv1beta1.Timestamp{
Seconds: uint64(expirationTime.Unix()),
Nanos: 0,
},
Description: description,
}
}
@@ -0,0 +1,147 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package ocmproviderauthorizer
import (
"context"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/ocm/provider"
"github.com/opencloud-eu/reva/v2/pkg/ocm/provider/authorizer/registry"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("ocmproviderauthorizer", New)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
}
type service struct {
conf *config
pa provider.Authorizer
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "json"
}
}
func (s *service) Register(ss *grpc.Server) {
ocmprovider.RegisterProviderAPIServer(ss, s)
}
func getProviderAuthorizer(c *config) (provider.Authorizer, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound("driver not found: " + c.Driver)
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
return c, nil
}
// New creates a new OCM provider authorizer svc
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
pa, err := getProviderAuthorizer(c)
if err != nil {
return nil, err
}
service := &service{
conf: c,
pa: pa,
}
return service, nil
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{
"/cs3.ocm.provider.v1beta1.ProviderAPI/IsProviderAllowed",
"/cs3.ocm.provider.v1beta1.ProviderAPI/ListAllProviders",
}
}
func (s *service) GetInfoByDomain(ctx context.Context, req *ocmprovider.GetInfoByDomainRequest) (*ocmprovider.GetInfoByDomainResponse, error) {
domainInfo, err := s.pa.GetInfoByDomain(ctx, req.Domain)
if err != nil {
return &ocmprovider.GetInfoByDomainResponse{
Status: status.NewInternal(ctx, "error getting provider info"),
}, nil
}
return &ocmprovider.GetInfoByDomainResponse{
Status: status.NewOK(ctx),
ProviderInfo: domainInfo,
}, nil
}
func (s *service) IsProviderAllowed(ctx context.Context, req *ocmprovider.IsProviderAllowedRequest) (*ocmprovider.IsProviderAllowedResponse, error) {
err := s.pa.IsProviderAllowed(ctx, req.Provider)
if err != nil {
return &ocmprovider.IsProviderAllowedResponse{
Status: status.NewInternal(ctx, "error verifying mesh provider"),
}, nil
}
return &ocmprovider.IsProviderAllowedResponse{
Status: status.NewOK(ctx),
}, nil
}
func (s *service) ListAllProviders(ctx context.Context, req *ocmprovider.ListAllProvidersRequest) (*ocmprovider.ListAllProvidersResponse, error) {
providers, err := s.pa.ListAllProviders(ctx)
if err != nil {
return &ocmprovider.ListAllProvidersResponse{
Status: status.NewInternal(ctx, "error retrieving mesh providers"),
}, nil
}
return &ocmprovider.ListAllProvidersResponse{
Status: status.NewOK(ctx),
Providers: providers,
}, nil
}
@@ -0,0 +1,518 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package ocmshareprovider
import (
"context"
"net/url"
"path/filepath"
"strings"
"text/template"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/internal/http/services/ocmd"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/ocm/client"
"github.com/opencloud-eu/reva/v2/pkg/ocm/share"
"github.com/opencloud-eu/reva/v2/pkg/ocm/share/repository/registry"
ocmuser "github.com/opencloud-eu/reva/v2/pkg/ocm/user"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/walker"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/opencloud-eu/reva/v2/pkg/utils/cfg"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("ocmshareprovider", New)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
ClientTimeout int `mapstructure:"client_timeout"`
ClientInsecure bool `mapstructure:"client_insecure"`
GatewaySVC string `mapstructure:"gatewaysvc" validate:"required"`
ProviderDomain string `mapstructure:"provider_domain" validate:"required" docs:"The same domain registered in the provider authorizer"`
WebDAVEndpoint string `mapstructure:"webdav_endpoint" validate:"required"`
WebappTemplate string `mapstructure:"webapp_template"`
}
type service struct {
conf *config
repo share.Repository
client *client.OCMClient
gatewaySelector *pool.Selector[gateway.GatewayAPIClient]
webappTmpl *template.Template
walker walker.Walker
}
func (c *config) ApplyDefaults() {
if c.Driver == "" {
c.Driver = "json"
}
if c.ClientTimeout == 0 {
c.ClientTimeout = 10
}
if c.WebappTemplate == "" {
c.WebappTemplate = "https://cernbox.cern.ch/external/sciencemesh/{{.Token}}{relative-path-to-shared-resource}"
}
c.GatewaySVC = sharedconf.GetGatewaySVC(c.GatewaySVC)
}
func (s *service) Register(ss *grpc.Server) {
ocm.RegisterOcmAPIServer(ss, s)
}
func getShareRepository(c *config) (share.Repository, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound("driver not found: " + c.Driver)
}
// New creates a new ocm share provider svc.
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
repo, err := getShareRepository(&c)
if err != nil {
return nil, err
}
client := client.New(&client.Config{
Timeout: time.Duration(c.ClientTimeout) * time.Second,
Insecure: c.ClientInsecure,
})
gatewaySelector, err := pool.GatewaySelector(c.GatewaySVC)
if err != nil {
return nil, err
}
tpl, err := template.New("webapp_template").Parse(c.WebappTemplate)
if err != nil {
return nil, err
}
walker := walker.NewWalker(gatewaySelector)
service := &service{
conf: &c,
repo: repo,
client: client,
gatewaySelector: gatewaySelector,
webappTmpl: tpl,
walker: walker,
}
return service, nil
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{"/cs3.sharing.ocm.v1beta1.OcmAPI/GetOCMShareByToken"}
}
func getOCMEndpoint(originProvider *ocmprovider.ProviderInfo) (string, error) {
for _, s := range originProvider.Services {
if s.Endpoint.Type.Name == "OCM" {
return s.Endpoint.Path, nil
}
}
return "", errors.New("ocm endpoint not specified for mesh provider")
}
func getResourceType(info *providerpb.ResourceInfo) string {
switch info.Type {
case providerpb.ResourceType_RESOURCE_TYPE_FILE:
return "file"
case providerpb.ResourceType_RESOURCE_TYPE_CONTAINER:
return "folder"
}
return "unknown"
}
func (s *service) webdavURL(_ context.Context, share *ocm.Share) string {
// the url is in the form of https://cernbox.cern.ch/remote.php/dav/ocm/token
p, _ := url.JoinPath(s.conf.WebDAVEndpoint, "/dav/ocm", share.GetId().GetOpaqueId())
return p
}
func (s *service) getWebdavProtocol(ctx context.Context, share *ocm.Share, m *ocm.AccessMethod_WebdavOptions) *ocmd.WebDAV {
var perms []string
if m.WebdavOptions.Permissions.InitiateFileDownload {
perms = append(perms, "read")
}
if m.WebdavOptions.Permissions.InitiateFileUpload {
perms = append(perms, "write")
}
return &ocmd.WebDAV{
Permissions: perms,
URI: s.webdavURL(ctx, share),
SharedSecret: share.Token,
}
}
func (s *service) getWebappProtocol(share *ocm.Share) *ocmd.Webapp {
var b strings.Builder
if err := s.webappTmpl.Execute(&b, share); err != nil {
return nil
}
return &ocmd.Webapp{
URITemplate: b.String(),
}
}
func (s *service) getProtocols(ctx context.Context, share *ocm.Share) ocmd.Protocols {
var p ocmd.Protocols
for _, m := range share.AccessMethods {
var newProtocol ocmd.Protocol
switch t := m.Term.(type) {
case *ocm.AccessMethod_WebdavOptions:
newProtocol = s.getWebdavProtocol(ctx, share, t)
case *ocm.AccessMethod_WebappOptions:
newProtocol = s.getWebappProtocol(share)
}
if newProtocol != nil {
p = append(p, newProtocol)
}
}
return p
}
func (s *service) CreateOCMShare(ctx context.Context, req *ocm.CreateOCMShareRequest) (*ocm.CreateOCMShareResponse, error) {
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
statRes, err := gatewayClient.Stat(ctx, &providerpb.StatRequest{
Ref: &providerpb.Reference{
ResourceId: req.ResourceId,
},
})
if err != nil {
return nil, err
}
if statRes.Status.Code != rpc.Code_CODE_OK {
if statRes.Status.Code == rpc.Code_CODE_NOT_FOUND {
return &ocm.CreateOCMShareResponse{
Status: status.NewNotFound(ctx, statRes.Status.Message),
}, nil
}
return &ocm.CreateOCMShareResponse{
Status: status.NewInternal(ctx, statRes.Status.Message),
}, nil
}
info := statRes.Info
user := ctxpkg.ContextMustGetUser(ctx)
tkn := utils.RandString(32)
now := time.Now().UnixNano()
ts := &typespb.Timestamp{
Seconds: uint64(now / 1000000000),
Nanos: uint32(now % 1000000000),
}
// 1. persist the share in the repository
ocmshare := &ocm.Share{
Token: tkn,
Name: filepath.Base(info.Path),
ResourceId: req.ResourceId,
Grantee: req.Grantee,
ShareType: ocm.ShareType_SHARE_TYPE_USER,
Owner: info.Owner,
Creator: user.Id,
Ctime: ts,
Mtime: ts,
Expiration: req.Expiration,
AccessMethods: req.AccessMethods,
}
ocmshare, err = s.repo.StoreShare(ctx, ocmshare)
if err != nil {
if errors.Is(err, share.ErrShareAlreadyExisting) {
return &ocm.CreateOCMShareResponse{
Status: status.NewAlreadyExists(ctx, err, "share already exists"),
}, nil
}
return &ocm.CreateOCMShareResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
// 2. create the share on the remote provider
// 2.a get the ocm endpoint of the remote provider
ocmEndpoint, err := getOCMEndpoint(req.RecipientMeshProvider)
if err != nil {
return &ocm.CreateOCMShareResponse{
Status: status.NewInvalidArg(ctx, "the selected provider does not have an OCM endpoint"),
}, nil
}
// 2.b replace outgoing user ids with ocm user ids
// unpack the federated user id
shareWith := ocmuser.FormatOCMUser(ocmuser.LocalUserFederatedID(req.GetGrantee().GetUserId(), ""))
// wrap the local user id in a local federated user id
owner := ocmuser.FormatOCMUser(ocmuser.LocalUserFederatedID(info.Owner, s.conf.ProviderDomain))
sender := ocmuser.FormatOCMUser(ocmuser.LocalUserFederatedID(user.Id, s.conf.ProviderDomain))
newShareReq := &client.NewShareRequest{
ShareWith: shareWith,
Name: ocmshare.Name,
ProviderID: ocmshare.Id.OpaqueId,
Owner: owner,
Sender: sender,
SenderDisplayName: user.DisplayName,
ShareType: "user",
ResourceType: getResourceType(info),
Protocols: s.getProtocols(ctx, ocmshare),
}
if req.Expiration != nil {
newShareReq.Expiration = req.Expiration.Seconds
}
// 2.c make POST /shares request
newShareRes, err := s.client.NewShare(ctx, ocmEndpoint, newShareReq)
if err != nil {
err2 := s.repo.DeleteShare(ctx, user, &ocm.ShareReference{Spec: &ocm.ShareReference_Id{Id: ocmshare.Id}})
if err2 != nil {
appctx.GetLogger(ctx).Error().Err(err2).Str("shareid", ocmshare.GetId().GetOpaqueId()).Msg("could not delete local ocm share")
}
// TODO remove the share from the local storage
switch {
case errors.Is(err, client.ErrInvalidParameters):
return &ocm.CreateOCMShareResponse{
Status: status.NewInvalidArg(ctx, err.Error()),
}, nil
case errors.Is(err, client.ErrServiceNotTrusted):
return &ocm.CreateOCMShareResponse{
Status: status.NewInvalidArg(ctx, err.Error()),
}, nil
default:
return &ocm.CreateOCMShareResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
}
res := &ocm.CreateOCMShareResponse{
Status: status.NewOK(ctx),
Share: ocmshare,
RecipientDisplayName: newShareRes.RecipientDisplayName,
}
return res, nil
}
func (s *service) RemoveOCMShare(ctx context.Context, req *ocm.RemoveOCMShareRequest) (*ocm.RemoveOCMShareResponse, error) {
// TODO (gdelmont): notify the remote provider using the /notification ocm endpoint
// https://cs3org.github.io/OCM-API/docs.html?branch=develop&repo=OCM-API&user=cs3org#/paths/~1notifications/post
user := ctxpkg.ContextMustGetUser(ctx)
if err := s.repo.DeleteShare(ctx, user, req.Ref); err != nil {
if errors.Is(err, share.ErrShareNotFound) {
return &ocm.RemoveOCMShareResponse{
Status: status.NewNotFound(ctx, "share does not exist"),
}, nil
}
return &ocm.RemoveOCMShareResponse{
Status: status.NewInternal(ctx, "error removing share"),
}, nil
}
return &ocm.RemoveOCMShareResponse{
Status: status.NewOK(ctx),
}, nil
}
func (s *service) GetOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest) (*ocm.GetOCMShareResponse, error) {
// if the request is by token, the user does not need to be in the ctx
var user *userpb.User
if req.Ref.GetToken() == "" {
user = ctxpkg.ContextMustGetUser(ctx)
}
ocmshare, err := s.repo.GetShare(ctx, user, req.Ref)
if err != nil {
if errors.Is(err, share.ErrShareNotFound) {
return &ocm.GetOCMShareResponse{
Status: status.NewNotFound(ctx, "share does not exist"),
}, nil
}
return &ocm.GetOCMShareResponse{
Status: status.NewInternal(ctx, "error getting share"),
}, nil
}
return &ocm.GetOCMShareResponse{
Status: status.NewOK(ctx),
Share: ocmshare,
}, nil
}
func (s *service) GetOCMShareByToken(ctx context.Context, req *ocm.GetOCMShareByTokenRequest) (*ocm.GetOCMShareByTokenResponse, error) {
ocmshare, err := s.repo.GetShare(ctx, nil, &ocm.ShareReference{
Spec: &ocm.ShareReference_Token{
Token: req.Token,
},
})
if err != nil {
if errors.Is(err, share.ErrShareNotFound) {
return &ocm.GetOCMShareByTokenResponse{
Status: status.NewNotFound(ctx, "share does not exist"),
}, nil
}
return &ocm.GetOCMShareByTokenResponse{
Status: status.NewInternal(ctx, "error getting share"),
}, nil
}
return &ocm.GetOCMShareByTokenResponse{
Status: status.NewOK(ctx),
Share: ocmshare,
}, nil
}
func (s *service) ListOCMShares(ctx context.Context, req *ocm.ListOCMSharesRequest) (*ocm.ListOCMSharesResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
shares, err := s.repo.ListShares(ctx, user, req.Filters)
if err != nil {
return &ocm.ListOCMSharesResponse{
Status: status.NewInternal(ctx, "error listing shares"),
}, nil
}
res := &ocm.ListOCMSharesResponse{
Status: status.NewOK(ctx),
Shares: shares,
}
return res, nil
}
func (s *service) UpdateOCMShare(ctx context.Context, req *ocm.UpdateOCMShareRequest) (*ocm.UpdateOCMShareResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
if len(req.Field) == 0 {
return &ocm.UpdateOCMShareResponse{
Status: status.NewOK(ctx),
}, nil
}
_, err := s.repo.UpdateShare(ctx, user, req.Ref, req.Field...)
if err != nil {
if errors.Is(err, share.ErrShareNotFound) {
return &ocm.UpdateOCMShareResponse{
Status: status.NewNotFound(ctx, "share does not exist"),
}, nil
}
return &ocm.UpdateOCMShareResponse{
Status: status.NewInternal(ctx, "error updating share"),
}, nil
}
res := &ocm.UpdateOCMShareResponse{
Status: status.NewOK(ctx),
}
return res, nil
}
func (s *service) ListReceivedOCMShares(ctx context.Context, req *ocm.ListReceivedOCMSharesRequest) (*ocm.ListReceivedOCMSharesResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
shares, err := s.repo.ListReceivedShares(ctx, user)
if err != nil {
return &ocm.ListReceivedOCMSharesResponse{
Status: status.NewInternal(ctx, "error listing received shares"),
}, nil
}
res := &ocm.ListReceivedOCMSharesResponse{
Status: status.NewOK(ctx),
Shares: shares,
}
return res, nil
}
func (s *service) UpdateReceivedOCMShare(ctx context.Context, req *ocm.UpdateReceivedOCMShareRequest) (*ocm.UpdateReceivedOCMShareResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
_, err := s.repo.UpdateReceivedShare(ctx, user, req.Share, req.UpdateMask)
if err != nil {
if errors.Is(err, share.ErrShareNotFound) {
return &ocm.UpdateReceivedOCMShareResponse{
Status: status.NewNotFound(ctx, "share does not exist"),
}, nil
}
return &ocm.UpdateReceivedOCMShareResponse{
Status: status.NewInternal(ctx, "error updating received share"),
}, nil
}
res := &ocm.UpdateReceivedOCMShareResponse{
Status: status.NewOK(ctx),
}
return res, nil
}
func (s *service) GetReceivedOCMShare(ctx context.Context, req *ocm.GetReceivedOCMShareRequest) (*ocm.GetReceivedOCMShareResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
if user.Id.GetType() == userpb.UserType_USER_TYPE_SERVICE {
var uid userpb.UserId
_ = utils.ReadJSONFromOpaque(req.Opaque, "userid", &uid)
user = &userpb.User{
Id: &uid,
}
}
ocmshare, err := s.repo.GetReceivedShare(ctx, user, req.Ref)
if err != nil {
if errors.Is(err, share.ErrShareNotFound) {
return &ocm.GetReceivedOCMShareResponse{
Status: status.NewNotFound(ctx, "share does not exist"),
}, nil
}
return &ocm.GetReceivedOCMShareResponse{
Status: status.NewInternal(ctx, "error getting received share: "+err.Error()),
}, nil
}
res := &ocm.GetReceivedOCMShareResponse{
Status: status.NewOK(ctx),
Share: ocmshare,
}
return res, nil
}
@@ -0,0 +1,105 @@
// Copyright 2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package permissions
import (
"context"
"fmt"
permissions "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/permission"
"github.com/opencloud-eu/reva/v2/pkg/permission/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("permissions", New)
}
type config struct {
Driver string `mapstructure:"driver" docs:"localhome;The permission driver to be used."`
Drivers map[string]map[string]interface{} `mapstructure:"drivers" docs:"url:pkg/permission/permission.go"`
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
return c, nil
}
type service struct {
manager permission.Manager
}
// New returns a new PermissionsServiceServer
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
f, ok := registry.NewFuncs[c.Driver]
if !ok {
return nil, fmt.Errorf("could not get permission manager '%s'", c.Driver)
}
manager, err := f(c.Drivers[c.Driver])
if err != nil {
return nil, err
}
service := &service{manager: manager}
return service, nil
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{}
}
func (s *service) Register(ss *grpc.Server) {
permissions.RegisterPermissionsAPIServer(ss, s)
}
func (s *service) CheckPermission(ctx context.Context, req *permissions.CheckPermissionRequest) (*permissions.CheckPermissionResponse, error) {
var subject string
switch ref := req.SubjectRef.Spec.(type) {
case *permissions.SubjectReference_UserId:
subject = ref.UserId.OpaqueId
case *permissions.SubjectReference_GroupId:
subject = ref.GroupId.OpaqueId
}
var status *rpc.Status
if ok := s.manager.CheckPermission(req.Permission, subject, req.Ref); ok {
status = &rpc.Status{Code: rpc.Code_CODE_OK}
} else {
status = &rpc.Status{Code: rpc.Code_CODE_PERMISSION_DENIED}
}
return &permissions.CheckPermissionResponse{Status: status}, nil
}
@@ -0,0 +1,134 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package preferences
import (
"context"
"google.golang.org/grpc"
preferencespb "github.com/cs3org/go-cs3apis/cs3/preferences/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/preferences"
"github.com/opencloud-eu/reva/v2/pkg/preferences/registry"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/pkg/errors"
"github.com/rs/zerolog"
)
func init() {
rgrpc.Register("preferences", New)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "memory"
}
}
type service struct {
conf *config
pm preferences.Manager
}
func getPreferencesManager(c *config) (preferences.Manager, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound("driver not found: " + c.Driver)
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
return c, nil
}
// New returns a new PreferencesServiceServer
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
pm, err := getPreferencesManager(c)
if err != nil {
return nil, err
}
return &service{
conf: c,
pm: pm,
}, nil
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{}
}
func (s *service) Register(ss *grpc.Server) {
preferencespb.RegisterPreferencesAPIServer(ss, s)
}
func (s *service) SetKey(ctx context.Context, req *preferencespb.SetKeyRequest) (*preferencespb.SetKeyResponse, error) {
err := s.pm.SetKey(ctx, req.Key.Key, req.Key.Namespace, req.Val)
if err != nil {
return &preferencespb.SetKeyResponse{
Status: status.NewInternal(ctx, "error setting key"),
}, nil
}
return &preferencespb.SetKeyResponse{
Status: status.NewOK(ctx),
}, nil
}
func (s *service) GetKey(ctx context.Context, req *preferencespb.GetKeyRequest) (*preferencespb.GetKeyResponse, error) {
val, err := s.pm.GetKey(ctx, req.Key.Key, req.Key.Namespace)
if err != nil {
st := status.NewInternal(ctx, "error retrieving key")
if _, ok := err.(errtypes.IsNotFound); ok {
st = status.NewNotFound(ctx, "key not found")
}
return &preferencespb.GetKeyResponse{
Status: st,
}, nil
}
return &preferencespb.GetKeyResponse{
Status: status.NewOK(ctx),
Val: val,
}, nil
}
@@ -0,0 +1,654 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package publicshareprovider
import (
"context"
"encoding/json"
"fmt"
"regexp"
"strconv"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/password"
"github.com/opencloud-eu/reva/v2/pkg/permission"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/grants"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/conversions"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/publicshare"
"github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
)
const getUserCtxErrMsg = "error getting user from context"
func init() {
rgrpc.Register("publicshareprovider", NewDefault)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
GatewayAddr string `mapstructure:"gateway_addr"`
AllowedPathsForShares []string `mapstructure:"allowed_paths_for_shares"`
EnableExpiredSharesCleanup bool `mapstructure:"enable_expired_shares_cleanup"`
WriteableShareMustHavePassword bool `mapstructure:"writeable_share_must_have_password"`
PublicShareMustHavePassword bool `mapstructure:"public_share_must_have_password"`
PasswordPolicy map[string]interface{} `mapstructure:"password_policy"`
}
type passwordPolicy struct {
MinCharacters int `mapstructure:"min_characters"`
MinLowerCaseCharacters int `mapstructure:"min_lowercase_characters"`
MinUpperCaseCharacters int `mapstructure:"min_uppercase_characters"`
MinDigits int `mapstructure:"min_digits"`
MinSpecialCharacters int `mapstructure:"min_special_characters"`
BannedPasswordsList map[string]struct{} `mapstructure:"banned_passwords_list"`
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "json"
}
}
type service struct {
conf *config
sm publicshare.Manager
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
allowedPathsForShares []*regexp.Regexp
passwordValidator password.Validator
}
func getShareManager(c *config) (publicshare.Manager, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound("driver not found: " + c.Driver)
}
// TODO(labkode): add ctx to Close.
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{"/cs3.sharing.link.v1beta1.LinkAPI/GetPublicShareByToken"}
}
func (s *service) Register(ss *grpc.Server) {
link.RegisterLinkAPIServer(ss, s)
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding config")
return nil, err
}
return c, nil
}
func parsePasswordPolicy(m map[string]interface{}) (*passwordPolicy, error) {
p := &passwordPolicy{}
if err := mapstructure.Decode(m, p); err != nil {
err = errors.Wrap(err, "error decoding password policy config")
return nil, err
}
return p, nil
}
// New creates a new public share provider svc initialized from defaults
func NewDefault(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
p, err := parsePasswordPolicy(c.PasswordPolicy)
if err != nil {
return nil, err
}
c.init()
sm, err := getShareManager(c)
if err != nil {
return nil, err
}
gatewaySelector, err := pool.GatewaySelector(sharedconf.GetGatewaySVC(c.GatewayAddr))
if err != nil {
return nil, err
}
return New(gatewaySelector, sm, c, p)
}
// New creates a new user share provider svc
func New(gatewaySelector pool.Selectable[gateway.GatewayAPIClient], sm publicshare.Manager, c *config, p *passwordPolicy) (rgrpc.Service, error) {
allowedPathsForShares := make([]*regexp.Regexp, 0, len(c.AllowedPathsForShares))
for _, s := range c.AllowedPathsForShares {
regex, err := regexp.Compile(s)
if err != nil {
return nil, err
}
allowedPathsForShares = append(allowedPathsForShares, regex)
}
service := &service{
conf: c,
sm: sm,
gatewaySelector: gatewaySelector,
allowedPathsForShares: allowedPathsForShares,
passwordValidator: newPasswordPolicy(p),
}
return service, nil
}
func newPasswordPolicy(c *passwordPolicy) password.Validator {
if c == nil {
return password.NewPasswordPolicy(0, 0, 0, 0, 0, nil)
}
return password.NewPasswordPolicy(
c.MinCharacters,
c.MinLowerCaseCharacters,
c.MinUpperCaseCharacters,
c.MinDigits,
c.MinSpecialCharacters,
c.BannedPasswordsList,
)
}
func (s *service) isPathAllowed(path string) bool {
if len(s.allowedPathsForShares) == 0 {
return true
}
for _, reg := range s.allowedPathsForShares {
if reg.MatchString(path) {
return true
}
}
return false
}
func (s *service) CreatePublicShare(ctx context.Context, req *link.CreatePublicShareRequest) (*link.CreatePublicShareResponse, error) {
log := appctx.GetLogger(ctx)
log.Info().Str("publicshareprovider", "create").Msg("create public share")
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
isInternalLink := grants.PermissionsEqual(req.GetGrant().GetPermissions().GetPermissions(), &provider.ResourcePermissions{})
sRes, err := gatewayClient.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: req.GetResourceInfo().GetId()}})
if err != nil {
log.Err(err).Interface("resource_id", req.GetResourceInfo().GetId()).Msg("failed to stat resource to share")
return &link.CreatePublicShareResponse{
Status: status.NewInternal(ctx, "failed to stat resource to share"),
}, err
}
// all users can create internal links
if !isInternalLink {
// check if the user has the permission in the user role
ok, err := utils.CheckPermission(ctx, permission.WritePublicLink, gatewayClient)
if err != nil {
return &link.CreatePublicShareResponse{
Status: status.NewInternal(ctx, "failed check user permission to write public link"),
}, err
}
if !ok {
return &link.CreatePublicShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "no permission to create public links"),
}, nil
}
}
// check that user has share permissions
if !isInternalLink && !sRes.GetInfo().GetPermissionSet().AddGrant {
return &link.CreatePublicShareResponse{
Status: status.NewInvalidArg(ctx, "no share permission"),
}, nil
}
// check if the user can share with the desired permissions. For internal links this is skipped,
// users can always create internal links provided they have the AddGrant permission, which was already
// checked above
if !isInternalLink && !conversions.SufficientCS3Permissions(sRes.GetInfo().GetPermissionSet(), req.GetGrant().GetPermissions().GetPermissions()) {
return &link.CreatePublicShareResponse{
Status: status.NewInvalidArg(ctx, "insufficient permissions to create that kind of share"),
}, nil
}
// validate path
if !s.isPathAllowed(req.GetResourceInfo().GetPath()) {
return &link.CreatePublicShareResponse{
Status: status.NewFailedPrecondition(ctx, nil, "share creation is not allowed for the specified path"),
}, nil
}
// check that this is a not a personal space root
if req.GetResourceInfo().GetId().GetOpaqueId() == req.GetResourceInfo().GetId().GetSpaceId() &&
req.GetResourceInfo().GetSpace().GetSpaceType() == "personal" {
return &link.CreatePublicShareResponse{
Status: status.NewInvalidArg(ctx, "cannot create link on personal space root"),
}, nil
}
// quick link returns the existing one if already present
quickLink, err := checkQuicklink(req.GetResourceInfo())
if err != nil {
return &link.CreatePublicShareResponse{
Status: status.NewInvalidArg(ctx, "invalid quicklink value"),
}, nil
}
if quickLink {
f := []*link.ListPublicSharesRequest_Filter{publicshare.ResourceIDFilter(req.GetResourceInfo().GetId())}
req := link.ListPublicSharesRequest{Filters: f}
res, err := s.ListPublicShares(ctx, &req)
if err != nil || res.GetStatus().GetCode() != rpc.Code_CODE_OK {
return &link.CreatePublicShareResponse{
Status: status.NewInternal(ctx, "could not list public links"),
}, nil
}
for _, l := range res.GetShare() {
if l.Quicklink {
return &link.CreatePublicShareResponse{
Status: status.NewOK(ctx),
Share: l,
}, nil
}
}
}
grant := req.GetGrant()
// validate expiration date
if grant.GetExpiration() != nil {
expirationDateTime := utils.TSToTime(grant.GetExpiration()).UTC()
if expirationDateTime.Before(time.Now().UTC()) {
msg := fmt.Sprintf("expiration date is in the past: %s", expirationDateTime.Format(time.RFC3339))
return &link.CreatePublicShareResponse{
Status: status.NewInvalidArg(ctx, msg),
}, nil
}
}
// enforce password if needed
setPassword := grant.GetPassword()
if !isInternalLink && enforcePassword(false, grant.GetPermissions().GetPermissions(), s.conf) && len(setPassword) == 0 {
return &link.CreatePublicShareResponse{
Status: status.NewInvalidArg(ctx, "password protection is enforced"),
}, nil
}
// validate password policy
if len(setPassword) > 0 {
if err := s.passwordValidator.Validate(setPassword); err != nil {
return &link.CreatePublicShareResponse{
Status: status.NewInvalidArg(ctx, err.Error()),
}, nil
}
}
user := ctxpkg.ContextMustGetUser(ctx)
res := &link.CreatePublicShareResponse{}
share, err := s.sm.CreatePublicShare(ctx, user, req.GetResourceInfo(), req.GetGrant())
switch {
case err != nil:
log.Error().Err(err).Interface("request", req).Msg("could not write public share")
res.Status = status.NewInternal(ctx, "error persisting public share:"+err.Error())
default:
res.Status = status.NewOK(ctx)
res.Share = share
res.Opaque = utils.AppendPlainToOpaque(nil, "resourcename", sRes.GetInfo().GetName())
}
return res, nil
}
func (s *service) RemovePublicShare(ctx context.Context, req *link.RemovePublicShareRequest) (*link.RemovePublicShareResponse, error) {
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
log := appctx.GetLogger(ctx)
log.Info().Str("publicshareprovider", "remove").Msg("remove public share")
user := ctxpkg.ContextMustGetUser(ctx)
ps, err := s.sm.GetPublicShare(ctx, user, req.GetRef(), false)
if err != nil {
return &link.RemovePublicShareResponse{
Status: status.NewInternal(ctx, "error loading public share"),
}, err
}
sRes, err := gatewayClient.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: ps.ResourceId}})
if err != nil {
log.Err(err).Interface("resource_id", ps.ResourceId).Msg("failed to stat shared resource")
return &link.RemovePublicShareResponse{
Status: status.NewInternal(ctx, "failed to stat shared resource"),
}, err
}
if !publicshare.IsCreatedByUser(ps, user) {
if !sRes.GetInfo().GetPermissionSet().RemoveGrant {
return &link.RemovePublicShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "no permission to delete public share"),
}, err
}
}
err = s.sm.RevokePublicShare(ctx, user, req.Ref)
if err != nil {
return &link.RemovePublicShareResponse{
Status: status.NewInternal(ctx, "error deleting public share"),
}, err
}
o := utils.AppendJSONToOpaque(nil, "resourceid", ps.GetResourceId())
o = utils.AppendPlainToOpaque(o, "resourcename", sRes.GetInfo().GetName())
return &link.RemovePublicShareResponse{
Opaque: o,
Status: status.NewOK(ctx),
}, nil
}
func (s *service) GetPublicShareByToken(ctx context.Context, req *link.GetPublicShareByTokenRequest) (*link.GetPublicShareByTokenResponse, error) {
log := appctx.GetLogger(ctx)
log.Debug().Msg("getting public share by token")
// there are 2 passes here, and the second request has no password
found, err := s.sm.GetPublicShareByToken(ctx, req.GetToken(), req.GetAuthentication(), req.GetSign())
switch v := err.(type) {
case nil:
return &link.GetPublicShareByTokenResponse{
Status: status.NewOK(ctx),
Share: found,
}, nil
case errtypes.InvalidCredentials:
return &link.GetPublicShareByTokenResponse{
Status: status.NewPermissionDenied(ctx, v, "wrong password"),
}, nil
case errtypes.NotFound:
return &link.GetPublicShareByTokenResponse{
Status: status.NewNotFound(ctx, "unknown token"),
}, nil
default:
return &link.GetPublicShareByTokenResponse{
Status: status.NewInternal(ctx, "unexpected error"),
}, nil
}
}
func (s *service) GetPublicShare(ctx context.Context, req *link.GetPublicShareRequest) (*link.GetPublicShareResponse, error) {
log := appctx.GetLogger(ctx)
log.Info().Str("publicshareprovider", "get").Msg("get public share")
u, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
log.Error().Msg(getUserCtxErrMsg)
}
ps, err := s.sm.GetPublicShare(ctx, u, req.Ref, req.GetSign())
switch {
case err != nil:
var st *rpc.Status
switch err.(type) {
case errtypes.IsNotFound:
st = status.NewNotFound(ctx, err.Error())
default:
st = status.NewInternal(ctx, err.Error())
}
return &link.GetPublicShareResponse{
Status: st,
}, nil
case ps == nil:
return &link.GetPublicShareResponse{
Status: status.NewNotFound(ctx, "not found"),
}, nil
default:
return &link.GetPublicShareResponse{
Status: status.NewOK(ctx),
Share: ps,
}, nil
}
}
func (s *service) ListPublicShares(ctx context.Context, req *link.ListPublicSharesRequest) (*link.ListPublicSharesResponse, error) {
log := appctx.GetLogger(ctx)
log.Info().Str("publicshareprovider", "list").Msg("list public share")
user, _ := ctxpkg.ContextGetUser(ctx)
shares, err := s.sm.ListPublicShares(ctx, user, req.Filters, req.GetSign())
if err != nil {
log.Err(err).Msg("error listing shares")
return &link.ListPublicSharesResponse{
Status: status.NewInternal(ctx, "error listing public shares"),
}, nil
}
res := &link.ListPublicSharesResponse{
Status: status.NewOK(ctx),
Share: shares,
}
return res, nil
}
func (s *service) UpdatePublicShare(ctx context.Context, req *link.UpdatePublicShareRequest) (*link.UpdatePublicShareResponse, error) {
log := appctx.GetLogger(ctx)
log.Info().Str("publicshareprovider", "update").Msg("update public share")
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
user := ctxpkg.ContextMustGetUser(ctx)
ps, err := s.sm.GetPublicShare(ctx, user, req.GetRef(), false)
if err != nil {
return &link.UpdatePublicShareResponse{
Status: status.NewInternal(ctx, "error loading public share"),
}, err
}
isInternalLink := isInternalLink(req, ps)
// check if the user has the permission in the user role
if !publicshare.IsCreatedByUser(ps, user) {
canWriteLink, err := utils.CheckPermission(ctx, permission.WritePublicLink, gatewayClient)
if err != nil {
return &link.UpdatePublicShareResponse{
Status: status.NewInternal(ctx, "error checking permission to write public share"),
}, err
}
if !canWriteLink {
return &link.UpdatePublicShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "no permission to update public share"),
}, nil
}
}
sRes, err := gatewayClient.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: ps.ResourceId}})
if err != nil {
log.Err(err).Interface("resource_id", ps.ResourceId).Msg("failed to stat shared resource")
return &link.UpdatePublicShareResponse{
Status: status.NewInternal(ctx, "failed to stat shared resource"),
}, err
}
if sRes.Status.Code != rpc.Code_CODE_OK {
return &link.UpdatePublicShareResponse{
Status: sRes.GetStatus(),
}, nil
}
if !isInternalLink && !publicshare.IsCreatedByUser(ps, user) {
if !sRes.GetInfo().GetPermissionSet().UpdateGrant {
return &link.UpdatePublicShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "no permission to update public share"),
}, err
}
}
// check if the user can change the permissions to the desired permissions
updatePermissions := req.GetUpdate().GetType() == link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS
if updatePermissions &&
!isInternalLink &&
!conversions.SufficientCS3Permissions(
sRes.GetInfo().GetPermissionSet(),
req.GetUpdate().GetGrant().GetPermissions().GetPermissions(),
) {
return &link.UpdatePublicShareResponse{
Status: status.NewInvalidArg(ctx, "insufficient permissions to update that kind of share"),
}, nil
}
if updatePermissions {
beforePerm, _ := json.Marshal(sRes.GetInfo().GetPermissionSet())
afterPerm, _ := json.Marshal(req.GetUpdate().GetGrant().GetPermissions())
log.Info().
Str("shares", "update").
Msgf("updating permissions from %v to: %v",
string(beforePerm),
string(afterPerm),
)
}
grant := req.GetUpdate().GetGrant()
// validate expiration date
if grant.GetExpiration() != nil {
expirationDateTime := utils.TSToTime(grant.GetExpiration()).UTC()
if expirationDateTime.Before(time.Now().UTC()) {
msg := fmt.Sprintf("expiration date is in the past: %s", expirationDateTime.Format(time.RFC3339))
return &link.UpdatePublicShareResponse{
Status: status.NewInvalidArg(ctx, msg),
}, nil
}
}
// enforce password if needed
var canOptOut bool
if !isInternalLink {
canOptOut, err = utils.CheckPermission(ctx, permission.DeleteReadOnlyPassword, gatewayClient)
if err != nil {
return &link.UpdatePublicShareResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
}
updatePassword := req.GetUpdate().GetType() == link.UpdatePublicShareRequest_Update_TYPE_PASSWORD
setPassword := grant.GetPassword()
// we update permissions with an empty password and password is not set on the public share
emptyPasswordInPermissionUpdate := len(setPassword) == 0 && updatePermissions && !ps.PasswordProtected
// password is updated, we use the current permissions to check if the user can opt out
if updatePassword && !isInternalLink && enforcePassword(canOptOut, ps.GetPermissions().GetPermissions(), s.conf) && len(setPassword) == 0 {
return &link.UpdatePublicShareResponse{
Status: status.NewInvalidArg(ctx, "password protection is enforced"),
}, nil
}
// permissions are updated, we use the new permissions to check if the user can opt out
if emptyPasswordInPermissionUpdate && !isInternalLink && enforcePassword(canOptOut, grant.GetPermissions().GetPermissions(), s.conf) && len(setPassword) == 0 {
return &link.UpdatePublicShareResponse{
Status: status.NewInvalidArg(ctx, "password protection is enforced"),
}, nil
}
// validate password policy
if updatePassword && len(setPassword) > 0 {
if err := s.passwordValidator.Validate(setPassword); err != nil {
return &link.UpdatePublicShareResponse{
Status: status.NewInvalidArg(ctx, err.Error()),
}, nil
}
}
updateR, err := s.sm.UpdatePublicShare(ctx, user, req)
if err != nil {
return &link.UpdatePublicShareResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
res := &link.UpdatePublicShareResponse{
Status: status.NewOK(ctx),
Share: updateR,
Opaque: utils.AppendPlainToOpaque(nil, "resourcename", sRes.GetInfo().GetName()),
}
return res, nil
}
func isInternalLink(req *link.UpdatePublicShareRequest, ps *link.PublicShare) bool {
switch {
case req.GetUpdate().GetType() == link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS:
return grants.PermissionsEqual(req.GetUpdate().GetGrant().GetPermissions().GetPermissions(), &provider.ResourcePermissions{})
default:
return grants.PermissionsEqual(ps.GetPermissions().GetPermissions(), &provider.ResourcePermissions{})
}
}
func enforcePassword(canOptOut bool, permissions *provider.ResourcePermissions, conf *config) bool {
isReadOnly := conversions.SufficientCS3Permissions(conversions.NewViewerRole().CS3ResourcePermissions(), permissions)
if isReadOnly && canOptOut {
return false
}
if conf.PublicShareMustHavePassword {
return true
}
return !isReadOnly && conf.WriteableShareMustHavePassword
}
func checkQuicklink(info *provider.ResourceInfo) (bool, error) {
if info == nil {
return false, nil
}
if m := info.GetArbitraryMetadata().GetMetadata(); m != nil {
q, ok := m["quicklink"]
// empty string would trigger an error in ParseBool()
if !ok || q == "" {
return false, nil
}
quickLink, err := strconv.ParseBool(q)
if err != nil {
return false, err
}
return quickLink, nil
}
return false, nil
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,77 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package storageprovider
import (
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
)
// XS defines an hex-encoded string as checksum.
type XS string
func (x XS) String() string { return string(x) }
const (
// XSInvalid means the checksum type is invalid.
XSInvalid XS = "invalid"
// XSUnset means the checksum is optional.
XSUnset = "unset"
// XSAdler32 means the checksum is adler32
XSAdler32 = "adler32"
// XSMD5 means the checksum is md5
XSMD5 = "md5"
// XSSHA1 means the checksum is SHA1
XSSHA1 = "sha1"
// XSSHA256 means the checksum is SHA256.
XSSHA256 = "sha256"
)
// GRPC2PKGXS converts the grpc checksum type to an internal pkg type.
func GRPC2PKGXS(t provider.ResourceChecksumType) XS {
switch t {
case provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_INVALID:
return XSInvalid
case provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_UNSET:
return XSUnset
case provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_SHA1:
return XSSHA1
case provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_ADLER32:
return XSAdler32
case provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_MD5:
return XSMD5
default:
return XSInvalid
}
}
// PKG2GRPCXS converts an internal checksum type to the grpc checksum type.
func PKG2GRPCXS(xsType string) provider.ResourceChecksumType {
switch xsType {
case XSUnset:
return provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_UNSET
case XSAdler32:
return provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_ADLER32
case XSMD5:
return provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_MD5
case XSSHA1:
return provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_SHA1
default:
return provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_INVALID
}
}
@@ -0,0 +1,177 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package storageregistry
import (
"context"
"encoding/json"
"encoding/xml"
"fmt"
"github.com/BurntSushi/toml"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
registrypb "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
sdk "github.com/opencloud-eu/reva/v2/pkg/sdk/common"
"github.com/opencloud-eu/reva/v2/pkg/storage"
"github.com/opencloud-eu/reva/v2/pkg/storage/registry/registry"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("storageregistry", New)
}
type service struct {
reg storage.Registry
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{}
}
func (s *service) Register(ss *grpc.Server) {
registrypb.RegisterRegistryAPIServer(ss, s)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "static"
}
}
// New creates a new StorageBrokerService
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
reg, err := getRegistry(c)
if err != nil {
return nil, err
}
service := &service{
reg: reg,
}
return service, nil
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
return c, nil
}
func getRegistry(c *config) (storage.Registry, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound("driver not found: " + c.Driver)
}
func (s *service) ListStorageProviders(ctx context.Context, req *registrypb.ListStorageProvidersRequest) (*registrypb.ListStorageProvidersResponse, error) {
pinfos, err := s.reg.ListProviders(ctx, sdk.DecodeOpaqueMap(req.Opaque))
if err != nil {
return &registrypb.ListStorageProvidersResponse{
Status: status.NewInternal(ctx, "error getting list of storage providers"),
}, nil
}
res := &registrypb.ListStorageProvidersResponse{
Status: status.NewOK(ctx),
Providers: pinfos,
}
return res, nil
}
func (s *service) GetStorageProviders(ctx context.Context, req *registrypb.GetStorageProvidersRequest) (*registrypb.GetStorageProvidersResponse, error) {
space, err := decodeSpace(req.Opaque)
if err != nil {
return &registrypb.GetStorageProvidersResponse{
Status: status.NewInvalid(ctx, err.Error()),
}, nil
}
p, err := s.reg.GetProvider(ctx, space)
if err != nil {
switch err.(type) {
case errtypes.IsNotFound:
return &registrypb.GetStorageProvidersResponse{
Status: status.NewNotFound(ctx, err.Error()),
}, nil
default:
return &registrypb.GetStorageProvidersResponse{
Status: status.NewInternal(ctx, "error finding storage provider"),
}, nil
}
}
res := &registrypb.GetStorageProvidersResponse{
Status: status.NewOK(ctx),
Providers: []*registrypb.ProviderInfo{p},
}
return res, nil
}
func decodeSpace(o *typespb.Opaque) (*provider.StorageSpace, error) {
if entry, ok := o.Map["space"]; ok {
var unmarshal func([]byte, interface{}) error
switch entry.Decoder {
case "json":
unmarshal = json.Unmarshal
case "toml":
unmarshal = toml.Unmarshal
case "xml":
unmarshal = xml.Unmarshal
}
space := &provider.StorageSpace{}
return space, unmarshal(entry.Value, space)
}
return nil, fmt.Errorf("missing space in opaque property")
}
func (s *service) GetHome(ctx context.Context, req *registrypb.GetHomeRequest) (*registrypb.GetHomeResponse, error) {
res := &registrypb.GetHomeResponse{
Status: status.NewUnimplemented(ctx, nil, "getHome is no longer used. use List"),
}
return res, nil
}
@@ -0,0 +1,316 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package userprovider
import (
"context"
"fmt"
"path/filepath"
"sort"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
tenantpb "github.com/cs3org/go-cs3apis/cs3/identity/tenant/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/plugin"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/tenant"
tenantRegistry "github.com/opencloud-eu/reva/v2/pkg/tenant/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/user"
userRegistry "github.com/opencloud-eu/reva/v2/pkg/user/manager/registry"
)
func init() {
rgrpc.Register("userprovider", New)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
TenantDriver string `mapstructure:"tenant_driver"`
TenantDrivers map[string]map[string]interface{} `mapstructure:"tenant_drivers"`
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "json"
}
// Fall back to user driver/drivers when no tenant-specific config is provided.
if c.TenantDriver == "" {
c.TenantDriver = c.Driver
}
if c.TenantDrivers == nil {
c.TenantDrivers = c.Drivers
}
// Force "null" driver if multi-tenancy is disabled
if !sharedconf.MultiTenantEnabled() {
c.TenantDriver = "null"
}
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
c.init()
return c, nil
}
func getDriver(c *config) (user.Manager, *plugin.RevaPlugin, error) {
p, err := plugin.Load("userprovider", c.Driver)
if err == nil {
manager, ok := p.Plugin.(user.Manager)
if !ok {
return nil, nil, fmt.Errorf("could not assert the loaded plugin")
}
pluginConfig := filepath.Base(c.Driver)
err = manager.Configure(c.Drivers[pluginConfig])
if err != nil {
return nil, nil, err
}
return manager, p, nil
} else if _, ok := err.(errtypes.NotFound); ok {
// plugin not found, fetch the driver from the in-memory registry
if f, ok := userRegistry.NewFuncs[c.Driver]; ok {
mgr, err := f(c.Drivers[c.Driver])
return mgr, nil, err
}
} else {
return nil, nil, err
}
return nil, nil, errtypes.NotFound(fmt.Sprintf("driver %s not found for user manager", c.Driver))
}
func getTenantManager(c *config) (tenant.Manager, error) {
if f, ok := tenantRegistry.NewFuncs[c.TenantDriver]; ok {
mgr, err := f(c.TenantDrivers[c.TenantDriver])
return mgr, err
}
return nil, errtypes.NotFound(fmt.Sprintf("driver %s not found for tenant manager", c.TenantDriver))
}
// New returns a new UserProviderServiceServer.
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
userManager, plug, err := getDriver(c)
if err != nil {
return nil, err
}
tenantManager, err := getTenantManager(c)
if err != nil {
return nil, err
}
return NewWithManagers(userManager, tenantManager, plug), nil
}
// NewWithManagers returns a new UserProviderService with the given managers.
func NewWithManagers(um user.Manager, tm tenant.Manager, plug *plugin.RevaPlugin) rgrpc.Service {
return &service{
usermgr: um,
tenantmgr: tm,
plugin: plug,
}
}
type service struct {
usermgr user.Manager
tenantmgr tenant.Manager
plugin *plugin.RevaPlugin
}
func (s *service) Close() error {
if s.plugin != nil {
s.plugin.Kill()
}
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{"/cs3.identity.user.v1beta1.UserAPI/GetUser", "/cs3.identity.user.v1beta1.UserAPI/GetUserByClaim", "/cs3.identity.user.v1beta1.UserAPI/GetUserGroups"}
}
func (s *service) Register(ss *grpc.Server) {
userpb.RegisterUserAPIServer(ss, s)
tenantpb.RegisterTenantAPIServer(ss, s)
}
func (s *service) GetUser(ctx context.Context, req *userpb.GetUserRequest) (*userpb.GetUserResponse, error) {
if req.UserId == nil {
res := &userpb.GetUserResponse{
Status: status.NewInvalid(ctx, "userid missing"),
}
return res, nil
}
// Only request users from the same tenant as the current user
if currentUser, ok := revactx.ContextGetUser(ctx); ok {
req.UserId.TenantId = currentUser.GetId().GetTenantId()
}
user, err := s.usermgr.GetUser(ctx, req.UserId, req.SkipFetchingUserGroups)
if err != nil {
res := &userpb.GetUserResponse{}
switch err.(type) {
case errtypes.NotFound:
res.Status = status.NewNotFound(ctx, "user not found")
case errtypes.Unavailable:
res.Status = status.NewUnavailable(ctx, "user provider temporarily unavailable")
default:
res.Status = status.NewInternal(ctx, "error getting user")
}
return res, nil
}
res := &userpb.GetUserResponse{
Status: status.NewOK(ctx),
User: user,
}
return res, nil
}
func (s *service) GetUserByClaim(ctx context.Context, req *userpb.GetUserByClaimRequest) (*userpb.GetUserByClaimResponse, error) {
tenantID := ""
if currentUser, ok := revactx.ContextGetUser(ctx); ok {
tenantID = currentUser.GetId().GetTenantId()
}
user, err := s.usermgr.GetUserByClaim(ctx, req.Claim, req.Value, tenantID, req.SkipFetchingUserGroups)
if err != nil {
res := &userpb.GetUserByClaimResponse{}
switch err.(type) {
case errtypes.NotFound:
res.Status = status.NewNotFound(ctx, fmt.Sprintf("user not found %s %s", req.Claim, req.Value))
case errtypes.Unavailable:
res.Status = status.NewUnavailable(ctx, "user provider temporarily unavailable")
default:
res.Status = status.NewInternal(ctx, "error getting user by claim")
}
return res, nil
}
res := &userpb.GetUserByClaimResponse{
Status: status.NewOK(ctx),
User: user,
}
return res, nil
}
func (s *service) FindUsers(ctx context.Context, req *userpb.FindUsersRequest) (*userpb.FindUsersResponse, error) {
if len(req.Filters) > 1 || req.Filters[0].GetType() != userpb.Filter_TYPE_QUERY {
return nil, fmt.Errorf("only one query filter supported")
}
currentUser := revactx.ContextMustGetUser(ctx)
users, err := s.usermgr.FindUsers(ctx, req.Filters[0].GetQuery(), currentUser.GetId().GetTenantId(), req.SkipFetchingUserGroups)
if err != nil {
res := &userpb.FindUsersResponse{
Status: status.NewInternal(ctx, "error finding users"),
}
return res, nil
}
// sort users by username
sort.Slice(users, func(i, j int) bool {
return users[i].Username <= users[j].Username
})
res := &userpb.FindUsersResponse{
Status: status.NewOK(ctx),
Users: users,
}
return res, nil
}
func (s *service) GetUserGroups(ctx context.Context, req *userpb.GetUserGroupsRequest) (*userpb.GetUserGroupsResponse, error) {
log := appctx.GetLogger(ctx)
if req.UserId == nil {
res := &userpb.GetUserGroupsResponse{
Status: status.NewInvalid(ctx, "userid missing"),
}
return res, nil
}
groups, err := s.usermgr.GetUserGroups(ctx, req.UserId)
if err != nil {
log.Warn().Err(err).Interface("userid", req.UserId).Msg("error getting user groups")
res := &userpb.GetUserGroupsResponse{
Status: status.NewInternal(ctx, "error getting user groups"),
}
return res, nil
}
res := &userpb.GetUserGroupsResponse{
Status: status.NewOK(ctx),
Groups: groups,
}
return res, nil
}
func (s *service) GetTenant(ctx context.Context, req *tenantpb.GetTenantRequest) (*tenantpb.GetTenantResponse, error) {
log := appctx.GetLogger(ctx)
t, err := s.tenantmgr.GetTenant(ctx, req.GetTenantId())
if err != nil {
log.Warn().Err(err).Interface("tenantid", req.GetTenantId()).Msg("error getting tenant")
res := &tenantpb.GetTenantResponse{
Status: status.NewInternal(ctx, "error getting tenant"),
}
if _, ok := err.(errtypes.NotFound); ok {
res.Status = status.NewNotFound(ctx, "tenant not found")
}
return res, nil
}
return &tenantpb.GetTenantResponse{
Status: status.NewOK(ctx),
Tenant: t,
}, nil
}
func (s *service) GetTenantByClaim(ctx context.Context, req *tenantpb.GetTenantByClaimRequest) (*tenantpb.GetTenantByClaimResponse, error) {
log := appctx.GetLogger(ctx)
t, err := s.tenantmgr.GetTenantByClaim(ctx, req.GetClaim(), req.GetValue())
if err != nil {
log.Warn().Err(err).Interface("claim", req.GetClaim()).Interface("value", req.GetValue()).Msg("error getting tenant")
res := &tenantpb.GetTenantByClaimResponse{
Status: status.NewInternal(ctx, "error getting tenant"),
}
if _, ok := err.(errtypes.NotFound); ok {
res.Status = status.NewNotFound(ctx, "tenant not found")
}
return res, nil
}
return &tenantpb.GetTenantByClaimResponse{
Status: status.NewOK(ctx),
Tenant: t,
}, nil
}
@@ -0,0 +1,857 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package usershareprovider
import (
"context"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/conversions"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/permission"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/share"
"github.com/opencloud-eu/reva/v2/pkg/share/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
const (
_fieldMaskPathMountPoint = "mount_point"
_fieldMaskPathPermissions = "permissions"
_fieldMaskPathState = "state"
_spaceTypePersonal = "personal"
_spaceTypeProject = "project"
_spaceTypeVirtual = "virtual"
)
func init() {
rgrpc.Register("usershareprovider", NewDefault)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
GatewayAddr string `mapstructure:"gateway_addr"`
AllowedPathsForShares []string `mapstructure:"allowed_paths_for_shares"`
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "json"
}
}
type service struct {
sm share.Manager
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
allowedPathsForShares []*regexp.Regexp
}
func getShareManager(c *config, logger *zerolog.Logger) (share.Manager, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver], logger)
}
return nil, errtypes.NotFound("driver not found: " + c.Driver)
}
// TODO(labkode): add ctx to Close.
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{}
}
func (s *service) Register(ss *grpc.Server) {
collaboration.RegisterCollaborationAPIServer(ss, s)
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
return c, nil
}
// New creates a new user share provider svc initialized from defaults
func NewDefault(m map[string]any, ss *grpc.Server, logger *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
sm, err := getShareManager(c, logger)
if err != nil {
return nil, err
}
allowedPathsForShares := make([]*regexp.Regexp, 0, len(c.AllowedPathsForShares))
for _, s := range c.AllowedPathsForShares {
regex, err := regexp.Compile(s)
if err != nil {
return nil, err
}
allowedPathsForShares = append(allowedPathsForShares, regex)
}
gatewaySelector, err := pool.GatewaySelector(sharedconf.GetGatewaySVC(c.GatewayAddr))
if err != nil {
return nil, err
}
return New(gatewaySelector, sm, allowedPathsForShares), nil
}
// New creates a new user share provider svc
func New(gatewaySelector pool.Selectable[gateway.GatewayAPIClient], sm share.Manager, allowedPathsForShares []*regexp.Regexp) rgrpc.Service {
service := &service{
sm: sm,
gatewaySelector: gatewaySelector,
allowedPathsForShares: allowedPathsForShares,
}
return service
}
func (s *service) isPathAllowed(path string) bool {
if len(s.allowedPathsForShares) == 0 {
return true
}
for _, reg := range s.allowedPathsForShares {
if reg.MatchString(path) {
return true
}
}
return false
}
func (s *service) CreateShare(ctx context.Context, req *collaboration.CreateShareRequest) (*collaboration.CreateShareResponse, error) {
log := appctx.GetLogger(ctx)
user := ctxpkg.ContextMustGetUser(ctx)
// check if the grantee is a user or group
if req.GetGrant().GetGrantee().GetType() == provider.GranteeType_GRANTEE_TYPE_USER {
// check if the tenantId of the user matches the tenantId of the target user
if user.GetId().GetTenantId() != req.GetGrant().GetGrantee().GetUserId().GetTenantId() {
log.Warn().Msg("user tenantId does not match the target user tenantId, this is not supported yet")
return &collaboration.CreateShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "user tenantId does not match the target user tenantId"),
}, nil
}
}
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
// check if the user has the permission to create shares at all
ok, err := utils.CheckPermission(ctx, permission.WriteShare, gatewayClient)
if err != nil {
return &collaboration.CreateShareResponse{
Status: status.NewInternal(ctx, "failed check user permission to write public link"),
}, err
}
if !ok {
return &collaboration.CreateShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "no permission to create public links"),
}, nil
}
// use logged in user Idp as default, if the Grantee does not have an IDP set.
if req.GetGrant().GetGrantee().GetType() == provider.GranteeType_GRANTEE_TYPE_USER && req.GetGrant().GetGrantee().GetUserId().GetIdp() == "" {
req.GetGrant().GetGrantee().Id = &provider.Grantee_UserId{
UserId: &userpb.UserId{
OpaqueId: req.GetGrant().GetGrantee().GetUserId().GetOpaqueId(),
Idp: user.GetId().GetIdp(),
Type: userpb.UserType_USER_TYPE_PRIMARY},
}
}
// some for group grantees
if req.GetGrant().GetGrantee().GetType() == provider.GranteeType_GRANTEE_TYPE_GROUP && req.GetGrant().GetGrantee().GetGroupId().GetIdp() == "" {
// use logged in user Idp as default.
req.GetGrant().GetGrantee().Id = &provider.Grantee_GroupId{
GroupId: &grouppb.GroupId{
OpaqueId: req.GetGrant().GetGrantee().GetGroupId().GetOpaqueId(),
Idp: user.GetId().GetIdp(),
Type: grouppb.GroupType_GROUP_TYPE_REGULAR},
}
}
sRes, err := gatewayClient.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: req.GetResourceInfo().GetId()}})
if err != nil {
log.Err(err).Interface("resource_id", req.GetResourceInfo().GetId()).Msg("failed to stat resource to share")
return &collaboration.CreateShareResponse{
Status: status.NewInternal(ctx, "failed to stat shared resource"),
}, err
}
// the user needs to have the AddGrant permissions on the Resource to be able to create a share
if !sRes.GetInfo().GetPermissionSet().AddGrant {
return &collaboration.CreateShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "no permission to add grants on shared resource"),
}, err
}
isSpaceRoot := utils.IsSpaceRoot(sRes.GetInfo())
var noEvent bool
if isSpaceRoot {
if sRes.GetInfo().GetSpace().GetSpaceType() == _spaceTypePersonal || sRes.GetInfo().GetSpace().GetSpaceType() == _spaceTypeVirtual {
return &collaboration.CreateShareResponse{Status: status.NewInvalid(ctx, "space type is not eligible for sharing")}, nil
}
}
// do not allow share to myself or the owner if share is for a user
if req.GetGrant().GetGrantee().GetType() == provider.GranteeType_GRANTEE_TYPE_USER &&
(utils.UserEqual(req.GetGrant().GetGrantee().GetUserId(), user.Id) || utils.UserEqual(req.GetGrant().GetGrantee().GetUserId(), sRes.GetInfo().GetOwner())) {
denySelfShare := true
// To allow adding the initial "mananger" share for the creator of a space when need to make an exception here
// if the shared resource is a space root, that does not have any Share existin yet. Note, that we have already
// verified that the user adding the share does really have "AddGrant" permissions on the affected resource, so
// this "hack" should be ok.
if isSpaceRoot {
shares, err := s.sm.ListShares(
ctx,
[]*collaboration.Filter{share.ResourceIDFilter(req.GetResourceInfo().GetId())},
)
if err != nil {
return &collaboration.CreateShareResponse{
Status: status.NewInternal(ctx, "failed to list existing shares"),
}, nil
}
if len(shares) == 0 {
denySelfShare = false
noEvent = true
}
}
if denySelfShare {
err := errtypes.BadRequest("jsoncs3: owner/creator and grantee are the same")
return nil, err
}
}
// resharing is forbidden for not space roots
if !isSpaceRoot {
// Resharing of Files/Directories is forbidden. So the grants must not allow the "grant" permissions
if HasGrantPermissions(req.GetGrant().GetPermissions().GetPermissions()) {
return &collaboration.CreateShareResponse{
Status: status.NewInvalidArg(ctx, "resharing not supported"),
}, nil
}
}
// check if the share creator has sufficient permissions to do so.
if shareCreationAllowed := conversions.SufficientCS3Permissions(
sRes.GetInfo().GetPermissionSet(),
req.GetGrant().GetPermissions().GetPermissions(),
); !shareCreationAllowed {
return &collaboration.CreateShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "insufficient permissions to create that kind of share"),
}, nil
}
// check if the requested permission are plausible for the Resource
if sRes.GetInfo().GetType() == provider.ResourceType_RESOURCE_TYPE_FILE {
if newPermissions := req.GetGrant().GetPermissions().GetPermissions(); newPermissions.GetCreateContainer() || newPermissions.GetMove() || newPermissions.GetDelete() {
return &collaboration.CreateShareResponse{
Status: status.NewInvalid(ctx, "cannot set the requested permissions on that type of resource"),
}, nil
}
}
if !s.isPathAllowed(req.GetResourceInfo().GetPath()) {
return &collaboration.CreateShareResponse{
Status: status.NewFailedPrecondition(ctx, nil, "share creation is not allowed for the specified path"),
}, nil
}
createdShare, err := s.sm.Share(ctx, req.GetResourceInfo(), req.GetGrant())
if err != nil {
return &collaboration.CreateShareResponse{
Status: status.NewStatusFromErrType(ctx, "error creating share", err),
}, nil
}
var opaque *typesv1beta1.Opaque
if isSpaceRoot {
opaque = utils.SpaceGrantOpaque()
}
if noEvent {
opaque = &typesv1beta1.Opaque{
Map: map[string]*typesv1beta1.OpaqueEntry{
"noevent": {},
},
}
}
return &collaboration.CreateShareResponse{
Status: status.NewOK(ctx),
Share: createdShare,
Opaque: utils.AppendPlainToOpaque(opaque, "resourcename", sRes.GetInfo().GetName()),
}, nil
}
func HasGrantPermissions(p *provider.ResourcePermissions) bool {
return p.GetAddGrant() || p.GetUpdateGrant() || p.GetRemoveGrant() || p.GetDenyGrant()
}
// spaceRootHasRemainingManager returns true if the given resource has at least one share
// (excluding the share with excludeShareOpaqueID) that grants both AddGrant and RemoveGrant.
func (s *service) spaceRootHasRemainingManager(ctx context.Context, resourceID *provider.ResourceId, excludeShareOpaqueID string) (bool, error) {
shares, err := s.sm.ListShares(ctx, []*collaboration.Filter{
{
Type: collaboration.Filter_TYPE_RESOURCE_ID,
Term: &collaboration.Filter_ResourceId{ResourceId: resourceID},
},
})
if err != nil {
return false, err
}
for _, s := range shares {
if s.GetId().GetOpaqueId() == excludeShareOpaqueID {
continue
}
perms := s.GetPermissions().GetPermissions()
if perms.GetAddGrant() && perms.GetRemoveGrant() {
return true, nil
}
}
return false, nil
}
func (s *service) RemoveShare(ctx context.Context, req *collaboration.RemoveShareRequest) (*collaboration.RemoveShareResponse, error) {
log := appctx.GetLogger(ctx)
user := ctxpkg.ContextMustGetUser(ctx)
share, err := s.sm.GetShare(ctx, req.Ref)
if err != nil {
return &collaboration.RemoveShareResponse{
Status: status.NewInternal(ctx, "error getting share"),
}, nil
}
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
sRes, err := gatewayClient.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: share.GetResourceId()}})
if err != nil {
log.Err(err).Interface("resource_id", share.GetResourceId()).Msg("failed to stat shared resource")
return &collaboration.RemoveShareResponse{
Status: status.NewInternal(ctx, "failed to stat shared resource"),
}, err
}
// the requesting user needs to be either the Owner/Creator of the share or have the RemoveGrant permissions on the Resource
switch {
case utils.UserEqual(user.GetId(), share.GetCreator()) || utils.UserEqual(user.GetId(), share.GetOwner()):
fallthrough
case sRes.GetInfo().GetPermissionSet().RemoveGrant:
break
default:
return &collaboration.RemoveShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "no permission to remove grants on shared resource"),
}, err
}
// For space root shares, ensure at least one share with both AddGrant and RemoveGrant will remain
// so the space always has a manager who can administer grants.
if utils.IsSpaceRoot(sRes.GetInfo()) {
if ok, err := s.spaceRootHasRemainingManager(ctx, share.GetResourceId(), share.GetId().GetOpaqueId()); err != nil {
return &collaboration.RemoveShareResponse{
Status: status.NewInternal(ctx, "failed to list shares for space root"),
}, nil
} else if !ok {
return &collaboration.RemoveShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "cannot remove the last share with manager permissions on a space root"),
}, nil
}
}
err = s.sm.Unshare(ctx, req.Ref)
if err != nil {
return &collaboration.RemoveShareResponse{
Status: status.NewInternal(ctx, "error removing share"),
}, nil
}
var opaque *typesv1beta1.Opaque
if utils.IsSpaceRoot(sRes.GetInfo()) {
opaque = utils.SpaceGrantOpaque()
}
opaque = utils.AppendJSONToOpaque(opaque, "resourceid", share.GetResourceId())
opaque = utils.AppendPlainToOpaque(opaque, "resourcename", sRes.GetInfo().GetName())
if user := share.GetGrantee().GetUserId(); user != nil {
opaque = utils.AppendJSONToOpaque(opaque, "granteeuserid", user)
} else {
opaque = utils.AppendJSONToOpaque(opaque, "granteegroupid", share.GetGrantee().GetGroupId())
}
return &collaboration.RemoveShareResponse{
Opaque: opaque,
Status: status.NewOK(ctx),
}, nil
}
func (s *service) GetShare(ctx context.Context, req *collaboration.GetShareRequest) (*collaboration.GetShareResponse, error) {
share, err := s.sm.GetShare(ctx, req.Ref)
if err != nil {
var st *rpc.Status
switch err.(type) {
case errtypes.IsNotFound:
st = status.NewNotFound(ctx, err.Error())
default:
st = status.NewInternal(ctx, err.Error())
}
return &collaboration.GetShareResponse{
Status: st,
}, nil
}
return &collaboration.GetShareResponse{
Status: status.NewOK(ctx),
Share: share,
}, nil
}
func (s *service) ListShares(ctx context.Context, req *collaboration.ListSharesRequest) (*collaboration.ListSharesResponse, error) {
shares, err := s.sm.ListShares(ctx, req.Filters) // TODO(labkode): add filter to share manager
if err != nil {
return &collaboration.ListSharesResponse{
Status: status.NewInternal(ctx, "error listing shares"),
}, nil
}
res := &collaboration.ListSharesResponse{
Status: status.NewOK(ctx),
Shares: shares,
}
return res, nil
}
func (s *service) UpdateShare(ctx context.Context, req *collaboration.UpdateShareRequest) (*collaboration.UpdateShareResponse, error) {
log := appctx.GetLogger(ctx)
user := ctxpkg.ContextMustGetUser(ctx)
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
// check if the user has the permission to create shares at all
ok, err := utils.CheckPermission(ctx, permission.WriteShare, gatewayClient)
if err != nil {
return &collaboration.UpdateShareResponse{
Status: status.NewInternal(ctx, "failed check user permission to write share"),
}, err
}
if !ok {
return &collaboration.UpdateShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "no permission to create user share"),
}, nil
}
// Read share from backend. We need the shared resource's id for STATing it, it might not be in
// the incoming request
currentShare, err := s.sm.GetShare(ctx,
&collaboration.ShareReference{
Spec: &collaboration.ShareReference_Id{
Id: req.GetShare().GetId(),
},
},
)
if err != nil {
var st *rpc.Status
switch err.(type) {
case errtypes.IsNotFound:
st = status.NewNotFound(ctx, err.Error())
default:
st = status.NewInternal(ctx, err.Error())
}
return &collaboration.UpdateShareResponse{
Status: st,
}, nil
}
sRes, err := gatewayClient.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: currentShare.GetResourceId()}})
if err != nil {
log.Err(err).Interface("resource_id", req.GetShare().GetResourceId()).Msg("failed to stat resource to share")
return &collaboration.UpdateShareResponse{
Status: status.NewInternal(ctx, "failed to stat shared resource"),
}, err
}
// the requesting user needs to be either the Owner/Creator of the share or have the UpdateGrant permissions on the Resource
switch {
case utils.UserEqual(user.GetId(), currentShare.GetCreator()) || utils.UserEqual(user.GetId(), currentShare.GetOwner()):
fallthrough
case sRes.GetInfo().GetPermissionSet().UpdateGrant:
break
default:
return &collaboration.UpdateShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "no permission to remove grants on shared resource"),
}, err
}
// resharing is forbidden for not space roots
if !utils.IsSpaceRoot(sRes.GetInfo()) {
// Resharing of Files/Directories is forbidden. So the grants must not allow the "grant" permissions
if HasGrantPermissions(req.GetShare().GetPermissions().GetPermissions()) {
return &collaboration.UpdateShareResponse{
Status: status.NewInvalidArg(ctx, "resharing not supported"),
}, nil
}
}
// If this is a permissions update, check if user's permissions on the resource are sufficient to set the desired permissions
var newPermissions *provider.ResourcePermissions
if slices.Contains(req.GetUpdateMask().GetPaths(), _fieldMaskPathPermissions) {
newPermissions = req.GetShare().GetPermissions().GetPermissions()
} else {
newPermissions = req.GetField().GetPermissions().GetPermissions()
}
if newPermissions != nil && !conversions.SufficientCS3Permissions(sRes.GetInfo().GetPermissionSet(), newPermissions) {
return &collaboration.UpdateShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "insufficient permissions to create that kind of share"),
}, nil
}
// For space root shares, if the new permissions would strip AddGrant or RemoveGrant from this share,
// ensure at least one other share still has both so the space retains a manager.
if utils.IsSpaceRoot(sRes.GetInfo()) && newPermissions != nil && (!newPermissions.GetAddGrant() || !newPermissions.GetRemoveGrant()) {
if ok, err := s.spaceRootHasRemainingManager(ctx, currentShare.GetResourceId(), currentShare.GetId().GetOpaqueId()); err != nil {
return &collaboration.UpdateShareResponse{
Status: status.NewInternal(ctx, "failed to list shares for space root"),
}, nil
} else if !ok {
return &collaboration.UpdateShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "cannot remove the last share with manager permissions on a space root"),
}, nil
}
}
// check if the requested permission are plausible for the Resource
// do we need more here?
if sRes.GetInfo().GetType() == provider.ResourceType_RESOURCE_TYPE_FILE {
if newPermissions.GetCreateContainer() || newPermissions.GetMove() || newPermissions.GetDelete() {
return &collaboration.UpdateShareResponse{
Status: status.NewInvalid(ctx, "cannot set the requested permissions on that type of resource"),
}, nil
}
}
share, err := s.sm.UpdateShare(ctx, req.Ref, req.Field.GetPermissions(), req.Share, req.UpdateMask) // TODO(labkode): check what to update
if err != nil {
return &collaboration.UpdateShareResponse{
Status: status.NewInternal(ctx, "error updating share"),
}, nil
}
var opaque *typesv1beta1.Opaque
if utils.IsSpaceRoot(sRes.GetInfo()) {
opaque = utils.SpaceGrantOpaque()
}
res := &collaboration.UpdateShareResponse{
Status: status.NewOK(ctx),
Share: share,
Opaque: utils.AppendPlainToOpaque(opaque, "resourcename", sRes.GetInfo().GetName()),
}
return res, nil
}
func (s *service) ListReceivedShares(ctx context.Context, req *collaboration.ListReceivedSharesRequest) (*collaboration.ListReceivedSharesResponse, error) {
// For the UI add a filter to not display the denial shares
foundExclude := false
for _, f := range req.Filters {
if f.Type == collaboration.Filter_TYPE_EXCLUDE_DENIALS {
foundExclude = true
break
}
}
if !foundExclude {
req.Filters = append(req.Filters, &collaboration.Filter{Type: collaboration.Filter_TYPE_EXCLUDE_DENIALS})
}
var uid userpb.UserId
_ = utils.ReadJSONFromOpaque(req.Opaque, "userid", &uid)
shares, err := s.sm.ListReceivedShares(ctx, req.Filters, &uid) // TODO(labkode): check what to update
if err != nil {
return &collaboration.ListReceivedSharesResponse{
Status: status.NewInternal(ctx, "error listing received shares"),
}, nil
}
res := &collaboration.ListReceivedSharesResponse{
Status: status.NewOK(ctx),
Shares: shares,
}
return res, nil
}
func (s *service) GetReceivedShare(ctx context.Context, req *collaboration.GetReceivedShareRequest) (*collaboration.GetReceivedShareResponse, error) {
log := appctx.GetLogger(ctx)
share, err := s.sm.GetReceivedShare(ctx, req.Ref)
if err != nil {
log.Debug().Err(err).Msg("error getting received share")
switch err.(type) {
case errtypes.NotFound:
return &collaboration.GetReceivedShareResponse{
Status: status.NewNotFound(ctx, "error getting received share"),
}, nil
default:
return &collaboration.GetReceivedShareResponse{
Status: status.NewInternal(ctx, "error getting received share"),
}, nil
}
}
res := &collaboration.GetReceivedShareResponse{
Status: status.NewOK(ctx),
Share: share,
}
return res, nil
}
func (s *service) UpdateReceivedShare(ctx context.Context, req *collaboration.UpdateReceivedShareRequest) (*collaboration.UpdateReceivedShareResponse, error) {
if req.GetShare().GetShare().GetId().GetOpaqueId() == "" {
return &collaboration.UpdateReceivedShareResponse{
Status: status.NewInvalid(ctx, "share id empty"),
}, nil
}
isStateTransitionShareAccepted := slices.Contains(req.GetUpdateMask().GetPaths(), _fieldMaskPathState) && req.GetShare().GetState() == collaboration.ShareState_SHARE_STATE_ACCEPTED
isMountPointSet := slices.Contains(req.GetUpdateMask().GetPaths(), _fieldMaskPathMountPoint) && req.GetShare().GetMountPoint().GetPath() != ""
// we calculate a valid mountpoint only if the share should be accepted and the mount point is not set explicitly
if isStateTransitionShareAccepted && !isMountPointSet {
s, err := s.setReceivedShareMountPoint(ctx, req)
switch {
case err != nil:
fallthrough
case s.GetCode() != rpc.Code_CODE_OK:
return &collaboration.UpdateReceivedShareResponse{
Status: s,
}, err
}
}
var uid userpb.UserId
_ = utils.ReadJSONFromOpaque(req.Opaque, "userid", &uid)
updatedShare, err := s.sm.UpdateReceivedShare(ctx, req.Share, req.UpdateMask, &uid)
switch err.(type) {
case nil:
return &collaboration.UpdateReceivedShareResponse{
Status: status.NewOK(ctx),
Share: updatedShare,
}, nil
case errtypes.NotFound:
return &collaboration.UpdateReceivedShareResponse{
Status: status.NewNotFound(ctx, "error getting received share"),
}, nil
default:
return &collaboration.UpdateReceivedShareResponse{
Status: status.NewInternal(ctx, "error getting received share"),
}, nil
}
}
func (s *service) setReceivedShareMountPoint(ctx context.Context, req *collaboration.UpdateReceivedShareRequest) (*rpc.Status, error) {
gwc, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
receivedShare, err := gwc.GetReceivedShare(ctx, &collaboration.GetReceivedShareRequest{
Ref: &collaboration.ShareReference{
Spec: &collaboration.ShareReference_Id{
Id: req.GetShare().GetShare().GetId(),
},
},
})
switch {
case err != nil:
fallthrough
case receivedShare.GetStatus().GetCode() != rpc.Code_CODE_OK:
return receivedShare.GetStatus(), err
}
if receivedShare.GetShare().GetMountPoint().GetPath() != "" {
return status.NewOK(ctx), nil
}
gwc, err = s.gatewaySelector.Next()
if err != nil {
return nil, err
}
resourceStat, err := gwc.Stat(ctx, &provider.StatRequest{
Ref: &provider.Reference{
ResourceId: receivedShare.GetShare().GetShare().GetResourceId(),
},
})
switch {
case err != nil:
fallthrough
case resourceStat.GetStatus().GetCode() != rpc.Code_CODE_OK:
return resourceStat.GetStatus(), err
}
// handle mount point related updates
{
var userID *userpb.UserId
_ = utils.ReadJSONFromOpaque(req.Opaque, "userid", &userID)
receivedShares, err := s.sm.ListReceivedShares(ctx, []*collaboration.Filter{}, userID)
if err != nil {
return nil, err
}
// check if the requested mount point is available and if not, find a suitable one
availableMountpoint, _, err := getMountpointAndUnmountedShares(ctx, receivedShares, s.gatewaySelector, nil,
resourceStat.GetInfo().GetId(),
resourceStat.GetInfo().GetName(),
)
if err != nil {
return status.NewInternal(ctx, err.Error()), nil
}
if !slices.Contains(req.GetUpdateMask().GetPaths(), _fieldMaskPathMountPoint) {
req.GetUpdateMask().Paths = append(req.GetUpdateMask().GetPaths(), _fieldMaskPathMountPoint)
}
req.GetShare().MountPoint = &provider.Reference{
Path: availableMountpoint,
}
}
return status.NewOK(ctx), nil
}
// GetMountpointAndUnmountedShares returns a new or existing mountpoint for the given info and produces a list of unmounted received shares for the same resource
func GetMountpointAndUnmountedShares(ctx context.Context, gwc gateway.GatewayAPIClient, id *provider.ResourceId, name string, userId *userpb.UserId) (string, []*collaboration.ReceivedShare, error) {
listReceivedSharesReq := &collaboration.ListReceivedSharesRequest{}
if userId != nil {
listReceivedSharesReq.Opaque = utils.AppendJSONToOpaque(nil, "userid", userId)
}
listReceivedSharesRes, err := gwc.ListReceivedShares(ctx, listReceivedSharesReq)
if err != nil {
return "", nil, errtypes.InternalError("grpc list received shares request failed")
}
if err := errtypes.NewErrtypeFromStatus(listReceivedSharesRes.GetStatus()); err != nil {
return "", nil, err
}
return getMountpointAndUnmountedShares(ctx, listReceivedSharesRes.GetShares(), nil, gwc, id, name)
}
// GetMountpointAndUnmountedShares returns a new or existing mountpoint for the given info and produces a list of unmounted received shares for the same resource
func getMountpointAndUnmountedShares(ctx context.Context, receivedShares []*collaboration.ReceivedShare, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], gwc gateway.GatewayAPIClient, id *provider.ResourceId, name string) (string, []*collaboration.ReceivedShare, error) {
unmountedShares := []*collaboration.ReceivedShare{}
base := filepath.Clean(name)
mount := base
existingMountpoint := ""
mountedShares := make([]string, 0, len(receivedShares))
var pathExists bool
var err error
for _, s := range receivedShares {
resourceIDEqual := utils.ResourceIDEqual(s.GetShare().GetResourceId(), id)
if resourceIDEqual && s.State == collaboration.ShareState_SHARE_STATE_ACCEPTED {
if gatewaySelector != nil {
gwc, err = gatewaySelector.Next()
if err != nil {
return "", nil, err
}
}
// a share to the resource already exists and is mounted, remembers the mount point
_, err := utils.GetResourceByID(ctx, s.GetShare().GetResourceId(), gwc)
if err == nil {
existingMountpoint = s.GetMountPoint().GetPath()
}
}
if resourceIDEqual && s.State != collaboration.ShareState_SHARE_STATE_ACCEPTED {
// a share to the resource already exists but is not mounted, collect the unmounted share
unmountedShares = append(unmountedShares, s)
}
if s.State == collaboration.ShareState_SHARE_STATE_ACCEPTED {
// collect all accepted mount points
mountedShares = append(mountedShares, s.GetMountPoint().GetPath())
if s.GetMountPoint().GetPath() == mount {
// does the shared resource still exist?
if gatewaySelector != nil {
gwc, err = gatewaySelector.Next()
if err != nil {
return "", nil, err
}
}
_, err := utils.GetResourceByID(ctx, s.GetShare().GetResourceId(), gwc)
if err == nil {
pathExists = true
}
// TODO we could delete shares here if the stat returns code NOT FOUND ... but listening for file deletes would be better
}
}
}
if existingMountpoint != "" {
// we want to reuse the same mountpoint for all unmounted shares to the same resource
return existingMountpoint, unmountedShares, nil
}
// If the mount point really already exists, we need to insert a number into the filename
if pathExists {
// now we have a list of shares, we want to iterate over all of them and check for name collisions agents a mount points list
for i := 1; i <= len(mountedShares)+1; i++ {
ext := filepath.Ext(base)
name := strings.TrimSuffix(base, ext)
mount = name + " (" + strconv.Itoa(i) + ")" + ext
if !slices.Contains(mountedShares, mount) {
return mount, unmountedShares, nil
}
}
}
return mount, unmountedShares, nil
}
@@ -0,0 +1,60 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
// Package appctx creates a context with useful
// components attached to the context like loggers and
// token managers.
package appctx
import (
"net/http"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/trace"
)
// name is the Tracer name used to identify this instrumentation library.
const tracerName = "appctx"
// New returns a new HTTP middleware that stores the log
// in the context with request ID information.
func New(log zerolog.Logger, tp trace.TracerProvider) func(http.Handler) http.Handler {
chain := func(h http.Handler) http.Handler {
return handler(log, tp, h)
}
return chain
}
func handler(log zerolog.Logger, tp trace.TracerProvider, h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
span := trace.SpanFromContext(ctx)
defer span.End()
if !span.SpanContext().HasTraceID() {
ctx, span = tp.Tracer(tracerName).Start(ctx, "http interceptor")
}
sub := log.With().Str("traceid", span.SpanContext().TraceID().String()).Logger()
ctx = appctx.WithLogger(ctx, &sub)
ctx = appctx.WithTracerProvider(ctx, tp)
r = r.WithContext(ctx)
h.ServeHTTP(w, r)
})
}
@@ -0,0 +1,420 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package auth
import (
"context"
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/bluele/gcache"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/credential/registry"
tokenregistry "github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/token/registry"
tokenwriterregistry "github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/tokenwriter/registry"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/token"
tokenmgr "github.com/opencloud-eu/reva/v2/pkg/token/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/pkg/errors"
"github.com/rs/zerolog"
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc/metadata"
)
// name is the Tracer name used to identify this instrumentation library.
const tracerName = "auth"
var (
cacheOnce sync.Once
userGroupsCache gcache.Cache
)
type config struct {
Priority int `mapstructure:"priority"`
GatewaySvc string `mapstructure:"gatewaysvc"`
// TODO(jdf): Realm is optional, will be filled with request host if not given?
Realm string `mapstructure:"realm"`
CredentialsByUserAgent map[string]string `mapstructure:"credentials_by_user_agent"`
CredentialChain []string `mapstructure:"credential_chain"`
CredentialStrategies map[string]map[string]interface{} `mapstructure:"credential_strategies"`
TokenStrategyChain []string `mapstructure:"token_strategy_chain"`
TokenStrategies map[string]map[string]interface{} `mapstructure:"token_strategies"`
TokenManager string `mapstructure:"token_manager"`
TokenManagers map[string]map[string]interface{} `mapstructure:"token_managers"`
TokenWriter string `mapstructure:"token_writer"`
TokenWriters map[string]map[string]interface{} `mapstructure:"token_writers"`
UserGroupsCacheSize int `mapstructure:"usergroups_cache_size"`
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
return c, nil
}
// New returns a new middleware with defined priority.
func New(m map[string]interface{}, unprotected []string, tp trace.TracerProvider) (global.Middleware, error) {
conf, err := parseConfig(m)
if err != nil {
return nil, err
}
conf.GatewaySvc = sharedconf.GetGatewaySVC(conf.GatewaySvc)
// set defaults
if len(conf.TokenStrategyChain) == 0 {
conf.TokenStrategyChain = []string{"header"}
}
if conf.TokenWriter == "" {
conf.TokenWriter = "header"
}
if conf.TokenManager == "" {
conf.TokenManager = "jwt"
}
if conf.CredentialsByUserAgent == nil {
conf.CredentialsByUserAgent = map[string]string{}
}
if conf.UserGroupsCacheSize == 0 {
conf.UserGroupsCacheSize = 5000
}
cacheOnce.Do(func() {
userGroupsCache = gcache.New(conf.UserGroupsCacheSize).LFU().Build()
})
credChain := map[string]auth.CredentialStrategy{}
for i, key := range conf.CredentialChain {
f, ok := registry.NewCredentialFuncs[conf.CredentialChain[i]]
if !ok {
return nil, fmt.Errorf("credential strategy not found: %s", conf.CredentialChain[i])
}
credStrategy, err := f(conf.CredentialStrategies[conf.CredentialChain[i]])
if err != nil {
return nil, err
}
credChain[key] = credStrategy
}
tokenStrategyChain := make([]auth.TokenStrategy, 0, len(conf.TokenStrategyChain))
for _, strategy := range conf.TokenStrategyChain {
g, ok := tokenregistry.NewTokenFuncs[strategy]
if !ok {
return nil, fmt.Errorf("token strategy not found: %s", strategy)
}
tokenStrategy, err := g(conf.TokenStrategies[strategy])
if err != nil {
return nil, err
}
tokenStrategyChain = append(tokenStrategyChain, tokenStrategy)
}
h, ok := tokenmgr.NewFuncs[conf.TokenManager]
if !ok {
return nil, fmt.Errorf("token manager not found: %s", conf.TokenManager)
}
tokenManager, err := h(conf.TokenManagers[conf.TokenManager])
if err != nil {
return nil, err
}
i, ok := tokenwriterregistry.NewTokenFuncs[conf.TokenWriter]
if !ok {
return nil, fmt.Errorf("token writer not found: %s", conf.TokenWriter)
}
tokenWriter, err := i(conf.TokenWriters[conf.TokenWriter])
if err != nil {
return nil, err
}
chain := func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// OPTION requests need to pass for preflight requests
// TODO(labkode): this will break options for auth protected routes.
// Maybe running the CORS middleware before auth kicks in is enough.
ctx := r.Context()
span := trace.SpanFromContext(ctx)
defer span.End()
if !span.SpanContext().HasTraceID() {
_, span = tp.Tracer(tracerName).Start(ctx, "http auth interceptor")
}
if r.Method == "OPTIONS" {
h.ServeHTTP(w, r)
return
}
log := appctx.GetLogger(r.Context())
isUnprotectedEndpoint := false
// For unprotected URLs, we try to authenticate the request in case some service needs it,
// but don't return any errors if it fails.
if utils.Skip(r.URL.Path, unprotected) {
log.Info().Msg("skipping auth check for: " + r.URL.Path)
isUnprotectedEndpoint = true
}
ctx, err := authenticateUser(w, r, conf, tokenStrategyChain, tokenManager, tokenWriter, credChain, isUnprotectedEndpoint)
if err != nil {
if !isUnprotectedEndpoint {
return
}
} else {
u, ok := ctxpkg.ContextGetUser(ctx)
if ok {
span.SetAttributes(semconv.EnduserIDKey.String(u.Id.OpaqueId))
}
r = r.WithContext(ctx)
}
h.ServeHTTP(w, r)
})
}
return chain, nil
}
func authenticateUser(w http.ResponseWriter, r *http.Request, conf *config, tokenStrategies []auth.TokenStrategy, tokenManager token.Manager, tokenWriter auth.TokenWriter, credChain map[string]auth.CredentialStrategy, isUnprotectedEndpoint bool) (context.Context, error) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
// Add the request user-agent to the ctx
ctx = metadata.NewIncomingContext(ctx, metadata.New(map[string]string{ctxpkg.UserAgentHeader: r.UserAgent()}))
client, err := pool.GetGatewayServiceClient(conf.GatewaySvc)
if err != nil {
logError(isUnprotectedEndpoint, log, err, "error getting the authsvc client", http.StatusUnauthorized, w)
return nil, err
}
// reva token or auth token can be passed using the same technique (for example bearer)
// before validating it against an auth provider, we can check directly if it's a reva
// token and if not try to use it for authenticating the user.
for _, tokenStrategy := range tokenStrategies {
token := tokenStrategy.GetToken(r)
if token != "" {
if user, tokenScope, ok := isTokenValid(r, tokenManager, token); ok {
if err := insertGroupsInUser(ctx, userGroupsCache, client, user); err != nil {
logError(isUnprotectedEndpoint, log, err, "got an error retrieving groups for user "+user.Username, http.StatusInternalServerError, w)
return nil, err
}
return ctxWithUserInfo(ctx, r, user, token, tokenScope, r.Header.Get(ctxpkg.InitiatorHeader)), nil
}
}
}
log.Warn().Msg("core access token not set")
userAgentCredKeys := getCredsForUserAgent(r.UserAgent(), conf.CredentialsByUserAgent, conf.CredentialChain)
// obtain credentials (basic auth, bearer token, ...) based on user agent
var creds *auth.Credentials
for _, k := range userAgentCredKeys {
creds, err = credChain[k].GetCredentials(w, r)
if err != nil {
log.Debug().Err(err).Msg("error retrieving credentials")
}
if creds != nil {
log.Debug().Msgf("credentials obtained from credential strategy: type: %s, client_id: %s", creds.Type, creds.ClientID)
break
}
}
// if no credentials are found, reply with authentication challenge depending on user agent
if creds == nil {
if !isUnprotectedEndpoint {
for _, key := range userAgentCredKeys {
if cred, ok := credChain[key]; ok {
cred.AddWWWAuthenticate(w, r, conf.Realm)
} else {
log.Error().Msg("auth credential strategy: " + key + "must have been loaded in init method")
w.WriteHeader(http.StatusInternalServerError)
return nil, errtypes.InternalError("no credentials found")
}
}
w.WriteHeader(http.StatusUnauthorized)
}
return nil, errtypes.PermissionDenied("no credentials found")
}
req := &gateway.AuthenticateRequest{
Type: creds.Type,
ClientId: creds.ClientID,
ClientSecret: creds.ClientSecret,
}
log.Debug().Msgf("AuthenticateRequest: type: %s, client_id: %s against %s", req.Type, req.ClientId, conf.GatewaySvc)
res, err := client.Authenticate(ctx, req)
if err != nil {
logError(isUnprotectedEndpoint, log, err, "error calling Authenticate", http.StatusUnauthorized, w)
return nil, err
}
if res.Status.Code != rpc.Code_CODE_OK {
err := status.NewErrorFromCode(res.Status.Code, "auth")
logError(isUnprotectedEndpoint, log, err, "error generating access token from credentials", http.StatusUnauthorized, w)
return nil, err
}
log.Info().Msg("core access token generated") // write token to response
// write token to response
token := res.Token
tokenWriter.WriteToken(token, w)
// validate token
u, tokenScope, err := tokenManager.DismantleToken(r.Context(), token)
if err != nil {
logError(isUnprotectedEndpoint, log, err, "error dismantling token", http.StatusUnauthorized, w)
return nil, err
}
if sharedconf.SkipUserGroupsInToken() {
var groups []string
if groupsIf, err := userGroupsCache.Get(u.Id.OpaqueId); err == nil {
groups = groupsIf.([]string)
} else {
groupsRes, err := client.GetUserGroups(ctx, &userpb.GetUserGroupsRequest{UserId: u.Id})
if err != nil {
logError(isUnprotectedEndpoint, log, err, "error retrieving user groups", http.StatusInternalServerError, w)
return nil, err
}
groups = groupsRes.Groups
_ = userGroupsCache.SetWithExpire(u.Id.OpaqueId, groupsRes.Groups, 3600*time.Second)
}
u.Groups = groups
}
// ensure access to the resource is allowed
ok, err := scope.VerifyScope(ctx, tokenScope, r.URL.Path)
if err != nil {
logError(isUnprotectedEndpoint, log, err, "error verifying scope of access token", http.StatusInternalServerError, w)
return nil, err
}
if !ok {
err := errtypes.PermissionDenied("access to resource not allowed")
logError(isUnprotectedEndpoint, log, err, "access to resource not allowed", http.StatusUnauthorized, w)
return nil, err
}
return ctxWithUserInfo(ctx, r, u, token, tokenScope, r.Header.Get(ctxpkg.InitiatorHeader)), nil
}
func ctxWithUserInfo(ctx context.Context, r *http.Request, user *userpb.User, token string, tokenScope map[string]*authpb.Scope, initiatorid string) context.Context {
ctx = ctxpkg.ContextSetUser(ctx, user)
ctx = ctxpkg.ContextSetToken(ctx, token)
ctx = ctxpkg.ContextSetInitiator(ctx, initiatorid)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, token)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.UserAgentHeader, r.UserAgent())
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.InitiatorHeader, initiatorid)
ctx = ctxpkg.ContextSetScopes(ctx, tokenScope)
return ctx
}
func insertGroupsInUser(ctx context.Context, userGroupsCache gcache.Cache, client gateway.GatewayAPIClient, user *userpb.User) error {
if sharedconf.SkipUserGroupsInToken() {
var groups []string
if groupsIf, err := userGroupsCache.Get(user.Id.OpaqueId); err == nil {
groups = groupsIf.([]string)
} else {
groupsRes, err := client.GetUserGroups(ctx, &userpb.GetUserGroupsRequest{UserId: user.Id})
if err != nil {
return err
}
groups = groupsRes.Groups
_ = userGroupsCache.SetWithExpire(user.Id.OpaqueId, groupsRes.Groups, 3600*time.Second)
}
user.Groups = groups
}
return nil
}
func isTokenValid(r *http.Request, tokenManager token.Manager, token string) (*userpb.User, map[string]*authpb.Scope, bool) {
ctx := r.Context()
u, tokenScope, err := tokenManager.DismantleToken(ctx, token)
if err != nil {
return nil, nil, false
}
// ensure access to the resource is allowed
ok, err := scope.VerifyScope(ctx, tokenScope, r.URL.Path)
if err != nil {
return nil, nil, false
}
return u, tokenScope, ok
}
func logError(isUnprotectedEndpoint bool, log *zerolog.Logger, err error, msg string, status int, w http.ResponseWriter) {
if !isUnprotectedEndpoint {
log.Error().Err(err).Msg(msg)
w.WriteHeader(status)
}
}
// getCredsForUserAgent returns the WWW Authenticate challenges keys to use given an http request
// and available credentials.
func getCredsForUserAgent(ua string, uam map[string]string, creds []string) []string {
if ua == "" || len(uam) == 0 {
return creds
}
for u, cred := range uam {
if strings.Contains(ua, u) {
for _, v := range creds {
if v == cred {
return []string{cred}
}
}
return creds
}
}
return creds
}
@@ -0,0 +1,27 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package loader
import (
// Load core authentication strategies.
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/credential/strategy/basic"
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/credential/strategy/bearer"
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/credential/strategy/ocmshares"
// Add your own here.
)
@@ -0,0 +1,36 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package registry
import (
"github.com/opencloud-eu/reva/v2/pkg/auth"
)
// NewCredentialFunc is the function that credential strategies
// should register at init time.
type NewCredentialFunc func(map[string]interface{}) (auth.CredentialStrategy, error)
// NewCredentialFuncs is a map containing all the registered auth strategies.
var NewCredentialFuncs = map[string]NewCredentialFunc{}
// Register registers a new auth strategy new function.
// Not safe for concurrent use. Safe for use from package init.
func Register(name string, f NewCredentialFunc) {
NewCredentialFuncs[name] = f
}
@@ -0,0 +1,56 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package basic
import (
"fmt"
"net/http"
"github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/credential/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth"
)
func init() {
registry.Register("basic", New)
}
type strategy struct{}
// New returns a new auth strategy that checks for basic auth.
// See https://tools.ietf.org/html/rfc7617
func New(m map[string]interface{}) (auth.CredentialStrategy, error) {
return &strategy{}, nil
}
func (s *strategy) GetCredentials(w http.ResponseWriter, r *http.Request) (*auth.Credentials, error) {
id, secret, ok := r.BasicAuth()
if !ok {
return nil, fmt.Errorf("no basic auth provided")
}
return &auth.Credentials{Type: "basic", ClientID: id, ClientSecret: secret}, nil
}
func (s *strategy) AddWWWAuthenticate(w http.ResponseWriter, r *http.Request, realm string) {
// TODO read realm from forwarded header?
if realm == "" {
// fall back to hostname if not configured
realm = r.Host
}
w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, realm))
}
@@ -0,0 +1,67 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package bearer
import (
"fmt"
"net/http"
"strings"
"github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/credential/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth"
)
func init() {
registry.Register("bearer", New)
}
type strategy struct{}
// New returns a new auth strategy that checks "Bearer" OAuth Access Tokens
// See https://tools.ietf.org/html/rfc6750#section-6.1
func New(m map[string]interface{}) (auth.CredentialStrategy, error) {
return &strategy{}, nil
}
func (s *strategy) GetCredentials(w http.ResponseWriter, r *http.Request) (*auth.Credentials, error) {
// 1. check Authorization header
hdr := r.Header.Get("Authorization")
token := strings.TrimPrefix(hdr, "Bearer ")
if token != "" {
return &auth.Credentials{Type: "bearer", ClientSecret: token}, nil
}
// TODO 2. check form encoded body parameter for POST requests, see https://tools.ietf.org/html/rfc6750#section-2.2
// 3. check uri query parameter, see https://tools.ietf.org/html/rfc6750#section-2.3
tokens, ok := r.URL.Query()["access_token"]
if !ok || len(tokens[0]) < 1 {
return nil, fmt.Errorf("no bearer auth provided")
}
return &auth.Credentials{Type: "bearer", ClientSecret: tokens[0]}, nil
}
func (s *strategy) AddWWWAuthenticate(w http.ResponseWriter, r *http.Request, realm string) {
// TODO read realm from forwarded header?
if realm == "" {
// fall back to hostname if not configured
realm = r.Host
}
w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Bearer realm="%s"`, realm))
}
@@ -0,0 +1,58 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package ocmshares
import (
"fmt"
"net/http"
"github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/credential/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth"
)
func init() {
registry.Register("ocmshares", New)
}
const (
headerShareToken = "ocm-token"
)
type strategy struct{}
// New returns a new auth strategy that handles public share verification.
func New(m map[string]interface{}) (auth.CredentialStrategy, error) {
return &strategy{}, nil
}
func (s *strategy) GetCredentials(w http.ResponseWriter, r *http.Request) (*auth.Credentials, error) {
token := r.Header.Get(headerShareToken)
if token == "" {
token = r.URL.Query().Get(headerShareToken)
}
if token == "" {
return nil, fmt.Errorf("no ocm token provided")
}
return &auth.Credentials{Type: "ocmshares", ClientID: token}, nil
}
func (s *strategy) AddWWWAuthenticate(w http.ResponseWriter, r *http.Request, realm string) {
// TODO read realm from forwarded header?
}
@@ -0,0 +1,26 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package loader
import (
// Load core token strategies.
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/token/strategy/bearer"
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/token/strategy/header"
// Add your own here.
)
@@ -0,0 +1,34 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package registry
import "github.com/opencloud-eu/reva/v2/pkg/auth"
// NewTokenFunc is the function that token strategies
// should register at init time.
type NewTokenFunc func(map[string]interface{}) (auth.TokenStrategy, error)
// NewTokenFuncs is a map containing all the registered auth strategies.
var NewTokenFuncs = map[string]NewTokenFunc{}
// Register registers a new auth strategy new function.
// Not safe for concurrent use. Safe for use from package init.
func Register(name string, f NewTokenFunc) {
NewTokenFuncs[name] = f
}
@@ -0,0 +1,84 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package header
import (
"mime"
"net/http"
"strings"
"github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/token/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth"
)
func init() {
registry.Register("bearer", New)
}
type b struct{}
// New returns a new auth strategy that checks for bearer auth.
func New(m map[string]interface{}) (auth.TokenStrategy, error) {
return b{}, nil
}
func (b) GetToken(r *http.Request) string {
// Authorization Request Header Field: https://www.rfc-editor.org/rfc/rfc6750#section-2.1
if tkn, ok := getFromAuthorizationHeader(r); ok {
return tkn
}
// Form-Encoded Body Parameter: https://www.rfc-editor.org/rfc/rfc6750#section-2.2
if tkn, ok := getFromBody(r); ok {
return tkn
}
// URI Query Parameter: https://www.rfc-editor.org/rfc/rfc6750#section-2.3
if tkn, ok := getFromQueryParam(r); ok {
return tkn
}
return ""
}
func getFromAuthorizationHeader(r *http.Request) (string, bool) {
auth := r.Header.Get("Authorization")
tkn := strings.TrimPrefix(auth, "Bearer ")
return tkn, tkn != ""
}
func getFromBody(r *http.Request) (string, bool) {
mediatype, _, err := mime.ParseMediaType(r.Header.Get("content-type"))
if err != nil {
return "", false
}
if mediatype != "application/x-www-form-urlencoded" {
return "", false
}
if err = r.ParseForm(); err != nil {
return "", false
}
tkn := r.Form.Get("access-token")
return tkn, tkn != ""
}
func getFromQueryParam(r *http.Request) (string, bool) {
tkn := r.URL.Query().Get("access_token")
return tkn, tkn != ""
}
@@ -0,0 +1,44 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package header
import (
"net/http"
"github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/token/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
)
func init() {
registry.Register("header", New)
}
type strategy struct {
header string
}
// New returns a new auth strategy that checks for basic auth.
func New(m map[string]interface{}) (auth.TokenStrategy, error) {
return &strategy{header: ctxpkg.TokenHeader}, nil
}
func (s *strategy) GetToken(r *http.Request) string {
return r.Header.Get(s.header)
}
@@ -0,0 +1,25 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package loader
import (
// Load core token writer strategies.
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/tokenwriter/strategy/header"
// Add your own here.
)
@@ -0,0 +1,34 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package registry
import "github.com/opencloud-eu/reva/v2/pkg/auth"
// NewTokenFunc is the function that token writers
// should register at init time.
type NewTokenFunc func(map[string]interface{}) (auth.TokenWriter, error)
// NewTokenFuncs is a map containing all the registered token writers.
var NewTokenFuncs = map[string]NewTokenFunc{}
// Register registers a new token writer strategy new function.
// Not safe for concurrent use. Safe for use from package init.
func Register(name string, f NewTokenFunc) {
NewTokenFuncs[name] = f
}
@@ -0,0 +1,44 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package header
import (
"net/http"
"github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/tokenwriter/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
)
func init() {
registry.Register("header", New)
}
type strategy struct {
header string
}
// New returns a new token writer strategy that stores token in a header.
func New(m map[string]interface{}) (auth.TokenWriter, error) {
return &strategy{header: ctxpkg.TokenHeader}, nil
}
func (s *strategy) WriteToken(token string, w http.ResponseWriter) {
w.Header().Set(s.header, token)
}
@@ -0,0 +1,128 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package cors
import (
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/rs/cors"
)
const (
defaultPriority = 200
)
func init() {
global.RegisterMiddleware("cors", New)
}
type config struct {
AllowCredentials bool `mapstructure:"allow_credentials"`
OptionsPassthrough bool `mapstructure:"options_passthrough"`
Debug bool `mapstructure:"debug"`
MaxAge int `mapstructure:"max_age"`
Priority int `mapstructure:"priority"`
AllowedMethods []string `mapstructure:"allowed_methods"`
AllowedHeaders []string `mapstructure:"allowed_headers"`
ExposedHeaders []string `mapstructure:"exposed_headers"`
AllowedOrigins []string `mapstructure:"allowed_origins"`
}
// New creates a new CORS middleware.
func New(m map[string]interface{}) (global.Middleware, int, error) {
conf := &config{}
if err := mapstructure.Decode(m, conf); err != nil {
return nil, 0, err
}
if conf.Priority == 0 {
conf.Priority = defaultPriority
}
// apply some defaults to reduce configuration boilerplate
if len(conf.AllowedOrigins) == 0 {
conf.AllowedOrigins = []string{"*"}
}
if len(conf.AllowedMethods) == 0 {
conf.AllowedMethods = []string{
"OPTIONS",
"HEAD",
"GET",
"PUT",
"POST",
"DELETE",
"MKCOL",
"PROPFIND",
"PROPPATCH",
"MOVE",
"COPY",
"REPORT",
"SEARCH",
}
}
if len(conf.AllowedHeaders) == 0 {
conf.AllowedHeaders = []string{
"Origin",
"Accept",
"Content-Type",
"Depth",
"Authorization",
"Ocs-Apirequest",
"If-None-Match",
"If-Match",
"Destination",
"Overwrite",
"X-Request-Id",
"X-Requested-With",
"Tus-Resumable",
"Tus-Checksum-Algorithm",
"Upload-Concat",
"Upload-Length",
"Upload-Metadata",
"Upload-Defer-Length",
"Upload-Expires",
"Upload-Checksum",
"Upload-Offset",
"X-HTTP-Method-Override",
}
}
if len(conf.ExposedHeaders) == 0 {
conf.ExposedHeaders = []string{
"Location",
}
}
// TODO(jfd): use log from request context, otherwise fmt will be used to log,
// preventing us from piping the log to eg jq
c := cors.New(cors.Options{
AllowCredentials: conf.AllowCredentials,
AllowedHeaders: conf.AllowedHeaders,
AllowedMethods: conf.AllowedMethods,
AllowedOrigins: conf.AllowedOrigins,
ExposedHeaders: conf.ExposedHeaders,
MaxAge: conf.MaxAge,
OptionsPassthrough: conf.OptionsPassthrough,
Debug: conf.Debug,
})
return c.Handler, conf.Priority, nil
}
@@ -0,0 +1,28 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package loader
import (
// Load core HTTP middlewares.
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/cors"
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/prometheus"
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/providerauthorizer"
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/requestid"
// Add your own middleware.
)
@@ -0,0 +1,200 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package log
import (
"bufio"
"fmt"
"net"
"net/http"
"net/url"
"time"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/rs/zerolog"
)
// New returns a new HTTP middleware that logs HTTP requests and responses.
// TODO(labkode): maybe log to another file?
func New() func(http.Handler) http.Handler {
return handler
}
// handler is a logging middleware
func handler(h http.Handler) http.Handler {
return newLoggingHandler(h)
}
func newLoggingHandler(h http.Handler) http.Handler {
return loggingHandler{handler: h}
}
type loggingHandler struct {
handler http.Handler
}
func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
log := appctx.GetLogger(req.Context())
t := time.Now()
logger := makeLogger(w)
url := *req.URL
h.handler.ServeHTTP(logger, req)
writeLog(log, req, url, t, logger.Status(), logger.Size())
}
func makeLogger(w http.ResponseWriter) loggingResponseWriter {
var logger loggingResponseWriter = &responseLogger{w: w, status: http.StatusOK}
if _, ok := w.(http.Hijacker); ok {
logger = &hijackLogger{responseLogger{w: w, status: http.StatusOK}}
}
h, ok1 := logger.(http.Hijacker)
c, ok2 := w.(http.CloseNotifier)
if ok1 && ok2 {
return hijackCloseNotifier{logger, h, c}
}
if ok2 {
return &closeNotifyWriter{logger, c}
}
return logger
}
func writeLog(log *zerolog.Logger, req *http.Request, url url.URL, ts time.Time, status, size int) {
end := time.Now()
host, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
host = req.RemoteAddr
}
uri := req.RequestURI
u := req.URL.String()
if req.ProtoMajor == 2 && req.Method == "CONNECT" {
uri = req.Host
}
if uri == "" {
uri = url.RequestURI()
}
diff := end.Sub(ts).Nanoseconds()
var event *zerolog.Event
switch {
case status < 400:
event = log.Debug()
case status < 500:
event = log.Warn()
default:
event = log.Error()
}
event.Str("host", host).Str("method", req.Method).
Str("uri", uri).Str("url", u).Str("proto", req.Proto).Int("status", status).
Int("size", size).
Str("start", ts.Format("02/Jan/2006:15:04:05 -0700")).
Str("end", end.Format("02/Jan/2006:15:04:05 -0700")).Int("time_ns", int(diff)).
Msg("http")
}
type loggingResponseWriter interface {
commonLoggingResponseWriter
http.Pusher
}
func (l *responseLogger) Push(target string, opts *http.PushOptions) error {
p, ok := l.w.(http.Pusher)
if !ok {
return fmt.Errorf("responseLogger does not implement http.Pusher")
}
return p.Push(target, opts)
}
type commonLoggingResponseWriter interface {
http.ResponseWriter
http.Flusher
Status() int
Size() int
Unwrap() http.ResponseWriter
}
// responseLogger is wrapper of http.ResponseWriter that keeps track of its HTTP
// status code and body size
type responseLogger struct {
w http.ResponseWriter
status int
size int
}
func (l *responseLogger) Header() http.Header {
return l.w.Header()
}
func (l *responseLogger) Write(b []byte) (int, error) {
size, err := l.w.Write(b)
l.size += size
return size, err
}
func (l *responseLogger) WriteHeader(s int) {
l.w.WriteHeader(s)
l.status = s
}
func (l *responseLogger) Status() int {
return l.status
}
func (l *responseLogger) Size() int {
return l.size
}
func (l *responseLogger) Flush() {
f, ok := l.w.(http.Flusher)
if ok {
f.Flush()
}
}
func (l *responseLogger) Unwrap() http.ResponseWriter {
return l.w
}
type hijackLogger struct {
responseLogger
}
func (l *hijackLogger) Hijack() (net.Conn, *bufio.ReadWriter, error) {
h := l.w.(http.Hijacker)
conn, rw, err := h.Hijack()
if err == nil && l.status == 0 {
l.status = http.StatusSwitchingProtocols
}
return conn, rw, err
}
type closeNotifyWriter struct {
loggingResponseWriter
http.CloseNotifier
}
type hijackCloseNotifier struct {
loggingResponseWriter
http.Hijacker
http.CloseNotifier
}
@@ -0,0 +1,69 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package prometheus
import (
"net/http"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
const (
defaultPriority = 100
)
func init() {
global.RegisterMiddleware("prometheus", New)
}
// New returns a new HTTP middleware that counts requests for prometheus metrics
func New(m map[string]interface{}) (global.Middleware, int, error) {
namespace := m["namespace"].(string)
if namespace == "" {
namespace = "reva"
}
subsystem := m["subsystem"].(string)
ph := prometheusHandler{
counter: promauto.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: m["subsystem"].(string),
Name: "http_requests_total",
Help: "The total number of processed " + subsystem + " HTTP requests for " + namespace,
}),
}
return ph.handler, defaultPriority, nil
}
type prometheusHandler struct {
h http.Handler
counter prometheus.Counter
}
// handler is a logging middleware
func (ph prometheusHandler) handler(h http.Handler) http.Handler {
ph.h = h
return ph
}
func (ph prometheusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ph.h.ServeHTTP(w, r)
ph.counter.Inc()
}
@@ -0,0 +1,114 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package providerauthorizer
import (
"fmt"
"net/http"
"net/url"
"strings"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/ocm/provider"
"github.com/opencloud-eu/reva/v2/pkg/ocm/provider/authorizer/registry"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "json"
}
}
func getDriver(c *config) (provider.Authorizer, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, fmt.Errorf("driver %s not found for provider authorizer", c.Driver)
}
// New returns a new HTTP middleware that verifies that the provider is registered in OCM.
func New(m map[string]interface{}, unprotected []string, ocmPrefix string) (global.Middleware, error) {
if ocmPrefix == "" {
ocmPrefix = "ocm"
}
conf := &config{}
if err := mapstructure.Decode(m, conf); err != nil {
return nil, err
}
conf.init()
authorizer, err := getDriver(conf)
if err != nil {
return nil, err
}
handler := func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
head, _ := router.ShiftPath(r.URL.Path)
if r.Method == "OPTIONS" || head != ocmPrefix || utils.Skip(r.URL.Path, unprotected) {
log.Info().Msg("skipping provider authorizer check for: " + r.URL.Path)
h.ServeHTTP(w, r)
return
}
userIdp := ctxpkg.ContextMustGetUser(ctx).Id.Idp
if !(strings.Contains(userIdp, "://")) {
userIdp = "https://" + userIdp
}
userIdpURL, err := url.Parse(userIdp)
if err != nil {
log.Error().Err(err).Msg("error parsing user idp in provider authorizer")
w.WriteHeader(http.StatusUnauthorized)
return
}
err = authorizer.IsProviderAllowed(ctx, &ocmprovider.ProviderInfo{
Domain: userIdpURL.Hostname(),
})
if err != nil {
log.Error().Err(err).Msg("provider not registered in OCM")
w.WriteHeader(http.StatusUnauthorized)
return
}
h.ServeHTTP(w, r)
})
}
return handler, nil
}
@@ -0,0 +1,54 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package requestid
import (
"net/http"
"github.com/go-chi/chi/v5/middleware"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
)
const (
defaultPriority = 100
)
func init() {
global.RegisterMiddleware("requestid", New)
}
// New returns a new HTTP middleware that adds the X-Request-ID to the context
func New(m map[string]interface{}) (global.Middleware, int, error) {
rh := requestIDHandler{}
return rh.handler, defaultPriority, nil
}
type requestIDHandler struct {
h http.Handler
}
// handler is a request id middleware
func (rh requestIDHandler) handler(h http.Handler) http.Handler {
rh.h = middleware.RequestID(h)
return rh
}
func (rh requestIDHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
rh.h.ServeHTTP(w, r)
}
@@ -0,0 +1,600 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package appprovider
import (
"context"
"encoding/json"
"net/http"
"net/url"
"path"
"strings"
appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
iso6391 "github.com/emvi/iso-639-1"
"github.com/go-chi/chi/v5"
ua "github.com/mileusna/useragent"
"github.com/mitchellh/mapstructure"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/protobuf/proto"
)
func init() {
global.Register("appprovider", New)
}
// Config holds the config options for the HTTP appprovider service
type Config struct {
Prefix string `mapstructure:"prefix"`
GatewaySvc string `mapstructure:"gatewaysvc"`
Insecure bool `mapstructure:"insecure"`
WebBaseURI string `mapstructure:"webbaseuri"`
Web Web `mapstructure:"web"`
SecureViewAppAddr string `mapstructure:"secure_view_app_addr"`
}
// Web holds the config options for the URL parameters for Web
type Web struct {
URLParamsMapping map[string]string `mapstructure:"urlparamsmapping"`
StaticURLParams map[string]string `mapstructure:"staticurlparams"`
}
func (c *Config) init() {
if c.Prefix == "" {
c.Prefix = "app"
}
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
}
type svc struct {
conf *Config
router *chi.Mux
}
// New returns a new ocmd object
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
conf := &Config{}
if err := mapstructure.Decode(m, conf); err != nil {
return nil, err
}
conf.init()
r := chi.NewRouter()
s := &svc{
conf: conf,
router: r,
}
if err := s.routerInit(); err != nil {
return nil, err
}
_ = chi.Walk(s.router, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
log.Debug().Str("service", "approvider").Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint")
return nil
})
return s, nil
}
const (
openModeNormal = iota
openModeWeb
)
func (s *svc) routerInit() error {
s.router.Get("/list", s.handleList)
s.router.Post("/new", s.handleNew)
s.router.Post("/open", s.handleOpen(openModeNormal))
s.router.Post("/open-with-web", s.handleOpen(openModeWeb))
return nil
}
// Close performs cleanup.
func (s *svc) Close() error {
return nil
}
func (s *svc) Prefix() string {
return s.conf.Prefix
}
func (s *svc) Unprotected() []string {
return []string{"/list"}
}
func (s *svc) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s.router.ServeHTTP(w, r)
})
}
func (s *svc) handleNew(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc)
if err != nil {
writeError(w, r, appErrorServerError, "error getting grpc gateway client", err)
return
}
err = r.ParseForm()
if err != nil {
writeError(w, r, appErrorInvalidParameter, "parameters could not be parsed", nil)
}
if r.Form.Get("template") != "" {
// TODO in the future we want to create a file out of the given template
writeError(w, r, appErrorUnimplemented, "template is not implemented", nil)
return
}
parentContainerIDStr := r.Form.Get("parent_container_id")
if parentContainerIDStr == "" {
writeError(w, r, appErrorInvalidParameter, "missing parent container ID", nil)
return
}
parentContainerID, err := storagespace.ParseID(parentContainerIDStr)
if err != nil {
writeError(w, r, appErrorInvalidParameter, "invalid parent container ID", nil)
return
}
filename := r.Form.Get("filename")
if filename == "" {
writeError(w, r, appErrorInvalidParameter, "missing filename", nil)
return
}
dirPart, filePart := path.Split(filename)
if dirPart != "" || filePart != filename {
writeError(w, r, appErrorInvalidParameter, "the filename must not contain a path segment", nil)
return
}
statParentContainerReq := &provider.StatRequest{
Ref: &provider.Reference{
ResourceId: &parentContainerID,
},
}
parentContainer, err := client.Stat(ctx, statParentContainerReq)
if err != nil {
writeError(w, r, appErrorServerError, "error sending a grpc stat request", err)
return
}
if parentContainer.Status.Code != rpc.Code_CODE_OK {
writeError(w, r, appErrorNotFound, "the parent container is not accessible or does not exist", err)
return
}
if parentContainer.Info.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER {
writeError(w, r, appErrorInvalidParameter, "the parent container id does not point to a container", nil)
return
}
fileRef := &provider.Reference{
ResourceId: &parentContainerID,
Path: utils.MakeRelativePath(filename),
}
statFileReq := &provider.StatRequest{
Ref: fileRef,
}
statFileRes, err := client.Stat(ctx, statFileReq)
if err != nil {
writeError(w, r, appErrorServerError, "failed to stat the file", err)
return
}
if statFileRes.Status.Code != rpc.Code_CODE_NOT_FOUND {
if statFileRes.Status.Code == rpc.Code_CODE_OK {
writeError(w, r, appErrorAlreadyExists, "the file already exists", nil)
return
}
writeError(w, r, appErrorServerError, "statting the file returned unexpected status code", err)
return
}
touchFileReq := &provider.TouchFileRequest{
Ref: fileRef,
}
touchRes, err := client.TouchFile(ctx, touchFileReq)
if err != nil {
writeError(w, r, appErrorServerError, "error sending a grpc touchfile request", err)
return
}
if touchRes.Status.Code != rpc.Code_CODE_OK {
if touchRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
writeError(w, r, appErrorPermissionDenied, "permission denied to create the file", nil)
return
}
writeError(w, r, appErrorServerError, "touching the file failed", nil)
return
}
// Stat the newly created file
statRes, err := client.Stat(ctx, statFileReq)
if err != nil {
writeError(w, r, appErrorServerError, "statting the created file failed", err)
return
}
if statRes.Status.Code != rpc.Code_CODE_OK {
writeError(w, r, appErrorServerError, "statting the created file failed", nil)
return
}
if statRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_FILE {
writeError(w, r, appErrorInvalidParameter, "the given file id does not point to a file", nil)
return
}
fileid := storagespace.FormatResourceID(statRes.Info.Id)
js, err := json.Marshal(
map[string]interface{}{
"file_id": fileid,
},
)
if err != nil {
writeError(w, r, appErrorServerError, "error marshalling JSON response", err)
return
}
w.Header().Set("Content-Type", "application/json")
if _, err = w.Write(js); err != nil {
writeError(w, r, appErrorServerError, "error writing JSON response", err)
return
}
}
func (s *svc) handleList(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc)
if err != nil {
writeError(w, r, appErrorServerError, "error getting grpc gateway client", err)
return
}
listRes, err := client.ListSupportedMimeTypes(ctx, &appregistry.ListSupportedMimeTypesRequest{})
if err != nil {
writeError(w, r, appErrorServerError, "error listing supported mime types", err)
return
}
if listRes.Status.Code != rpc.Code_CODE_OK {
writeError(w, r, appErrorServerError, "error listing supported mime types", nil)
return
}
res := buildApps(listRes.MimeTypes, r.UserAgent(), s.conf.SecureViewAppAddr)
js, err := json.Marshal(map[string]interface{}{"mime-types": res})
if err != nil {
writeError(w, r, appErrorServerError, "error marshalling JSON response", err)
return
}
w.Header().Set("Content-Type", "application/json")
if _, err = w.Write(js); err != nil {
writeError(w, r, appErrorServerError, "error writing JSON response", err)
return
}
}
func (s *svc) handleOpen(openMode int) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc)
if err != nil {
writeError(w, r, appErrorServerError, "Internal error with the gateway, please try again later", err)
return
}
err = r.ParseForm()
if err != nil {
writeError(w, r, appErrorInvalidParameter, "parameters could not be parsed", nil)
}
lang := r.Form.Get("lang")
parts := strings.Split(lang, "_")
if lang != "" && !iso6391.ValidCode(parts[0]) {
writeError(w, r, appErrorInvalidParameter, "lang parameter does not contain a valid ISO 639-1 language code in the language tag", nil)
return
}
fileID := r.Form.Get("file_id")
if fileID == "" {
writeError(w, r, appErrorInvalidParameter, "missing file ID", nil)
return
}
resourceID, err := storagespace.ParseID(fileID)
if err != nil {
writeError(w, r, appErrorInvalidParameter, "invalid file ID", nil)
return
}
fileRef := &provider.Reference{
ResourceId: &resourceID,
Path: ".",
}
statRes, err := client.Stat(ctx, &provider.StatRequest{Ref: fileRef})
if err != nil {
writeError(w, r, appErrorServerError, "Internal error accessing the file, please try again later", err)
return
}
if status := utils.ReadPlainFromOpaque(statRes.GetInfo().GetOpaque(), "status"); status == "processing" {
writeError(w, r, appErrorTooEarly, "The requested file is not yet available, please try again later", nil)
return
}
viewMode, err := getViewModeFromPublicScope(ctx)
if err != nil {
writeError(w, r, appErrorPermissionDenied, "permission denied to open the application", err)
return
}
if viewMode == gateway.OpenInAppRequest_VIEW_MODE_INVALID {
// we have no publicshare Role in the token scope
// do a stat request to assemble the permissions for this user
statRes, err := client.Stat(ctx, &provider.StatRequest{Ref: fileRef})
if err != nil {
writeError(w, r, appErrorServerError, "Internal error accessing the file, please try again later", err)
return
}
if statRes.Status.Code == rpc.Code_CODE_NOT_FOUND {
writeError(w, r, appErrorNotFound, "file does not exist", nil)
return
} else if statRes.Status.Code != rpc.Code_CODE_OK {
writeError(w, r, appErrorServerError, "failed to stat the file", nil)
return
}
if statRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_FILE {
writeError(w, r, appErrorInvalidParameter, "the given file id does not point to a file", nil)
return
}
// Calculate the view mode from the resource permissions
viewMode = getViewMode(statRes.Info, r.Form.Get("view_mode"))
if viewMode == gateway.OpenInAppRequest_VIEW_MODE_INVALID {
writeError(w, r, appErrorInvalidParameter, "invalid view mode", err)
return
}
}
openReq := gateway.OpenInAppRequest{
Ref: fileRef,
ViewMode: viewMode,
App: r.Form.Get("app_name"),
Opaque: utils.AppendPlainToOpaque(nil, "lang", lang),
}
templateID := r.Form.Get("template_id")
if templateID != "" {
openReq.Opaque = utils.AppendPlainToOpaque(openReq.Opaque, "template", templateID)
}
openRes, err := client.OpenInApp(ctx, &openReq)
if err != nil {
writeError(w, r, appErrorServerError,
"Error contacting the requested application, please use a different one or try again later", err)
return
}
if openRes.Status.Code != rpc.Code_CODE_OK {
if openRes.Status.Code == rpc.Code_CODE_NOT_FOUND {
writeError(w, r, appErrorNotFound, openRes.Status.Message, nil)
return
}
writeError(w, r, appErrorServerError, openRes.Status.Message,
status.NewErrorFromCode(openRes.Status.Code, "error calling OpenInApp"))
return
}
var payload interface{}
switch openMode {
case openModeNormal:
payload = openRes.AppUrl
case openModeWeb:
payload, err = newOpenInWebResponse(s.conf.WebBaseURI, s.conf.Web.URLParamsMapping, s.conf.Web.StaticURLParams, fileID, r.Form.Get("app_name"), r.Form.Get("view_mode"))
if err != nil {
writeError(w, r, appErrorServerError, "Internal error",
errors.Wrap(err, "error building OpenInWeb response"))
return
}
default:
writeError(w, r, appErrorServerError, "Internal error with the open mode",
errors.New("unknown open mode"))
return
}
js, err := json.Marshal(payload)
if err != nil {
writeError(w, r, appErrorServerError, "Internal error with JSON payload",
errors.Wrap(err, "error marshalling JSON response"))
return
}
w.Header().Set("Content-Type", "application/json")
if _, err = w.Write(js); err != nil {
writeError(w, r, appErrorServerError, "Internal error with JSON payload",
errors.Wrap(err, "error writing JSON response"))
return
}
}
}
type openInWebResponse struct {
URI string `json:"uri"`
}
func newOpenInWebResponse(baseURI string, params, staticParams map[string]string, fileID, appName, viewMode string) (openInWebResponse, error) {
uri, err := url.Parse(baseURI)
if err != nil {
return openInWebResponse{}, err
}
query := uri.Query()
for key, val := range params {
switch val {
case "fileid":
if fileID != "" {
query.Add(key, fileID)
}
case "appname":
if appName != "" {
query.Add(key, appName)
}
case "viewmode":
if viewMode != "" {
query.Add(key, viewMode)
}
default:
return openInWebResponse{}, errors.New("unknown parameter mapper")
}
}
for key, val := range staticParams {
query.Add(key, val)
}
uri.RawQuery = query.Encode()
return openInWebResponse{URI: uri.String()}, nil
}
// MimeTypeInfo wraps the appregistry.MimeTypeInfo to change the app providers to ProviderInfos with a secure view flag
type MimeTypeInfo struct {
appregistry.MimeTypeInfo
AppProviders []*ProviderInfo `json:"app_providers"`
}
// ProviderInfo wraps the appregistry.ProviderInfo to add a secure view flag
type ProviderInfo struct {
appregistry.ProviderInfo
// TODO make this part of the CS3 provider info
SecureView bool `json:"secure_view"`
TargetExt string `json:"target_ext,omitempty"`
}
// buildApps rewrites the mime type info to only include apps that
// * have a name
// * can be called by the user agent, eg Desktop-only
//
// it also
// * wraps the provider info to be able to add a secure view flag
// * adds a secure view flag if the address matches the secure view app address and
// * removes the address from the provider info to not expose internal addresses
func buildApps(mimeTypes []*appregistry.MimeTypeInfo, userAgent, secureViewAppAddr string) []*MimeTypeInfo {
ua := ua.Parse(userAgent)
res := []*MimeTypeInfo{}
for _, m := range mimeTypes {
apps := []*ProviderInfo{}
for _, p := range m.AppProviders {
ep := &ProviderInfo{}
proto.Merge(&ep.ProviderInfo, p)
if p.Address == secureViewAppAddr {
ep.SecureView = true
}
p.Address = "" // address is internal only and not needed in the client
// apps are called by name, so if it has no name it cannot be called and should not be advertised
// also filter Desktop-only apps if ua is not Desktop
if p.Name != "" && (ua.Desktop || !p.DesktopOnly) {
apps = append(apps, ep)
}
}
if len(apps) > 0 {
mt := &MimeTypeInfo{}
addTemplateInfo(m, apps)
proto.Merge(&mt.MimeTypeInfo, m)
mt.AppProviders = apps
res = append(res, mt)
}
}
return res
}
func getViewMode(res *provider.ResourceInfo, vm string) gateway.OpenInAppRequest_ViewMode {
if vm != "" {
return utils.GetViewMode(vm)
}
var viewMode gateway.OpenInAppRequest_ViewMode
canEdit := res.PermissionSet.InitiateFileUpload
canView := res.PermissionSet.InitiateFileDownload
switch {
case canEdit && canView:
viewMode = gateway.OpenInAppRequest_VIEW_MODE_READ_WRITE
case canView:
viewMode = gateway.OpenInAppRequest_VIEW_MODE_READ_ONLY
default:
viewMode = gateway.OpenInAppRequest_VIEW_MODE_INVALID
}
return viewMode
}
// try to get the view mode from a publicshare scope
func getViewModeFromPublicScope(ctx context.Context) (gateway.OpenInAppRequest_ViewMode, error) {
scopes, ok := ctxpkg.ContextGetScopes(ctx)
if ok {
for key, scope := range scopes {
if strings.HasPrefix(key, "publicshare:") {
switch scope.GetRole() {
case providerv1beta1.Role_ROLE_VIEWER:
return gateway.OpenInAppRequest_VIEW_MODE_VIEW_ONLY, nil
case providerv1beta1.Role_ROLE_EDITOR:
return gateway.OpenInAppRequest_VIEW_MODE_READ_WRITE, nil
default:
return gateway.OpenInAppRequest_VIEW_MODE_INVALID, errors.New("invalid view mode in publicshare scope")
}
}
}
}
return gateway.OpenInAppRequest_VIEW_MODE_INVALID, nil
}
@@ -0,0 +1,82 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package appprovider
import (
"encoding/json"
"net/http"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
)
// appErrorCode stores the type of error encountered
type appErrorCode string
const (
appErrorNotFound appErrorCode = "RESOURCE_NOT_FOUND"
appErrorAlreadyExists appErrorCode = "RESOURCE_ALREADY_EXISTS"
appErrorUnauthenticated appErrorCode = "UNAUTHENTICATED"
appErrorPermissionDenied appErrorCode = "PERMISSION_DENIED"
appErrorUnimplemented appErrorCode = "NOT_IMPLEMENTED"
appErrorInvalidParameter appErrorCode = "INVALID_PARAMETER"
appErrorServerError appErrorCode = "SERVER_ERROR"
appErrorTooEarly appErrorCode = "TOO_EARLY"
)
// appErrorCodeMapping stores the HTTP error code mapping for various APIErrorCodes
var appErrorCodeMapping = map[appErrorCode]int{
appErrorNotFound: http.StatusNotFound,
appErrorAlreadyExists: http.StatusForbidden,
appErrorUnauthenticated: http.StatusUnauthorized,
appErrorUnimplemented: http.StatusNotImplemented,
appErrorInvalidParameter: http.StatusBadRequest,
appErrorServerError: http.StatusInternalServerError,
appErrorPermissionDenied: http.StatusForbidden,
appErrorTooEarly: http.StatusTooEarly,
}
// APIError encompasses the error type and message
type appError struct {
Code appErrorCode `json:"code"`
Message string `json:"message"`
}
// writeError handles writing error responses
func writeError(w http.ResponseWriter, r *http.Request, code appErrorCode, message string, err error) {
if err != nil {
appctx.GetLogger(r.Context()).Error().Err(err).Msg(message)
}
var encoded []byte
w.Header().Set("Content-Type", "application/json")
encoded, err = json.MarshalIndent(appError{Code: code, Message: message}, "", " ")
if err != nil {
appctx.GetLogger(r.Context()).Error().Err(err).Msg("error encoding response")
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(appErrorCodeMapping[code])
_, err = w.Write(encoded)
if err != nil {
appctx.GetLogger(r.Context()).Error().Err(err).Msg("error writing response")
w.WriteHeader(http.StatusInternalServerError)
}
}
@@ -0,0 +1,90 @@
package appprovider
import (
"strings"
appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
)
type TemplateList struct {
Templates map[string][]Template `json:"templates"`
}
type Template struct {
Extension string `json:"extension"`
MimeType string `json:"mime_type"`
TargetExtension string `json:"target_extension"`
}
var tl = TemplateList{
Templates: map[string][]Template{
"collabora": {
{
MimeType: "application/vnd.oasis.opendocument.spreadsheet-template",
TargetExtension: "ods",
},
{
MimeType: "application/vnd.oasis.opendocument.text-template",
TargetExtension: "odt",
},
{
MimeType: "application/vnd.oasis.opendocument.presentation-template",
TargetExtension: "odp",
},
},
"onlyoffice": {
{
MimeType: "application/vnd.ms-word.template.macroenabled.12",
TargetExtension: "docx",
},
{
MimeType: "application/vnd.oasis.opendocument.text-template",
TargetExtension: "docx",
},
{
MimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
TargetExtension: "docx",
},
{
MimeType: "application/vnd.oasis.opendocument.spreadsheet-template",
TargetExtension: "xlsx",
},
{
MimeType: "application/vnd.ms-excel.template.macroenabled.12",
TargetExtension: "xlsx",
},
{
MimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
TargetExtension: "xlsx",
},
{
MimeType: "application/vnd.oasis.opendocument.presentation-template",
TargetExtension: "pptx",
},
{
MimeType: "application/vnd.ms-powerpoint.template.macroenabled.12",
TargetExtension: "pptx",
},
{
MimeType: "application/vnd.openxmlformats-officedocument.presentationml.template",
TargetExtension: "pptx",
},
},
},
}
func addTemplateInfo(mt *appregistry.MimeTypeInfo, apps []*ProviderInfo) {
for _, app := range apps {
if tls, ok := tl.Templates[strings.ToLower(app.ProductName)]; ok {
for _, tmpl := range tls {
if tmpl.Extension != "" && tmpl.Extension == mt.Ext {
app.TargetExt = tmpl.TargetExtension
continue
}
if tmpl.MimeType == mt.MimeType {
app.TargetExt = tmpl.TargetExtension
}
}
}
}
}
@@ -0,0 +1,293 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package archiver
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"regexp"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/gdexlab/go-render/render"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/internal/http/services/archiver/manager"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/downloader"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/walker"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/rs/zerolog"
)
type svc struct {
config *Config
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
log *zerolog.Logger
walker walker.Walker
downloader downloader.Downloader
allowedFolders []*regexp.Regexp
}
// Config holds the config options that need to be passed down to all ocdav handlers
type Config struct {
Prefix string `mapstructure:"prefix"`
GatewaySvc string `mapstructure:"gatewaysvc"`
Timeout int64 `mapstructure:"timeout"`
Insecure bool `mapstructure:"insecure"`
Name string `mapstructure:"name"`
MaxNumFiles int64 `mapstructure:"max_num_files"`
MaxSize int64 `mapstructure:"max_size"`
AllowedFolders []string `mapstructure:"allowed_folders"`
}
func init() {
global.Register("archiver", New)
}
// New creates a new archiver service
func New(conf map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
c := &Config{}
err := mapstructure.Decode(conf, c)
if err != nil {
return nil, err
}
c.init()
gatewaySelector, err := pool.GatewaySelector(c.GatewaySvc)
if err != nil {
return nil, err
}
// compile all the regex for filtering folders
allowedFolderRegex := make([]*regexp.Regexp, 0, len(c.AllowedFolders))
for _, s := range c.AllowedFolders {
regex, err := regexp.Compile(s)
if err != nil {
return nil, err
}
allowedFolderRegex = append(allowedFolderRegex, regex)
}
return &svc{
config: c,
gatewaySelector: gatewaySelector,
downloader: downloader.NewDownloader(gatewaySelector, rhttp.Insecure(c.Insecure), rhttp.Timeout(time.Duration(c.Timeout*int64(time.Second)))),
walker: walker.NewWalker(gatewaySelector),
log: log,
allowedFolders: allowedFolderRegex,
}, nil
}
func (c *Config) init() {
if c.Prefix == "" {
c.Prefix = "download_archive"
}
if c.Name == "" {
c.Name = "download"
}
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
}
func (s *svc) getResources(ctx context.Context, paths, ids []string) ([]*provider.ResourceId, error) {
if len(paths) == 0 && len(ids) == 0 {
return nil, errtypes.BadRequest("path and id lists are both empty")
}
resources := make([]*provider.ResourceId, 0, len(paths)+len(ids))
for _, id := range ids {
// id is base64 encoded and after decoding has the form <storage_id>:<resource_id>
decodedID, err := storagespace.ParseID(id)
if err != nil {
return nil, errors.New("could not unwrap given file id")
}
resources = append(resources, &decodedID)
}
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
for _, p := range paths {
// id is base64 encoded and after decoding has the form <storage_id>:<resource_id>
resp, err := gatewayClient.Stat(ctx, &provider.StatRequest{
Ref: &provider.Reference{
Path: p,
},
})
switch {
case err != nil:
return nil, err
case resp.Status.Code == rpc.Code_CODE_NOT_FOUND:
return nil, errtypes.NotFound(p)
case resp.Status.Code != rpc.Code_CODE_OK:
return nil, errtypes.InternalError(fmt.Sprintf("error stating %s", p))
}
resources = append(resources, resp.Info.Id)
}
// check if all the folders are allowed to be archived
/* FIXME bring back filtering
err := s.allAllowed(resources)
if err != nil {
return nil, err
}
*/
return resources, nil
}
// return true if path match with at least with one allowed folder regex
/*
func (s *svc) isPathAllowed(path string) bool {
for _, reg := range s.allowedFolders {
if reg.MatchString(path) {
return true
}
}
return false
}
// return nil if all the paths in the slide match with at least one allowed folder regex
func (s *svc) allAllowed(paths []string) error {
if len(s.allowedFolders) == 0 {
return nil
}
for _, f := range paths {
if !s.isPathAllowed(f) {
return errtypes.BadRequest(fmt.Sprintf("resource at %s not allowed to be archived", f))
}
}
return nil
}
*/
func (s *svc) writeHTTPError(rw http.ResponseWriter, err error) {
s.log.Error().Msg(err.Error())
switch err.(type) {
case errtypes.NotFound, errtypes.PermissionDenied:
rw.WriteHeader(http.StatusNotFound)
case manager.ErrMaxSize, manager.ErrMaxFileCount:
rw.WriteHeader(http.StatusRequestEntityTooLarge)
case errtypes.BadRequest:
rw.WriteHeader(http.StatusBadRequest)
default:
rw.WriteHeader(http.StatusInternalServerError)
}
_, _ = rw.Write([]byte(err.Error()))
}
func (s *svc) Handler() http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
// get the paths and/or the resources id from the query
ctx := r.Context()
v := r.URL.Query()
paths, ok := v["path"]
if !ok {
paths = []string{}
}
ids, ok := v["id"]
if !ok {
ids = []string{}
}
format := v.Get("output-format")
if format == "" {
format = "zip"
}
resources, err := s.getResources(ctx, paths, ids)
if err != nil {
s.writeHTTPError(rw, err)
return
}
arch, err := manager.NewArchiver(resources, s.walker, s.downloader, manager.Config{
MaxNumFiles: s.config.MaxNumFiles,
MaxSize: s.config.MaxSize,
})
if err != nil {
s.writeHTTPError(rw, err)
return
}
archName := s.config.Name
if format == "tar" {
archName += ".tar"
} else {
archName += ".zip"
}
s.log.Debug().Msg("Requested the following resources to archive: " + render.Render(resources))
rw.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", archName))
rw.Header().Set("Content-Transfer-Encoding", "binary")
// create the archive
var closeArchive func()
if format == "tar" {
closeArchive, err = arch.CreateTar(ctx, rw)
} else {
closeArchive, err = arch.CreateZip(ctx, rw)
}
defer closeArchive()
if err != nil {
s.writeHTTPError(rw, err)
return
}
})
}
func (s *svc) Prefix() string {
return s.config.Prefix
}
func (s *svc) Close() error {
return nil
}
func (s *svc) Unprotected() []string {
return nil
}
@@ -0,0 +1,208 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package manager
import (
"archive/tar"
"archive/zip"
"context"
"io"
"path/filepath"
"time"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/downloader"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/walker"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
// Config is the config for the Archiver
type Config struct {
MaxNumFiles int64
MaxSize int64
}
// Archiver is the struct able to create an archive
type Archiver struct {
resources []*provider.ResourceId
walker walker.Walker
downloader downloader.Downloader
config Config
}
// NewArchiver creates a new archiver able to create an archive containing the files in the list
func NewArchiver(r []*provider.ResourceId, w walker.Walker, d downloader.Downloader, config Config) (*Archiver, error) {
if len(r) == 0 {
return nil, ErrEmptyList{}
}
arc := &Archiver{
resources: r,
walker: w,
downloader: d,
config: config,
}
return arc, nil
}
// CreateTar creates a tar and write it into the dst Writer
func (a *Archiver) CreateTar(ctx context.Context, dst io.Writer) (func(), error) {
w := tar.NewWriter(dst)
closer := func() {
_ = w.Close()
}
var filesCount, sizeFiles int64
for _, root := range a.resources {
err := a.walker.Walk(ctx, root, func(wd string, info *provider.ResourceInfo, err error) error {
if err != nil {
return err
}
// when archiving a space we can omit the spaceroot
if utils.IsSpaceRoot(info) {
return nil
}
isDir := info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER
filesCount++
if filesCount > a.config.MaxNumFiles {
return ErrMaxFileCount{}
}
if !isDir {
// only add the size if the resource is not a directory
// as its size could be resursive-computed, and we would
// count the files not only once
sizeFiles += int64(info.Size)
if sizeFiles > a.config.MaxSize {
return ErrMaxSize{}
}
}
header := tar.Header{
Name: filepath.Join(wd, info.Path),
ModTime: time.Unix(int64(info.Mtime.Seconds), 0),
}
if isDir {
// the resource is a folder
header.Mode = 0755
header.Typeflag = tar.TypeDir
} else {
header.Mode = 0644
header.Typeflag = tar.TypeReg
header.Size = int64(info.Size)
}
err = w.WriteHeader(&header)
if err != nil {
return err
}
if !isDir {
err = a.downloader.Download(ctx, info.Id, w)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return closer, err
}
}
return closer, nil
}
// CreateZip creates a zip and write it into the dst Writer
func (a *Archiver) CreateZip(ctx context.Context, dst io.Writer) (func(), error) {
w := zip.NewWriter(dst)
closer := func() {
_ = w.Close()
}
var filesCount, sizeFiles int64
for _, root := range a.resources {
err := a.walker.Walk(ctx, root, func(wd string, info *provider.ResourceInfo, err error) error {
if err != nil {
return err
}
// when archiving a space we can omit the spaceroot
if utils.IsSpaceRoot(info) {
return nil
}
isDir := info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER
filesCount++
if filesCount > a.config.MaxNumFiles {
return ErrMaxFileCount{}
}
if !isDir {
// only add the size if the resource is not a directory
// as its size could be resursive-computed, and we would
// count the files not only once
sizeFiles += int64(info.Size)
if sizeFiles > a.config.MaxSize {
return ErrMaxSize{}
}
}
header := zip.FileHeader{
Name: filepath.Join(wd, info.Path),
Modified: time.Unix(int64(info.Mtime.Seconds), 0),
}
if isDir {
header.Name += "/"
} else {
header.UncompressedSize64 = info.Size
}
dst, err := w.CreateHeader(&header)
if err != nil {
return err
}
if !isDir {
err = a.downloader.Download(ctx, info.Id, dst)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return closer, err
}
}
return closer, nil
}
@@ -0,0 +1,43 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package manager
// ErrMaxFileCount is the error returned when the max files count specified in the config has reached
type ErrMaxFileCount struct{}
// ErrMaxSize is the error returned when the max total files size specified in the config has reached
type ErrMaxSize struct{}
// ErrEmptyList is the error returned when an empty list is passed when an archiver is created
type ErrEmptyList struct{}
// Error returns the string error msg for ErrMaxFileCount
func (ErrMaxFileCount) Error() string {
return "reached max files count"
}
// Error returns the string error msg for ErrMaxSize
func (ErrMaxSize) Error() string {
return "reached max total files size"
}
// Error returns the string error msg for ErrEmptyList
func (ErrEmptyList) Error() string {
return "list of files to archive empty"
}
@@ -0,0 +1,237 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package datagateway
import (
"context"
"io"
"net/http"
"net/url"
"path"
"strconv"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel"
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
"go.opentelemetry.io/otel/trace"
)
var tracer trace.Tracer
func init() {
tracer = otel.Tracer("github.com/opencloud-eu/reva/v2/pkg/storage/utils/decomposedfs/tree")
}
const (
// TokenTransportHeader holds the header key for the reva transfer token
TokenTransportHeader = "X-Reva-Transfer"
)
func init() {
global.Register("datagateway", New)
}
// transferClaims are custom claims for a JWT token to be used between the metadata and data gateways.
type transferClaims struct {
jwt.RegisteredClaims
Target string `json:"target"`
}
type config struct {
Prefix string `mapstructure:"prefix"`
TransferSharedSecret string `mapstructure:"transfer_shared_secret"`
Timeout int64 `mapstructure:"timeout"`
Insecure bool `mapstructure:"insecure"`
}
func (c *config) init() {
if c.Prefix == "" {
c.Prefix = "datagateway"
}
c.TransferSharedSecret = sharedconf.GetJWTSecret(c.TransferSharedSecret)
}
type svc struct {
conf *config
handler http.Handler
client *http.Client
}
// New returns a new datagateway
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
conf := &config{}
if err := mapstructure.Decode(m, conf); err != nil {
return nil, err
}
conf.init()
s := &svc{
conf: conf,
client: rhttp.GetHTTPClient(
rhttp.Timeout(time.Duration(conf.Timeout*int64(time.Second))),
rhttp.Insecure(conf.Insecure),
),
}
s.setHandler()
return s, nil
}
// Close performs cleanup.
func (s *svc) Close() error {
return nil
}
func (s *svc) Prefix() string {
return s.conf.Prefix
}
func (s *svc) Handler() http.Handler {
return s.handler
}
func (s *svc) Unprotected() []string {
return []string{
"/",
}
}
func (s *svc) setHandler() {
s.handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx, span := tracer.Start(ctx, "HandlerFunc")
defer span.End()
span.SetAttributes(
semconv.HTTPRequestMethodKey.String(r.Method),
semconv.URLFullKey.String(r.URL.String()),
)
r = r.WithContext(ctx)
s.doRequest(w, r)
})
}
// verify extracts the transfer token from the request
// If it is not set as header we assume that it's the last path segment instead.
func (s *svc) verify(ctx context.Context, r *http.Request) (*transferClaims, error) {
token := r.Header.Get(TokenTransportHeader)
if token == "" {
token = path.Base(r.URL.Path)
r.Header.Set(TokenTransportHeader, token)
}
j, err := jwt.ParseWithClaims(token, &transferClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(s.conf.TransferSharedSecret), nil
})
if err != nil {
return nil, errors.Wrap(err, "error parsing token")
}
if claims, ok := j.Claims.(*transferClaims); ok && j.Valid {
return claims, nil
}
err = errtypes.InvalidCredentials("token invalid")
return nil, err
}
func (s *svc) doRequest(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
claims, err := s.verify(ctx, r)
if err != nil {
err = errors.Wrap(err, "datagateway: error validating transfer token")
log.Err(err).Str("token", r.Header.Get(TokenTransportHeader)).Msg("invalid transfer token")
w.WriteHeader(http.StatusForbidden)
return
}
target := claims.Target
// add query params to target, clients can send checksums and other information.
targetURL, err := url.Parse(target)
if err != nil {
log.Err(err).Msg("datagateway: error parsing target url")
w.WriteHeader(http.StatusInternalServerError)
return
}
targetURL.RawQuery = r.URL.RawQuery
target = targetURL.String()
log.Debug().Str("target", target).Msg("sending request to internal data server")
httpReq, err := rhttp.NewRequest(ctx, r.Method, target, r.Body)
if err != nil {
log.Err(err).Msg("wrong request")
w.WriteHeader(http.StatusInternalServerError)
return
}
httpReq.Header = r.Header
httpReq.ContentLength = r.ContentLength
httpRes, err := s.client.Do(httpReq)
if err != nil {
log.Err(err).Msg("error doing " + r.Method + " request to data service")
w.WriteHeader(http.StatusInternalServerError)
return
}
defer httpRes.Body.Close()
copyHeader(w.Header(), httpRes.Header)
if httpRes.StatusCode != http.StatusOK && httpRes.StatusCode != http.StatusPartialContent {
// swallow the body and set content-length to 0 to prevent reverse proxies from trying to read from it
w.Header().Set("Content-Length", "0")
w.WriteHeader(httpRes.StatusCode)
return
}
w.WriteHeader(httpRes.StatusCode)
var c int64
c, err = io.Copy(w, httpRes.Body)
if err != nil {
log.Err(err).Msg("error writing body after header were set")
}
if httpRes.Header.Get("Content-Length") != "" {
i, err := strconv.ParseInt(httpRes.Header.Get("Content-Length"), 10, 64)
if err != nil {
log.Error().Err(err).Str("content-length", httpRes.Header.Get("Content-Length")).Msg("invalid content length in dataprovider response")
}
if i != c {
log.Error().Int64("content-length", i).Int64("transferred-bytes", c).Msg("content length vs transferred bytes mismatch")
}
}
}
func copyHeader(dst, src http.Header) {
for key, values := range src {
for i := range values {
dst.Set(key, values[i])
}
}
}
@@ -0,0 +1,201 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package dataprovider
import (
"fmt"
"net/http"
"github.com/mitchellh/mapstructure"
"github.com/rs/zerolog"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/events/stream"
datatxregistry "github.com/opencloud-eu/reva/v2/pkg/rhttp/datatx/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
"github.com/opencloud-eu/reva/v2/pkg/storage"
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/registry"
)
func init() {
global.Register("dataprovider", New)
}
type config struct {
Prefix string `mapstructure:"prefix" docs:"data;The prefix to be used for this HTTP service"`
Driver string `mapstructure:"driver" docs:"localhome;The storage driver to be used."`
Drivers map[string]map[string]interface{} `mapstructure:"drivers" docs:"url:pkg/storage/fs/localhome/localhome.go;The configuration for the storage driver"`
DataTXs map[string]map[string]interface{} `mapstructure:"data_txs" docs:"url:pkg/rhttp/datatx/manager/simple/simple.go;The configuration for the data tx protocols"`
NatsAddress string `mapstructure:"nats_address"`
NatsClusterID string `mapstructure:"nats_clusterID"`
NatsTLSInsecure bool `mapstructure:"nats_tls_insecure"`
NatsRootCACertPath string `mapstructure:"nats_root_ca_cert_path"`
NatsEnableTLS bool `mapstructure:"nats_enable_tls"`
NatsUsername string `mapstructure:"nats_username"`
NatsPassword string `mapstructure:"nats_password"`
}
func (c *config) init() {
if c.Prefix == "" {
c.Prefix = "data"
}
if c.Driver == "" {
c.Driver = "localhome"
}
}
type svc struct {
conf *config
handler http.Handler
storage storage.FS
dataTXs map[string]http.Handler
}
// New returns a new datasvc
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
conf := &config{}
if err := mapstructure.Decode(m, conf); err != nil {
return nil, err
}
conf.init()
var evstream events.Stream
if conf.NatsAddress == "" || conf.NatsClusterID == "" {
log.Warn().Msg("missing or incomplete nats configuration. Events will not be published.")
} else {
s, err := stream.NatsFromConfig("dataprovider", false, stream.NatsConfig{
Endpoint: conf.NatsAddress,
Cluster: conf.NatsClusterID,
EnableTLS: conf.NatsEnableTLS,
TLSInsecure: conf.NatsTLSInsecure,
TLSRootCACertificate: conf.NatsRootCACertPath,
AuthUsername: conf.NatsUsername,
AuthPassword: conf.NatsPassword,
})
if err != nil {
return nil, err
}
evstream = s
}
fs, err := getFS(conf, evstream, log)
if err != nil {
return nil, err
}
dataTXs, err := getDataTXs(conf, fs, evstream, log)
if err != nil {
return nil, err
}
s := &svc{
storage: fs,
conf: conf,
dataTXs: dataTXs,
}
err = s.setHandler()
return s, err
}
func getFS(c *config, stream events.Stream, log *zerolog.Logger) (storage.FS, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver], stream, log)
}
return nil, fmt.Errorf("driver not found: %s", c.Driver)
}
func getDataTXs(c *config, fs storage.FS, publisher events.Publisher, log *zerolog.Logger) (map[string]http.Handler, error) {
if c.DataTXs == nil {
c.DataTXs = make(map[string]map[string]interface{})
}
if len(c.DataTXs) == 0 {
c.DataTXs["simple"] = make(map[string]interface{})
c.DataTXs["simple"]["cache_store"] = "noop"
c.DataTXs["simple"]["cache_database"] = "reva"
c.DataTXs["spaces"] = make(map[string]interface{})
c.DataTXs["spaces"]["cache_store"] = "noop"
c.DataTXs["spaces"]["cache_database"] = "reva"
c.DataTXs["tus"] = make(map[string]interface{})
c.DataTXs["tus"]["cache_store"] = "noop"
c.DataTXs["tus"]["cache_database"] = "reva"
}
txs := make(map[string]http.Handler)
for t := range c.DataTXs {
if f, ok := datatxregistry.NewFuncs[t]; ok {
if tx, err := f(c.DataTXs[t], publisher, log); err == nil {
if handler, err := tx.Handler(fs); err == nil {
txs[t] = handler
}
}
}
}
return txs, nil
}
func (s *svc) Close() error {
return nil
}
func (s *svc) Unprotected() []string {
return []string{
"/tus",
}
}
func (s *svc) Prefix() string {
return s.conf.Prefix
}
func (s *svc) Handler() http.Handler {
return s.handler
}
func (s *svc) setHandler() error {
s.handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := appctx.GetLogger(r.Context())
log.Debug().Msgf("dataprovider routing: path=%s", r.URL.Path)
head, tail := router.ShiftPath(r.URL.Path)
if handler, ok := s.dataTXs[head]; ok {
r.URL.Path = tail
handler.ServeHTTP(w, r)
return
}
// If we don't find a prefix match for any of the protocols, upload the resource
// through the direct HTTP protocol
if handler, ok := s.dataTXs["simple"]; ok {
handler.ServeHTTP(w, r)
return
}
w.WriteHeader(http.StatusInternalServerError)
})
return nil
}
@@ -0,0 +1,85 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package helloworld
import (
"net/http"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/rs/zerolog"
)
func init() {
global.Register("helloworld", New)
}
// New returns a new helloworld service
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
conf := &config{}
if err := mapstructure.Decode(m, conf); err != nil {
return nil, err
}
conf.init()
return &svc{conf: conf}, nil
}
// Close performs cleanup.
func (s *svc) Close() error {
return nil
}
type config struct {
Prefix string `mapstructure:"prefix"`
HelloMessage string `mapstructure:"message"`
}
func (c *config) init() {
if c.HelloMessage == "" {
c.HelloMessage = "Hello World!"
}
if c.Prefix == "" {
c.Prefix = "helloworld"
}
}
type svc struct {
conf *config
}
func (s *svc) Prefix() string {
return s.conf.Prefix
}
func (s *svc) Unprotected() []string {
return []string{"/"}
}
func (s *svc) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := appctx.GetLogger(r.Context())
if _, err := w.Write([]byte(s.conf.HelloMessage)); err != nil {
log.Err(err).Msg("error writing response")
}
})
}
@@ -0,0 +1,41 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package loader
import (
// Load core HTTP services
_ "github.com/opencloud-eu/reva/v2/internal/http/services/appprovider"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/archiver"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/datagateway"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/dataprovider"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/helloworld"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/mentix"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/metrics"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/ocmd"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/preferences"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/prometheus"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/reverseproxy"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/sciencemesh"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/siteacc"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/sysinfo"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/wellknown"
// Add your own service here
)
@@ -0,0 +1,194 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package mentix
import (
"net/http"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/mentix/meshdata"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/opencloud-eu/reva/v2/pkg/mentix"
"github.com/opencloud-eu/reva/v2/pkg/mentix/config"
"github.com/opencloud-eu/reva/v2/pkg/mentix/exchangers"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
)
func init() {
global.Register(serviceName, New)
}
type svc struct {
conf *config.Configuration
mntx *mentix.Mentix
log *zerolog.Logger
stopSignal chan struct{}
}
const (
serviceName = "mentix"
)
func (s *svc) Close() error {
// Trigger and close the stopSignal signal channel to stop Mentix
s.stopSignal <- struct{}{}
close(s.stopSignal)
return nil
}
func (s *svc) Prefix() string {
return s.conf.Prefix
}
func (s *svc) Unprotected() []string {
// Get all endpoints exposed by the RequestExchangers
importers := s.mntx.GetRequestImporters()
exporters := s.mntx.GetRequestExporters()
getEndpoints := func(exchangers []exchangers.RequestExchanger) []string {
endpoints := make([]string, 0, len(exchangers))
for _, exchanger := range exchangers {
if !exchanger.IsProtectedEndpoint() {
endpoints = append(endpoints, exchanger.Endpoint())
}
}
return endpoints
}
endpoints := make([]string, 0, len(importers)+len(exporters))
endpoints = append(endpoints, getEndpoints(importers)...)
endpoints = append(endpoints, getEndpoints(exporters)...)
return endpoints
}
func (s *svc) Handler() http.Handler {
// Forward requests to Mentix
return http.HandlerFunc(s.mntx.RequestHandler)
}
func (s *svc) startBackgroundService() {
// Just run Mentix in the background
go func() {
if err := s.mntx.Run(s.stopSignal); err != nil {
s.log.Err(err).Msg("error while running mentix")
}
}()
}
func parseConfig(m map[string]interface{}) (*config.Configuration, error) {
cfg := &config.Configuration{}
if err := mapstructure.Decode(m, &cfg); err != nil {
return nil, errors.Wrap(err, "mentix: error decoding configuration")
}
applyInternalConfig(m, cfg)
applyDefaultConfig(cfg)
return cfg, nil
}
func applyInternalConfig(m map[string]interface{}, conf *config.Configuration) {
getSubsections := func(section string) []string {
subsections := make([]string, 0, 5)
if list, ok := m[section].(map[string]interface{}); ok {
for name := range list {
subsections = append(subsections, name)
}
}
return subsections
}
conf.EnabledConnectors = getSubsections("connectors")
conf.EnabledImporters = getSubsections("importers")
conf.EnabledExporters = getSubsections("exporters")
}
func applyDefaultConfig(conf *config.Configuration) {
// General
if conf.Prefix == "" {
conf.Prefix = serviceName
}
if conf.UpdateInterval == "" {
conf.UpdateInterval = "1h" // Update once per hour
}
// Connectors
if conf.Connectors.GOCDB.Scope == "" {
conf.Connectors.GOCDB.Scope = "SM" // TODO(Daniel-WWU-IT): This might change in the future
}
// Exporters
addDefaultConnector := func(enabledList *[]string) {
if len(*enabledList) == 0 {
*enabledList = append(*enabledList, "*")
}
}
if conf.Exporters.WebAPI.Endpoint == "" {
conf.Exporters.WebAPI.Endpoint = "/sites"
}
addDefaultConnector(&conf.Exporters.WebAPI.EnabledConnectors)
if conf.Exporters.CS3API.Endpoint == "" {
conf.Exporters.CS3API.Endpoint = "/cs3"
}
addDefaultConnector(&conf.Exporters.CS3API.EnabledConnectors)
if len(conf.Exporters.CS3API.ElevatedServiceTypes) == 0 {
conf.Exporters.CS3API.ElevatedServiceTypes = append(conf.Exporters.CS3API.ElevatedServiceTypes, meshdata.EndpointGateway, meshdata.EndpointOCM, meshdata.EndpointWebdav)
}
if conf.Exporters.SiteLocations.Endpoint == "" {
conf.Exporters.SiteLocations.Endpoint = "/loc"
}
addDefaultConnector(&conf.Exporters.SiteLocations.EnabledConnectors)
addDefaultConnector(&conf.Exporters.PrometheusSD.EnabledConnectors)
addDefaultConnector(&conf.Exporters.Metrics.EnabledConnectors)
}
// New returns a new Mentix service.
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
// Prepare the configuration
conf, err := parseConfig(m)
if err != nil {
return nil, err
}
conf.Init()
// Create the Mentix instance
mntx, err := mentix.New(conf, log)
if err != nil {
return nil, errors.Wrap(err, "mentix: error creating Mentix")
}
// Create the service and start its background activity
s := &svc{
conf: conf,
mntx: mntx,
log: log,
stopSignal: make(chan struct{}),
}
s.startBackgroundService()
return s, nil
}
@@ -0,0 +1,94 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package metrics
/*
This service initializes the metrics package according to the metrics configuration.
*/
import (
"net/http"
"os"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/logger"
"github.com/rs/zerolog"
"github.com/opencloud-eu/reva/v2/pkg/metrics"
"github.com/opencloud-eu/reva/v2/pkg/metrics/config"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
)
func init() {
global.Register(serviceName, New)
}
const (
serviceName = "metrics"
)
// Close is called when this service is being stopped.
func (s *svc) Close() error {
return nil
}
// Prefix returns the main endpoint of this service.
func (s *svc) Prefix() string {
// We use a dummy endpoint as the service is not expected to be exposed
// directly to the user, but just start a background process.
return "register_metrics"
}
// Unprotected returns all endpoints that can be queried without prior authorization.
func (s *svc) Unprotected() []string {
return []string{}
}
// Handler serves all HTTP requests.
func (s *svc) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := logger.New().With().Int("pid", os.Getpid()).Logger()
if _, err := w.Write([]byte("This is the metrics service.\n")); err != nil {
log.Error().Err(err).Msg("error writing metrics response")
}
})
}
// New returns a new metrics service.
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
// Prepare the configuration
conf := &config.Config{}
if err := mapstructure.Decode(m, conf); err != nil {
return nil, err
}
conf.Init()
// initialize metrics using the configuration
err := metrics.Init(conf)
if err != nil {
return nil, err
}
// Create the service
s := &svc{}
return s, nil
}
type svc struct {
}

Some files were not shown because too many files have changed in this diff Show More