mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-05-08 04:20:59 -05:00
Use the opencloud reva from now on
This commit is contained in:
Generated
Vendored
+60
@@ -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)
|
||||
})
|
||||
}
|
||||
+420
@@ -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.20.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
|
||||
}
|
||||
Generated
Vendored
+27
@@ -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.
|
||||
)
|
||||
Generated
Vendored
+36
@@ -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
|
||||
}
|
||||
Generated
Vendored
+56
@@ -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))
|
||||
}
|
||||
Generated
Vendored
+67
@@ -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))
|
||||
}
|
||||
Generated
Vendored
+58
@@ -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?
|
||||
}
|
||||
Generated
Vendored
+26
@@ -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.
|
||||
)
|
||||
Generated
Vendored
+34
@@ -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
|
||||
}
|
||||
Generated
Vendored
+84
@@ -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 != ""
|
||||
}
|
||||
Generated
Vendored
+44
@@ -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)
|
||||
}
|
||||
Generated
Vendored
+25
@@ -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.
|
||||
)
|
||||
Generated
Vendored
+34
@@ -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
|
||||
}
|
||||
Generated
Vendored
+44
@@ -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)
|
||||
}
|
||||
+128
@@ -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
|
||||
}
|
||||
Generated
Vendored
+28
@@ -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.
|
||||
)
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
type hijackLogger struct {
|
||||
responseLogger
|
||||
}
|
||||
|
||||
func (l *hijackLogger) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
h := l.responseLogger.w.(http.Hijacker)
|
||||
conn, rw, err := h.Hijack()
|
||||
if err == nil && l.responseLogger.status == 0 {
|
||||
l.responseLogger.status = http.StatusSwitchingProtocols
|
||||
}
|
||||
return conn, rw, err
|
||||
}
|
||||
|
||||
type closeNotifyWriter struct {
|
||||
loggingResponseWriter
|
||||
http.CloseNotifier
|
||||
}
|
||||
|
||||
type hijackCloseNotifier struct {
|
||||
loggingResponseWriter
|
||||
http.Hijacker
|
||||
http.CloseNotifier
|
||||
}
|
||||
Generated
Vendored
+69
@@ -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()
|
||||
}
|
||||
Generated
Vendored
+114
@@ -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
|
||||
|
||||
}
|
||||
Generated
Vendored
+54
@@ -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)
|
||||
}
|
||||
Generated
Vendored
+600
@@ -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
|
||||
}
|
||||
Generated
Vendored
+82
@@ -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)
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+90
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+293
@@ -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
|
||||
}
|
||||
Generated
Vendored
+208
@@ -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
|
||||
}
|
||||
Generated
Vendored
+43
@@ -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"
|
||||
}
|
||||
Generated
Vendored
+237
@@ -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.20.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.HTTPMethodKey.String(r.Method),
|
||||
semconv.HTTPURLKey.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])
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+201
@@ -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
|
||||
}
|
||||
Generated
Vendored
+85
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
+41
@@ -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
|
||||
)
|
||||
+194
@@ -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
|
||||
}
|
||||
+94
@@ -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 {
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
// 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 ocmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/http"
|
||||
|
||||
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"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/reqres"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
ocmuser "github.com/opencloud-eu/reva/v2/pkg/ocm/user"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
type invitesHandler struct {
|
||||
gatewaySelector *pool.Selector[gateway.GatewayAPIClient]
|
||||
}
|
||||
|
||||
func (h *invitesHandler) init(c *config) error {
|
||||
var err error
|
||||
|
||||
gatewaySelector, err := pool.GatewaySelector(c.GatewaySvc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.gatewaySelector = gatewaySelector
|
||||
return nil
|
||||
}
|
||||
|
||||
type acceptInviteRequest struct {
|
||||
Token string `json:"token"`
|
||||
UserID string `json:"userID"`
|
||||
RecipientProvider string `json:"recipientProvider"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// AcceptInvite informs avout an accepted invitation so that the users
|
||||
// can initiate the OCM share creation.
|
||||
func (h *invitesHandler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
log := appctx.GetLogger(ctx)
|
||||
|
||||
req, err := getAcceptInviteRequest(r)
|
||||
if err != nil {
|
||||
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "missing parameters in request", err)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Token == "" || req.UserID == "" || req.RecipientProvider == "" {
|
||||
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "token, userID and recipiendProvider must not be null", nil)
|
||||
return
|
||||
}
|
||||
|
||||
clientIP, err := utils.GetClientIP(r)
|
||||
if err != nil {
|
||||
reqres.WriteError(w, r, reqres.APIErrorServerError, fmt.Sprintf("error retrieving client IP from request: %s", r.RemoteAddr), err)
|
||||
return
|
||||
}
|
||||
|
||||
providerInfo := ocmprovider.ProviderInfo{
|
||||
Domain: req.RecipientProvider,
|
||||
Services: []*ocmprovider.Service{
|
||||
{
|
||||
Host: clientIP,
|
||||
},
|
||||
},
|
||||
}
|
||||
gatewayClient, err := h.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
reqres.WriteError(w, r, reqres.APIErrorServerError, "error getting gateway client", err)
|
||||
return
|
||||
}
|
||||
providerAllowedResp, err := gatewayClient.IsProviderAllowed(ctx, &ocmprovider.IsProviderAllowedRequest{
|
||||
Provider: &providerInfo,
|
||||
})
|
||||
if err != nil {
|
||||
reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc is provider allowed request", err)
|
||||
return
|
||||
}
|
||||
if providerAllowedResp.Status.Code != rpc.Code_CODE_OK {
|
||||
reqres.WriteError(w, r, reqres.APIErrorUntrustedService, "provider not trusted", errors.New(providerAllowedResp.Status.Message))
|
||||
return
|
||||
}
|
||||
|
||||
userObj := &userpb.User{
|
||||
Id: &userpb.UserId{
|
||||
OpaqueId: req.UserID,
|
||||
Idp: req.RecipientProvider,
|
||||
Type: userpb.UserType_USER_TYPE_FEDERATED,
|
||||
},
|
||||
Mail: req.Email,
|
||||
DisplayName: req.Name,
|
||||
}
|
||||
acceptInviteRequest := &invitepb.AcceptInviteRequest{
|
||||
InviteToken: &invitepb.InviteToken{
|
||||
Token: req.Token,
|
||||
},
|
||||
RemoteUser: userObj,
|
||||
}
|
||||
acceptInviteResponse, err := gatewayClient.AcceptInvite(ctx, acceptInviteRequest)
|
||||
if err != nil {
|
||||
reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc accept invite request", err)
|
||||
return
|
||||
}
|
||||
if acceptInviteResponse.Status.Code != rpc.Code_CODE_OK {
|
||||
switch acceptInviteResponse.Status.Code {
|
||||
case rpc.Code_CODE_NOT_FOUND:
|
||||
reqres.WriteError(w, r, reqres.APIErrorNotFound, "token not found", nil)
|
||||
return
|
||||
case rpc.Code_CODE_INVALID_ARGUMENT:
|
||||
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "token has expired", nil)
|
||||
return
|
||||
case rpc.Code_CODE_ALREADY_EXISTS:
|
||||
reqres.WriteError(w, r, reqres.APIErrorAlreadyExist, "user already known", nil)
|
||||
return
|
||||
default:
|
||||
reqres.WriteError(w, r, reqres.APIErrorServerError, "unexpected error: "+acceptInviteResponse.Status.Message, errors.New(acceptInviteResponse.Status.Message))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(&user{
|
||||
UserID: ocmuser.FederatedID(acceptInviteResponse.UserId, "").GetOpaqueId(),
|
||||
Email: acceptInviteResponse.Email,
|
||||
Name: acceptInviteResponse.DisplayName,
|
||||
}); err != nil {
|
||||
reqres.WriteError(w, r, reqres.APIErrorServerError, "error encoding response", err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
log.Info().Str("user", fmt.Sprintf("%s@%s", userObj.Id.OpaqueId, userObj.Id.Idp)).Str("token", req.Token).Msg("added to accepted users")
|
||||
}
|
||||
|
||||
type user struct {
|
||||
UserID string `json:"userID"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func getAcceptInviteRequest(r *http.Request) (*acceptInviteRequest, error) {
|
||||
var req acceptInviteRequest
|
||||
contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err == nil && contentType == "application/json" {
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
req.Token, req.UserID, req.RecipientProvider = r.FormValue("token"), r.FormValue("userID"), r.FormValue("recipientProvider")
|
||||
req.Name, req.Email = r.FormValue("name"), r.FormValue("email")
|
||||
}
|
||||
return &req, nil
|
||||
}
|
||||
Generated
Vendored
+66
@@ -0,0 +1,66 @@
|
||||
// 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 ocmd
|
||||
|
||||
import (
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/reqres"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
)
|
||||
|
||||
// var validate = validator.New()
|
||||
|
||||
type notifHandler struct {
|
||||
}
|
||||
|
||||
func (h *notifHandler) init(c *config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Notifications dispatches any notifications received from remote OCM sites
|
||||
// according to the specifications at:
|
||||
// https://cs3org.github.io/OCM-API/docs.html?branch=v1.1.0&repo=OCM-API&user=cs3org#/paths/~1notifications/post
|
||||
func (h *notifHandler) Notifications(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
log := appctx.GetLogger(ctx)
|
||||
req, err := getNotification(r)
|
||||
if err != nil {
|
||||
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(lopresti) this is all to be implemented. For now we just log what we got
|
||||
log.Debug().Msgf("Received OCM notification: %+v", req)
|
||||
|
||||
// this is to please Nextcloud
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
|
||||
func getNotification(r *http.Request) (string, error) {
|
||||
// var req notificationRequest
|
||||
contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err == nil && contentType == "application/json" {
|
||||
bytes, _ := io.ReadAll(r.Body)
|
||||
return string(bytes), nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
// 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 ocmd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils/cfg"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
global.Register("ocmd", New)
|
||||
}
|
||||
|
||||
type config struct {
|
||||
Prefix string `mapstructure:"prefix"`
|
||||
GatewaySvc string `mapstructure:"gatewaysvc" validate:"required"`
|
||||
ExposeRecipientDisplayName bool `mapstructure:"expose_recipient_display_name"`
|
||||
}
|
||||
|
||||
func (c *config) ApplyDefaults() {
|
||||
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
|
||||
if c.Prefix == "" {
|
||||
c.Prefix = "ocm"
|
||||
}
|
||||
}
|
||||
|
||||
type svc struct {
|
||||
Conf *config
|
||||
router chi.Router
|
||||
}
|
||||
|
||||
// New returns a new ocmd object, that implements
|
||||
// the OCM APIs specified in https://cs3org.github.io/OCM-API/docs.html
|
||||
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
|
||||
var c config
|
||||
if err := cfg.Decode(m, &c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
s := &svc{
|
||||
Conf: &c,
|
||||
router: r,
|
||||
}
|
||||
|
||||
if err := s.routerInit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *svc) routerInit() error {
|
||||
sharesHandler := new(sharesHandler)
|
||||
invitesHandler := new(invitesHandler)
|
||||
notifHandler := new(notifHandler)
|
||||
|
||||
if err := sharesHandler.init(s.Conf); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := invitesHandler.init(s.Conf); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := notifHandler.init(s.Conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.router.Post("/shares", sharesHandler.CreateShare)
|
||||
s.router.Post("/invite-accepted", invitesHandler.AcceptInvite)
|
||||
s.router.Post("/notifications", notifHandler.Notifications)
|
||||
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{"/invite-accepted", "/shares", "/notifications"}
|
||||
}
|
||||
|
||||
func (s *svc) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log := appctx.GetLogger(r.Context())
|
||||
log.Debug().Str("path", r.URL.Path).Msg("ocm routing")
|
||||
|
||||
// unset raw path, otherwise chi uses it to route and then fails to match percent encoded path segments
|
||||
r.URL.RawPath = ""
|
||||
s.router.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
// 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 ocmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
ocmshare "github.com/opencloud-eu/reva/v2/pkg/ocm/share"
|
||||
utils "github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
// Protocols is the list of protocols.
|
||||
type Protocols []Protocol
|
||||
|
||||
// Protocol represents the way of access the resource
|
||||
// in the OCM share.
|
||||
type Protocol interface {
|
||||
// ToOCMProtocol convert the protocol to a ocm Protocol struct
|
||||
ToOCMProtocol() *ocm.Protocol
|
||||
}
|
||||
|
||||
// protocols supported by the OCM API
|
||||
|
||||
// WebDAV contains the parameters for the WebDAV protocol.
|
||||
type WebDAV struct {
|
||||
SharedSecret string `json:"sharedSecret" validate:"required"`
|
||||
Permissions []string `json:"permissions" validate:"required,dive,required,oneof=read write share"`
|
||||
URL string `json:"url" validate:"required"`
|
||||
}
|
||||
|
||||
// ToOCMProtocol convert the protocol to a ocm Protocol struct.
|
||||
func (w *WebDAV) ToOCMProtocol() *ocm.Protocol {
|
||||
perms := &ocm.SharePermissions{
|
||||
Permissions: &providerv1beta1.ResourcePermissions{},
|
||||
}
|
||||
for _, p := range w.Permissions {
|
||||
switch p {
|
||||
case "read":
|
||||
perms.Permissions.GetPath = true
|
||||
perms.Permissions.GetQuota = true
|
||||
perms.Permissions.InitiateFileDownload = true
|
||||
perms.Permissions.ListContainer = true
|
||||
perms.Permissions.ListRecycle = true
|
||||
perms.Permissions.Stat = true
|
||||
case "write":
|
||||
perms.Permissions.InitiateFileUpload = true
|
||||
perms.Permissions.RestoreRecycleItem = true
|
||||
perms.Permissions.CreateContainer = true
|
||||
perms.Permissions.Delete = true
|
||||
perms.Permissions.Move = true
|
||||
perms.Permissions.ListGrants = true
|
||||
case "share":
|
||||
perms.Reshare = true
|
||||
}
|
||||
}
|
||||
|
||||
return ocmshare.NewWebDAVProtocol(w.URL, w.SharedSecret, perms)
|
||||
}
|
||||
|
||||
// Webapp contains the parameters for the Webapp protocol.
|
||||
type Webapp struct {
|
||||
URITemplate string `json:"uriTemplate" validate:"required"`
|
||||
ViewMode string `json:"viewMode" validate:"required,dive,required,oneof=view read write"`
|
||||
}
|
||||
|
||||
// ToOCMProtocol convert the protocol to a ocm Protocol struct.
|
||||
func (w *Webapp) ToOCMProtocol() *ocm.Protocol {
|
||||
return ocmshare.NewWebappProtocol(w.URITemplate, utils.GetAppViewMode(w.ViewMode))
|
||||
}
|
||||
|
||||
// Datatx contains the parameters for the Datatx protocol.
|
||||
type Datatx struct {
|
||||
SharedSecret string `json:"sharedSecret" validate:"required"`
|
||||
SourceURI string `json:"srcUri" validate:"required"`
|
||||
Size uint64 `json:"size" validate:"required"`
|
||||
}
|
||||
|
||||
// ToOCMProtocol convert the protocol to a ocm Protocol struct.
|
||||
func (w *Datatx) ToOCMProtocol() *ocm.Protocol {
|
||||
return ocmshare.NewTransferProtocol(w.SourceURI, w.SharedSecret, w.Size)
|
||||
}
|
||||
|
||||
var protocolImpl = map[string]reflect.Type{
|
||||
"webdav": reflect.TypeOf(WebDAV{}),
|
||||
"webapp": reflect.TypeOf(Webapp{}),
|
||||
"datatx": reflect.TypeOf(Datatx{}),
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the Unmarshaler interface.
|
||||
func (p *Protocols) UnmarshalJSON(data []byte) error {
|
||||
var prot map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &prot); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*p = []Protocol{}
|
||||
|
||||
for name, d := range prot {
|
||||
var res Protocol
|
||||
|
||||
// we do not support the OCM v1.0 properties for now, therefore just skip or bail out
|
||||
if name == "name" {
|
||||
continue
|
||||
}
|
||||
if name == "options" {
|
||||
var opt map[string]any
|
||||
if err := json.Unmarshal(d, &opt); err != nil || len(opt) > 0 {
|
||||
return fmt.Errorf("protocol options not supported: %s", string(d))
|
||||
}
|
||||
continue
|
||||
}
|
||||
ctype, ok := protocolImpl[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("protocol %s not recognised", name)
|
||||
}
|
||||
res = reflect.New(ctype).Interface().(Protocol)
|
||||
if err := json.Unmarshal(d, &res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*p = append(*p, res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements the Marshaler interface.
|
||||
func (p Protocols) MarshalJSON() ([]byte, error) {
|
||||
if len(p) == 0 {
|
||||
return nil, errors.New("no protocol defined")
|
||||
}
|
||||
d := make(map[string]any)
|
||||
for _, prot := range p {
|
||||
d[getProtocolName(prot)] = prot
|
||||
}
|
||||
// fill in the OCM v1.0 properties
|
||||
d["name"] = "multi"
|
||||
d["options"] = map[string]any{}
|
||||
return json.Marshal(d)
|
||||
}
|
||||
|
||||
func getProtocolName(p Protocol) string {
|
||||
n := reflect.TypeOf(p).String()
|
||||
s := strings.Split(n, ".")
|
||||
return strings.ToLower(s[len(s)-1])
|
||||
}
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
// 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 ocmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1"
|
||||
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
|
||||
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
|
||||
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/reqres"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
var validate = validator.New()
|
||||
|
||||
type sharesHandler struct {
|
||||
gatewaySelector *pool.Selector[gateway.GatewayAPIClient]
|
||||
exposeRecipientDisplayName bool
|
||||
}
|
||||
|
||||
func (h *sharesHandler) init(c *config) error {
|
||||
var err error
|
||||
|
||||
gatewaySelector, err := pool.GatewaySelector(c.GatewaySvc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.gatewaySelector = gatewaySelector
|
||||
|
||||
h.exposeRecipientDisplayName = c.ExposeRecipientDisplayName
|
||||
return nil
|
||||
}
|
||||
|
||||
type createShareRequest struct {
|
||||
ShareWith string `json:"shareWith" validate:"required"` // identifier of the recipient of the share
|
||||
Name string `json:"name" validate:"required"` // name of the resource
|
||||
Description string `json:"description"` // (optional) description of the resource
|
||||
ProviderID string `json:"providerId" validate:"required"` // unique identifier of the resource at provider side
|
||||
Owner string `json:"owner" validate:"required"` // unique identifier of the owner at provider side
|
||||
Sender string `json:"sender" validate:"required"` // unique indentifier of the user who wants to share the resource at provider side
|
||||
OwnerDisplayName string `json:"ownerDisplayName"` // display name of the owner of the resource
|
||||
SenderDisplayName string `json:"senderDisplayName"` // dispay name of the user who wants to share the resource
|
||||
ShareType string `json:"shareType" validate:"required,oneof=user group"` // recipient share type (user or group)
|
||||
ResourceType string `json:"resourceType" validate:"required,oneof=file folder"`
|
||||
Expiration uint64 `json:"expiration"`
|
||||
Protocols Protocols `json:"protocol" validate:"required"`
|
||||
}
|
||||
|
||||
// CreateShare sends all the informations to the consumer needed to start
|
||||
// synchronization between the two services.
|
||||
func (h *sharesHandler) CreateShare(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
log := appctx.GetLogger(ctx)
|
||||
req, err := getCreateShareRequest(r)
|
||||
if err != nil {
|
||||
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
_, meshProvider, err := getIDAndMeshProvider(req.Sender)
|
||||
log.Debug().Msgf("Determined Mesh Provider '%s' from req.Sender '%s'", meshProvider, req.Sender)
|
||||
if err != nil {
|
||||
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
clientIP, err := utils.GetClientIP(r)
|
||||
if err != nil {
|
||||
reqres.WriteError(w, r, reqres.APIErrorServerError, fmt.Sprintf("error retrieving client IP from request: %s", r.RemoteAddr), err)
|
||||
return
|
||||
}
|
||||
providerInfo := ocmprovider.ProviderInfo{
|
||||
Domain: meshProvider,
|
||||
Services: []*ocmprovider.Service{
|
||||
{
|
||||
Host: clientIP,
|
||||
},
|
||||
},
|
||||
}
|
||||
gatewayClient, err := h.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
reqres.WriteError(w, r, reqres.APIErrorServerError, "error getting gateway client", err)
|
||||
return
|
||||
}
|
||||
providerAllowedResp, err := gatewayClient.IsProviderAllowed(ctx, &ocmprovider.IsProviderAllowedRequest{
|
||||
Provider: &providerInfo,
|
||||
})
|
||||
if err != nil {
|
||||
reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc is provider allowed request", err)
|
||||
return
|
||||
}
|
||||
if providerAllowedResp.Status.Code != rpc.Code_CODE_OK {
|
||||
reqres.WriteError(w, r, reqres.APIErrorUnauthenticated, "provider not authorized", errors.New(providerAllowedResp.Status.Message))
|
||||
return
|
||||
}
|
||||
|
||||
shareWith, _, err := getIDAndMeshProvider(req.ShareWith)
|
||||
if err != nil {
|
||||
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
userRes, err := gatewayClient.GetUser(ctx, &userpb.GetUserRequest{
|
||||
UserId: &userpb.UserId{OpaqueId: shareWith}, SkipFetchingUserGroups: true,
|
||||
})
|
||||
if err != nil {
|
||||
reqres.WriteError(w, r, reqres.APIErrorServerError, "error searching recipient", err)
|
||||
return
|
||||
}
|
||||
if userRes.Status.Code != rpc.Code_CODE_OK {
|
||||
reqres.WriteError(w, r, reqres.APIErrorNotFound, "user not found", errors.New(userRes.Status.Message))
|
||||
return
|
||||
}
|
||||
|
||||
owner, err := getUserIDFromOCMUser(req.Owner)
|
||||
if err != nil {
|
||||
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
sender, err := getUserIDFromOCMUser(req.Sender)
|
||||
if err != nil {
|
||||
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
createShareReq := &ocmcore.CreateOCMCoreShareRequest{
|
||||
Description: req.Description,
|
||||
Name: req.Name,
|
||||
ResourceId: req.ProviderID,
|
||||
Owner: owner,
|
||||
Sender: sender,
|
||||
ShareWith: userRes.User.Id,
|
||||
ResourceType: getResourceTypeFromOCMRequest(req.ResourceType),
|
||||
ShareType: getOCMShareType(req.ShareType),
|
||||
Protocols: getProtocols(req.Protocols),
|
||||
}
|
||||
|
||||
if req.Expiration != 0 {
|
||||
createShareReq.Expiration = &types.Timestamp{
|
||||
Seconds: req.Expiration,
|
||||
}
|
||||
}
|
||||
|
||||
createShareResp, err := gatewayClient.CreateOCMCoreShare(ctx, createShareReq)
|
||||
if err != nil {
|
||||
reqres.WriteError(w, r, reqres.APIErrorServerError, "error creating ocm share", err)
|
||||
return
|
||||
}
|
||||
|
||||
if userRes.Status.Code != rpc.Code_CODE_OK {
|
||||
// TODO: define errors in the cs3apis
|
||||
reqres.WriteError(w, r, reqres.APIErrorServerError, "error creating ocm share", errors.New(createShareResp.Status.Message))
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]any{}
|
||||
|
||||
if h.exposeRecipientDisplayName {
|
||||
response["recipientDisplayName"] = userRes.User.DisplayName
|
||||
}
|
||||
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
|
||||
func getUserIDFromOCMUser(user string) (*userpb.UserId, error) {
|
||||
id, idp, err := getIDAndMeshProvider(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &userpb.UserId{
|
||||
OpaqueId: id,
|
||||
Idp: idp,
|
||||
// the remote user is a federated account for the local reva
|
||||
Type: userpb.UserType_USER_TYPE_FEDERATED,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getIDAndMeshProvider(user string) (string, string, error) {
|
||||
// the user is in the form of dimitri@apiwise.nl
|
||||
split := strings.Split(user, "@")
|
||||
if len(split) < 2 {
|
||||
return "", "", errors.New("not in the form <id>@<provider>")
|
||||
}
|
||||
return strings.Join(split[:len(split)-1], "@"), split[len(split)-1], nil
|
||||
}
|
||||
|
||||
func getCreateShareRequest(r *http.Request) (*createShareRequest, error) {
|
||||
var req createShareRequest
|
||||
contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err == nil && contentType == "application/json" {
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("body request not recognised")
|
||||
}
|
||||
// validate the request
|
||||
if err := validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func getResourceTypeFromOCMRequest(t string) providerpb.ResourceType {
|
||||
switch t {
|
||||
case "file":
|
||||
return providerpb.ResourceType_RESOURCE_TYPE_FILE
|
||||
case "folder":
|
||||
return providerpb.ResourceType_RESOURCE_TYPE_CONTAINER
|
||||
default:
|
||||
return providerpb.ResourceType_RESOURCE_TYPE_INVALID
|
||||
}
|
||||
}
|
||||
|
||||
func getOCMShareType(t string) ocm.ShareType {
|
||||
if t == "user" {
|
||||
return ocm.ShareType_SHARE_TYPE_USER
|
||||
}
|
||||
return ocm.ShareType_SHARE_TYPE_GROUP
|
||||
}
|
||||
|
||||
func getProtocols(p Protocols) []*ocm.Protocol {
|
||||
prot := make([]*ocm.Protocol, 0, len(p))
|
||||
for _, data := range p {
|
||||
prot = append(prot, data.ToOCMProtocol())
|
||||
}
|
||||
return prot
|
||||
}
|
||||
Generated
Vendored
+71
@@ -0,0 +1,71 @@
|
||||
// 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 ocdav
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/config"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
|
||||
)
|
||||
|
||||
// AvatarsHandler handles avatar requests
|
||||
type AvatarsHandler struct {
|
||||
}
|
||||
|
||||
func (h *AvatarsHandler) init(c *config.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handler handles requests
|
||||
func (h *AvatarsHandler) Handler(s *svc) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
log := appctx.GetLogger(ctx)
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
// no need for the user, and we need to be able
|
||||
// to answer preflight checks, which have no auth headers
|
||||
r.URL.Path = "/" // always use / ... we just want the options answered so phoenix doesnt hiccup
|
||||
s.handleOptions(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
_, r.URL.Path = router.ShiftPath(r.URL.Path)
|
||||
if r.Method == http.MethodGet && r.URL.Path == "/128.png" {
|
||||
// TODO load avatar url from user context?
|
||||
const img = "89504E470D0A1A0A0000000D4948445200000080000000800806000000C33E61CB00000006624B474400FF00FF00FFA0BDA793000000097048597300000B1300000B1301009A9C180000000774494D4507E3061B080516D3ECF61E000008F24944415478DAED9D7D8C1D5515C07FDB76774B775BB7454AA54BBB2D5DDD765B6BD34A140B464CB07EA0113518016B4848FC438A1A448D9A18FF40316942524D544C900F49A17C2882120A28604D506C915AB160B7B2A14275B7BB606BCB76779F7FDC79F4CE79F7BD7DEFED7B3377E69E5FF2B233DBED9B7B3EEECCB977CE3D171445511445511445098B9680645D0BAC8C7EAE020A0E5D0C027B80DDC033EA1ED96521B001D80A3C1F19BB9ECF007003F0CEE83B15CFB90C781A189986D1CB7D8E007F06AE5035FBC599C0359181AA35E6716014188A3EA3D1EFAAFDFFAF025F06DEA2EA4F97EB81935318EB047037F0396035300FE8043A8039D1A723FADD3CA01FB80AB817989CE2BB4F0237AA1992E703C00B150CB313D812057DD36555D4DB7756B8DE41E0236A9664B8A982216E897A72B3980BDC5CE1CE70AB9AA779744541984BF1DF03BA136C4B77F4F871B5E519E074355763590E8C9519A62D4DB15DDDC07E47BBC681156AB6C6D0071C7328F93A60A607ED9B017CDED1BEA35140A94C83259122ED67EE316093876DDD28E61F26A3B69EAD66AC9F61D1AB463D1F7BCF075E126D1E5233D6C74EC7E4CBEA0CB47B317048B4FD6135676D5C2E14F83A705686DA3FD771F7D229E41A823E19507D2A83729CEF90A34FCD3B35F70BA5DD906159AE14B2FC5ACD5B99F384C20E016D19966726B04FC874819AB93C434259EFCD814C2B1C2319C5C14542513FCF916C5B856C17ABB94BF915F1A9D43CCDA2AD20FEDAFA5135779CD9A287FC2D8732EE12322E52B39FE28742391B722863BF90F17635BBA115386C296630C7B2DA492CFFC16423A5CA0C0F94B214938A55E4DE9CC73945E691EEAB6C6F1C605D140314F96D8E1DE009EBB82D923D78EE14CFC63C67DA9E2D64DDA1E687D7882751E49D717452E80DE692DFC99F723C26646E0F390638579C3F1280033CEE888182758035E27C57000EF09438EF0BD9017AC5F940000EB0479CF784EC004BACE362E66FDE1916E7DD213BC07CEBF8BF8104BE72B4B330640768B58E8F0734FA39661D7785EA002DE2FA2703790448676F0DD901EC123593013D02267CB90BCF48591105E110A13051A12304E500E3BEDC0A136666858E105410683B407B20778116605699BB41700E30621DCF09E80E709A757C22640778D93A9E1B501C603BFB70C80EF092753C0B3FD6FB27815DC6E65F213B80CCFFEB0DC0F8B27CCC3F43768003E27C6D000E20339E5F08D9019E9B423979E43C71BE97C0B1B3639E0A40DE3F089983E72FC4EBEAE41DBBDED1F36937C687B4703B55BA050F72E59B488F18EA3EAE0E509A07B826C70E2083DC87D5014C143C669DAFCFB103D8B28D3B82E020E9225EEA3DCF2B839EB4E41C414BCABEC19E4022635BC67D3E346886278AF99138BF3487C6DF2CCE7FA2FD3EEE8876EF78368732CA6251AD6AF6D2D180BDA54B9E6AEC2E25BE25CD633EF53C5FD86E1DCF06DE9D2307D8487C09FC1DDADF4B5981C98E29F692277224DB1F2DB926D0BD04CAF2AC784E2ECB814CB236D05E3573792E10CABA270732FD46C874A19AB9320396B286C9F664C9424C1188A23C2FFA38FCF20D3B185C80D9222EAB7C0C7893757EA7F6EFA9E9A174E3C7AC22B797D3E0AF4AEE168AFB520665F8AA90E101356BF57489DEB39F6C958D6FA77467D337AB59ABA705784828F033196AFF15A2ED8F12D6DAC786B086D22D57B2B07A688EA3DDEBD59CF5F103A1C86D1968F336D1E69FAA19EB6701A6744C5666079789B61ED367FFF4F99650EA11FC5C42D64A3CB3A9007C57CDD7189E168AFDBE876DBC91FCE734A4463F66F3485BC11FF4A87D978AB68D11C632B744B99AD2DD44CFF1A05DEB89BFC62E00D7AAB99AC30EA1E8D7800E8F82BE02709F9AA9799C46E9DE820748A7E2F65B8997BA2F06A81D6AA6E6D289C9A9B7153F98F070EB3D8E9E3F4AFCCD9FD244563B0C3044325BB17DC271ED02F02E354BF2C1D70987219AB9A6E0DA32C6FFA49A231DFACA18647B13AE7553996B6D5333A4CB324CA125DB2813C0CA065EA3D731D42B00B7A9FAFDC136CCFF68ECEE638BA2EF94A38F3655BB1FC8F705CDD87CF23E718D6FAADAFD19168E0AE3346338D625AE314C7CB58F921232FBA6995BCFDD21AEF551557FBAB4736AA38924B26F36503AF9A3A95E29F26002C33F89CC58BE4BCD900E1FA2741E3E89A8BC8D78E2C704F03E3547B2F43AC6E4572778FD2D8EEBF7A859926101F04A0AB77E89DCF5FC1029EF0016024B89EFBE5D00FE413AAF83DB319341765B4E92EF4297A97215A519C2C749E60D603916112FFD52DC14F2323557633803F3EEFD49C73377043F52C2CE1141617149DB4398323767AA19AB6739F005E09798248F51DC6FE00EE357DD80D3817F9769EBAB517CF040143C6AB018B10CB818F80EA61ED0781905CACF0EFC4CBBEAC45434A9468613983AC15F073691AF8A6815E9C1E4CF8F44069FAC5261C5D2EAABF07BE6AD0593A3F05C0D724D46BA18C2AC77E8CE93C1DB804F03B746B7F4420D9F21E07EE02BC0BA0CCADE0F5C8399391CA851F641E076E072329864DA1605463B6A10780CB38E6E2F701D8D7D97EF13E702D7037F8F460B6355EAE741E06D789E7FB004933675A04AA186819B31397C6B896FA516029D98E4D64B22BD1DA9426703C08F7DEA20B380B7535A0ACDF59C3B0CEC06BE019CA531B0933E4C8EE100A51948AE6078252916FADA8829803C51A191AF005FC32CA298AFF6ADA9632D8E628017A77874EE8E3A6162F402BFA8D0A8039852E8FD6AC786B10E938CF27205BD6F4F628EE18B94AED22D7E0E621226B40C7AF368053E4EE9CA287B7E6173332EDC8149B4745DF477C087D53689B3391A4DB86C720B0DAEA774D07191A3C0F96A87D4D952663839D8A85BCE2EC7977F9B6C54EC0A851EE0670E3BED9EEEDCC1FB1D51E73AD5B7B75CE888D12E99CE17CAE95B5D04E93F17519A2B5917B2FAC53ED56D6678840614A9FEACF8924DAAD7CCB0BEDA3B77A569C47788F3DFAB5E33C37E71BEB81E07586E1D1F45F7BACF1AF67ECC67D4E30036AFAB03648A494CB26A91B28B6567D5F0A573D07570596176E40045C3774ED7011670EA3DBFE23F2DC2E8EDF538408B389EA77ACD2C6DF5C40007556FB9E1AFD5F472175762B66D9B2D6EFF05F19332E7D4F877AE7F6FF66327EF8FB53F015BB50F288AA2288AA2288A62F83FEC37068C6750398B0000000049454E44AE426082"
|
||||
decoded, err := hex.DecodeString(img)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error decoding string")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
w.Header().Set(net.HeaderContentType, "image/png")
|
||||
if _, err := w.Write(decoded); err != nil {
|
||||
log.Error().Err(err).Msg("error writing data response")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+89
@@ -0,0 +1,89 @@
|
||||
package config
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/sharedconf"
|
||||
|
||||
// Config holds the config options that need to be passed down to all ocdav handlers
|
||||
type Config struct {
|
||||
Prefix string `mapstructure:"prefix"`
|
||||
// FilesNamespace prefixes the namespace, optionally with user information.
|
||||
// Example: if FilesNamespace is /users/{{substr 0 1 .Username}}/{{.Username}}
|
||||
// and received path is /docs the internal path will be:
|
||||
// /users/<first char of username>/<username>/docs
|
||||
FilesNamespace string `mapstructure:"files_namespace"`
|
||||
// WebdavNamespace prefixes the namespace, optionally with user information.
|
||||
// Example: if WebdavNamespace is /users/{{substr 0 1 .Username}}/{{.Username}}
|
||||
// and received path is /docs the internal path will be:
|
||||
// /users/<first char of username>/<username>/docs
|
||||
WebdavNamespace string `mapstructure:"webdav_namespace"`
|
||||
SharesNamespace string `mapstructure:"shares_namespace"`
|
||||
OCMNamespace string `mapstructure:"ocm_namespace"`
|
||||
GatewaySvc string `mapstructure:"gatewaysvc"`
|
||||
Timeout int64 `mapstructure:"timeout"`
|
||||
Insecure bool `mapstructure:"insecure"`
|
||||
// If true, HTTP COPY will expect the HTTP-TPC (third-party copy) headers
|
||||
EnableHTTPTpc bool `mapstructure:"enable_http_tpc"`
|
||||
PublicURL string `mapstructure:"public_url"`
|
||||
FavoriteStorageDriver string `mapstructure:"favorite_storage_driver"`
|
||||
FavoriteStorageDrivers map[string]map[string]interface{} `mapstructure:"favorite_storage_drivers"`
|
||||
Version string `mapstructure:"version"`
|
||||
VersionString string `mapstructure:"version_string"`
|
||||
Edition string `mapstructure:"edition"`
|
||||
Product string `mapstructure:"product"`
|
||||
ProductName string `mapstructure:"product_name"`
|
||||
ProductVersion string `mapstructure:"product_version"`
|
||||
AllowPropfindDepthInfinitiy bool `mapstructure:"allow_depth_infinity"`
|
||||
|
||||
TransferSharedSecret string `mapstructure:"transfer_shared_secret"`
|
||||
|
||||
NameValidation NameValidation `mapstructure:"validation"`
|
||||
|
||||
MachineAuthAPIKey string `mapstructure:"machine_auth_apikey"`
|
||||
}
|
||||
|
||||
// NameValidation is the validation configuration for file and folder names
|
||||
type NameValidation struct {
|
||||
InvalidChars []string `mapstructure:"invalid_chars"`
|
||||
MaxLength int `mapstructure:"max_length"`
|
||||
}
|
||||
|
||||
// Init initializes the configuration
|
||||
func (c *Config) Init() {
|
||||
// note: default c.Prefix is an empty string
|
||||
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
|
||||
|
||||
if c.FavoriteStorageDriver == "" {
|
||||
c.FavoriteStorageDriver = "memory"
|
||||
}
|
||||
|
||||
if c.Version == "" {
|
||||
c.Version = "10.0.11.5"
|
||||
}
|
||||
|
||||
if c.VersionString == "" {
|
||||
c.VersionString = "10.0.11"
|
||||
}
|
||||
|
||||
if c.Product == "" {
|
||||
c.Product = "reva"
|
||||
}
|
||||
|
||||
if c.ProductName == "" {
|
||||
c.ProductName = "reva"
|
||||
}
|
||||
|
||||
if c.ProductVersion == "" {
|
||||
c.ProductVersion = "10.0.11"
|
||||
}
|
||||
|
||||
if c.Edition == "" {
|
||||
c.Edition = "community"
|
||||
}
|
||||
|
||||
if c.NameValidation.InvalidChars == nil {
|
||||
c.NameValidation.InvalidChars = []string{"\f", "\r", "\n", "\\"}
|
||||
}
|
||||
|
||||
if c.NameValidation.MaxLength == 0 {
|
||||
c.NameValidation.MaxLength = 255
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
package ocdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
cs3storage "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
)
|
||||
|
||||
type tokenStatInfoKey struct{}
|
||||
|
||||
// ContextWithTokenStatInfo adds the token stat info to the context
|
||||
func ContextWithTokenStatInfo(ctx context.Context, info *cs3storage.ResourceInfo) context.Context {
|
||||
return context.WithValue(ctx, tokenStatInfoKey{}, info)
|
||||
}
|
||||
|
||||
// TokenStatInfoFromContext returns the token stat info from the context
|
||||
func TokenStatInfoFromContext(ctx context.Context) (*cs3storage.ResourceInfo, bool) {
|
||||
v, ok := ctx.Value(tokenStatInfoKey{}).(*cs3storage.ResourceInfo)
|
||||
return v, ok
|
||||
}
|
||||
Generated
Vendored
+756
@@ -0,0 +1,756 @@
|
||||
// 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 ocdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
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"
|
||||
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/datagateway"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
|
||||
"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/opencloud-eu/reva/v2/pkg/rhttp"
|
||||
"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"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type copy struct {
|
||||
source *provider.Reference
|
||||
sourceInfo *provider.ResourceInfo
|
||||
destination *provider.Reference
|
||||
depth net.Depth
|
||||
successCode int
|
||||
}
|
||||
|
||||
func (s *svc) handlePathCopy(w http.ResponseWriter, r *http.Request, ns string) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "copy")
|
||||
defer span.End()
|
||||
|
||||
if r.Body != http.NoBody {
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
b, err := errors.Marshal(http.StatusUnsupportedMediaType, "body must be empty", "", "")
|
||||
errors.HandleWebdavError(appctx.GetLogger(ctx), w, b, err)
|
||||
return
|
||||
}
|
||||
|
||||
if s.c.EnableHTTPTpc {
|
||||
if r.Header.Get("Source") != "" {
|
||||
// HTTP Third-Party Copy Pull mode
|
||||
s.handleTPCPull(ctx, w, r, ns)
|
||||
return
|
||||
} else if r.Header.Get("Destination") != "" {
|
||||
// HTTP Third-Party Copy Push mode
|
||||
s.handleTPCPush(ctx, w, r, ns)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Local copy: in this case Destination is mandatory
|
||||
src := path.Join(ns, r.URL.Path)
|
||||
|
||||
dh := r.Header.Get(net.HeaderDestination)
|
||||
baseURI := r.Context().Value(net.CtxKeyBaseURI).(string)
|
||||
dst, err := net.ParseDestination(baseURI, dh)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
b, err := errors.Marshal(http.StatusBadRequest, "failed to extract destination", "", "")
|
||||
errors.HandleWebdavError(appctx.GetLogger(ctx), w, b, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ValidateName(filename(src), s.nameValidators); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
b, err := errors.Marshal(http.StatusBadRequest, "source failed naming rules", "", "")
|
||||
errors.HandleWebdavError(appctx.GetLogger(ctx), w, b, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ValidateDestination(filename(dst), s.nameValidators); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
b, err := errors.Marshal(http.StatusBadRequest, "destination failed naming rules", "", "")
|
||||
errors.HandleWebdavError(appctx.GetLogger(ctx), w, b, err)
|
||||
return
|
||||
}
|
||||
|
||||
dst = path.Join(ns, dst)
|
||||
|
||||
sublog := appctx.GetLogger(ctx).With().Str("src", src).Str("dst", dst).Logger()
|
||||
|
||||
srcSpace, status, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, src)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Str("path", src).Msg("failed to look up storage space")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if status.Code != rpc.Code_CODE_OK {
|
||||
errors.HandleErrorStatus(&sublog, w, status)
|
||||
return
|
||||
}
|
||||
dstSpace, status, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, dst)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Str("path", dst).Msg("failed to look up storage space")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if status.Code != rpc.Code_CODE_OK {
|
||||
errors.HandleErrorStatus(&sublog, w, status)
|
||||
return
|
||||
}
|
||||
|
||||
cp := s.prepareCopy(ctx, w, r, spacelookup.MakeRelativeReference(srcSpace, src, false), spacelookup.MakeRelativeReference(dstSpace, dst, false), &sublog, dstSpace.GetRoot().GetStorageId() == utils.ShareStorageProviderID)
|
||||
if cp == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.executePathCopy(ctx, s.gatewaySelector, w, r, cp); err != nil {
|
||||
sublog.Error().Err(err).Str("depth", cp.depth.String()).Msg("error executing path copy")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
w.WriteHeader(cp.successCode)
|
||||
}
|
||||
|
||||
func (s *svc) executePathCopy(ctx context.Context, selector pool.Selectable[gateway.GatewayAPIClient], w http.ResponseWriter, r *http.Request, cp *copy) error {
|
||||
log := appctx.GetLogger(ctx)
|
||||
log.Debug().Str("src", cp.sourceInfo.Path).Str("dst", cp.destination.Path).Msg("descending")
|
||||
|
||||
client, err := selector.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var fileid string
|
||||
if cp.sourceInfo.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
|
||||
// create dir
|
||||
createReq := &provider.CreateContainerRequest{
|
||||
Ref: cp.destination,
|
||||
}
|
||||
createRes, err := client.CreateContainer(ctx, createReq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error performing create container grpc request")
|
||||
return err
|
||||
}
|
||||
if createRes.Status.Code != rpc.Code_CODE_OK {
|
||||
if createRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
m := fmt.Sprintf("Permission denied to create %v", createReq.Ref.Path)
|
||||
b, err := errors.Marshal(http.StatusForbidden, m, "", "")
|
||||
errors.HandleWebdavError(log, w, b, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: also copy properties: https://tools.ietf.org/html/rfc4918#section-9.8.2
|
||||
|
||||
if cp.depth != net.DepthInfinity {
|
||||
return nil
|
||||
}
|
||||
|
||||
// descend for children
|
||||
listReq := &provider.ListContainerRequest{
|
||||
Ref: cp.source,
|
||||
}
|
||||
res, err := client.ListContainer(ctx, listReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.Status.Code != rpc.Code_CODE_OK {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := range res.Infos {
|
||||
child := filepath.Base(res.Infos[i].Path)
|
||||
src := &provider.Reference{
|
||||
ResourceId: cp.source.ResourceId,
|
||||
Path: utils.MakeRelativePath(filepath.Join(cp.source.Path, child)),
|
||||
}
|
||||
childDst := &provider.Reference{
|
||||
ResourceId: cp.destination.ResourceId,
|
||||
Path: utils.MakeRelativePath(filepath.Join(cp.destination.Path, child)),
|
||||
}
|
||||
err := s.executePathCopy(ctx, selector, w, r, ©{source: src, sourceInfo: res.Infos[i], destination: childDst, depth: cp.depth, successCode: cp.successCode})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// we need to stat again to get the fileid
|
||||
r, err := client.Stat(ctx, &provider.StatRequest{Ref: cp.destination})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
return fmt.Errorf("status code %d", r.GetStatus().GetCode())
|
||||
}
|
||||
|
||||
fileid = storagespace.FormatResourceID(r.GetInfo().GetId())
|
||||
} else {
|
||||
// copy file
|
||||
|
||||
// 1. get download url
|
||||
|
||||
dReq := &provider.InitiateFileDownloadRequest{
|
||||
Ref: cp.source,
|
||||
}
|
||||
|
||||
dRes, err := client.InitiateFileDownload(ctx, dReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dRes.Status.Code != rpc.Code_CODE_OK {
|
||||
return fmt.Errorf("status code %d", dRes.Status.Code)
|
||||
}
|
||||
|
||||
var downloadEP, downloadToken string
|
||||
for _, p := range dRes.Protocols {
|
||||
if p.Protocol == "spaces" {
|
||||
downloadEP, downloadToken = p.DownloadEndpoint, p.Token
|
||||
}
|
||||
}
|
||||
|
||||
// 2. get upload url
|
||||
|
||||
uReq := &provider.InitiateFileUploadRequest{
|
||||
Ref: cp.destination,
|
||||
Opaque: &typespb.Opaque{
|
||||
Map: map[string]*typespb.OpaqueEntry{
|
||||
"Upload-Length": {
|
||||
Decoder: "plain",
|
||||
// TODO: handle case where size is not known in advance
|
||||
Value: []byte(strconv.FormatUint(cp.sourceInfo.GetSize(), 10)),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
uRes, err := client.InitiateFileUpload(ctx, uReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if uRes.Status.Code != rpc.Code_CODE_OK {
|
||||
if uRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
m := fmt.Sprintf("Permissions denied to create %v", uReq.Ref.Path)
|
||||
b, err := errors.Marshal(http.StatusForbidden, m, "", "")
|
||||
errors.HandleWebdavError(log, w, b, err)
|
||||
return nil
|
||||
}
|
||||
errors.HandleErrorStatus(log, w, uRes.Status)
|
||||
return nil
|
||||
}
|
||||
|
||||
var uploadEP, uploadToken string
|
||||
for _, p := range uRes.Protocols {
|
||||
if p.Protocol == "simple" {
|
||||
uploadEP, uploadToken = p.UploadEndpoint, p.Token
|
||||
}
|
||||
}
|
||||
|
||||
// 3. do download
|
||||
|
||||
httpDownloadReq, err := rhttp.NewRequest(ctx, "GET", downloadEP, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpDownloadReq.Header.Set(datagateway.TokenTransportHeader, downloadToken)
|
||||
|
||||
httpDownloadRes, err := s.client.Do(httpDownloadReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer httpDownloadRes.Body.Close()
|
||||
if httpDownloadRes.StatusCode == http.StatusForbidden {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
b, err := errors.Marshal(http.StatusForbidden, http.StatusText(http.StatusForbidden), "", strconv.Itoa(http.StatusForbidden))
|
||||
errors.HandleWebdavError(log, w, b, err)
|
||||
return nil
|
||||
}
|
||||
if httpDownloadRes.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("status code %d", httpDownloadRes.StatusCode)
|
||||
}
|
||||
|
||||
// 4. do upload
|
||||
|
||||
httpUploadReq, err := rhttp.NewRequest(ctx, "PUT", uploadEP, httpDownloadRes.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpUploadReq.Header.Set(datagateway.TokenTransportHeader, uploadToken)
|
||||
httpUploadReq.ContentLength = int64(cp.sourceInfo.GetSize())
|
||||
|
||||
httpUploadRes, err := s.client.Do(httpUploadReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer httpUploadRes.Body.Close()
|
||||
if httpUploadRes.StatusCode != http.StatusOK {
|
||||
return err
|
||||
}
|
||||
|
||||
fileid = httpUploadRes.Header.Get(net.HeaderOCFileID)
|
||||
}
|
||||
|
||||
w.Header().Set(net.HeaderOCFileID, fileid)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *svc) handleSpacesCopy(w http.ResponseWriter, r *http.Request, spaceID string) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "spaces_copy")
|
||||
defer span.End()
|
||||
|
||||
if r.Body != http.NoBody {
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
b, err := errors.Marshal(http.StatusUnsupportedMediaType, "body must be empty", "", "")
|
||||
errors.HandleWebdavError(appctx.GetLogger(ctx), w, b, err)
|
||||
return
|
||||
}
|
||||
|
||||
dh := r.Header.Get(net.HeaderDestination)
|
||||
baseURI := r.Context().Value(net.CtxKeyBaseURI).(string)
|
||||
dst, err := net.ParseDestination(baseURI, dh)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sublog := appctx.GetLogger(ctx).With().Str("spaceid", spaceID).Str("path", r.URL.Path).Str("destination", dst).Logger()
|
||||
|
||||
srcRef, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
dstSpaceID, dstRelPath := router.ShiftPath(dst)
|
||||
|
||||
dstRef, err := spacelookup.MakeStorageSpaceReference(dstSpaceID, dstRelPath)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
cp := s.prepareCopy(ctx, w, r, &srcRef, &dstRef, &sublog, dstRef.GetResourceId().GetStorageId() == utils.ShareStorageProviderID)
|
||||
if cp == nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = s.executeSpacesCopy(ctx, w, s.gatewaySelector, cp)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Str("depth", cp.depth.String()).Msg("error descending directory")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
w.WriteHeader(cp.successCode)
|
||||
}
|
||||
|
||||
func (s *svc) executeSpacesCopy(ctx context.Context, w http.ResponseWriter, selector pool.Selectable[gateway.GatewayAPIClient], cp *copy) error {
|
||||
log := appctx.GetLogger(ctx)
|
||||
log.Debug().Interface("src", cp.sourceInfo).Interface("dst", cp.destination).Msg("descending")
|
||||
|
||||
client, err := selector.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var fileid string
|
||||
if cp.sourceInfo.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
|
||||
// create dir
|
||||
createReq := &provider.CreateContainerRequest{
|
||||
Ref: cp.destination,
|
||||
}
|
||||
createRes, err := client.CreateContainer(ctx, createReq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error performing create container grpc request")
|
||||
return err
|
||||
}
|
||||
if createRes.Status.Code != rpc.Code_CODE_OK {
|
||||
if createRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
// TODO path could be empty or relative...
|
||||
m := fmt.Sprintf("Permission denied to create %v", createReq.Ref.Path)
|
||||
b, err := errors.Marshal(http.StatusForbidden, m, "", "")
|
||||
errors.HandleWebdavError(log, w, b, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: also copy properties: https://tools.ietf.org/html/rfc4918#section-9.8.2
|
||||
|
||||
if cp.depth != net.DepthInfinity {
|
||||
return nil
|
||||
}
|
||||
|
||||
// descend for children
|
||||
listReq := &provider.ListContainerRequest{Ref: &provider.Reference{ResourceId: cp.sourceInfo.Id, Path: "."}}
|
||||
res, err := client.ListContainer(ctx, listReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.Status.Code != rpc.Code_CODE_OK {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := range res.Infos {
|
||||
childRef := &provider.Reference{
|
||||
ResourceId: cp.destination.ResourceId,
|
||||
Path: utils.MakeRelativePath(path.Join(cp.destination.Path, res.Infos[i].Path)),
|
||||
}
|
||||
err := s.executeSpacesCopy(ctx, w, selector, ©{sourceInfo: res.Infos[i], destination: childRef, depth: cp.depth, successCode: cp.successCode})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// we need to stat again to get the fileid
|
||||
r, err := client.Stat(ctx, &provider.StatRequest{Ref: cp.destination})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
return fmt.Errorf("stat: status code %d", r.GetStatus().GetCode())
|
||||
}
|
||||
|
||||
fileid = storagespace.FormatResourceID(r.GetInfo().GetId())
|
||||
} else {
|
||||
// copy file
|
||||
// 1. get download url
|
||||
dReq := &provider.InitiateFileDownloadRequest{Ref: &provider.Reference{ResourceId: cp.sourceInfo.Id, Path: "."}}
|
||||
dRes, err := client.InitiateFileDownload(ctx, dReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dRes.Status.Code != rpc.Code_CODE_OK {
|
||||
return fmt.Errorf("status code %d", dRes.Status.Code)
|
||||
}
|
||||
|
||||
var downloadEP, downloadToken string
|
||||
for _, p := range dRes.Protocols {
|
||||
if p.Protocol == "spaces" {
|
||||
downloadEP, downloadToken = p.DownloadEndpoint, p.Token
|
||||
}
|
||||
}
|
||||
// 2. get upload url
|
||||
uReq := &provider.InitiateFileUploadRequest{
|
||||
Ref: cp.destination,
|
||||
Opaque: &typespb.Opaque{
|
||||
Map: map[string]*typespb.OpaqueEntry{
|
||||
net.HeaderUploadLength: {
|
||||
Decoder: "plain",
|
||||
// TODO: handle case where size is not known in advance
|
||||
Value: []byte(strconv.FormatUint(cp.sourceInfo.GetSize(), 10)),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
uRes, err := client.InitiateFileUpload(ctx, uReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if uRes.Status.Code != rpc.Code_CODE_OK {
|
||||
if uRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
// TODO path can be empty or relative
|
||||
m := fmt.Sprintf("Permissions denied to create %v", uReq.Ref.Path)
|
||||
b, err := errors.Marshal(http.StatusForbidden, m, "", "")
|
||||
errors.HandleWebdavError(log, w, b, err)
|
||||
return nil
|
||||
}
|
||||
errors.HandleErrorStatus(log, w, uRes.Status)
|
||||
return nil
|
||||
}
|
||||
|
||||
var uploadEP, uploadToken string
|
||||
for _, p := range uRes.Protocols {
|
||||
if p.Protocol == "simple" {
|
||||
uploadEP, uploadToken = p.UploadEndpoint, p.Token
|
||||
}
|
||||
}
|
||||
|
||||
// 3. do download
|
||||
httpDownloadReq, err := rhttp.NewRequest(ctx, http.MethodGet, downloadEP, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if downloadToken != "" {
|
||||
httpDownloadReq.Header.Set(datagateway.TokenTransportHeader, downloadToken)
|
||||
}
|
||||
|
||||
httpDownloadRes, err := s.client.Do(httpDownloadReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer httpDownloadRes.Body.Close()
|
||||
if httpDownloadRes.StatusCode == http.StatusForbidden {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
b, err := errors.Marshal(http.StatusForbidden, http.StatusText(http.StatusForbidden), "", strconv.Itoa(http.StatusForbidden))
|
||||
errors.HandleWebdavError(log, w, b, err)
|
||||
return nil
|
||||
}
|
||||
if httpDownloadRes.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("status code %d", httpDownloadRes.StatusCode)
|
||||
}
|
||||
|
||||
// 4. do upload
|
||||
|
||||
httpUploadReq, err := rhttp.NewRequest(ctx, http.MethodPut, uploadEP, httpDownloadRes.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpUploadReq.Header.Set(datagateway.TokenTransportHeader, uploadToken)
|
||||
httpUploadReq.ContentLength = int64(cp.sourceInfo.GetSize())
|
||||
|
||||
httpUploadRes, err := s.client.Do(httpUploadReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer httpUploadRes.Body.Close()
|
||||
if httpUploadRes.StatusCode != http.StatusOK {
|
||||
return err
|
||||
}
|
||||
|
||||
fileid = httpUploadRes.Header.Get(net.HeaderOCFileID)
|
||||
}
|
||||
|
||||
w.Header().Set(net.HeaderOCFileID, fileid)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *svc) prepareCopy(ctx context.Context, w http.ResponseWriter, r *http.Request, srcRef, dstRef *provider.Reference, log *zerolog.Logger, destInShareJail bool) *copy {
|
||||
isChild, err := s.referenceIsChildOf(ctx, s.gatewaySelector, dstRef, srcRef)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case errtypes.IsNotSupported:
|
||||
log.Error().Err(err).Msg("can not detect recursive copy operation. missing machine auth configuration?")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
default:
|
||||
log.Error().Err(err).Msg("error while trying to detect recursive copy operation")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
if isChild {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
b, err := errors.Marshal(http.StatusBadRequest, "can not copy a folder into one of its children", "", "")
|
||||
errors.HandleWebdavError(log, w, b, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
isParent, err := s.referenceIsChildOf(ctx, s.gatewaySelector, srcRef, dstRef)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case errtypes.IsNotFound:
|
||||
isParent = false
|
||||
case errtypes.IsNotSupported:
|
||||
log.Error().Err(err).Msg("can not detect recursive copy operation. missing machine auth configuration?")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return nil
|
||||
default:
|
||||
log.Error().Err(err).Msg("error while trying to detect recursive copy operation")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if isParent {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
b, err := errors.Marshal(http.StatusBadRequest, "can not copy a folder into its parent", "", "")
|
||||
errors.HandleWebdavError(log, w, b, err)
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
if srcRef.Path == dstRef.Path && srcRef.ResourceId == dstRef.ResourceId {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
b, err := errors.Marshal(http.StatusBadRequest, "source and destination are the same", "", "")
|
||||
errors.HandleWebdavError(log, w, b, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
oh := r.Header.Get(net.HeaderOverwrite)
|
||||
overwrite, err := net.ParseOverwrite(oh)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
m := fmt.Sprintf("Overwrite header is set to incorrect value %v", overwrite)
|
||||
b, err := errors.Marshal(http.StatusBadRequest, m, "", "")
|
||||
errors.HandleWebdavError(log, w, b, err)
|
||||
return nil
|
||||
}
|
||||
dh := r.Header.Get(net.HeaderDepth)
|
||||
depth, err := net.ParseDepth(dh)
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
m := fmt.Sprintf("Depth header is set to incorrect value %v", dh)
|
||||
b, err := errors.Marshal(http.StatusBadRequest, m, "", "")
|
||||
errors.HandleWebdavError(log, w, b, err)
|
||||
return nil
|
||||
}
|
||||
if dh == "" {
|
||||
// net.ParseDepth returns "1" for an empty value but copy expects "infinity"
|
||||
// so we overwrite it here
|
||||
depth = net.DepthInfinity
|
||||
}
|
||||
|
||||
log.Debug().Bool("overwrite", overwrite).Str("depth", depth.String()).Msg("copy")
|
||||
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error selecting next client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil
|
||||
}
|
||||
|
||||
srcStatReq := &provider.StatRequest{Ref: srcRef}
|
||||
srcStatRes, err := client.Stat(ctx, srcStatReq)
|
||||
switch {
|
||||
case err != nil:
|
||||
log.Error().Err(err).Msg("error sending grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil
|
||||
case srcStatRes.Status.Code == rpc.Code_CODE_NOT_FOUND:
|
||||
errors.HandleErrorStatus(log, w, srcStatRes.Status)
|
||||
m := fmt.Sprintf("Resource %v not found", srcStatReq.Ref.Path)
|
||||
b, err := errors.Marshal(http.StatusNotFound, m, "", "")
|
||||
errors.HandleWebdavError(log, w, b, err)
|
||||
return nil
|
||||
case srcStatRes.Status.Code != rpc.Code_CODE_OK:
|
||||
errors.HandleErrorStatus(log, w, srcStatRes.Status)
|
||||
return nil
|
||||
}
|
||||
if utils.IsSpaceRoot(srcStatRes.GetInfo()) {
|
||||
log.Error().Msg("the source is disallowed")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
|
||||
dstStatReq := &provider.StatRequest{Ref: dstRef}
|
||||
dstStatRes, err := client.Stat(ctx, dstStatReq)
|
||||
switch {
|
||||
case err != nil:
|
||||
log.Error().Err(err).Msg("error sending grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil
|
||||
case dstStatRes.Status.Code != rpc.Code_CODE_OK && dstStatRes.Status.Code != rpc.Code_CODE_NOT_FOUND:
|
||||
errors.HandleErrorStatus(log, w, dstStatRes.Status)
|
||||
return nil
|
||||
}
|
||||
|
||||
successCode := http.StatusCreated // 201 if new resource was created, see https://tools.ietf.org/html/rfc4918#section-9.8.5
|
||||
if dstStatRes.Status.Code == rpc.Code_CODE_OK {
|
||||
successCode = http.StatusNoContent // 204 if target already existed, see https://tools.ietf.org/html/rfc4918#section-9.8.5
|
||||
|
||||
if !overwrite {
|
||||
log.Warn().Bool("overwrite", overwrite).Msg("dst already exists")
|
||||
w.WriteHeader(http.StatusPreconditionFailed)
|
||||
m := fmt.Sprintf("Could not overwrite Resource %v", dstRef.Path)
|
||||
b, err := errors.Marshal(http.StatusPreconditionFailed, m, "", "")
|
||||
errors.HandleWebdavError(log, w, b, err) // 412, see https://tools.ietf.org/html/rfc4918#section-9.8.5
|
||||
return nil
|
||||
}
|
||||
|
||||
if utils.IsSpaceRoot(dstStatRes.GetInfo()) {
|
||||
log.Error().Msg("overwriting is not allowed")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
|
||||
// delete existing tree when overwriting a directory or replacing a file with a directory
|
||||
if dstStatRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER ||
|
||||
(dstStatRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_FILE &&
|
||||
srcStatRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER) {
|
||||
|
||||
// we must not allow to override mountpoints - so we check if we have access to the parent. If not this is a mountpoint
|
||||
if destInShareJail {
|
||||
res, err := client.GetPath(ctx, &provider.GetPathRequest{ResourceId: dstStatRes.GetInfo().GetId()})
|
||||
if err != nil || res.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
log.Error().Err(err).Msg("error sending grpc get path request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil
|
||||
}
|
||||
|
||||
dir, file := filepath.Split(filepath.Clean(res.GetPath()))
|
||||
if dir == "/" || dir == "" || file == "" {
|
||||
log.Error().Msg("must not overwrite mount points")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte("must not overwrite mount points"))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
delReq := &provider.DeleteRequest{Ref: dstRef}
|
||||
delRes, err := client.Delete(ctx, delReq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error sending grpc delete request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil
|
||||
}
|
||||
|
||||
if delRes.Status.Code != rpc.Code_CODE_OK && delRes.Status.Code != rpc.Code_CODE_NOT_FOUND {
|
||||
errors.HandleErrorStatus(log, w, delRes.Status)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else if p := path.Dir(dstRef.Path); p != "" {
|
||||
// check if an intermediate path / the parent exists
|
||||
pRef := &provider.Reference{
|
||||
ResourceId: dstRef.ResourceId,
|
||||
Path: utils.MakeRelativePath(p),
|
||||
}
|
||||
intStatReq := &provider.StatRequest{Ref: pRef}
|
||||
intStatRes, err := client.Stat(ctx, intStatReq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error sending grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil
|
||||
}
|
||||
if intStatRes.Status.Code != rpc.Code_CODE_OK {
|
||||
if intStatRes.Status.Code == rpc.Code_CODE_NOT_FOUND {
|
||||
// 409 if intermediate dir is missing, see https://tools.ietf.org/html/rfc4918#section-9.8.5
|
||||
log.Debug().Interface("parent", pRef).Interface("status", intStatRes.Status).Msg("conflict")
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
} else {
|
||||
errors.HandleErrorStatus(log, w, intStatRes.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// TODO what if intermediate is a file?
|
||||
}
|
||||
|
||||
return ©{source: srcRef, sourceInfo: srcStatRes.Info, depth: depth, successCode: successCode, destination: dstRef}
|
||||
}
|
||||
Generated
Vendored
+454
@@ -0,0 +1,454 @@
|
||||
// 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 ocdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/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/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/config"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/grants"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
_trashbinPath = "trash-bin"
|
||||
|
||||
// WwwAuthenticate captures the Www-Authenticate header string.
|
||||
WwwAuthenticate = "Www-Authenticate"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrListingMembers = "ERR_LISTING_MEMBERS_NOT_ALLOWED"
|
||||
ErrInvalidCredentials = "ERR_INVALID_CREDENTIALS"
|
||||
ErrMissingBasicAuth = "ERR_MISSING_BASIC_AUTH"
|
||||
ErrMissingBearerAuth = "ERR_MISSING_BEARER_AUTH"
|
||||
ErrFileNotFoundInRoot = "ERR_FILE_NOT_FOUND_IN_ROOT"
|
||||
)
|
||||
|
||||
// DavHandler routes to the different sub handlers
|
||||
type DavHandler struct {
|
||||
AvatarsHandler *AvatarsHandler
|
||||
FilesHandler *WebDavHandler
|
||||
FilesHomeHandler *WebDavHandler
|
||||
MetaHandler *MetaHandler
|
||||
TrashbinHandler *TrashbinHandler
|
||||
SpacesHandler *SpacesHandler
|
||||
PublicFolderHandler *WebDavHandler
|
||||
PublicFileHandler *PublicFileHandler
|
||||
SharesHandler *WebDavHandler
|
||||
OCMSharesHandler *WebDavHandler
|
||||
}
|
||||
|
||||
func (h *DavHandler) init(c *config.Config) error {
|
||||
h.AvatarsHandler = new(AvatarsHandler)
|
||||
if err := h.AvatarsHandler.init(c); err != nil {
|
||||
return err
|
||||
}
|
||||
h.FilesHandler = new(WebDavHandler)
|
||||
if err := h.FilesHandler.init(c.FilesNamespace, false); err != nil {
|
||||
return err
|
||||
}
|
||||
h.FilesHomeHandler = new(WebDavHandler)
|
||||
if err := h.FilesHomeHandler.init(c.WebdavNamespace, true); err != nil {
|
||||
return err
|
||||
}
|
||||
h.MetaHandler = new(MetaHandler)
|
||||
if err := h.MetaHandler.init(c); err != nil {
|
||||
return err
|
||||
}
|
||||
h.TrashbinHandler = new(TrashbinHandler)
|
||||
if err := h.TrashbinHandler.init(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.SpacesHandler = new(SpacesHandler)
|
||||
if err := h.SpacesHandler.init(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.PublicFolderHandler = new(WebDavHandler)
|
||||
if err := h.PublicFolderHandler.init("public", true); err != nil { // jail public file requests to /public/ prefix
|
||||
return err
|
||||
}
|
||||
|
||||
h.PublicFileHandler = new(PublicFileHandler)
|
||||
if err := h.PublicFileHandler.init("public"); err != nil { // jail public file requests to /public/ prefix
|
||||
return err
|
||||
}
|
||||
|
||||
h.OCMSharesHandler = new(WebDavHandler)
|
||||
if err := h.OCMSharesHandler.init(c.OCMNamespace, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isOwner(userIDorName string, user *userv1beta1.User) bool {
|
||||
return userIDorName != "" && (userIDorName == user.Id.OpaqueId || strings.EqualFold(userIDorName, user.Username))
|
||||
}
|
||||
|
||||
// Handler handles requests
|
||||
func (h *DavHandler) Handler(s *svc) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
log := appctx.GetLogger(ctx)
|
||||
|
||||
// if there is no file in the request url we assume the request url is: "/remote.php/dav/files"
|
||||
// https://github.com/owncloud/core/blob/18475dac812064b21dabcc50f25ef3ffe55691a5/tests/acceptance/features/apiWebdavOperations/propfind.feature
|
||||
if r.URL.Path == "/files" {
|
||||
log.Debug().Str("path", r.URL.Path).Msg("method not allowed")
|
||||
contextUser, ok := ctxpkg.ContextGetUser(ctx)
|
||||
if ok {
|
||||
r.URL.Path = path.Join(r.URL.Path, contextUser.Username)
|
||||
}
|
||||
|
||||
if r.Header.Get(net.HeaderDepth) == "" {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
b, err := errors.Marshal(http.StatusMethodNotAllowed, "Listing members of this collection is disabled", "", ErrListingMembers)
|
||||
if err != nil {
|
||||
log.Error().Msgf("error marshaling xml response: %s", b)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_, err = w.Write(b)
|
||||
if err != nil {
|
||||
log.Error().Msgf("error writing xml response: %s", b)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var head string
|
||||
head, r.URL.Path = router.ShiftPath(r.URL.Path)
|
||||
|
||||
switch head {
|
||||
case "avatars":
|
||||
h.AvatarsHandler.Handler(s).ServeHTTP(w, r)
|
||||
case "files":
|
||||
var requestUserID string
|
||||
var oldPath = r.URL.Path
|
||||
|
||||
// detect and check current user in URL
|
||||
requestUserID, r.URL.Path = router.ShiftPath(r.URL.Path)
|
||||
|
||||
// note: some requests like OPTIONS don't forward the user
|
||||
contextUser, ok := ctxpkg.ContextGetUser(ctx)
|
||||
if ok && isOwner(requestUserID, contextUser) {
|
||||
// use home storage handler when user was detected
|
||||
base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "files", requestUserID)
|
||||
ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
h.FilesHomeHandler.Handler(s).ServeHTTP(w, r)
|
||||
} else {
|
||||
r.URL.Path = oldPath
|
||||
base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "files")
|
||||
ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
h.FilesHandler.Handler(s).ServeHTTP(w, r)
|
||||
}
|
||||
case "meta":
|
||||
base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "meta")
|
||||
ctx = context.WithValue(ctx, net.CtxKeyBaseURI, base)
|
||||
r = r.WithContext(ctx)
|
||||
h.MetaHandler.Handler(s).ServeHTTP(w, r)
|
||||
case "ocm":
|
||||
base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "ocm")
|
||||
ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
|
||||
c, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// OC10 and Nextcloud (OCM 1.0) are using basic auth for carrying the
|
||||
// ocm share id.
|
||||
var ocmshare, sharedSecret string
|
||||
username, _, ok := r.BasicAuth()
|
||||
if ok {
|
||||
// OCM 1.0
|
||||
ocmshare = username
|
||||
sharedSecret = username
|
||||
r.URL.Path = filepath.Join("/", ocmshare, r.URL.Path)
|
||||
} else {
|
||||
ocmshare, _ = router.ShiftPath(r.URL.Path)
|
||||
sharedSecret = strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
||||
}
|
||||
|
||||
authRes, err := handleOCMAuth(ctx, c, ocmshare, sharedSecret)
|
||||
switch {
|
||||
case err != nil:
|
||||
log.Error().Err(err).Msg("error during ocm authentication")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
case authRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED:
|
||||
log.Debug().Str("ocmshare", ocmshare).Msg("permission denied")
|
||||
fallthrough
|
||||
case authRes.Status.Code == rpc.Code_CODE_UNAUTHENTICATED:
|
||||
log.Debug().Str("ocmshare", ocmshare).Msg("unauthorized")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
case authRes.Status.Code == rpc.Code_CODE_NOT_FOUND:
|
||||
log.Debug().Str("ocmshare", ocmshare).Msg("not found")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
case authRes.Status.Code != rpc.Code_CODE_OK:
|
||||
log.Error().Str("ocmshare", ocmshare).Interface("status", authRes.Status).Msg("grpc auth request failed")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ctx = ctxpkg.ContextSetToken(ctx, authRes.Token)
|
||||
ctx = ctxpkg.ContextSetUser(ctx, authRes.User)
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, authRes.Token)
|
||||
|
||||
log.Debug().Str("ocmshare", ocmshare).Interface("user", authRes.User).Msg("OCM user authenticated")
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
h.OCMSharesHandler.Handler(s).ServeHTTP(w, r)
|
||||
case "trash-bin":
|
||||
base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "trash-bin")
|
||||
ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
|
||||
r = r.WithContext(ctx)
|
||||
h.TrashbinHandler.Handler(s).ServeHTTP(w, r)
|
||||
case "spaces":
|
||||
base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "spaces")
|
||||
ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
|
||||
r = r.WithContext(ctx)
|
||||
h.SpacesHandler.Handler(s, h.TrashbinHandler).ServeHTTP(w, r)
|
||||
case "public-files":
|
||||
base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "public-files")
|
||||
ctx = context.WithValue(ctx, net.CtxKeyBaseURI, base)
|
||||
|
||||
var res *gatewayv1beta1.AuthenticateResponse
|
||||
token, _ := router.ShiftPath(r.URL.Path)
|
||||
var hasValidBasicAuthHeader bool
|
||||
var pass string
|
||||
var err error
|
||||
// If user is authenticated
|
||||
_, userExists := ctxpkg.ContextGetUser(ctx)
|
||||
if userExists {
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error sending grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
psRes, err := client.GetPublicShare(ctx, &link.GetPublicShareRequest{
|
||||
Ref: &link.PublicShareReference{
|
||||
Spec: &link.PublicShareReference_Token{
|
||||
Token: token,
|
||||
},
|
||||
}})
|
||||
if err != nil && !strings.Contains(err.Error(), "core access token not found") {
|
||||
log.Error().Err(err).Msg("error sending grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// If the link is internal then 307 redirect
|
||||
if psRes.Status.Code == rpc.Code_CODE_OK && grants.PermissionsEqual(psRes.Share.Permissions.GetPermissions(), &provider.ResourcePermissions{}) {
|
||||
if psRes.GetShare().GetResourceId() != nil {
|
||||
rUrl := path.Join("/dav/spaces", storagespace.FormatResourceID(psRes.GetShare().GetResourceId()))
|
||||
http.Redirect(w, r, rUrl, http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
log.Debug().Str("token", token).Interface("status", psRes.Status).Msg("resource id not found")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, pass, hasValidBasicAuthHeader = r.BasicAuth(); hasValidBasicAuthHeader {
|
||||
res, err = handleBasicAuth(r.Context(), s.gatewaySelector, token, pass)
|
||||
} else {
|
||||
q := r.URL.Query()
|
||||
sig := q.Get("signature")
|
||||
expiration := q.Get("expiration")
|
||||
// We restrict the pre-signed urls to downloads.
|
||||
if sig != "" && expiration != "" && !(r.Method == http.MethodGet || r.Method == http.MethodHead) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
res, err = handleSignatureAuth(r.Context(), s.gatewaySelector, token, sig, expiration)
|
||||
}
|
||||
|
||||
switch {
|
||||
case err != nil:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
case res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED:
|
||||
fallthrough
|
||||
case res.Status.Code == rpc.Code_CODE_UNAUTHENTICATED:
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
if hasValidBasicAuthHeader {
|
||||
b, err := errors.Marshal(http.StatusUnauthorized, "Username or password was incorrect", "", ErrInvalidCredentials)
|
||||
errors.HandleWebdavError(log, w, b, err)
|
||||
return
|
||||
}
|
||||
b, err := errors.Marshal(http.StatusUnauthorized, "No 'Authorization: Basic' header found", "", ErrMissingBasicAuth)
|
||||
errors.HandleWebdavError(log, w, b, err)
|
||||
return
|
||||
case res.Status.Code == rpc.Code_CODE_NOT_FOUND:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
case res.Status.Code != rpc.Code_CODE_OK:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if userExists {
|
||||
// Build new context without an authenticated user.
|
||||
// the public link should be resolved by the 'publicshares' authenticated user
|
||||
baseURI := ctx.Value(net.CtxKeyBaseURI).(string)
|
||||
logger := appctx.GetLogger(ctx)
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.End()
|
||||
ctx = trace.ContextWithSpan(context.Background(), span)
|
||||
ctx = appctx.WithLogger(ctx, logger)
|
||||
ctx = context.WithValue(ctx, net.CtxKeyBaseURI, baseURI)
|
||||
}
|
||||
ctx = ctxpkg.ContextSetToken(ctx, res.Token)
|
||||
ctx = ctxpkg.ContextSetUser(ctx, res.User)
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, res.Token)
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
// the public share manager knew the token, but does the referenced target still exist?
|
||||
sRes, err := getTokenStatInfo(ctx, s.gatewaySelector, token)
|
||||
switch {
|
||||
case err != nil:
|
||||
log.Error().Err(err).Msg("error sending grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
case sRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED:
|
||||
fallthrough
|
||||
case sRes.Status.Code == rpc.Code_CODE_OK && grants.PermissionsEqual(sRes.GetInfo().GetPermissionSet(), &provider.ResourcePermissions{}):
|
||||
// If the link is internal
|
||||
if !userExists {
|
||||
w.Header().Add(WwwAuthenticate, fmt.Sprintf("Bearer realm=\"%s\", charset=\"UTF-8\"", r.Host))
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
b, err := errors.Marshal(http.StatusUnauthorized, "No 'Authorization: Bearer' header found", "", ErrMissingBearerAuth)
|
||||
errors.HandleWebdavError(log, w, b, err)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
case sRes.Status.Code == rpc.Code_CODE_NOT_FOUND:
|
||||
log.Debug().Str("token", token).Interface("status", res.Status).Msg("resource not found")
|
||||
w.WriteHeader(http.StatusNotFound) // log the difference
|
||||
return
|
||||
case sRes.Status.Code == rpc.Code_CODE_UNAUTHENTICATED:
|
||||
log.Debug().Str("token", token).Interface("status", res.Status).Msg("unauthorized")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
case sRes.Status.Code != rpc.Code_CODE_OK:
|
||||
log.Error().Str("token", token).Interface("status", res.Status).Msg("grpc stat request failed")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.Debug().Interface("statInfo", sRes.Info).Msg("Stat info from public link token path")
|
||||
|
||||
ctx := ContextWithTokenStatInfo(ctx, sRes.Info)
|
||||
r = r.WithContext(ctx)
|
||||
if sRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER {
|
||||
h.PublicFileHandler.Handler(s).ServeHTTP(w, r)
|
||||
} else {
|
||||
h.PublicFolderHandler.Handler(s).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
b, err := errors.Marshal(http.StatusNotFound, "File not found in root", "", ErrFileNotFoundInRoot)
|
||||
errors.HandleWebdavError(log, w, b, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func getTokenStatInfo(ctx context.Context, selector pool.Selectable[gatewayv1beta1.GatewayAPIClient], token string) (*provider.StatResponse, error) {
|
||||
client, err := selector.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{
|
||||
ResourceId: &provider.ResourceId{
|
||||
StorageId: utils.PublicStorageProviderID,
|
||||
SpaceId: utils.PublicStorageSpaceID,
|
||||
OpaqueId: token,
|
||||
},
|
||||
}})
|
||||
}
|
||||
|
||||
func handleBasicAuth(ctx context.Context, selector pool.Selectable[gatewayv1beta1.GatewayAPIClient], token, pw string) (*gatewayv1beta1.AuthenticateResponse, error) {
|
||||
c, err := selector.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authenticateRequest := gatewayv1beta1.AuthenticateRequest{
|
||||
Type: "publicshares",
|
||||
ClientId: token,
|
||||
ClientSecret: "password|" + pw,
|
||||
}
|
||||
|
||||
return c.Authenticate(ctx, &authenticateRequest)
|
||||
}
|
||||
|
||||
func handleSignatureAuth(ctx context.Context, selector pool.Selectable[gatewayv1beta1.GatewayAPIClient], token, sig, expiration string) (*gatewayv1beta1.AuthenticateResponse, error) {
|
||||
c, err := selector.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authenticateRequest := gatewayv1beta1.AuthenticateRequest{
|
||||
Type: "publicshares",
|
||||
ClientId: token,
|
||||
ClientSecret: "signature|" + sig + "|" + expiration,
|
||||
}
|
||||
|
||||
return c.Authenticate(ctx, &authenticateRequest)
|
||||
}
|
||||
|
||||
func handleOCMAuth(ctx context.Context, c gatewayv1beta1.GatewayAPIClient, ocmshare, sharedSecret string) (*gatewayv1beta1.AuthenticateResponse, error) {
|
||||
return c.Authenticate(ctx, &gatewayv1beta1.AuthenticateRequest{
|
||||
Type: "ocmshares",
|
||||
ClientId: ocmshare,
|
||||
ClientSecret: sharedSecret,
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+149
@@ -0,0 +1,149 @@
|
||||
// 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 ocdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
rstatus "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
func (s *svc) handlePathDelete(w http.ResponseWriter, r *http.Request, ns string) (status int, err error) {
|
||||
ctx := r.Context()
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(ctx, "path_delete")
|
||||
defer span.End()
|
||||
|
||||
if r.Body != http.NoBody {
|
||||
return http.StatusUnsupportedMediaType, errors.New("body must be empty")
|
||||
}
|
||||
|
||||
fn := path.Join(ns, r.URL.Path)
|
||||
|
||||
space, rpcStatus, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, fn)
|
||||
switch {
|
||||
case err != nil:
|
||||
span.RecordError(err)
|
||||
return http.StatusInternalServerError, err
|
||||
case rpcStatus.Code != rpc.Code_CODE_OK:
|
||||
return rstatus.HTTPStatusFromCode(rpcStatus.Code), errtypes.NewErrtypeFromStatus(rpcStatus)
|
||||
}
|
||||
|
||||
return s.handleDelete(ctx, w, r, spacelookup.MakeRelativeReference(space, fn, false))
|
||||
}
|
||||
|
||||
func (s *svc) handleDelete(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference) (status int, err error) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(ctx, "delete")
|
||||
defer span.End()
|
||||
|
||||
req := &provider.DeleteRequest{
|
||||
Ref: ref,
|
||||
LockId: requestLockToken(r),
|
||||
}
|
||||
|
||||
// FIXME the lock token is part of the application level protocol, it should be part of the DeleteRequest message not the opaque
|
||||
ih, ok := parseIfHeader(r.Header.Get(net.HeaderIf))
|
||||
if ok {
|
||||
if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 {
|
||||
req.Opaque = utils.AppendPlainToOpaque(req.Opaque, "lockid", ih.lists[0].conditions[0].Token)
|
||||
}
|
||||
} else if r.Header.Get(net.HeaderIf) != "" {
|
||||
return http.StatusBadRequest, errtypes.BadRequest("invalid if header")
|
||||
}
|
||||
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, errtypes.InternalError(err.Error())
|
||||
}
|
||||
|
||||
res, err := client.Delete(ctx, req)
|
||||
switch {
|
||||
case err != nil:
|
||||
span.RecordError(err)
|
||||
return http.StatusInternalServerError, err
|
||||
case res.Status.Code == rpc.Code_CODE_OK:
|
||||
return http.StatusNoContent, nil
|
||||
case res.Status.Code == rpc.Code_CODE_NOT_FOUND:
|
||||
//lint:ignore ST1005 mimic the exact oc10 error message
|
||||
return http.StatusNotFound, errors.New("Resource not found")
|
||||
case res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED:
|
||||
status = http.StatusForbidden
|
||||
if lockID := utils.ReadPlainFromOpaque(res.Opaque, "lockid"); lockID != "" {
|
||||
// http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the
|
||||
// Lock-Token value is a Coded-URL. We add angle brackets.
|
||||
w.Header().Set("Lock-Token", "<"+lockID+">")
|
||||
status = http.StatusLocked
|
||||
}
|
||||
// check if user has access to resource
|
||||
sRes, err := client.Stat(ctx, &provider.StatRequest{Ref: ref})
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
if sRes.Status.Code != rpc.Code_CODE_OK {
|
||||
// return not found error so we do not leak existence of a file
|
||||
// TODO hide permission failed for users without access in every kind of request
|
||||
// TODO should this be done in the driver?
|
||||
//lint:ignore ST1005 mimic the exact oc10 error message
|
||||
return http.StatusNotFound, errors.New("Resource not found")
|
||||
}
|
||||
return status, errors.New("") // mimic the oc10 error messages which have an empty message in this case
|
||||
case res.Status.Code == rpc.Code_CODE_INTERNAL && res.Status.Message == "can't delete mount path":
|
||||
// 405 must generate an Allow header
|
||||
w.Header().Set("Allow", "PROPFIND, MOVE, COPY, POST, PROPPATCH, HEAD, GET, OPTIONS, LOCK, UNLOCK, REPORT, SEARCH, PUT")
|
||||
return http.StatusMethodNotAllowed, errtypes.PermissionDenied(res.Status.Message)
|
||||
}
|
||||
return rstatus.HTTPStatusFromCode(res.Status.Code), errtypes.NewErrtypeFromStatus(res.Status)
|
||||
}
|
||||
|
||||
func (s *svc) handleSpacesDelete(w http.ResponseWriter, r *http.Request, spaceID string) (status int, err error) {
|
||||
ctx := r.Context()
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(ctx, "spaces_delete")
|
||||
defer span.End()
|
||||
|
||||
if r.Body != http.NoBody {
|
||||
return http.StatusUnsupportedMediaType, errors.New("body must be empty")
|
||||
}
|
||||
|
||||
ref, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
// do not allow deleting spaces via dav endpoint - use graph endpoint instead
|
||||
// we get a relative reference coming from the space root
|
||||
// so if the path is "empty" and no opaque id is present or the opaque id equals
|
||||
// the space id, we are referencing the space
|
||||
rid := ref.GetResourceId()
|
||||
if ref.GetPath() == "." &&
|
||||
(rid.GetOpaqueId() == "" || rid.GetOpaqueId() == rid.GetSpaceId()) {
|
||||
return http.StatusMethodNotAllowed, errors.New("deleting spaces via dav is not allowed")
|
||||
}
|
||||
|
||||
return s.handleDelete(ctx, w, r, &ref)
|
||||
}
|
||||
Generated
Vendored
+214
@@ -0,0 +1,214 @@
|
||||
// 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 errors
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
var sabreException = map[int]string{
|
||||
|
||||
// the commented states have no corresponding exception in sabre/dav,
|
||||
// see https://github.com/sabre-io/dav/tree/master/lib/DAV/Exception
|
||||
|
||||
// http.StatusMultipleChoices: "Multiple Choices",
|
||||
// http.StatusMovedPermanently: "Moved Permanently",
|
||||
// http.StatusFound: "Found",
|
||||
// http.StatusSeeOther: "See Other",
|
||||
// http.StatusNotModified: "Not Modified",
|
||||
// http.StatusUseProxy: "Use Proxy",
|
||||
// http.StatusTemporaryRedirect: "Temporary Redirect",
|
||||
// http.StatusPermanentRedirect: "Permanent Redirect",
|
||||
|
||||
http.StatusBadRequest: "Sabre\\DAV\\Exception\\BadRequest",
|
||||
http.StatusUnauthorized: "Sabre\\DAV\\Exception\\NotAuthenticated",
|
||||
http.StatusPaymentRequired: "Sabre\\DAV\\Exception\\PaymentRequired",
|
||||
http.StatusForbidden: "Sabre\\DAV\\Exception\\Forbidden", // InvalidResourceType, InvalidSyncToken, TooManyMatches
|
||||
http.StatusNotFound: "Sabre\\DAV\\Exception\\NotFound",
|
||||
http.StatusMethodNotAllowed: "Sabre\\DAV\\Exception\\MethodNotAllowed",
|
||||
// http.StatusNotAcceptable: "Not Acceptable",
|
||||
// http.StatusProxyAuthRequired: "Proxy Authentication Required",
|
||||
// http.StatusRequestTimeout: "Request Timeout",
|
||||
http.StatusConflict: "Sabre\\DAV\\Exception\\Conflict", // LockTokenMatchesRequestUri
|
||||
// http.StatusGone: "Gone",
|
||||
http.StatusLengthRequired: "Sabre\\DAV\\Exception\\LengthRequired",
|
||||
http.StatusPreconditionFailed: "Sabre\\DAV\\Exception\\PreconditionFailed",
|
||||
// http.StatusRequestEntityTooLarge: "Request Entity Too Large",
|
||||
// http.StatusRequestURITooLong: "Request URI Too Long",
|
||||
http.StatusUnsupportedMediaType: "Sabre\\DAV\\Exception\\UnsupportedMediaType", // ReportNotSupported
|
||||
http.StatusRequestedRangeNotSatisfiable: "Sabre\\DAV\\Exception\\RequestedRangeNotSatisfiable",
|
||||
// http.StatusExpectationFailed: "Expectation Failed",
|
||||
// http.StatusTeapot: "I'm a teapot",
|
||||
// http.StatusMisdirectedRequest: "Misdirected Request",
|
||||
// http.StatusUnprocessableEntity: "Unprocessable Entity",
|
||||
http.StatusLocked: "Sabre\\DAV\\Exception\\Locked", // ConflictingLock
|
||||
// http.StatusFailedDependency: "Failed Dependency",
|
||||
// http.StatusTooEarly: "Too Early",
|
||||
// http.StatusUpgradeRequired: "Upgrade Required",
|
||||
// http.StatusPreconditionRequired: "Precondition Required",
|
||||
// http.StatusTooManyRequests: "Too Many Requests",
|
||||
// http.StatusRequestHeaderFieldsTooLarge: "Request Header Fields Too Large",
|
||||
// http.StatusUnavailableForLegalReasons: "Unavailable For Legal Reasons",
|
||||
|
||||
// http.StatusInternalServerError: "Internal Server Error",
|
||||
http.StatusNotImplemented: "Sabre\\DAV\\Exception\\NotImplemented",
|
||||
// http.StatusBadGateway: "Bad Gateway",
|
||||
http.StatusServiceUnavailable: "Sabre\\DAV\\Exception\\ServiceUnavailable",
|
||||
// http.StatusGatewayTimeout: "Gateway Timeout",
|
||||
// http.StatusHTTPVersionNotSupported: "HTTP Version Not Supported",
|
||||
// http.StatusVariantAlsoNegotiates: "Variant Also Negotiates",
|
||||
http.StatusInsufficientStorage: "Sabre\\DAV\\Exception\\InsufficientStorage",
|
||||
// http.StatusLoopDetected: "Loop Detected",
|
||||
// http.StatusNotExtended: "Not Extended",
|
||||
// http.StatusNetworkAuthenticationRequired: "Network Authentication Required",
|
||||
}
|
||||
|
||||
// SabreException returns a sabre exception text for the HTTP status code. It returns the empty
|
||||
// string if the code is unknown.
|
||||
func SabreException(code int) string {
|
||||
return sabreException[code]
|
||||
}
|
||||
|
||||
// Exception represents a ocdav exception
|
||||
type Exception struct {
|
||||
Code int
|
||||
Message string
|
||||
Header string
|
||||
}
|
||||
|
||||
// Marshal just calls the xml marshaller for a given exception.
|
||||
func Marshal(code int, message string, header string, errorCode string) ([]byte, error) {
|
||||
xmlstring, err := xml.Marshal(&ErrorXML{
|
||||
Xmlnsd: "DAV",
|
||||
Xmlnss: "http://sabredav.org/ns",
|
||||
Exception: sabreException[code],
|
||||
Message: message,
|
||||
Header: header,
|
||||
ErrorCode: errorCode,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(xml.Header)
|
||||
buf.Write(xmlstring)
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
|
||||
// ErrorXML holds the xml representation of an error
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_error
|
||||
type ErrorXML struct {
|
||||
XMLName xml.Name `xml:"d:error"`
|
||||
Xmlnsd string `xml:"xmlns:d,attr"`
|
||||
Xmlnss string `xml:"xmlns:s,attr"`
|
||||
Exception string `xml:"s:exception"`
|
||||
Message string `xml:"s:message"`
|
||||
ErrorCode string `xml:"s:errorcode"`
|
||||
InnerXML []byte `xml:",innerxml"`
|
||||
// Header is used to indicate the conflicting request header
|
||||
Header string `xml:"s:header,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrInvalidDepth is an invalid depth header error
|
||||
ErrInvalidDepth = errors.New("webdav: invalid depth")
|
||||
// ErrInvalidPropfind is an invalid propfind error
|
||||
ErrInvalidPropfind = errors.New("webdav: invalid propfind")
|
||||
// ErrInvalidProppatch is an invalid proppatch error
|
||||
ErrInvalidProppatch = errors.New("webdav: invalid proppatch")
|
||||
// ErrInvalidLockInfo is an invalid lock error
|
||||
ErrInvalidLockInfo = errors.New("webdav: invalid lock info")
|
||||
// ErrUnsupportedLockInfo is an unsupported lock error
|
||||
ErrUnsupportedLockInfo = errors.New("webdav: unsupported lock info")
|
||||
// ErrInvalidTimeout is an invalid timeout error
|
||||
ErrInvalidTimeout = errors.New("webdav: invalid timeout")
|
||||
// ErrInvalidIfHeader is an invalid if header error
|
||||
ErrInvalidIfHeader = errors.New("webdav: invalid If header")
|
||||
// ErrUnsupportedMethod is an unsupported method error
|
||||
ErrUnsupportedMethod = errors.New("webdav: unsupported method")
|
||||
// ErrInvalidLockToken is an invalid lock token error
|
||||
ErrInvalidLockToken = errors.New("webdav: invalid lock token")
|
||||
// ErrConfirmationFailed is returned by a LockSystem's Confirm method.
|
||||
ErrConfirmationFailed = errors.New("webdav: confirmation failed")
|
||||
// ErrForbidden is returned by a LockSystem's Unlock method.
|
||||
ErrForbidden = errors.New("webdav: forbidden")
|
||||
// ErrLocked is returned by a LockSystem's Create, Refresh and Unlock methods.
|
||||
ErrLocked = errors.New("webdav: locked")
|
||||
// ErrNoSuchLock is returned by a LockSystem's Refresh and Unlock methods.
|
||||
ErrNoSuchLock = errors.New("webdav: no such lock")
|
||||
// ErrNotImplemented is returned when hitting not implemented code paths
|
||||
ErrNotImplemented = errors.New("webdav: not implemented")
|
||||
// ErrTokenNotFound is returned when a token is not found
|
||||
ErrTokenStatInfoMissing = errors.New("webdav: token stat info missing")
|
||||
)
|
||||
|
||||
// HandleErrorStatus checks the status code, logs a Debug or Error level message
|
||||
// and writes an appropriate http status
|
||||
func HandleErrorStatus(log *zerolog.Logger, w http.ResponseWriter, s *rpc.Status) {
|
||||
hsc := status.HTTPStatusFromCode(s.Code)
|
||||
if s.Code == rpc.Code_CODE_ABORTED {
|
||||
// aborted is used for etag an lock mismatches, which translates to 412
|
||||
// in case a real Conflict response is needed, the calling code needs to send the header
|
||||
hsc = http.StatusPreconditionFailed
|
||||
}
|
||||
if hsc == http.StatusInternalServerError {
|
||||
log.Error().Interface("status", s).Int("code", hsc).Msg(http.StatusText(hsc))
|
||||
} else {
|
||||
log.Debug().Interface("status", s).Int("code", hsc).Msg(http.StatusText(hsc))
|
||||
}
|
||||
w.WriteHeader(hsc)
|
||||
}
|
||||
|
||||
// HandleWebdavError checks the status code, logs an error and creates a webdav response body
|
||||
// if needed
|
||||
func HandleWebdavError(log *zerolog.Logger, w http.ResponseWriter, b []byte, err error) {
|
||||
if err != nil {
|
||||
log.Error().Msgf("error marshaling xml response: %s", b)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_, err = w.Write(b)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("error writing response")
|
||||
}
|
||||
}
|
||||
|
||||
func NewErrFromStatus(s *rpc.Status) error {
|
||||
switch s.GetCode() {
|
||||
case rpc.Code_CODE_OK:
|
||||
return nil
|
||||
case rpc.Code_CODE_DEADLINE_EXCEEDED:
|
||||
return ErrInvalidTimeout
|
||||
case rpc.Code_CODE_PERMISSION_DENIED:
|
||||
return ErrForbidden
|
||||
case rpc.Code_CODE_LOCKED, rpc.Code_CODE_FAILED_PRECONDITION:
|
||||
return ErrLocked
|
||||
case rpc.Code_CODE_UNIMPLEMENTED:
|
||||
return ErrNotImplemented
|
||||
default:
|
||||
return errors.New(s.GetMessage())
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
package ocdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
gatewayv1beta1 "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"
|
||||
)
|
||||
|
||||
// FindName returns the next filename available when the current
|
||||
func FindName(ctx context.Context, client gatewayv1beta1.GatewayAPIClient, name string, parentid *provider.ResourceId) (string, *rpc.Status, error) {
|
||||
lReq := &provider.ListContainerRequest{
|
||||
Ref: &provider.Reference{
|
||||
ResourceId: parentid,
|
||||
},
|
||||
}
|
||||
lRes, err := client.ListContainer(ctx, lReq)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if lRes.Status.Code != rpc.Code_CODE_OK {
|
||||
return "", lRes.Status, nil
|
||||
}
|
||||
// iterate over the listing to determine next suffix
|
||||
var itemMap = make(map[string]struct{})
|
||||
for _, fi := range lRes.Infos {
|
||||
itemMap[fi.GetName()] = struct{}{}
|
||||
}
|
||||
ext := filepath.Ext(name)
|
||||
fileName := strings.TrimSuffix(name, ext)
|
||||
if strings.HasSuffix(fileName, ".tar") {
|
||||
fileName = strings.TrimSuffix(fileName, ".tar")
|
||||
ext = filepath.Ext(fileName) + "." + ext
|
||||
}
|
||||
// starts with two because "normal" humans begin counting with 1 and we say the existing file is the first one
|
||||
for i := 2; i < len(itemMap)+3; i++ {
|
||||
if _, ok := itemMap[fileName+" ("+strconv.Itoa(i)+")"+ext]; !ok {
|
||||
return fileName + " (" + strconv.Itoa(i) + ")" + ext, lRes.GetStatus(), nil
|
||||
}
|
||||
}
|
||||
return "", nil, errors.New("could not determine new filename")
|
||||
}
|
||||
Generated
Vendored
+190
@@ -0,0 +1,190 @@
|
||||
// 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 ocdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/datagateway"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func (s *svc) handlePathGet(w http.ResponseWriter, r *http.Request, ns string) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "get")
|
||||
defer span.End()
|
||||
|
||||
fn := path.Join(ns, r.URL.Path)
|
||||
|
||||
sublog := appctx.GetLogger(ctx).With().Str("path", fn).Str("svc", "ocdav").Str("handler", "get").Logger()
|
||||
|
||||
space, status, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, fn)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Str("path", fn).Msg("failed to look up storage space")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if status.Code != rpc.Code_CODE_OK {
|
||||
errors.HandleErrorStatus(&sublog, w, status)
|
||||
return
|
||||
}
|
||||
|
||||
s.handleGet(ctx, w, r, spacelookup.MakeRelativeReference(space, fn, false), "spaces", sublog)
|
||||
}
|
||||
|
||||
func (s *svc) handleGet(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, dlProtocol string, log zerolog.Logger) {
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error selecting next client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
sReq := &provider.StatRequest{
|
||||
Ref: ref,
|
||||
}
|
||||
sRes, err := client.Stat(ctx, sReq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error stat resource")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
} else if sRes.Status.Code != rpc.Code_CODE_OK {
|
||||
errors.HandleErrorStatus(&log, w, sRes.Status)
|
||||
return
|
||||
}
|
||||
|
||||
if sRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_FILE {
|
||||
w.Header().Set("Content-Length", "0")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
if status := utils.ReadPlainFromOpaque(sRes.GetInfo().GetOpaque(), "status"); status == "processing" {
|
||||
w.WriteHeader(http.StatusTooEarly)
|
||||
return
|
||||
}
|
||||
|
||||
dReq := &provider.InitiateFileDownloadRequest{Ref: ref}
|
||||
dRes, err := client.InitiateFileDownload(ctx, dReq)
|
||||
switch {
|
||||
case err != nil:
|
||||
log.Error().Err(err).Msg("error initiating file download")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
case dRes.Status.Code != rpc.Code_CODE_OK:
|
||||
errors.HandleErrorStatus(&log, w, dRes.Status)
|
||||
return
|
||||
}
|
||||
|
||||
var ep, token string
|
||||
for _, p := range dRes.Protocols {
|
||||
if p.Protocol == dlProtocol {
|
||||
ep, token = p.DownloadEndpoint, p.Token
|
||||
}
|
||||
}
|
||||
|
||||
httpReq, err := rhttp.NewRequest(ctx, http.MethodGet, ep, nil)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error creating http request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
httpReq.Header.Set(datagateway.TokenTransportHeader, token)
|
||||
|
||||
if r.Header.Get(net.HeaderRange) != "" {
|
||||
httpReq.Header.Set(net.HeaderRange, r.Header.Get(net.HeaderRange))
|
||||
}
|
||||
|
||||
httpClient := s.client
|
||||
|
||||
httpRes, err := httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error performing http request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer httpRes.Body.Close()
|
||||
|
||||
// copy only the headers relevant for the content served by the datagateway
|
||||
// more headers are already present from the GET request
|
||||
copyHeader(w.Header(), httpRes.Header, net.HeaderContentType)
|
||||
copyHeader(w.Header(), httpRes.Header, net.HeaderContentLength)
|
||||
copyHeader(w.Header(), httpRes.Header, net.HeaderContentRange)
|
||||
copyHeader(w.Header(), httpRes.Header, net.HeaderOCFileID)
|
||||
copyHeader(w.Header(), httpRes.Header, net.HeaderOCETag)
|
||||
copyHeader(w.Header(), httpRes.Header, net.HeaderOCChecksum)
|
||||
copyHeader(w.Header(), httpRes.Header, net.HeaderETag)
|
||||
copyHeader(w.Header(), httpRes.Header, net.HeaderLastModified)
|
||||
copyHeader(w.Header(), httpRes.Header, net.HeaderAcceptRanges)
|
||||
copyHeader(w.Header(), httpRes.Header, net.HeaderContentDisposistion)
|
||||
|
||||
w.WriteHeader(httpRes.StatusCode)
|
||||
|
||||
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")
|
||||
return
|
||||
}
|
||||
|
||||
var c int64
|
||||
if c, err = io.Copy(w, httpRes.Body); err != nil {
|
||||
log.Error().Err(err).Msg("error finishing copying data to response")
|
||||
}
|
||||
if httpRes.Header.Get(net.HeaderContentLength) != "" {
|
||||
i, err := strconv.ParseInt(httpRes.Header.Get(net.HeaderContentLength), 10, 64)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("content-length", httpRes.Header.Get(net.HeaderContentLength)).Msg("invalid content length in datagateway response")
|
||||
}
|
||||
if i != c {
|
||||
log.Error().Int64("content-length", i).Int64("transferred-bytes", c).Msg("content length vs transferred bytes mismatch")
|
||||
}
|
||||
}
|
||||
// TODO we need to send the If-Match etag in the GET to the datagateway to prevent race conditions between stating and reading the file
|
||||
}
|
||||
|
||||
func copyHeader(dist, src http.Header, header string) {
|
||||
if src.Get(header) != "" {
|
||||
dist.Set(header, src.Get(header))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *svc) handleSpacesGet(w http.ResponseWriter, r *http.Request, spaceID string) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "spaces_get")
|
||||
defer span.End()
|
||||
|
||||
sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Str("spaceid", spaceID).Str("handler", "get").Logger()
|
||||
|
||||
ref, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s.handleGet(ctx, w, r, &ref, "spaces", sublog)
|
||||
}
|
||||
Generated
Vendored
+120
@@ -0,0 +1,120 @@
|
||||
// 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 ocdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
|
||||
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/internal/grpc/services/storageprovider"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func (s *svc) handlePathHead(w http.ResponseWriter, r *http.Request, ns string) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "head")
|
||||
defer span.End()
|
||||
|
||||
fn := path.Join(ns, r.URL.Path)
|
||||
|
||||
sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger()
|
||||
|
||||
space, status, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, fn)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Str("path", fn).Msg("failed to look up storage space")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if status.Code != rpc.Code_CODE_OK {
|
||||
errors.HandleErrorStatus(&sublog, w, status)
|
||||
return
|
||||
}
|
||||
|
||||
s.handleHead(ctx, w, r, spacelookup.MakeRelativeReference(space, fn, false), sublog)
|
||||
}
|
||||
|
||||
func (s *svc) handleHead(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, log zerolog.Logger) {
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error selecting next client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
req := &provider.StatRequest{Ref: ref}
|
||||
res, err := client.Stat(ctx, req)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error sending grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if res.Status.Code != rpc.Code_CODE_OK {
|
||||
errors.HandleErrorStatus(&log, w, res.Status)
|
||||
return
|
||||
}
|
||||
|
||||
info := res.Info
|
||||
w.Header().Set(net.HeaderContentType, info.MimeType)
|
||||
w.Header().Set(net.HeaderETag, info.Etag)
|
||||
w.Header().Set(net.HeaderOCFileID, storagespace.FormatResourceID(info.Id))
|
||||
w.Header().Set(net.HeaderOCETag, info.Etag)
|
||||
if info.Checksum != nil {
|
||||
w.Header().Set(net.HeaderOCChecksum, fmt.Sprintf("%s:%s", strings.ToUpper(string(storageprovider.GRPC2PKGXS(info.Checksum.Type))), info.Checksum.Sum))
|
||||
}
|
||||
t := utils.TSToTime(info.Mtime).UTC()
|
||||
lastModifiedString := t.Format(time.RFC1123Z)
|
||||
w.Header().Set(net.HeaderLastModified, lastModifiedString)
|
||||
w.Header().Set(net.HeaderContentLength, strconv.FormatUint(info.Size, 10))
|
||||
if info.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER {
|
||||
w.Header().Set(net.HeaderAcceptRanges, "bytes")
|
||||
}
|
||||
if utils.ReadPlainFromOpaque(res.GetInfo().GetOpaque(), "status") == "processing" {
|
||||
w.WriteHeader(http.StatusTooEarly)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *svc) handleSpacesHead(w http.ResponseWriter, r *http.Request, spaceID string) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "spaces_head")
|
||||
defer span.End()
|
||||
|
||||
sublog := appctx.GetLogger(ctx).With().Str("spaceid", spaceID).Str("path", r.URL.Path).Logger()
|
||||
|
||||
ref, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s.handleHead(ctx, w, r, &ref, sublog)
|
||||
}
|
||||
Generated
Vendored
+193
@@ -0,0 +1,193 @@
|
||||
// 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.
|
||||
|
||||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ocdav
|
||||
|
||||
// copy of https://github.com/golang/net/blob/master/webdav/if.go
|
||||
|
||||
// The If header is covered by Section 10.4.
|
||||
// http://www.webdav.org/specs/rfc4918.html#HEADER_If
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ifHeader is a disjunction (OR) of ifLists.
|
||||
type ifHeader struct {
|
||||
lists []ifList
|
||||
}
|
||||
|
||||
// ifList is a conjunction (AND) of Conditions, and an optional resource tag.
|
||||
type ifList struct {
|
||||
resourceTag string
|
||||
conditions []Condition
|
||||
}
|
||||
|
||||
// parseIfHeader parses the "If: foo bar" HTTP header. The httpHeader string
|
||||
// should omit the "If:" prefix and have any "\r\n"s collapsed to a " ", as is
|
||||
// returned by req.Header.Get("If") for a http.Request req.
|
||||
func parseIfHeader(httpHeader string) (h ifHeader, ok bool) {
|
||||
s := strings.TrimSpace(httpHeader)
|
||||
switch tokenType, _, _ := lex(s); tokenType {
|
||||
case '(':
|
||||
return parseNoTagLists(s)
|
||||
case angleTokenType:
|
||||
return parseTaggedLists(s)
|
||||
default:
|
||||
return ifHeader{}, false
|
||||
}
|
||||
}
|
||||
|
||||
func parseNoTagLists(s string) (h ifHeader, ok bool) {
|
||||
for {
|
||||
l, remaining, ok := parseList(s)
|
||||
if !ok {
|
||||
return ifHeader{}, false
|
||||
}
|
||||
h.lists = append(h.lists, l)
|
||||
if remaining == "" {
|
||||
return h, true
|
||||
}
|
||||
s = remaining
|
||||
}
|
||||
}
|
||||
|
||||
func parseTaggedLists(s string) (h ifHeader, ok bool) {
|
||||
resourceTag, n := "", 0
|
||||
for first := true; ; first = false {
|
||||
tokenType, tokenStr, remaining := lex(s)
|
||||
switch tokenType {
|
||||
case angleTokenType:
|
||||
if !first && n == 0 {
|
||||
return ifHeader{}, false
|
||||
}
|
||||
resourceTag, n = tokenStr, 0
|
||||
s = remaining
|
||||
case '(':
|
||||
n++
|
||||
l, remaining, ok := parseList(s)
|
||||
if !ok {
|
||||
return ifHeader{}, false
|
||||
}
|
||||
l.resourceTag = resourceTag
|
||||
h.lists = append(h.lists, l)
|
||||
if remaining == "" {
|
||||
return h, true
|
||||
}
|
||||
s = remaining
|
||||
default:
|
||||
return ifHeader{}, false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseList(s string) (l ifList, remaining string, ok bool) {
|
||||
tokenType, _, s := lex(s)
|
||||
if tokenType != '(' {
|
||||
return ifList{}, "", false
|
||||
}
|
||||
for {
|
||||
tokenType, _, remaining = lex(s)
|
||||
if tokenType == ')' {
|
||||
if len(l.conditions) == 0 {
|
||||
return ifList{}, "", false
|
||||
}
|
||||
return l, remaining, true
|
||||
}
|
||||
c, remaining, ok := parseCondition(s)
|
||||
if !ok {
|
||||
return ifList{}, "", false
|
||||
}
|
||||
l.conditions = append(l.conditions, c)
|
||||
s = remaining
|
||||
}
|
||||
}
|
||||
|
||||
func parseCondition(s string) (c Condition, remaining string, ok bool) {
|
||||
tokenType, tokenStr, s := lex(s)
|
||||
if tokenType == notTokenType {
|
||||
c.Not = true
|
||||
tokenType, tokenStr, s = lex(s)
|
||||
}
|
||||
switch tokenType {
|
||||
case strTokenType, angleTokenType:
|
||||
c.Token = tokenStr
|
||||
case squareTokenType:
|
||||
c.ETag = tokenStr
|
||||
default:
|
||||
return Condition{}, "", false
|
||||
}
|
||||
return c, s, true
|
||||
}
|
||||
|
||||
// Single-rune tokens like '(' or ')' have a token type equal to their rune.
|
||||
// All other tokens have a negative token type.
|
||||
const (
|
||||
errTokenType = rune(-1)
|
||||
eofTokenType = rune(-2)
|
||||
strTokenType = rune(-3)
|
||||
notTokenType = rune(-4)
|
||||
angleTokenType = rune(-5)
|
||||
squareTokenType = rune(-6)
|
||||
)
|
||||
|
||||
func lex(s string) (tokenType rune, tokenStr string, remaining string) {
|
||||
// The net/textproto Reader that parses the HTTP header will collapse
|
||||
// Linear White Space that spans multiple "\r\n" lines to a single " ",
|
||||
// so we don't need to look for '\r' or '\n'.
|
||||
for len(s) > 0 && (s[0] == '\t' || s[0] == ' ') {
|
||||
s = s[1:]
|
||||
}
|
||||
if len(s) == 0 {
|
||||
return eofTokenType, "", ""
|
||||
}
|
||||
i := 0
|
||||
loop:
|
||||
for ; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '\t', ' ', '(', ')', '<', '>', '[', ']':
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
if i != 0 {
|
||||
tokenStr, remaining = s[:i], s[i:]
|
||||
if tokenStr == "Not" {
|
||||
return notTokenType, "", remaining
|
||||
}
|
||||
return strTokenType, tokenStr, remaining
|
||||
}
|
||||
|
||||
j := 0
|
||||
switch s[0] {
|
||||
case '<':
|
||||
j, tokenType = strings.IndexByte(s, '>'), angleTokenType
|
||||
case '[':
|
||||
j, tokenType = strings.IndexByte(s, ']'), squareTokenType
|
||||
default:
|
||||
return rune(s[0]), "", s[1:]
|
||||
}
|
||||
if j < 0 {
|
||||
return errTokenType, "", ""
|
||||
}
|
||||
return tokenType, s[1:j], s[j+1:]
|
||||
}
|
||||
Generated
Vendored
+699
@@ -0,0 +1,699 @@
|
||||
// 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 ocdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
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"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/google/uuid"
|
||||
ocdavErrors "github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/prop"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
// Most of this is taken from https://github.com/golang/net/blob/master/webdav/lock.go
|
||||
|
||||
// From RFC4918 http://www.webdav.org/specs/rfc4918.html#lock-tokens
|
||||
// This specification encourages servers to create Universally Unique Identifiers (UUIDs) for lock tokens,
|
||||
// and to use the URI form defined by "A Universally Unique Identifier (UUID) URN Namespace" ([RFC4122]).
|
||||
// However, servers are free to use any URI (e.g., from another scheme) so long as it meets the uniqueness
|
||||
// requirements. For example, a valid lock token might be constructed using the "opaquelocktoken" scheme
|
||||
// defined in Appendix C.
|
||||
//
|
||||
// Example: "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6"
|
||||
//
|
||||
// we stick to the recommendation and use the URN Namespace
|
||||
const lockTokenPrefix = "urn:uuid:"
|
||||
|
||||
// TODO(jfd) implement lock
|
||||
// see Web Distributed Authoring and Versioning (WebDAV) Locking Protocol:
|
||||
// https://www.greenbytes.de/tech/webdav/draft-reschke-webdav-locking-latest.html
|
||||
// Webdav supports a Depth: infinity lock, wopi only needs locks on files
|
||||
|
||||
// https://www.greenbytes.de/tech/webdav/draft-reschke-webdav-locking-latest.html#write.locks.and.the.if.request.header
|
||||
// [...] a lock token MUST be submitted in the If header for all locked resources
|
||||
// that a method may interact with or the method MUST fail. [...]
|
||||
/*
|
||||
COPY /~fielding/index.html HTTP/1.1
|
||||
Host: example.com
|
||||
Destination: http://example.com/users/f/fielding/index.html
|
||||
If: <http://example.com/users/f/fielding/index.html>
|
||||
(<opaquelocktoken:f81d4fae-7dec-11d0-a765-00a0c91e6bf6>)
|
||||
*/
|
||||
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_lockinfo
|
||||
type lockInfo struct {
|
||||
XMLName xml.Name `xml:"lockinfo"`
|
||||
Exclusive *struct{} `xml:"lockscope>exclusive"`
|
||||
Shared *struct{} `xml:"lockscope>shared"`
|
||||
Write *struct{} `xml:"locktype>write"`
|
||||
Owner owner `xml:"owner"`
|
||||
LockID string `xml:"locktoken>href"`
|
||||
}
|
||||
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_owner
|
||||
type owner struct {
|
||||
InnerXML string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// Condition can match a WebDAV resource, based on a token or ETag.
|
||||
// Exactly one of Token and ETag should be non-empty.
|
||||
type Condition struct {
|
||||
Not bool
|
||||
Token string
|
||||
ETag string
|
||||
}
|
||||
|
||||
// LockSystem manages access to a collection of named resources. The elements
|
||||
// in a lock name are separated by slash ('/', U+002F) characters, regardless
|
||||
// of host operating system convention.
|
||||
type LockSystem interface {
|
||||
// Confirm confirms that the caller can claim all of the locks specified by
|
||||
// the given conditions, and that holding the union of all of those locks
|
||||
// gives exclusive access to all of the named resources. Up to two resources
|
||||
// can be named. Empty names are ignored.
|
||||
//
|
||||
// Exactly one of release and err will be non-nil. If release is non-nil,
|
||||
// all of the requested locks are held until release is called. Calling
|
||||
// release does not unlock the lock, in the WebDAV UNLOCK sense, but once
|
||||
// Confirm has confirmed that a lock claim is valid, that lock cannot be
|
||||
// Confirmed again until it has been released.
|
||||
//
|
||||
// If Confirm returns ErrConfirmationFailed then the Handler will continue
|
||||
// to try any other set of locks presented (a WebDAV HTTP request can
|
||||
// present more than one set of locks). If it returns any other non-nil
|
||||
// error, the Handler will write a "500 Internal Server Error" HTTP status.
|
||||
Confirm(ctx context.Context, now time.Time, name0, name1 string, conditions ...Condition) (release func(), err error)
|
||||
|
||||
// Create creates a lock with the given depth, duration, owner and root
|
||||
// (name). The depth will either be negative (meaning infinite) or zero.
|
||||
//
|
||||
// If Create returns ErrLocked then the Handler will write a "423 Locked"
|
||||
// HTTP status. If it returns any other non-nil error, the Handler will
|
||||
// write a "500 Internal Server Error" HTTP status.
|
||||
//
|
||||
// See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10.6 for
|
||||
// when to use each error.
|
||||
//
|
||||
// The token returned identifies the created lock. It should be an absolute
|
||||
// URI as defined by RFC 3986, Section 4.3. In particular, it should not
|
||||
// contain whitespace.
|
||||
Create(ctx context.Context, now time.Time, details LockDetails) (token string, err error)
|
||||
|
||||
// Refresh refreshes the lock with the given token.
|
||||
//
|
||||
// If Refresh returns ErrLocked then the Handler will write a "423 Locked"
|
||||
// HTTP Status. If Refresh returns ErrNoSuchLock then the Handler will write
|
||||
// a "412 Precondition Failed" HTTP Status. If it returns any other non-nil
|
||||
// error, the Handler will write a "500 Internal Server Error" HTTP status.
|
||||
//
|
||||
// See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10.6 for
|
||||
// when to use each error.
|
||||
Refresh(ctx context.Context, now time.Time, ref *provider.Reference, token string) error
|
||||
|
||||
// Unlock unlocks the lock with the given token.
|
||||
//
|
||||
// If Unlock returns ErrForbidden then the Handler will write a "403
|
||||
// Forbidden" HTTP Status. If Unlock returns ErrLocked then the Handler
|
||||
// will write a "423 Locked" HTTP status. If Unlock returns ErrNoSuchLock
|
||||
// then the Handler will write a "409 Conflict" HTTP Status. If it returns
|
||||
// any other non-nil error, the Handler will write a "500 Internal Server
|
||||
// Error" HTTP status.
|
||||
//
|
||||
// See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.11.1 for
|
||||
// when to use each error.
|
||||
Unlock(ctx context.Context, now time.Time, ref *provider.Reference, token string) error
|
||||
}
|
||||
|
||||
// NewCS3LS returns a new CS3 based LockSystem.
|
||||
func NewCS3LS(s pool.Selectable[gateway.GatewayAPIClient]) LockSystem {
|
||||
return &cs3LS{
|
||||
selector: s,
|
||||
}
|
||||
}
|
||||
|
||||
type cs3LS struct {
|
||||
selector pool.Selectable[gateway.GatewayAPIClient]
|
||||
}
|
||||
|
||||
func (cls *cs3LS) Confirm(ctx context.Context, now time.Time, name0, name1 string, conditions ...Condition) (func(), error) {
|
||||
return nil, ocdavErrors.ErrNotImplemented
|
||||
}
|
||||
|
||||
func (cls *cs3LS) Create(ctx context.Context, now time.Time, details LockDetails) (string, error) {
|
||||
// always assume depth infinity?
|
||||
/*
|
||||
if !details.ZeroDepth {
|
||||
The CS3 Lock api currently has no depth property, it only locks single resources
|
||||
return "", ocdavErrors.ErrUnsupportedLockInfo
|
||||
}
|
||||
*/
|
||||
|
||||
u := ctxpkg.ContextMustGetUser(ctx)
|
||||
|
||||
// add metadata via opaque
|
||||
// TODO: upate cs3api: https://github.com/cs3org/cs3apis/issues/213
|
||||
o := utils.AppendPlainToOpaque(nil, "lockownername", u.GetDisplayName())
|
||||
o = utils.AppendPlainToOpaque(o, "locktime", now.Format(time.RFC3339))
|
||||
|
||||
lockid := details.LockID
|
||||
if lockid == "" {
|
||||
// Having a lock token provides no special access rights. Anyone can find out anyone
|
||||
// else's lock token by performing lock discovery. Locks must be enforced based upon
|
||||
// whatever authentication mechanism is used by the server, not based on the secrecy
|
||||
// of the token values.
|
||||
// see: http://www.webdav.org/specs/rfc2518.html#n-lock-tokens
|
||||
token := uuid.New()
|
||||
|
||||
lockid = lockTokenPrefix + token.String()
|
||||
}
|
||||
r := &provider.SetLockRequest{
|
||||
Ref: details.Root,
|
||||
Lock: &provider.Lock{
|
||||
Opaque: o,
|
||||
Type: provider.LockType_LOCK_TYPE_EXCL,
|
||||
User: details.UserID, // no way to set an app lock? TODO maybe via the ownerxml
|
||||
//AppName: , // TODO use a urn scheme?
|
||||
LockId: lockid,
|
||||
},
|
||||
}
|
||||
if details.Duration > 0 {
|
||||
expiration := time.Now().UTC().Add(details.Duration)
|
||||
r.Lock.Expiration = &types.Timestamp{
|
||||
Seconds: uint64(expiration.Unix()),
|
||||
Nanos: uint32(expiration.Nanosecond()),
|
||||
}
|
||||
}
|
||||
|
||||
client, err := cls.selector.Next()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
res, err := client.SetLock(ctx, r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch res.GetStatus().GetCode() {
|
||||
case rpc.Code_CODE_OK:
|
||||
return lockid, nil
|
||||
default:
|
||||
return "", ocdavErrors.NewErrFromStatus(res.GetStatus())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (cls *cs3LS) Refresh(ctx context.Context, now time.Time, ref *provider.Reference, token string) error {
|
||||
u := ctxpkg.ContextMustGetUser(ctx)
|
||||
|
||||
// add metadata via opaque
|
||||
// TODO: upate cs3api: https://github.com/cs3org/cs3apis/issues/213
|
||||
o := utils.AppendPlainToOpaque(nil, "lockownername", u.GetDisplayName())
|
||||
o = utils.AppendPlainToOpaque(o, "locktime", now.Format(time.RFC3339))
|
||||
|
||||
if token == "" {
|
||||
return errors.New("token is empty")
|
||||
}
|
||||
|
||||
r := &provider.RefreshLockRequest{
|
||||
Ref: ref,
|
||||
Lock: &provider.Lock{
|
||||
Opaque: o,
|
||||
Type: provider.LockType_LOCK_TYPE_EXCL,
|
||||
//AppName: , // TODO use a urn scheme?
|
||||
LockId: token,
|
||||
User: u.GetId(),
|
||||
},
|
||||
}
|
||||
|
||||
client, err := cls.selector.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := client.RefreshLock(ctx, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch res.GetStatus().GetCode() {
|
||||
case rpc.Code_CODE_OK:
|
||||
return nil
|
||||
|
||||
default:
|
||||
return ocdavErrors.NewErrFromStatus(res.GetStatus())
|
||||
}
|
||||
}
|
||||
|
||||
func (cls *cs3LS) Unlock(ctx context.Context, now time.Time, ref *provider.Reference, token string) error {
|
||||
u := ctxpkg.ContextMustGetUser(ctx)
|
||||
|
||||
r := &provider.UnlockRequest{
|
||||
Ref: ref,
|
||||
Lock: &provider.Lock{
|
||||
LockId: token, // can be a token or a Coded-URL
|
||||
User: u.Id,
|
||||
},
|
||||
}
|
||||
|
||||
client, err := cls.selector.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := client.Unlock(ctx, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newErr := ocdavErrors.NewErrFromStatus(res.GetStatus())
|
||||
if newErr != nil {
|
||||
appctx.GetLogger(ctx).Error().Str("token", token).Interface("unlock", ref).Msg("could not unlock " + res.GetStatus().GetMessage())
|
||||
}
|
||||
return newErr
|
||||
}
|
||||
|
||||
// LockDetails are a lock's metadata.
|
||||
type LockDetails struct {
|
||||
// Root is the root resource name being locked. For a zero-depth lock, the
|
||||
// root is the only resource being locked.
|
||||
Root *provider.Reference
|
||||
// Duration is the lock timeout. A negative duration means infinite.
|
||||
Duration time.Duration
|
||||
// OwnerXML is the verbatim <owner> XML given in a LOCK HTTP request.
|
||||
//
|
||||
// TODO: does the "verbatim" nature play well with XML namespaces?
|
||||
// Does the OwnerXML field need to have more structure? See
|
||||
// https://codereview.appspot.com/175140043/#msg2
|
||||
OwnerXML string
|
||||
UserID *userpb.UserId
|
||||
// ZeroDepth is whether the lock has zero depth. If it does not have zero
|
||||
// depth, it has infinite depth.
|
||||
ZeroDepth bool
|
||||
// OwnerName is the name of the lock owner
|
||||
OwnerName string
|
||||
// Locktime is the time the lock was created
|
||||
Locktime time.Time
|
||||
// LockID is the lock token
|
||||
LockID string
|
||||
}
|
||||
|
||||
func readLockInfo(r io.Reader) (li lockInfo, status int, err error) {
|
||||
c := &countingReader{r: r}
|
||||
if err = xml.NewDecoder(c).Decode(&li); err != nil {
|
||||
if err == io.EOF {
|
||||
if c.n == 0 {
|
||||
// An empty body means to refresh the lock.
|
||||
// http://www.webdav.org/specs/rfc4918.html#refreshing-locks
|
||||
return lockInfo{}, 0, nil
|
||||
}
|
||||
err = ocdavErrors.ErrInvalidLockInfo
|
||||
}
|
||||
return lockInfo{}, http.StatusBadRequest, err
|
||||
}
|
||||
// We only support exclusive (non-shared) write locks. In practice, these are
|
||||
// the only types of locks that seem to matter.
|
||||
// We are ignoring the any properties in the lock details, and assume an exclusive write lock is requested.
|
||||
// https://datatracker.ietf.org/doc/html/rfc4918#section-7 only describes write locks
|
||||
//
|
||||
// if li.Exclusive == nil || li.Shared != nil {
|
||||
// return lockInfo{}, http.StatusNotImplemented, errors.ErrUnsupportedLockInfo
|
||||
// }
|
||||
// what should we return if the user requests a shared lock? or leaves out the locktype? the testsuite will only send the property lockscope, not locktype
|
||||
// the oc tests cover both shared and exclusive locks. What is the WOPI lock? a shared or an exclusive lock?
|
||||
// since it is issued by a service it seems to be an exclusive lock.
|
||||
// the owner could be a link to the collaborative app ... to join the session
|
||||
return li, 0, nil
|
||||
}
|
||||
|
||||
type countingReader struct {
|
||||
n int
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
func (c *countingReader) Read(p []byte) (int, error) {
|
||||
n, err := c.r.Read(p)
|
||||
c.n += n
|
||||
return n, err
|
||||
}
|
||||
|
||||
const infiniteTimeout = -1
|
||||
|
||||
// parseTimeout parses the Timeout HTTP header, as per section 10.7. If s is
|
||||
// empty, an infiniteTimeout is returned.
|
||||
func parseTimeout(s string) (time.Duration, error) {
|
||||
if s == "" {
|
||||
return infiniteTimeout, nil
|
||||
}
|
||||
if i := strings.IndexByte(s, ','); i >= 0 {
|
||||
s = s[:i]
|
||||
}
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "Infinite" {
|
||||
return infiniteTimeout, nil
|
||||
}
|
||||
const pre = "Second-"
|
||||
if !strings.HasPrefix(s, pre) {
|
||||
return 0, ocdavErrors.ErrInvalidTimeout
|
||||
}
|
||||
s = s[len(pre):]
|
||||
if s == "" || s[0] < '0' || '9' < s[0] {
|
||||
return 0, ocdavErrors.ErrInvalidTimeout
|
||||
}
|
||||
n, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil || 1<<32-1 < n {
|
||||
return 0, ocdavErrors.ErrInvalidTimeout
|
||||
}
|
||||
return time.Duration(n) * time.Second, nil
|
||||
}
|
||||
|
||||
const (
|
||||
infiniteDepth = -1
|
||||
invalidDepth = -2
|
||||
)
|
||||
|
||||
// parseDepth maps the strings "0", "1" and "infinity" to 0, 1 and
|
||||
// infiniteDepth. Parsing any other string returns invalidDepth.
|
||||
//
|
||||
// Different WebDAV methods have further constraints on valid depths:
|
||||
// - PROPFIND has no further restrictions, as per section 9.1.
|
||||
// - COPY accepts only "0" or "infinity", as per section 9.8.3.
|
||||
// - MOVE accepts only "infinity", as per section 9.9.2.
|
||||
// - LOCK accepts only "0" or "infinity", as per section 9.10.3.
|
||||
//
|
||||
// These constraints are enforced by the handleXxx methods.
|
||||
func parseDepth(s string) int {
|
||||
switch s {
|
||||
case "0":
|
||||
return 0
|
||||
case "1":
|
||||
return 1
|
||||
case "infinity":
|
||||
return infiniteDepth
|
||||
}
|
||||
return invalidDepth
|
||||
}
|
||||
|
||||
/*
|
||||
the oc 10 wopi app code locks like this:
|
||||
|
||||
$storage->lockNodePersistent($file->getInternalPath(), [
|
||||
'token' => $wopiLock,
|
||||
'owner' => "{$user->getDisplayName()} via Office Online"
|
||||
]);
|
||||
|
||||
if owner is empty it defaults to '{displayname} ({email})', which is not a url ... but ... shrug
|
||||
|
||||
The LockManager also defaults to exclusive locks:
|
||||
|
||||
$scope = ILock::LOCK_SCOPE_EXCLUSIVE;
|
||||
if (isset($lockInfo['scope'])) {
|
||||
$scope = $lockInfo['scope'];
|
||||
}
|
||||
*/
|
||||
func (s *svc) handleLock(w http.ResponseWriter, r *http.Request, ns string) (retStatus int, retErr error) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), fmt.Sprintf("%s %v", r.Method, r.URL.Path))
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(attribute.String("component", "ocdav"))
|
||||
|
||||
fn := path.Join(ns, r.URL.Path) // TODO do we still need to jail if we query the registry about the spaces?
|
||||
|
||||
// TODO instead of using a string namespace ns pass in the space with the request?
|
||||
ref, cs3Status, err := spacelookup.LookupReferenceForPath(ctx, s.gatewaySelector, fn)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
if cs3Status.Code != rpc.Code_CODE_OK {
|
||||
return http.StatusInternalServerError, ocdavErrors.NewErrFromStatus(cs3Status)
|
||||
}
|
||||
|
||||
return s.lockReference(ctx, w, r, ref)
|
||||
}
|
||||
|
||||
func (s *svc) handleSpacesLock(w http.ResponseWriter, r *http.Request, spaceID string) (retStatus int, retErr error) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), fmt.Sprintf("%s %v", r.Method, r.URL.Path))
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(attribute.String("component", "ocdav"))
|
||||
|
||||
ref, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid space id")
|
||||
}
|
||||
|
||||
return s.lockReference(ctx, w, r, &ref)
|
||||
}
|
||||
|
||||
func (s *svc) lockReference(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference) (retStatus int, retErr error) {
|
||||
sublog := appctx.GetLogger(ctx).With().Interface("ref", ref).Logger()
|
||||
duration, err := parseTimeout(r.Header.Get(net.HeaderTimeout))
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, ocdavErrors.ErrInvalidTimeout
|
||||
}
|
||||
|
||||
li, status, err := readLockInfo(r.Body)
|
||||
if err != nil {
|
||||
return status, ocdavErrors.ErrInvalidLockInfo
|
||||
}
|
||||
|
||||
u := ctxpkg.ContextMustGetUser(ctx)
|
||||
token, now, created := "", time.Now(), false
|
||||
ld := LockDetails{UserID: u.Id, Root: ref, Duration: duration, OwnerName: u.GetDisplayName(), Locktime: now, LockID: li.LockID}
|
||||
if li == (lockInfo{}) {
|
||||
// An empty lockInfo means to refresh the lock.
|
||||
ih, ok := parseIfHeader(r.Header.Get(net.HeaderIf))
|
||||
if !ok {
|
||||
return http.StatusBadRequest, ocdavErrors.ErrInvalidIfHeader
|
||||
}
|
||||
if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 {
|
||||
token = ih.lists[0].conditions[0].Token
|
||||
}
|
||||
if token == "" {
|
||||
return http.StatusBadRequest, ocdavErrors.ErrInvalidLockToken
|
||||
}
|
||||
err = s.LockSystem.Refresh(ctx, now, ref, token)
|
||||
if err != nil {
|
||||
if err == ocdavErrors.ErrNoSuchLock {
|
||||
return http.StatusPreconditionFailed, err
|
||||
}
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
ld.LockID = token
|
||||
|
||||
} else {
|
||||
// Section 9.10.3 says that "If no Depth header is submitted on a LOCK request,
|
||||
// then the request MUST act as if a "Depth:infinity" had been submitted."
|
||||
depth := infiniteDepth
|
||||
if hdr := r.Header.Get(net.HeaderDepth); hdr != "" {
|
||||
depth = parseDepth(hdr)
|
||||
if depth != 0 && depth != infiniteDepth {
|
||||
// Section 9.10.3 says that "Values other than 0 or infinity must not be
|
||||
// used with the Depth header on a LOCK method".
|
||||
return http.StatusBadRequest, ocdavErrors.ErrInvalidDepth
|
||||
}
|
||||
}
|
||||
/* our url path has been shifted, so we don't need to do this?
|
||||
reqPath, status, err := h.stripPrefix(r.URL.Path)
|
||||
if err != nil {
|
||||
return status, err
|
||||
}
|
||||
*/
|
||||
// TODO look up username and email
|
||||
// if li.Owner.InnerXML == "" {
|
||||
// // PHP version: 'owner' => "{$user->getDisplayName()} via Office Online"
|
||||
// ld.OwnerXML = ld.UserID.OpaqueId
|
||||
// }
|
||||
ld.OwnerXML = li.Owner.InnerXML // TODO optional, should be a URL
|
||||
ld.ZeroDepth = depth == 0
|
||||
|
||||
//TODO: @jfd the code tries to create a lock for a file that may not even exist,
|
||||
// should we do that in the decomposedfs as well? the node does not exist
|
||||
// this actually is a name based lock ... ugh
|
||||
token, err = s.LockSystem.Create(ctx, now, ld)
|
||||
|
||||
//
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, ocdavErrors.ErrLocked):
|
||||
return http.StatusLocked, err
|
||||
case errors.Is(err, ocdavErrors.ErrForbidden):
|
||||
return http.StatusForbidden, err
|
||||
default:
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
if err := s.LockSystem.Unlock(ctx, now, ref, token); err != nil {
|
||||
appctx.GetLogger(ctx).Error().Err(err).Interface("lock", ld).Msg("could not unlock after failed lock")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Create the resource if it didn't previously exist.
|
||||
// TODO use sdk to stat?
|
||||
/*
|
||||
if _, err := s.FileSystem.Stat(ctx, reqPath); err != nil {
|
||||
f, err := s.FileSystem.OpenFile(ctx, reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
// TODO: detect missing intermediate dirs and return http.StatusConflict?
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
f.Close()
|
||||
created = true
|
||||
}
|
||||
*/
|
||||
// http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the
|
||||
// Lock-Token value is a Coded-URL. We add angle brackets.
|
||||
w.Header().Set("Lock-Token", "<"+token+">")
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||
if created {
|
||||
// This is "w.WriteHeader(http.StatusCreated)" and not "return
|
||||
// http.StatusCreated, nil" because we write our own (XML) response to w
|
||||
// and Handler.ServeHTTP would otherwise write "Created".
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
n, err := writeLockInfo(w, token, ld)
|
||||
if err != nil {
|
||||
sublog.Err(err).Int("bytes_written", n).Msg("error writing response")
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func writeLockInfo(w io.Writer, token string, ld LockDetails) (int, error) {
|
||||
depth := "infinity"
|
||||
if ld.ZeroDepth {
|
||||
depth = "0"
|
||||
}
|
||||
href := ld.Root.Path // FIXME add base url and space?
|
||||
|
||||
lockdiscovery := strings.Builder{}
|
||||
lockdiscovery.WriteString(xml.Header)
|
||||
lockdiscovery.WriteString("<d:prop xmlns:d=\"DAV:\" xmlns:oc=\"http://owncloud.org/ns\"><d:lockdiscovery><d:activelock>\n")
|
||||
lockdiscovery.WriteString(" <d:locktype><d:write/></d:locktype>\n")
|
||||
lockdiscovery.WriteString(" <d:lockscope><d:exclusive/></d:lockscope>\n")
|
||||
lockdiscovery.WriteString(fmt.Sprintf(" <d:depth>%s</d:depth>\n", depth))
|
||||
if ld.OwnerXML != "" {
|
||||
lockdiscovery.WriteString(fmt.Sprintf(" <d:owner>%s</d:owner>\n", ld.OwnerXML))
|
||||
}
|
||||
if ld.Duration > 0 {
|
||||
timeout := ld.Duration / time.Second
|
||||
lockdiscovery.WriteString(fmt.Sprintf(" <d:timeout>Second-%d</d:timeout>\n", timeout))
|
||||
} else {
|
||||
lockdiscovery.WriteString(" <d:timeout>Infinite</d:timeout>\n")
|
||||
}
|
||||
if token != "" {
|
||||
lockdiscovery.WriteString(fmt.Sprintf(" <d:locktoken><d:href>%s</d:href></d:locktoken>\n", prop.Escape(token)))
|
||||
}
|
||||
if href != "" {
|
||||
lockdiscovery.WriteString(fmt.Sprintf(" <d:lockroot><d:href>%s</d:href></d:lockroot>\n", prop.Escape(href)))
|
||||
}
|
||||
if ld.OwnerName != "" {
|
||||
lockdiscovery.WriteString(fmt.Sprintf(" <oc:ownername>%s</oc:ownername>\n", prop.Escape(ld.OwnerName)))
|
||||
}
|
||||
if !ld.Locktime.IsZero() {
|
||||
lockdiscovery.WriteString(fmt.Sprintf(" <oc:locktime>%s</oc:locktime>\n", prop.Escape(ld.Locktime.Format(time.RFC3339))))
|
||||
}
|
||||
|
||||
lockdiscovery.WriteString("</d:activelock></d:lockdiscovery></d:prop>")
|
||||
|
||||
return fmt.Fprint(w, lockdiscovery.String())
|
||||
}
|
||||
|
||||
func (s *svc) handleUnlock(w http.ResponseWriter, r *http.Request, ns string) (status int, err error) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), fmt.Sprintf("%s %v", r.Method, r.URL.Path))
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(attribute.String("component", "ocdav"))
|
||||
|
||||
fn := path.Join(ns, r.URL.Path) // TODO do we still need to jail if we query the registry about the spaces?
|
||||
|
||||
// TODO instead of using a string namespace ns pass in the space with the request?
|
||||
ref, cs3Status, err := spacelookup.LookupReferenceForPath(ctx, s.gatewaySelector, fn)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
if cs3Status.Code != rpc.Code_CODE_OK {
|
||||
return http.StatusInternalServerError, ocdavErrors.NewErrFromStatus(cs3Status)
|
||||
}
|
||||
|
||||
return s.unlockReference(ctx, w, r, ref)
|
||||
}
|
||||
|
||||
func (s *svc) handleSpaceUnlock(w http.ResponseWriter, r *http.Request, spaceID string) (status int, err error) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), fmt.Sprintf("%s %v", r.Method, r.URL.Path))
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(attribute.String("component", "ocdav"))
|
||||
|
||||
ref, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid space id")
|
||||
}
|
||||
|
||||
return s.unlockReference(ctx, w, r, &ref)
|
||||
}
|
||||
|
||||
func (s *svc) unlockReference(ctx context.Context, _ http.ResponseWriter, r *http.Request, ref *provider.Reference) (retStatus int, retErr error) {
|
||||
// http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the
|
||||
// Lock-Token value should be a Coded-URL OR a token. We strip its angle brackets.
|
||||
t := r.Header.Get(net.HeaderLockToken)
|
||||
if len(t) > 2 && t[0] == '<' && t[len(t)-1] == '>' {
|
||||
t = t[1 : len(t)-1]
|
||||
}
|
||||
|
||||
err := s.LockSystem.Unlock(ctx, time.Now(), ref, t)
|
||||
switch {
|
||||
case err == nil:
|
||||
return http.StatusNoContent, nil
|
||||
case errors.Is(err, ocdavErrors.ErrLocked):
|
||||
return http.StatusLocked, err
|
||||
case errors.Is(err, ocdavErrors.ErrForbidden):
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
func requestLockToken(r *http.Request) string {
|
||||
return strings.TrimSuffix(strings.TrimPrefix(r.Header.Get(net.HeaderLockToken), "<"), ">")
|
||||
}
|
||||
Generated
Vendored
+245
@@ -0,0 +1,245 @@
|
||||
// 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 ocdav
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/config"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/prop"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
|
||||
)
|
||||
|
||||
// MetaHandler handles meta requests
|
||||
type MetaHandler struct {
|
||||
VersionsHandler *VersionsHandler
|
||||
}
|
||||
|
||||
func (h *MetaHandler) init(c *config.Config) error {
|
||||
h.VersionsHandler = new(VersionsHandler)
|
||||
return h.VersionsHandler.init(c)
|
||||
}
|
||||
|
||||
// Handler handles requests
|
||||
func (h *MetaHandler) Handler(s *svc) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var id string
|
||||
id, r.URL.Path = router.ShiftPath(r.URL.Path)
|
||||
if id == "" {
|
||||
if r.Method != MethodPropfind {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
h.handleEmptyID(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
did, err := storagespace.ParseID(id)
|
||||
if err != nil {
|
||||
logger := appctx.GetLogger(r.Context())
|
||||
logger.Debug().Str("prop", net.PropOcMetaPathForUser).Msg("invalid resource id")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
m := fmt.Sprintf("Invalid resource id %v", id)
|
||||
b, err := errors.Marshal(http.StatusBadRequest, m, "", "")
|
||||
errors.HandleWebdavError(logger, w, b, err)
|
||||
return
|
||||
}
|
||||
if did.StorageId == "" && did.OpaqueId == "" && strings.Count(id, ":") >= 2 {
|
||||
logger := appctx.GetLogger(r.Context())
|
||||
logger.Warn().Str("id", id).Msg("detected invalid : separated resourceid id, trying to split it ... but fix the client that made the request")
|
||||
// try splitting with :
|
||||
parts := strings.SplitN(id, ":", 3)
|
||||
did.StorageId = parts[0]
|
||||
did.SpaceId = parts[1]
|
||||
did.OpaqueId = parts[2]
|
||||
}
|
||||
|
||||
var head string
|
||||
head, r.URL.Path = router.ShiftPath(r.URL.Path)
|
||||
switch head {
|
||||
case "":
|
||||
if r.Method != MethodPropfind {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
h.handlePathForUser(w, r, s, &did)
|
||||
case "v":
|
||||
h.VersionsHandler.Handler(s, &did).ServeHTTP(w, r)
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func (h *MetaHandler) handlePathForUser(w http.ResponseWriter, r *http.Request, s *svc, rid *provider.ResourceId) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "meta_propfind")
|
||||
defer span.End()
|
||||
|
||||
id := storagespace.FormatResourceID(rid)
|
||||
sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Str("resourceid", id).Logger()
|
||||
sublog.Info().Msg("calling get path for user")
|
||||
|
||||
pf, status, err := propfind.ReadPropfind(r.Body)
|
||||
if err != nil {
|
||||
sublog.Debug().Err(err).Msg("error reading propfind request")
|
||||
w.WriteHeader(status)
|
||||
return
|
||||
}
|
||||
|
||||
if ok := hasProp(&pf, net.PropOcMetaPathForUser); !ok {
|
||||
sublog.Debug().Str("prop", net.PropOcMetaPathForUser).Msg("error finding prop in request")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error selecting next client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
pathReq := &provider.GetPathRequest{ResourceId: rid}
|
||||
pathRes, err := client.GetPath(ctx, pathReq)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("could not send GetPath grpc request: transport error")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
switch pathRes.Status.Code {
|
||||
case rpc.Code_CODE_NOT_FOUND:
|
||||
sublog.Debug().Str("code", string(pathRes.Status.Code)).Msg("resource not found")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
m := fmt.Sprintf("Resource %s not found", id)
|
||||
b, err := errors.Marshal(http.StatusNotFound, m, "", "")
|
||||
errors.HandleWebdavError(&sublog, w, b, err)
|
||||
return
|
||||
case rpc.Code_CODE_PERMISSION_DENIED:
|
||||
// raise StatusNotFound so that resources can't be enumerated
|
||||
sublog.Debug().Str("code", string(pathRes.Status.Code)).Msg("resource access denied")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
m := fmt.Sprintf("Resource %s not found", id)
|
||||
b, err := errors.Marshal(http.StatusNotFound, m, "", "")
|
||||
errors.HandleWebdavError(&sublog, w, b, err)
|
||||
return
|
||||
}
|
||||
|
||||
propstatOK := propfind.PropstatXML{
|
||||
Status: "HTTP/1.1 200 OK",
|
||||
Prop: []prop.PropertyXML{
|
||||
prop.Escaped("oc:meta-path-for-user", pathRes.Path),
|
||||
prop.Escaped("oc:id", id),
|
||||
prop.Escaped("oc:fileid", id),
|
||||
prop.Escaped("oc:spaceid", storagespace.FormatStorageID(rid.GetStorageId(), rid.GetSpaceId())),
|
||||
},
|
||||
}
|
||||
baseURI := ctx.Value(net.CtxKeyBaseURI).(string)
|
||||
msr := propfind.NewMultiStatusResponseXML()
|
||||
msr.Responses = []*propfind.ResponseXML{
|
||||
{
|
||||
Href: net.EncodePath(path.Join(baseURI, id) + "/"),
|
||||
Propstat: []propfind.PropstatXML{
|
||||
propstatOK,
|
||||
},
|
||||
},
|
||||
}
|
||||
propRes, err := xml.Marshal(msr)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error marshalling propfind response xml")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol")
|
||||
w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8")
|
||||
w.WriteHeader(http.StatusMultiStatus)
|
||||
if _, err := w.Write(propRes); err != nil {
|
||||
sublog.Error().Err(err).Msg("error writing propfind response")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *MetaHandler) handleEmptyID(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "meta_propfind")
|
||||
defer span.End()
|
||||
|
||||
sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Logger()
|
||||
pf, status, err := propfind.ReadPropfind(r.Body)
|
||||
if err != nil {
|
||||
sublog.Debug().Err(err).Msg("error reading propfind request")
|
||||
w.WriteHeader(status)
|
||||
return
|
||||
}
|
||||
|
||||
if ok := hasProp(&pf, net.PropOcMetaPathForUser); !ok {
|
||||
sublog.Debug().Str("prop", net.PropOcMetaPathForUser).Msg("error finding prop in request")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
propstatNotFound := propfind.PropstatXML{
|
||||
Status: "HTTP/1.1 404 Not Found",
|
||||
}
|
||||
baseURI := ctx.Value(net.CtxKeyBaseURI).(string)
|
||||
msr := propfind.NewMultiStatusResponseXML()
|
||||
msr.Responses = []*propfind.ResponseXML{
|
||||
{
|
||||
Href: net.EncodePath(baseURI + "/"),
|
||||
Propstat: []propfind.PropstatXML{
|
||||
propstatNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
propRes, err := xml.Marshal(msr)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error marshalling propfind response xml")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol")
|
||||
w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8")
|
||||
w.WriteHeader(http.StatusMultiStatus)
|
||||
if _, err := w.Write(propRes); err != nil {
|
||||
sublog.Error().Err(err).Msg("error writing propfind response")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func hasProp(pf *propfind.XML, key string) bool {
|
||||
for i := range pf.Prop {
|
||||
k := fmt.Sprintf("%s/%s", pf.Prop[i].Space, pf.Prop[i].Local)
|
||||
if k == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Generated
Vendored
+165
@@ -0,0 +1,165 @@
|
||||
// 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 ocdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
rstatus "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func (s *svc) handlePathMkcol(w http.ResponseWriter, r *http.Request, ns string) (status int, err error) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "mkcol")
|
||||
defer span.End()
|
||||
|
||||
if err := ValidateName(filename(r.URL.Path), s.nameValidators); err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
fn := path.Join(ns, r.URL.Path)
|
||||
sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger()
|
||||
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, errtypes.InternalError(err.Error())
|
||||
}
|
||||
|
||||
// stat requested path to make sure it isn't existing yet
|
||||
// NOTE: It could be on another storage provider than the 'parent' of it
|
||||
sr, err := client.Stat(ctx, &provider.StatRequest{
|
||||
Ref: &provider.Reference{
|
||||
Path: fn,
|
||||
},
|
||||
})
|
||||
switch {
|
||||
case err != nil:
|
||||
return http.StatusInternalServerError, err
|
||||
case sr.Status.Code == rpc.Code_CODE_OK:
|
||||
// https://www.rfc-editor.org/rfc/rfc4918#section-9.3.1:
|
||||
// 405 (Method Not Allowed) - MKCOL can only be executed on an unmapped URL.
|
||||
return http.StatusMethodNotAllowed, fmt.Errorf("The resource you tried to create already exists")
|
||||
case sr.Status.Code == rpc.Code_CODE_ABORTED:
|
||||
return http.StatusPreconditionFailed, errtypes.NewErrtypeFromStatus(sr.Status)
|
||||
case sr.Status.Code != rpc.Code_CODE_NOT_FOUND:
|
||||
return rstatus.HTTPStatusFromCode(sr.Status.Code), errtypes.NewErrtypeFromStatus(sr.Status)
|
||||
}
|
||||
|
||||
parentPath := path.Dir(fn)
|
||||
|
||||
space, rpcStatus, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, parentPath)
|
||||
switch {
|
||||
case err != nil:
|
||||
return http.StatusInternalServerError, err
|
||||
case rpcStatus.Code == rpc.Code_CODE_NOT_FOUND:
|
||||
// https://www.rfc-editor.org/rfc/rfc4918#section-9.3.1:
|
||||
// 409 (Conflict) - A collection cannot be made at the Request-URI until
|
||||
// one or more intermediate collections have been created. The server
|
||||
// MUST NOT create those intermediate collections automatically.
|
||||
return http.StatusConflict, fmt.Errorf("intermediate collection does not exist")
|
||||
case rpcStatus.Code == rpc.Code_CODE_ABORTED:
|
||||
return http.StatusPreconditionFailed, errtypes.NewErrtypeFromStatus(rpcStatus)
|
||||
case rpcStatus.Code != rpc.Code_CODE_OK:
|
||||
return rstatus.HTTPStatusFromCode(rpcStatus.Code), errtypes.NewErrtypeFromStatus(rpcStatus)
|
||||
}
|
||||
|
||||
return s.handleMkcol(ctx, w, r, spacelookup.MakeRelativeReference(space, parentPath, false), spacelookup.MakeRelativeReference(space, fn, false), sublog)
|
||||
}
|
||||
|
||||
func (s *svc) handleSpacesMkCol(w http.ResponseWriter, r *http.Request, spaceID string) (status int, err error) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "spaces_mkcol")
|
||||
defer span.End()
|
||||
|
||||
sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Str("spaceid", spaceID).Str("handler", "mkcol").Logger()
|
||||
|
||||
parentRef, err := spacelookup.MakeStorageSpaceReference(spaceID, path.Dir(r.URL.Path))
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid space id")
|
||||
}
|
||||
childRef, _ := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path)
|
||||
|
||||
return s.handleMkcol(ctx, w, r, &parentRef, &childRef, sublog)
|
||||
}
|
||||
|
||||
func (s *svc) handleMkcol(ctx context.Context, w http.ResponseWriter, r *http.Request, parentRef, childRef *provider.Reference, log zerolog.Logger) (status int, err error) {
|
||||
if r.Body != http.NoBody {
|
||||
// We currently do not support extended mkcol https://datatracker.ietf.org/doc/rfc5689/
|
||||
// TODO let clients send a body with properties to set on the new resource
|
||||
return http.StatusUnsupportedMediaType, fmt.Errorf("extended-mkcol not supported")
|
||||
}
|
||||
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, errtypes.InternalError(err.Error())
|
||||
}
|
||||
req := &provider.CreateContainerRequest{Ref: childRef}
|
||||
res, err := client.CreateContainer(ctx, req)
|
||||
switch {
|
||||
case err != nil:
|
||||
return http.StatusInternalServerError, err
|
||||
case res.Status.Code == rpc.Code_CODE_OK:
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
return 0, nil
|
||||
case res.Status.Code == rpc.Code_CODE_NOT_FOUND:
|
||||
// This should never happen because if the parent collection does not exist we should
|
||||
// get a Code_CODE_FAILED_PRECONDITION. We play stupid and return what the response gave us
|
||||
//lint:ignore ST1005 mimic the exact oc10 error message
|
||||
return http.StatusNotFound, errors.New("Resource not found")
|
||||
case res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED:
|
||||
// check if user has access to parent
|
||||
sRes, err := client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{
|
||||
ResourceId: childRef.GetResourceId(),
|
||||
Path: utils.MakeRelativePath(path.Dir(childRef.Path)),
|
||||
}})
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
if sRes.Status.Code != rpc.Code_CODE_OK {
|
||||
// return not found error so we do not leak existence of a file
|
||||
// TODO hide permission failed for users without access in every kind of request
|
||||
// TODO should this be done in the driver?
|
||||
//lint:ignore ST1005 mimic the exact oc10 error message
|
||||
return http.StatusNotFound, errors.New("Resource not found")
|
||||
}
|
||||
return http.StatusForbidden, errors.New(sRes.Status.Message)
|
||||
case res.Status.Code == rpc.Code_CODE_ABORTED:
|
||||
return http.StatusPreconditionFailed, errors.New(res.Status.Message)
|
||||
case res.Status.Code == rpc.Code_CODE_FAILED_PRECONDITION:
|
||||
// https://www.rfc-editor.org/rfc/rfc4918#section-9.3.1:
|
||||
// 409 (Conflict) - A collection cannot be made at the Request-URI until
|
||||
// one or more intermediate collections have been created. The server
|
||||
// MUST NOT create those intermediate collections automatically.
|
||||
return http.StatusConflict, errors.New(res.Status.Message)
|
||||
case res.Status.Code == rpc.Code_CODE_ALREADY_EXISTS:
|
||||
// https://www.rfc-editor.org/rfc/rfc4918#section-9.3.1:
|
||||
// 405 (Method Not Allowed) - MKCOL can only be executed on an unmapped URL.
|
||||
//lint:ignore ST1005 mimic the exact oc10 error message
|
||||
return http.StatusMethodNotAllowed, errors.New("The resource you tried to create already exists")
|
||||
}
|
||||
return rstatus.HTTPStatusFromCode(res.Status.Code), errtypes.NewErrtypeFromStatus(res.Status)
|
||||
}
|
||||
Generated
Vendored
+351
@@ -0,0 +1,351 @@
|
||||
// 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 ocdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
rstatus "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
|
||||
"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"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func (s *svc) handlePathMove(w http.ResponseWriter, r *http.Request, ns string) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "move")
|
||||
defer span.End()
|
||||
|
||||
if r.Body != http.NoBody {
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
b, err := errors.Marshal(http.StatusUnsupportedMediaType, "body must be empty", "", "")
|
||||
errors.HandleWebdavError(appctx.GetLogger(ctx), w, b, err)
|
||||
return
|
||||
}
|
||||
|
||||
srcPath := path.Join(ns, r.URL.Path)
|
||||
dh := r.Header.Get(net.HeaderDestination)
|
||||
baseURI := r.Context().Value(net.CtxKeyBaseURI).(string)
|
||||
dstPath, err := net.ParseDestination(baseURI, dh)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
b, err := errors.Marshal(http.StatusBadRequest, "failed to extract destination", "", "")
|
||||
errors.HandleWebdavError(appctx.GetLogger(ctx), w, b, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ValidateName(filename(srcPath), s.nameValidators); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
b, err := errors.Marshal(http.StatusBadRequest, "source failed naming rules", "", "")
|
||||
errors.HandleWebdavError(appctx.GetLogger(ctx), w, b, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ValidateDestination(filename(dstPath), s.nameValidators); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
b, err := errors.Marshal(http.StatusBadRequest, "destination naming rules", "", "")
|
||||
errors.HandleWebdavError(appctx.GetLogger(ctx), w, b, err)
|
||||
return
|
||||
}
|
||||
|
||||
dstPath = path.Join(ns, dstPath)
|
||||
|
||||
sublog := appctx.GetLogger(ctx).With().Str("src", srcPath).Str("dst", dstPath).Logger()
|
||||
|
||||
srcSpace, status, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, srcPath)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Str("path", srcPath).Msg("failed to look up source storage space")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if status.Code != rpc.Code_CODE_OK {
|
||||
errors.HandleErrorStatus(&sublog, w, status)
|
||||
return
|
||||
}
|
||||
dstSpace, status, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, dstPath)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Str("path", dstPath).Msg("failed to look up destination storage space")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if status.Code != rpc.Code_CODE_OK {
|
||||
errors.HandleErrorStatus(&sublog, w, status)
|
||||
return
|
||||
}
|
||||
|
||||
s.handleMove(ctx, w, r, spacelookup.MakeRelativeReference(srcSpace, srcPath, false), spacelookup.MakeRelativeReference(dstSpace, dstPath, false), sublog)
|
||||
}
|
||||
|
||||
func (s *svc) handleSpacesMove(w http.ResponseWriter, r *http.Request, srcSpaceID string) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "spaces_move")
|
||||
defer span.End()
|
||||
|
||||
if r.Body != http.NoBody {
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
b, err := errors.Marshal(http.StatusUnsupportedMediaType, "body must be empty", "", "")
|
||||
errors.HandleWebdavError(appctx.GetLogger(ctx), w, b, err)
|
||||
return
|
||||
}
|
||||
|
||||
dh := r.Header.Get(net.HeaderDestination)
|
||||
baseURI := r.Context().Value(net.CtxKeyBaseURI).(string)
|
||||
dst, err := net.ParseDestination(baseURI, dh)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sublog := appctx.GetLogger(ctx).With().Str("spaceid", srcSpaceID).Str("path", r.URL.Path).Logger()
|
||||
|
||||
srcRef, err := spacelookup.MakeStorageSpaceReference(srcSpaceID, r.URL.Path)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
dstSpaceID, dstRelPath := router.ShiftPath(dst)
|
||||
|
||||
dstRef, err := spacelookup.MakeStorageSpaceReference(dstSpaceID, dstRelPath)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s.handleMove(ctx, w, r, &srcRef, &dstRef, sublog)
|
||||
}
|
||||
|
||||
func (s *svc) handleMove(ctx context.Context, w http.ResponseWriter, r *http.Request, src, dst *provider.Reference, log zerolog.Logger) {
|
||||
isChild, err := s.referenceIsChildOf(ctx, s.gatewaySelector, dst, src)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case errtypes.IsNotFound:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case errtypes.IsNotSupported:
|
||||
log.Error().Err(err).Msg("can not detect recursive move operation. missing machine auth configuration?")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
default:
|
||||
log.Error().Err(err).Msg("error while trying to detect recursive move operation")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
if isChild {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
b, err := errors.Marshal(http.StatusBadRequest, "can not move a folder into one of its children", "", "")
|
||||
errors.HandleWebdavError(&log, w, b, err)
|
||||
return
|
||||
}
|
||||
|
||||
isParent, err := s.referenceIsChildOf(ctx, s.gatewaySelector, src, dst)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case errtypes.IsNotFound:
|
||||
isParent = false
|
||||
case errtypes.IsNotSupported:
|
||||
log.Error().Err(err).Msg("can not detect recursive move operation. missing machine auth configuration?")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
default:
|
||||
log.Error().Err(err).Msg("error while trying to detect recursive move operation")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
if isParent {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
b, err := errors.Marshal(http.StatusBadRequest, "can not move a folder into its parent", "", "")
|
||||
errors.HandleWebdavError(&log, w, b, err)
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
oh := r.Header.Get(net.HeaderOverwrite)
|
||||
log.Debug().Str("overwrite", oh).Msg("move")
|
||||
|
||||
overwrite, err := net.ParseOverwrite(oh)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error selecting next client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// check src exists
|
||||
srcStatReq := &provider.StatRequest{Ref: src}
|
||||
srcStatRes, err := client.Stat(ctx, srcStatReq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error sending grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if srcStatRes.Status.Code != rpc.Code_CODE_OK {
|
||||
if srcStatRes.Status.Code == rpc.Code_CODE_NOT_FOUND {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
m := fmt.Sprintf("Resource %v not found", srcStatReq.Ref.Path)
|
||||
b, err := errors.Marshal(http.StatusNotFound, m, "", "")
|
||||
errors.HandleWebdavError(&log, w, b, err)
|
||||
}
|
||||
errors.HandleErrorStatus(&log, w, srcStatRes.Status)
|
||||
return
|
||||
}
|
||||
if utils.IsSpaceRoot(srcStatRes.GetInfo()) {
|
||||
log.Error().Msg("the source is disallowed")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// check dst exists
|
||||
dstStatReq := &provider.StatRequest{Ref: dst}
|
||||
dstStatRes, err := client.Stat(ctx, dstStatReq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error sending grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if dstStatRes.Status.Code != rpc.Code_CODE_OK && dstStatRes.Status.Code != rpc.Code_CODE_NOT_FOUND {
|
||||
errors.HandleErrorStatus(&log, w, dstStatRes.Status)
|
||||
return
|
||||
}
|
||||
|
||||
successCode := http.StatusCreated // 201 if new resource was created, see https://tools.ietf.org/html/rfc4918#section-9.9.4
|
||||
if dstStatRes.Status.Code == rpc.Code_CODE_OK {
|
||||
successCode = http.StatusNoContent // 204 if target already existed, see https://tools.ietf.org/html/rfc4918#section-9.9.4
|
||||
|
||||
if utils.IsSpaceRoot(dstStatRes.GetInfo()) {
|
||||
log.Error().Msg("overwriting is not allowed")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !overwrite {
|
||||
log.Warn().Bool("overwrite", overwrite).Msg("dst already exists")
|
||||
w.WriteHeader(http.StatusPreconditionFailed) // 412, see https://tools.ietf.org/html/rfc4918#section-9.9.4
|
||||
return
|
||||
}
|
||||
// delete existing tree
|
||||
delReq := &provider.DeleteRequest{Ref: dst}
|
||||
delRes, err := client.Delete(ctx, delReq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error sending grpc delete request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if delRes.Status.Code != rpc.Code_CODE_OK && delRes.Status.Code != rpc.Code_CODE_NOT_FOUND {
|
||||
errors.HandleErrorStatus(&log, w, delRes.Status)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// check if an intermediate path / the parent exists
|
||||
intStatReq := &provider.StatRequest{Ref: &provider.Reference{
|
||||
ResourceId: dst.ResourceId,
|
||||
Path: utils.MakeRelativePath(path.Dir(dst.Path)),
|
||||
}}
|
||||
intStatRes, err := client.Stat(ctx, intStatReq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error sending grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if intStatRes.Status.Code != rpc.Code_CODE_OK {
|
||||
if intStatRes.Status.Code == rpc.Code_CODE_NOT_FOUND {
|
||||
// 409 if intermediate dir is missing, see https://tools.ietf.org/html/rfc4918#section-9.8.5
|
||||
log.Debug().Interface("parent", dst).Interface("status", intStatRes.Status).Msg("conflict")
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
} else {
|
||||
errors.HandleErrorStatus(&log, w, intStatRes.Status)
|
||||
}
|
||||
return
|
||||
}
|
||||
// TODO what if intermediate is a file?
|
||||
}
|
||||
// resolve the destination path
|
||||
if dst.Path == "." {
|
||||
dst.Path = utils.MakeRelativePath(dstStatRes.GetInfo().GetName())
|
||||
dst.ResourceId = dstStatRes.GetInfo().GetParentId()
|
||||
}
|
||||
mReq := &provider.MoveRequest{
|
||||
Source: src,
|
||||
Destination: dst,
|
||||
LockId: requestLockToken(r),
|
||||
}
|
||||
mRes, err := client.Move(ctx, mReq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error sending move grpc request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if mRes.Status.Code != rpc.Code_CODE_OK {
|
||||
status := rstatus.HTTPStatusFromCode(mRes.Status.Code)
|
||||
m := mRes.Status.Message
|
||||
switch mRes.Status.Code {
|
||||
case rpc.Code_CODE_ABORTED:
|
||||
status = http.StatusPreconditionFailed
|
||||
case rpc.Code_CODE_PERMISSION_DENIED:
|
||||
status = http.StatusForbidden
|
||||
case rpc.Code_CODE_UNIMPLEMENTED:
|
||||
// We translate this into a Bad Gateway error as per https://www.rfc-editor.org/rfc/rfc4918#section-9.9.4
|
||||
// > 502 (Bad Gateway) - This may occur when the destination is on another
|
||||
// > server and the destination server refuses to accept the resource.
|
||||
// > This could also occur when the destination is on another sub-section
|
||||
// > of the same server namespace.
|
||||
status = http.StatusBadGateway
|
||||
}
|
||||
|
||||
w.WriteHeader(status)
|
||||
|
||||
b, err := errors.Marshal(status, m, "", "")
|
||||
errors.HandleWebdavError(&log, w, b, err)
|
||||
return
|
||||
}
|
||||
|
||||
dstStatRes, err = client.Stat(ctx, dstStatReq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error sending grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if dstStatRes.Status.Code != rpc.Code_CODE_OK {
|
||||
errors.HandleErrorStatus(&log, w, dstStatRes.Status)
|
||||
return
|
||||
}
|
||||
|
||||
info := dstStatRes.Info
|
||||
w.Header().Set(net.HeaderContentType, info.MimeType)
|
||||
w.Header().Set(net.HeaderETag, info.Etag)
|
||||
w.Header().Set(net.HeaderOCFileID, storagespace.FormatResourceID(info.Id))
|
||||
w.Header().Set(net.HeaderOCETag, info.Etag)
|
||||
w.WriteHeader(successCode)
|
||||
}
|
||||
Generated
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
// Copyright 2018-2022 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 net
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
cs3types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
// ContentDispositionAttachment builds a ContentDisposition Attachment header with various filename encodings
|
||||
func ContentDispositionAttachment(filename string) string {
|
||||
return "attachment; filename*=UTF-8''" + url.PathEscape(filename) + "; filename=\"" + filename + "\""
|
||||
}
|
||||
|
||||
// RFC1123Z formats a CS3 Timestamp to be used in HTTP headers like Last-Modified
|
||||
func RFC1123Z(ts *cs3types.Timestamp) string {
|
||||
t := utils.TSToTime(ts).UTC()
|
||||
return t.Format(time.RFC1123Z)
|
||||
}
|
||||
Generated
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
// Copyright 2018-2022 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 net
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
)
|
||||
|
||||
// IsCurrentUserOwnerOrManager returns whether the context user is the given owner or not
|
||||
func IsCurrentUserOwnerOrManager(ctx context.Context, owner *userv1beta1.UserId, md *provider.ResourceInfo) bool {
|
||||
contextUser, ok := ctxpkg.ContextGetUser(ctx)
|
||||
// personal spaces have owners
|
||||
if ok && contextUser.Id != nil && owner != nil &&
|
||||
contextUser.Id.Idp == owner.Idp &&
|
||||
contextUser.Id.OpaqueId == owner.OpaqueId {
|
||||
return true
|
||||
}
|
||||
// check if the user is space manager
|
||||
if md != nil && md.Owner != nil && md.Owner.GetType() == userv1beta1.UserType_USER_TYPE_SPACE_OWNER {
|
||||
return md.GetPermissionSet().AddGrant
|
||||
}
|
||||
return false
|
||||
}
|
||||
Generated
Vendored
+77
@@ -0,0 +1,77 @@
|
||||
// Copyright 2018-2022 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 net
|
||||
|
||||
// Common HTTP headers.
|
||||
const (
|
||||
HeaderAcceptRanges = "Accept-Ranges"
|
||||
HeaderAccessControlAllowHeaders = "Access-Control-Allow-Headers"
|
||||
HeaderAccessControlExposeHeaders = "Access-Control-Expose-Headers"
|
||||
HeaderContentDisposistion = "Content-Disposition"
|
||||
HeaderContentEncoding = "Content-Encoding"
|
||||
HeaderContentLength = "Content-Length"
|
||||
HeaderContentRange = "Content-Range"
|
||||
HeaderContentType = "Content-Type"
|
||||
HeaderETag = "ETag"
|
||||
HeaderLastModified = "Last-Modified"
|
||||
HeaderLocation = "Location"
|
||||
HeaderRange = "Range"
|
||||
HeaderIfMatch = "If-Match"
|
||||
HeaderIfNoneMatch = "If-None-Match"
|
||||
HeaderPrefer = "Prefer"
|
||||
HeaderPreferenceApplied = "Preference-Applied"
|
||||
HeaderVary = "Vary"
|
||||
)
|
||||
|
||||
// webdav headers
|
||||
const (
|
||||
HeaderDav = "DAV" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.1
|
||||
HeaderDepth = "Depth" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.2
|
||||
HeaderDestination = "Destination" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.3
|
||||
HeaderIf = "If" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.4
|
||||
HeaderLockToken = "Lock-Token" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.5
|
||||
HeaderOverwrite = "Overwrite" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.6
|
||||
HeaderTimeout = "Timeout" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.7
|
||||
)
|
||||
|
||||
// Non standard HTTP headers.
|
||||
const (
|
||||
HeaderOCFileID = "OC-FileId"
|
||||
HeaderOCETag = "OC-ETag"
|
||||
HeaderOCChecksum = "OC-Checksum"
|
||||
HeaderOCPermissions = "OC-Perm"
|
||||
HeaderTusResumable = "Tus-Resumable"
|
||||
HeaderTusVersion = "Tus-Version"
|
||||
HeaderTusExtension = "Tus-Extension"
|
||||
HeaderTusChecksumAlgorithm = "Tus-Checksum-Algorithm"
|
||||
HeaderTusUploadExpires = "Upload-Expires"
|
||||
HeaderUploadChecksum = "Upload-Checksum"
|
||||
HeaderUploadLength = "Upload-Length"
|
||||
HeaderUploadMetadata = "Upload-Metadata"
|
||||
HeaderUploadOffset = "Upload-Offset"
|
||||
HeaderOCMtime = "X-OC-Mtime"
|
||||
HeaderExpectedEntityLength = "X-Expected-Entity-Length"
|
||||
HeaderLitmus = "X-Litmus"
|
||||
HeaderTransferAuth = "TransferHeaderAuthorization"
|
||||
)
|
||||
|
||||
// HTTP Prefer header values
|
||||
const (
|
||||
HeaderPreferReturn = "return" // eg. return=representation / return=minimal, depth-noroot
|
||||
)
|
||||
Generated
Vendored
+146
@@ -0,0 +1,146 @@
|
||||
// Copyright 2018-2022 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 net
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidHeaderValue defines an error which can occure when trying to parse a header value.
|
||||
ErrInvalidHeaderValue = errors.New("invalid value")
|
||||
)
|
||||
|
||||
type ctxKey int
|
||||
|
||||
const (
|
||||
// CtxKeyBaseURI is the key of the base URI context field
|
||||
CtxKeyBaseURI ctxKey = iota
|
||||
|
||||
// NsDav is the Dav ns
|
||||
NsDav = "DAV:"
|
||||
// NsOwncloud is the owncloud ns
|
||||
NsOwncloud = "http://owncloud.org/ns"
|
||||
// NsOCS is the OCS ns
|
||||
NsOCS = "http://open-collaboration-services.org/ns"
|
||||
|
||||
// RFC1123 time that mimics oc10. time.RFC1123 would end in "UTC", see https://github.com/golang/go/issues/13781
|
||||
RFC1123 = "Mon, 02 Jan 2006 15:04:05 GMT"
|
||||
|
||||
// PropQuotaUnknown is the quota unknown property
|
||||
PropQuotaUnknown = "-2"
|
||||
// PropOcFavorite is the favorite ns property
|
||||
PropOcFavorite = "http://owncloud.org/ns/favorite"
|
||||
// PropOcMetaPathForUser is the meta-path-for-user ns property
|
||||
PropOcMetaPathForUser = "http://owncloud.org/ns/meta-path-for-user"
|
||||
|
||||
// DepthZero represents the webdav zero depth value
|
||||
DepthZero Depth = "0"
|
||||
// DepthOne represents the webdav one depth value
|
||||
DepthOne Depth = "1"
|
||||
// DepthInfinity represents the webdav infinity depth value
|
||||
DepthInfinity Depth = "infinity"
|
||||
)
|
||||
|
||||
// Depth is a type representing the webdav depth header value
|
||||
type Depth string
|
||||
|
||||
// String returns the string representation of the webdav depth value
|
||||
func (d Depth) String() string {
|
||||
return string(d)
|
||||
}
|
||||
|
||||
// EncodePath encodes the path of a url.
|
||||
//
|
||||
// slashes (/) are treated as path-separators.
|
||||
func EncodePath(path string) string {
|
||||
return (&url.URL{Path: path}).EscapedPath()
|
||||
}
|
||||
|
||||
// ParseDepth parses the depth header value defined in https://tools.ietf.org/html/rfc4918#section-9.1
|
||||
// Valid values are "0", "1" and "infinity". An empty string will be parsed to "1".
|
||||
// For all other values this method returns an error.
|
||||
func ParseDepth(s string) (Depth, error) {
|
||||
if s == "" {
|
||||
return DepthOne, nil
|
||||
}
|
||||
|
||||
switch strings.ToLower(s) {
|
||||
case DepthZero.String():
|
||||
return DepthZero, nil
|
||||
case DepthOne.String():
|
||||
return DepthOne, nil
|
||||
case DepthInfinity.String():
|
||||
return DepthInfinity, nil
|
||||
default:
|
||||
return "", errors.Wrapf(ErrInvalidHeaderValue, "invalid depth: %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
// ParseOverwrite parses the overwrite header value defined in https://datatracker.ietf.org/doc/html/rfc4918#section-10.6
|
||||
// Valid values are "T" and "F". An empty string will be parse to true.
|
||||
func ParseOverwrite(s string) (bool, error) {
|
||||
if s == "" {
|
||||
s = "T"
|
||||
}
|
||||
if s != "T" && s != "F" {
|
||||
return false, errors.Wrapf(ErrInvalidHeaderValue, "invalid overwrite: %s", s)
|
||||
}
|
||||
return s == "T", nil
|
||||
}
|
||||
|
||||
// ParseDestination parses the destination header value defined in https://datatracker.ietf.org/doc/html/rfc4918#section-10.3
|
||||
// The returned path will be relative to the given baseURI.
|
||||
func ParseDestination(baseURI, s string) (string, error) {
|
||||
if s == "" {
|
||||
return "", errors.Wrap(ErrInvalidHeaderValue, "destination header is empty")
|
||||
}
|
||||
dstURL, err := url.ParseRequestURI(s)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(ErrInvalidHeaderValue, err.Error())
|
||||
}
|
||||
|
||||
// TODO check if path is on same storage, return 502 on problems, see https://tools.ietf.org/html/rfc4918#section-9.9.4
|
||||
// TODO make request.php optional in destination header
|
||||
// Strip the base URI from the destination. The destination might contain redirection prefixes which need to be handled
|
||||
urlSplit := strings.Split(dstURL.Path, baseURI)
|
||||
if len(urlSplit) != 2 {
|
||||
return "", errors.Wrap(ErrInvalidHeaderValue, "destination path does not contain base URI")
|
||||
}
|
||||
|
||||
return urlSplit[1], nil
|
||||
}
|
||||
|
||||
// ParsePrefer parses the prefer header value defined in https://datatracker.ietf.org/doc/html/rfc8144
|
||||
func ParsePrefer(s string) map[string]string {
|
||||
parts := strings.Split(s, ",")
|
||||
m := make(map[string]string, len(parts))
|
||||
for _, part := range parts {
|
||||
kv := strings.SplitN(strings.ToLower(strings.Trim(part, " ")), "=", 2)
|
||||
if len(kv) == 2 {
|
||||
m[kv[0]] = kv[1]
|
||||
} else {
|
||||
m[kv[0]] = "1" // mark it as set
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
Generated
Vendored
+401
@@ -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 ocdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
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"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/jellydator/ttlcache/v2"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/config"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
"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/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/rhttp/router"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/favorite"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/favorite/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/templates"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/rs/zerolog"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// name is the Tracer name used to identify this instrumentation library.
|
||||
const tracerName = "ocdav"
|
||||
|
||||
func init() {
|
||||
global.Register("ocdav", New)
|
||||
}
|
||||
|
||||
type svc struct {
|
||||
c *config.Config
|
||||
webDavHandler *WebDavHandler
|
||||
davHandler *DavHandler
|
||||
favoritesManager favorite.Manager
|
||||
client *http.Client
|
||||
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
||||
// LockSystem is the lock management system.
|
||||
LockSystem LockSystem
|
||||
userIdentifierCache *ttlcache.Cache
|
||||
nameValidators []Validator
|
||||
}
|
||||
|
||||
func (s *svc) Config() *config.Config {
|
||||
return s.c
|
||||
}
|
||||
|
||||
func getFavoritesManager(c *config.Config) (favorite.Manager, error) {
|
||||
if f, ok := registry.NewFuncs[c.FavoriteStorageDriver]; ok {
|
||||
return f(c.FavoriteStorageDrivers[c.FavoriteStorageDriver])
|
||||
}
|
||||
return nil, errtypes.NotFound("driver not found: " + c.FavoriteStorageDriver)
|
||||
}
|
||||
func getLockSystem(c *config.Config) (LockSystem, error) {
|
||||
// TODO in memory implementation
|
||||
selector, err := pool.GatewaySelector(c.GatewaySvc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewCS3LS(selector), nil
|
||||
}
|
||||
|
||||
// New returns a new ocdav service
|
||||
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
|
||||
conf := &config.Config{}
|
||||
if err := mapstructure.Decode(m, conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf.Init()
|
||||
|
||||
fm, err := getFavoritesManager(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ls, err := getLockSystem(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewWith(conf, fm, ls, log, nil)
|
||||
}
|
||||
|
||||
// NewWith returns a new ocdav service
|
||||
func NewWith(conf *config.Config, fm favorite.Manager, ls LockSystem, _ *zerolog.Logger, selector pool.Selectable[gateway.GatewayAPIClient]) (global.Service, error) {
|
||||
// be safe - init the conf again
|
||||
conf.Init()
|
||||
|
||||
s := &svc{
|
||||
c: conf,
|
||||
webDavHandler: new(WebDavHandler),
|
||||
davHandler: new(DavHandler),
|
||||
client: rhttp.GetHTTPClient(
|
||||
rhttp.Timeout(time.Duration(conf.Timeout*int64(time.Second))),
|
||||
rhttp.Insecure(conf.Insecure),
|
||||
),
|
||||
gatewaySelector: selector,
|
||||
favoritesManager: fm,
|
||||
LockSystem: ls,
|
||||
userIdentifierCache: ttlcache.NewCache(),
|
||||
nameValidators: ValidatorsFromConfig(conf),
|
||||
}
|
||||
_ = s.userIdentifierCache.SetTTL(60 * time.Second)
|
||||
|
||||
// initialize handlers and set default configs
|
||||
if err := s.webDavHandler.init(conf.WebdavNamespace, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.davHandler.init(conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if selector == nil {
|
||||
var err error
|
||||
s.gatewaySelector, err = pool.GatewaySelector(s.c.GatewaySvc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *svc) Prefix() string {
|
||||
return s.c.Prefix
|
||||
}
|
||||
|
||||
func (s *svc) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *svc) Unprotected() []string {
|
||||
return []string{"/status.php", "/status", "/remote.php/dav/public-files/", "/apps/files/", "/index.php/f/", "/index.php/s/", "/remote.php/dav/ocm/", "/dav/ocm/"}
|
||||
}
|
||||
|
||||
func (s *svc) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
log := appctx.GetLogger(ctx)
|
||||
|
||||
// TODO(jfd): do we need this?
|
||||
// fake litmus testing for empty namespace: see https://github.com/golang/net/blob/e514e69ffb8bc3c76a71ae40de0118d794855992/webdav/litmus_test_server.go#L58-L89
|
||||
if r.Header.Get(net.HeaderLitmus) == "props: 3 (propfind_invalid2)" {
|
||||
http.Error(w, "400 Bad Request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// to build correct href prop urls we need to keep track of the base path
|
||||
// always starts with /
|
||||
base := path.Join("/", s.Prefix())
|
||||
|
||||
var head string
|
||||
head, r.URL.Path = router.ShiftPath(r.URL.Path)
|
||||
log.Debug().Str("method", r.Method).Str("head", head).Str("tail", r.URL.Path).Msg("http routing")
|
||||
switch head {
|
||||
case "status.php", "status":
|
||||
s.doStatus(w, r)
|
||||
return
|
||||
case "remote.php":
|
||||
// skip optional "remote.php"
|
||||
head, r.URL.Path = router.ShiftPath(r.URL.Path)
|
||||
|
||||
// yet, add it to baseURI
|
||||
base = path.Join(base, "remote.php")
|
||||
case "apps":
|
||||
head, r.URL.Path = router.ShiftPath(r.URL.Path)
|
||||
if head == "files" {
|
||||
s.handleLegacyPath(w, r)
|
||||
return
|
||||
}
|
||||
case "index.php":
|
||||
head, r.URL.Path = router.ShiftPath(r.URL.Path)
|
||||
if head == "s" {
|
||||
token := r.URL.Path
|
||||
rURL := s.c.PublicURL + path.Join(head, token)
|
||||
|
||||
http.Redirect(w, r, rURL, http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
}
|
||||
switch head {
|
||||
// the old `/webdav` endpoint uses remote.php/webdav/$path
|
||||
case "webdav":
|
||||
// for oc we need to prepend /home as the path that will be passed to the home storage provider
|
||||
// will not contain the username
|
||||
base = path.Join(base, "webdav")
|
||||
ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
|
||||
r = r.WithContext(ctx)
|
||||
s.webDavHandler.Handler(s).ServeHTTP(w, r)
|
||||
return
|
||||
case "dav":
|
||||
// cern uses /dav/files/$namespace -> /$namespace/...
|
||||
// oc uses /dav/files/$user -> /$home/$user/...
|
||||
// for oc we need to prepend the path to user homes
|
||||
// or we take the path starting at /dav and allow rewriting it?
|
||||
base = path.Join(base, "dav")
|
||||
ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
|
||||
r = r.WithContext(ctx)
|
||||
s.davHandler.Handler(s).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
log.Warn().Msg("resource not found")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *svc) ApplyLayout(ctx context.Context, ns string, useLoggedInUserNS bool, requestPath string) (string, string, error) {
|
||||
// If useLoggedInUserNS is false, that implies that the request is coming from
|
||||
// the FilesHandler method invoked by a /dav/files/fileOwner where fileOwner
|
||||
// is not the same as the logged in user. In that case, we'll treat fileOwner
|
||||
// as the username whose files are to be accessed and use that in the
|
||||
// namespace template.
|
||||
u, ok := ctxpkg.ContextGetUser(ctx)
|
||||
if !ok || !useLoggedInUserNS {
|
||||
var requestUsernameOrID string
|
||||
requestUsernameOrID, requestPath = router.ShiftPath(requestPath)
|
||||
|
||||
// Check if this is a Userid
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
userRes, err := client.GetUser(ctx, &userpb.GetUserRequest{
|
||||
UserId: &userpb.UserId{OpaqueId: requestUsernameOrID},
|
||||
})
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// If it's not a userid try if it is a user name
|
||||
if userRes.Status.Code != rpc.Code_CODE_OK {
|
||||
res, err := client.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{
|
||||
Claim: "username",
|
||||
Value: requestUsernameOrID,
|
||||
})
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
userRes.Status = res.Status
|
||||
userRes.User = res.User
|
||||
}
|
||||
|
||||
// If still didn't find a user, fallback
|
||||
if userRes.Status.Code != rpc.Code_CODE_OK {
|
||||
userRes.User = &userpb.User{
|
||||
Username: requestUsernameOrID,
|
||||
Id: &userpb.UserId{OpaqueId: requestUsernameOrID},
|
||||
}
|
||||
}
|
||||
|
||||
u = userRes.User
|
||||
}
|
||||
|
||||
return templates.WithUser(u, ns), requestPath, nil
|
||||
}
|
||||
|
||||
func authContextForUser(client gateway.GatewayAPIClient, userID *userpb.UserId, machineAuthAPIKey string) (context.Context, error) {
|
||||
if machineAuthAPIKey == "" {
|
||||
return nil, errtypes.NotSupported("machine auth not configured")
|
||||
}
|
||||
// Get auth
|
||||
granteeCtx := ctxpkg.ContextSetUser(context.Background(), &userpb.User{Id: userID})
|
||||
|
||||
authRes, err := client.Authenticate(granteeCtx, &gateway.AuthenticateRequest{
|
||||
Type: "machine",
|
||||
ClientId: "userid:" + userID.OpaqueId,
|
||||
ClientSecret: machineAuthAPIKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if authRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
return nil, errtypes.NewErrtypeFromStatus(authRes.Status)
|
||||
}
|
||||
granteeCtx = metadata.AppendToOutgoingContext(granteeCtx, ctxpkg.TokenHeader, authRes.Token)
|
||||
return granteeCtx, nil
|
||||
}
|
||||
|
||||
func (s *svc) sspReferenceIsChildOf(ctx context.Context, selector pool.Selectable[gateway.GatewayAPIClient], child, parent *provider.Reference) (bool, error) {
|
||||
client, err := selector.Next()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
parentStatRes, err := client.Stat(ctx, &provider.StatRequest{Ref: parent})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if parentStatRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
return false, errtypes.NewErrtypeFromStatus(parentStatRes.GetStatus())
|
||||
}
|
||||
parentAuthCtx, err := authContextForUser(client, parentStatRes.GetInfo().GetOwner(), s.c.MachineAuthAPIKey)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
parentPathRes, err := client.GetPath(parentAuthCtx, &provider.GetPathRequest{ResourceId: parentStatRes.GetInfo().GetId()})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
childStatRes, err := client.Stat(ctx, &provider.StatRequest{Ref: child})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if childStatRes.GetStatus().GetCode() == rpc.Code_CODE_NOT_FOUND && utils.IsRelativeReference(child) && child.Path != "." {
|
||||
childParentRef := &provider.Reference{
|
||||
ResourceId: child.ResourceId,
|
||||
Path: utils.MakeRelativePath(path.Dir(child.Path)),
|
||||
}
|
||||
childStatRes, err = client.Stat(ctx, &provider.StatRequest{Ref: childParentRef})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
if childStatRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
return false, errtypes.NewErrtypeFromStatus(parentStatRes.Status)
|
||||
}
|
||||
// TODO: this should use service accounts https://github.com/owncloud/ocis/issues/7597
|
||||
childAuthCtx, err := authContextForUser(client, childStatRes.GetInfo().GetOwner(), s.c.MachineAuthAPIKey)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
childPathRes, err := client.GetPath(childAuthCtx, &provider.GetPathRequest{ResourceId: childStatRes.GetInfo().GetId()})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
cp := childPathRes.Path + "/"
|
||||
pp := parentPathRes.Path + "/"
|
||||
return strings.HasPrefix(cp, pp), nil
|
||||
}
|
||||
|
||||
func (s *svc) referenceIsChildOf(ctx context.Context, selector pool.Selectable[gateway.GatewayAPIClient], child, parent *provider.Reference) (bool, error) {
|
||||
if child.ResourceId.SpaceId != parent.ResourceId.SpaceId {
|
||||
return false, nil // Not on the same storage -> not a child
|
||||
}
|
||||
|
||||
if utils.ResourceIDEqual(child.ResourceId, parent.ResourceId) {
|
||||
return strings.HasPrefix(child.Path, parent.Path+"/"), nil // Relative to the same resource -> compare paths
|
||||
}
|
||||
|
||||
if child.ResourceId.SpaceId == utils.ShareStorageSpaceID || parent.ResourceId.SpaceId == utils.ShareStorageSpaceID {
|
||||
// the sharesstorageprovider needs some special handling
|
||||
return s.sspReferenceIsChildOf(ctx, selector, child, parent)
|
||||
}
|
||||
|
||||
client, err := selector.Next()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// the references are on the same storage but relative to different resources
|
||||
// -> we need to get the path for both resources
|
||||
childPathRes, err := client.GetPath(ctx, &provider.GetPathRequest{ResourceId: child.ResourceId})
|
||||
if err != nil {
|
||||
if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented {
|
||||
return false, nil // the storage provider doesn't support GetPath() -> rely on it taking care of recursion issues
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
parentPathRes, err := client.GetPath(ctx, &provider.GetPathRequest{ResourceId: parent.ResourceId})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
cp := path.Join(childPathRes.Path, child.Path) + "/"
|
||||
pp := path.Join(parentPathRes.Path, parent.Path) + "/"
|
||||
return strings.HasPrefix(cp, pp), nil
|
||||
}
|
||||
|
||||
// filename returns the base filename from a path and replaces any slashes with an empty string
|
||||
func filename(p string) string {
|
||||
return strings.Trim(path.Base(p), "/")
|
||||
}
|
||||
Generated
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
// 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 ocdav
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
)
|
||||
|
||||
func (s *svc) handleOptions(w http.ResponseWriter, r *http.Request) {
|
||||
allow := "OPTIONS, LOCK, GET, HEAD, POST, DELETE, PROPPATCH, COPY,"
|
||||
allow += " MOVE, UNLOCK, PROPFIND, MKCOL, REPORT, SEARCH,"
|
||||
allow += " PUT" // TODO(jfd): only for files ... but we cannot create the full path without a user ... which we only have when credentials are sent
|
||||
|
||||
isPublic := strings.Contains(r.Context().Value(net.CtxKeyBaseURI).(string), "public-files")
|
||||
|
||||
w.Header().Set(net.HeaderContentType, "application/xml")
|
||||
w.Header().Set("Allow", allow)
|
||||
w.Header().Set("DAV", "1, 2")
|
||||
w.Header().Set("MS-Author-Via", "DAV")
|
||||
if !isPublic {
|
||||
w.Header().Add(net.HeaderAccessControlAllowHeaders, net.HeaderTusResumable)
|
||||
w.Header().Add(net.HeaderAccessControlExposeHeaders, strings.Join([]string{net.HeaderTusResumable, net.HeaderTusVersion, net.HeaderTusExtension}, ","))
|
||||
w.Header().Set(net.HeaderTusResumable, "1.0.0") // TODO(jfd): only for dirs?
|
||||
w.Header().Set(net.HeaderTusVersion, "1.0.0")
|
||||
w.Header().Set(net.HeaderTusExtension, "creation,creation-with-upload,checksum,expiration")
|
||||
w.Header().Set(net.HeaderTusChecksumAlgorithm, "md5,sha1,crc32")
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
Generated
Vendored
+212
@@ -0,0 +1,212 @@
|
||||
// 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 prop
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// PropertyXML represents a single DAV resource property as defined in RFC 4918.
|
||||
// http://www.webdav.org/specs/rfc4918.html#data.model.for.resource.properties
|
||||
type PropertyXML struct {
|
||||
// XMLName is the fully qualified name that identifies this property.
|
||||
XMLName xml.Name
|
||||
|
||||
// Lang is an optional xml:lang attribute.
|
||||
Lang string `xml:"xml:lang,attr,omitempty"`
|
||||
|
||||
// InnerXML contains the XML representation of the property value.
|
||||
// See http://www.webdav.org/specs/rfc4918.html#property_values
|
||||
//
|
||||
// Property values of complex type or mixed-content must have fully
|
||||
// expanded XML namespaces or be self-contained with according
|
||||
// XML namespace declarations. They must not rely on any XML
|
||||
// namespace declarations within the scope of the XML document,
|
||||
// even including the DAV: namespace.
|
||||
InnerXML []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
func xmlEscaped(val string) []byte {
|
||||
buf := new(bytes.Buffer)
|
||||
xml.Escape(buf, []byte(val))
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// EscapedNS returns a new PropertyXML instance while xml-escaping the value
|
||||
func EscapedNS(namespace string, local string, val string) PropertyXML {
|
||||
return PropertyXML{
|
||||
XMLName: xml.Name{Space: namespace, Local: local},
|
||||
Lang: "",
|
||||
InnerXML: xmlEscaped(val),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
escAmp = []byte("&")
|
||||
escLT = []byte("<")
|
||||
escGT = []byte(">")
|
||||
escFFFD = []byte(string(utf8.RuneError)) // Unicode replacement character
|
||||
)
|
||||
|
||||
// Decide whether the given rune is in the XML Character Range, per
|
||||
// the Char production of https://www.xml.com/axml/testaxml.htm,
|
||||
// Section 2.2 Characters.
|
||||
func isInCharacterRange(r rune) (inrange bool) {
|
||||
return r == 0x09 ||
|
||||
r == 0x0A ||
|
||||
r == 0x0D ||
|
||||
r >= 0x20 && r <= 0xD7FF ||
|
||||
r >= 0xE000 && r <= utf8.RuneError ||
|
||||
r >= 0x10000 && r <= 0x10FFFF
|
||||
}
|
||||
|
||||
// Escaped returns a new PropertyXML instance while replacing only
|
||||
// * `&` with `&`
|
||||
// * `<` with `<`
|
||||
// * `>` with `>`
|
||||
// as defined in https://www.w3.org/TR/REC-xml/#syntax:
|
||||
//
|
||||
// > The ampersand character (&) and the left angle bracket (<) must not appear
|
||||
// > in their literal form, except when used as markup delimiters, or within a
|
||||
// > comment, a processing instruction, or a CDATA section. If they are needed
|
||||
// > elsewhere, they must be escaped using either numeric character references
|
||||
// > or the strings " & " and " < " respectively. The right angle
|
||||
// > bracket (>) may be represented using the string " > ", and must, for
|
||||
// > compatibility, be escaped using either " > " or a character reference
|
||||
// > when it appears in the string " ]]> " in content, when that string is not
|
||||
// > marking the end of a CDATA section.
|
||||
//
|
||||
// The code ignores errors as the legacy Escaped() does
|
||||
// TODO properly use the space
|
||||
func Escaped(key, val string) PropertyXML {
|
||||
s := []byte(val)
|
||||
w := bytes.NewBuffer(make([]byte, 0, len(s)))
|
||||
var esc []byte
|
||||
last := 0
|
||||
for i := 0; i < len(s); {
|
||||
r, width := utf8.DecodeRune(s[i:])
|
||||
i += width
|
||||
switch r {
|
||||
case '&':
|
||||
esc = escAmp
|
||||
case '<':
|
||||
esc = escLT
|
||||
case '>':
|
||||
esc = escGT
|
||||
default:
|
||||
if !isInCharacterRange(r) || (r == utf8.RuneError && width == 1) {
|
||||
esc = escFFFD
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, err := w.Write(s[last : i-width]); err != nil {
|
||||
break
|
||||
}
|
||||
if _, err := w.Write(esc); err != nil {
|
||||
break
|
||||
}
|
||||
last = i
|
||||
}
|
||||
_, _ = w.Write(s[last:])
|
||||
return PropertyXML{
|
||||
XMLName: xml.Name{Space: "", Local: key},
|
||||
Lang: "",
|
||||
InnerXML: w.Bytes(),
|
||||
}
|
||||
}
|
||||
|
||||
// NotFound returns a new PropertyXML instance with an empty value
|
||||
func NotFound(key string) PropertyXML {
|
||||
return PropertyXML{
|
||||
XMLName: xml.Name{Space: "", Local: key},
|
||||
Lang: "",
|
||||
}
|
||||
}
|
||||
|
||||
// NotFoundNS returns a new PropertyXML instance with the given namespace and an empty value
|
||||
func NotFoundNS(namespace, key string) PropertyXML {
|
||||
return PropertyXML{
|
||||
XMLName: xml.Name{Space: namespace, Local: key},
|
||||
Lang: "",
|
||||
}
|
||||
}
|
||||
|
||||
// Raw returns a new PropertyXML instance for the given key/value pair
|
||||
// TODO properly use the space
|
||||
func Raw(key, val string) PropertyXML {
|
||||
return PropertyXML{
|
||||
XMLName: xml.Name{Space: "", Local: key},
|
||||
Lang: "",
|
||||
InnerXML: []byte(val),
|
||||
}
|
||||
}
|
||||
|
||||
// Next returns the next token, if any, in the XML stream of d.
|
||||
// RFC 4918 requires to ignore comments, processing instructions
|
||||
// and directives.
|
||||
// http://www.webdav.org/specs/rfc4918.html#property_values
|
||||
// http://www.webdav.org/specs/rfc4918.html#xml-extensibility
|
||||
func Next(d *xml.Decoder) (xml.Token, error) {
|
||||
for {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
switch t.(type) {
|
||||
case xml.Comment, xml.Directive, xml.ProcInst:
|
||||
continue
|
||||
default:
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ActiveLock holds active lock xml data
|
||||
//
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_activelock
|
||||
//
|
||||
// <!ELEMENT activelock (lockscope, locktype, depth, owner?, timeout?,
|
||||
//
|
||||
// locktoken?, lockroot)>
|
||||
type ActiveLock struct {
|
||||
XMLName xml.Name `xml:"activelock"`
|
||||
Exclusive *struct{} `xml:"lockscope>exclusive,omitempty"`
|
||||
Shared *struct{} `xml:"lockscope>shared,omitempty"`
|
||||
Write *struct{} `xml:"locktype>write,omitempty"`
|
||||
Depth string `xml:"depth"`
|
||||
Owner Owner `xml:"owner,omitempty"`
|
||||
Timeout string `xml:"timeout,omitempty"`
|
||||
Locktoken string `xml:"locktoken>href"`
|
||||
Lockroot string `xml:"lockroot>href,omitempty"`
|
||||
}
|
||||
|
||||
// Owner captures the inner UML of a lock owner element http://www.webdav.org/specs/rfc4918.html#ELEMENT_owner
|
||||
type Owner struct {
|
||||
InnerXML string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// Escape repaces ", &, ', < and > with their xml representation
|
||||
func Escape(s string) string {
|
||||
b := bytes.NewBuffer(nil)
|
||||
_ = xml.EscapeText(b, []byte(s))
|
||||
return b.String()
|
||||
}
|
||||
Generated
Vendored
+1855
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+495
@@ -0,0 +1,495 @@
|
||||
// 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 ocdav
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/prop"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
|
||||
"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/permission"
|
||||
rstatus "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func (s *svc) handlePathProppatch(w http.ResponseWriter, r *http.Request, ns string) (status int, err error) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "proppatch")
|
||||
defer span.End()
|
||||
|
||||
fn := path.Join(ns, r.URL.Path)
|
||||
|
||||
sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger()
|
||||
|
||||
pp, status, err := readProppatch(r.Body)
|
||||
if err != nil {
|
||||
return status, err
|
||||
}
|
||||
|
||||
space, rpcStatus, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, fn)
|
||||
switch {
|
||||
case err != nil:
|
||||
return http.StatusInternalServerError, err
|
||||
case rpcStatus.Code == rpc.Code_CODE_ABORTED:
|
||||
return http.StatusPreconditionFailed, errtypes.NewErrtypeFromStatus(rpcStatus)
|
||||
case rpcStatus.Code != rpc.Code_CODE_OK:
|
||||
return rstatus.HTTPStatusFromCode(rpcStatus.Code), errtypes.NewErrtypeFromStatus(rpcStatus)
|
||||
}
|
||||
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, errtypes.InternalError(err.Error())
|
||||
}
|
||||
// check if resource exists
|
||||
statReq := &provider.StatRequest{Ref: spacelookup.MakeRelativeReference(space, fn, false)}
|
||||
statRes, err := client.Stat(ctx, statReq)
|
||||
switch {
|
||||
case err != nil:
|
||||
return http.StatusInternalServerError, err
|
||||
case statRes.Status.Code == rpc.Code_CODE_ABORTED:
|
||||
return http.StatusPreconditionFailed, errtypes.NewErrtypeFromStatus(statRes.Status)
|
||||
case statRes.Status.Code != rpc.Code_CODE_OK:
|
||||
return rstatus.HTTPStatusFromCode(rpcStatus.Code), errtypes.NewErrtypeFromStatus(statRes.Status)
|
||||
}
|
||||
|
||||
acceptedProps, removedProps, ok := s.handleProppatch(ctx, w, r, spacelookup.MakeRelativeReference(space, fn, false), pp, sublog)
|
||||
if !ok {
|
||||
// handleProppatch handles responses in error cases so return 0
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
nRef := strings.TrimPrefix(fn, ns)
|
||||
nRef = path.Join(ctx.Value(net.CtxKeyBaseURI).(string), nRef)
|
||||
if statRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
|
||||
nRef += "/"
|
||||
}
|
||||
|
||||
s.handleProppatchResponse(ctx, w, r, acceptedProps, removedProps, nRef, sublog)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (s *svc) handleSpacesProppatch(w http.ResponseWriter, r *http.Request, spaceID string) (status int, err error) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "spaces_proppatch")
|
||||
defer span.End()
|
||||
|
||||
sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Str("spaceid", spaceID).Logger()
|
||||
|
||||
pp, status, err := readProppatch(r.Body)
|
||||
if err != nil {
|
||||
return status, err
|
||||
}
|
||||
|
||||
ref, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
acceptedProps, removedProps, ok := s.handleProppatch(ctx, w, r, &ref, pp, sublog)
|
||||
if !ok {
|
||||
// handleProppatch handles responses in error cases so return 0
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
nRef := path.Join(spaceID, r.URL.Path)
|
||||
nRef = path.Join(ctx.Value(net.CtxKeyBaseURI).(string), nRef)
|
||||
|
||||
s.handleProppatchResponse(ctx, w, r, acceptedProps, removedProps, nRef, sublog)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (s *svc) handleProppatch(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, patches []Proppatch, log zerolog.Logger) ([]xml.Name, []xml.Name, bool) {
|
||||
|
||||
rreq := &provider.UnsetArbitraryMetadataRequest{
|
||||
Ref: ref,
|
||||
ArbitraryMetadataKeys: []string{""},
|
||||
LockId: requestLockToken(r),
|
||||
}
|
||||
sreq := &provider.SetArbitraryMetadataRequest{
|
||||
Ref: ref,
|
||||
ArbitraryMetadata: &provider.ArbitraryMetadata{
|
||||
Metadata: map[string]string{},
|
||||
},
|
||||
LockId: requestLockToken(r),
|
||||
}
|
||||
|
||||
acceptedProps := []xml.Name{}
|
||||
removedProps := []xml.Name{}
|
||||
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error selecting next gateway client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
for i := range patches {
|
||||
if len(patches[i].Props) < 1 {
|
||||
continue
|
||||
}
|
||||
for j := range patches[i].Props {
|
||||
propNameXML := patches[i].Props[j].XMLName
|
||||
// don't use path.Join. It removes the double slash! concatenate with a /
|
||||
key := fmt.Sprintf("%s/%s", patches[i].Props[j].XMLName.Space, patches[i].Props[j].XMLName.Local)
|
||||
value := string(patches[i].Props[j].InnerXML)
|
||||
remove := patches[i].Remove
|
||||
// boolean flags may be "set" to false as well
|
||||
if s.isBooleanProperty(key) {
|
||||
// Make boolean properties either "0" or "1"
|
||||
value = s.as0or1(value)
|
||||
if value == "0" {
|
||||
remove = true
|
||||
}
|
||||
}
|
||||
// Webdav spec requires the operations to be executed in the order
|
||||
// specified in the PROPPATCH request
|
||||
// http://www.webdav.org/specs/rfc2518.html#rfc.section.8.2
|
||||
// FIXME: batch this somehow
|
||||
if remove {
|
||||
rreq.ArbitraryMetadataKeys[0] = key
|
||||
res, err := client.UnsetArbitraryMetadata(ctx, rreq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error sending a grpc UnsetArbitraryMetadata request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
if res.Status.Code != rpc.Code_CODE_OK {
|
||||
status := rstatus.HTTPStatusFromCode(res.Status.Code)
|
||||
if res.Status.Code == rpc.Code_CODE_ABORTED {
|
||||
// aborted is used for etag an lock mismatches, which translates to 412
|
||||
// in case a real Conflict response is needed, the calling code needs to send the header
|
||||
status = http.StatusPreconditionFailed
|
||||
}
|
||||
m := res.Status.Message
|
||||
if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
|
||||
// check if user has access to resource
|
||||
sRes, err := client.Stat(ctx, &provider.StatRequest{Ref: ref})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error performing stat grpc request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
if sRes.Status.Code != rpc.Code_CODE_OK {
|
||||
// return not found error so we do not leak existence of a file
|
||||
// TODO hide permission failed for users without access in every kind of request
|
||||
// TODO should this be done in the driver?
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
}
|
||||
if status == http.StatusNotFound {
|
||||
m = "Resource not found" // mimic the oc10 error message
|
||||
}
|
||||
w.WriteHeader(status)
|
||||
b, err := errors.Marshal(status, m, "", "")
|
||||
errors.HandleWebdavError(&log, w, b, err)
|
||||
return nil, nil, false
|
||||
}
|
||||
if key == "http://owncloud.org/ns/favorite" {
|
||||
statRes, err := client.Stat(ctx, &provider.StatRequest{Ref: ref})
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
currentUser := ctxpkg.ContextMustGetUser(ctx)
|
||||
ok, err := utils.CheckPermission(ctx, permission.WriteFavorites, client)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error checking permission")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
if !ok {
|
||||
log.Info().Interface("user", currentUser).Msg("user not allowed to unset favorite")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return nil, nil, false
|
||||
}
|
||||
err = s.favoritesManager.UnsetFavorite(ctx, currentUser.Id, statRes.Info)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
}
|
||||
removedProps = append(removedProps, propNameXML)
|
||||
} else {
|
||||
sreq.ArbitraryMetadata.Metadata[key] = value
|
||||
res, err := client.SetArbitraryMetadata(ctx, sreq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("key", key).Str("value", value).Msg("error sending a grpc SetArbitraryMetadata request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
if res.Status.Code != rpc.Code_CODE_OK {
|
||||
status := rstatus.HTTPStatusFromCode(res.Status.Code)
|
||||
if res.Status.Code == rpc.Code_CODE_ABORTED {
|
||||
// aborted is used for etag an lock mismatches, which translates to 412
|
||||
// in case a real Conflict response is needed, the calling code needs to send the header
|
||||
status = http.StatusPreconditionFailed
|
||||
}
|
||||
m := res.Status.Message
|
||||
if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
|
||||
// check if user has access to resource
|
||||
sRes, err := client.Stat(ctx, &provider.StatRequest{Ref: ref})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error performing stat grpc request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
if sRes.Status.Code != rpc.Code_CODE_OK {
|
||||
// return not found error so we don't leak existence of a file
|
||||
// TODO hide permission failed for users without access in every kind of request
|
||||
// TODO should this be done in the driver?
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
}
|
||||
if status == http.StatusNotFound {
|
||||
m = "Resource not found" // mimic the oc10 error message
|
||||
}
|
||||
w.WriteHeader(status)
|
||||
b, err := errors.Marshal(status, m, "", "")
|
||||
errors.HandleWebdavError(&log, w, b, err)
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
acceptedProps = append(acceptedProps, propNameXML)
|
||||
delete(sreq.ArbitraryMetadata.Metadata, key)
|
||||
|
||||
if key == "http://owncloud.org/ns/favorite" {
|
||||
statRes, err := client.Stat(ctx, &provider.StatRequest{Ref: ref})
|
||||
if err != nil || statRes.Info == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
currentUser := ctxpkg.ContextMustGetUser(ctx)
|
||||
ok, err := utils.CheckPermission(ctx, permission.WriteFavorites, client)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error checking permission")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
if !ok {
|
||||
log.Info().Interface("user", currentUser).Msg("user not allowed to set favorite")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return nil, nil, false
|
||||
}
|
||||
err = s.favoritesManager.SetFavorite(ctx, currentUser.Id, statRes.Info)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// FIXME: in case of error, need to set all properties back to the original state,
|
||||
// and return the error in the matching propstat block, if applicable
|
||||
// http://www.webdav.org/specs/rfc2518.html#rfc.section.8.2
|
||||
}
|
||||
|
||||
return acceptedProps, removedProps, true
|
||||
}
|
||||
|
||||
func (s *svc) handleProppatchResponse(ctx context.Context, w http.ResponseWriter, r *http.Request, acceptedProps, removedProps []xml.Name, path string, log zerolog.Logger) {
|
||||
propRes, err := s.formatProppatchResponse(ctx, acceptedProps, removedProps, path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error formatting proppatch response")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol")
|
||||
w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8")
|
||||
w.WriteHeader(http.StatusMultiStatus)
|
||||
if _, err := w.Write(propRes); err != nil {
|
||||
log.Err(err).Msg("error writing response")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *svc) formatProppatchResponse(ctx context.Context, acceptedProps []xml.Name, removedProps []xml.Name, ref string) ([]byte, error) {
|
||||
responses := make([]propfind.ResponseXML, 0, 1)
|
||||
response := propfind.ResponseXML{
|
||||
Href: net.EncodePath(ref),
|
||||
Propstat: []propfind.PropstatXML{},
|
||||
}
|
||||
|
||||
if len(acceptedProps) > 0 {
|
||||
propstatBody := []prop.PropertyXML{}
|
||||
for i := range acceptedProps {
|
||||
propstatBody = append(propstatBody, prop.EscapedNS(acceptedProps[i].Space, acceptedProps[i].Local, ""))
|
||||
}
|
||||
response.Propstat = append(response.Propstat, propfind.PropstatXML{
|
||||
Status: "HTTP/1.1 200 OK",
|
||||
Prop: propstatBody,
|
||||
})
|
||||
}
|
||||
|
||||
if len(removedProps) > 0 {
|
||||
propstatBody := []prop.PropertyXML{}
|
||||
for i := range removedProps {
|
||||
propstatBody = append(propstatBody, prop.EscapedNS(removedProps[i].Space, removedProps[i].Local, ""))
|
||||
}
|
||||
response.Propstat = append(response.Propstat, propfind.PropstatXML{
|
||||
Status: "HTTP/1.1 204 No Content",
|
||||
Prop: propstatBody,
|
||||
})
|
||||
}
|
||||
|
||||
responses = append(responses, response)
|
||||
responsesXML, err := xml.Marshal(&responses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(`<?xml version="1.0" encoding="utf-8"?><d:multistatus xmlns:d="DAV:" `)
|
||||
buf.WriteString(`xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns">`)
|
||||
buf.Write(responsesXML)
|
||||
buf.WriteString(`</d:multistatus>`)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (s *svc) isBooleanProperty(prop string) bool {
|
||||
// TODO add other properties we know to be boolean?
|
||||
return prop == net.PropOcFavorite
|
||||
}
|
||||
|
||||
func (s *svc) as0or1(val string) string {
|
||||
switch strings.TrimSpace(val) {
|
||||
case "false":
|
||||
return "0"
|
||||
case "":
|
||||
return "0"
|
||||
case "0":
|
||||
return "0"
|
||||
case "no":
|
||||
return "0"
|
||||
case "off":
|
||||
return "0"
|
||||
}
|
||||
return "1"
|
||||
}
|
||||
|
||||
// Proppatch describes a property update instruction as defined in RFC 4918.
|
||||
// See http://www.webdav.org/specs/rfc4918.html#METHOD_PROPPATCH
|
||||
type Proppatch struct {
|
||||
// Remove specifies whether this patch removes properties. If it does not
|
||||
// remove them, it sets them.
|
||||
Remove bool
|
||||
// Props contains the properties to be set or removed.
|
||||
Props []prop.PropertyXML
|
||||
}
|
||||
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_prop (for proppatch)
|
||||
type proppatchProps []prop.PropertyXML
|
||||
|
||||
// UnmarshalXML appends the property names and values enclosed within start
|
||||
// to ps.
|
||||
//
|
||||
// An xml:lang attribute that is defined either on the DAV:prop or property
|
||||
// name XML element is propagated to the property's Lang field.
|
||||
//
|
||||
// UnmarshalXML returns an error if start does not contain any properties or if
|
||||
// property values contain syntactically incorrect XML.
|
||||
func (ps *proppatchProps) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
lang := xmlLang(start, "")
|
||||
for {
|
||||
t, err := prop.Next(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch elem := t.(type) {
|
||||
case xml.EndElement:
|
||||
if len(*ps) == 0 {
|
||||
return fmt.Errorf("%s must not be empty", start.Name.Local)
|
||||
}
|
||||
return nil
|
||||
case xml.StartElement:
|
||||
p := prop.PropertyXML{}
|
||||
err = d.DecodeElement(&p, &elem)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// special handling for the lang property
|
||||
p.Lang = xmlLang(t.(xml.StartElement), lang)
|
||||
*ps = append(*ps, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_set
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_remove
|
||||
type setRemove struct {
|
||||
XMLName xml.Name
|
||||
Lang string `xml:"xml:lang,attr,omitempty"`
|
||||
Prop proppatchProps `xml:"DAV: prop"`
|
||||
}
|
||||
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propertyupdate
|
||||
type propertyupdate struct {
|
||||
XMLName xml.Name `xml:"DAV: propertyupdate"`
|
||||
Lang string `xml:"xml:lang,attr,omitempty"`
|
||||
SetRemove []setRemove `xml:",any"`
|
||||
}
|
||||
|
||||
func readProppatch(r io.Reader) (patches []Proppatch, status int, err error) {
|
||||
var pu propertyupdate
|
||||
if err = xml.NewDecoder(r).Decode(&pu); err != nil {
|
||||
return nil, http.StatusBadRequest, err
|
||||
}
|
||||
for _, op := range pu.SetRemove {
|
||||
remove := false
|
||||
switch op.XMLName {
|
||||
case xml.Name{Space: net.NsDav, Local: "set"}:
|
||||
// No-op.
|
||||
case xml.Name{Space: net.NsDav, Local: "remove"}:
|
||||
for _, p := range op.Prop {
|
||||
if len(p.InnerXML) > 0 {
|
||||
return nil, http.StatusBadRequest, errors.ErrInvalidProppatch
|
||||
}
|
||||
}
|
||||
remove = true
|
||||
default:
|
||||
return nil, http.StatusBadRequest, errors.ErrInvalidProppatch
|
||||
}
|
||||
patches = append(patches, Proppatch{Remove: remove, Props: op.Prop})
|
||||
}
|
||||
return patches, 0, nil
|
||||
}
|
||||
|
||||
var xmlLangName = xml.Name{Space: "http://www.w3.org/XML/1998/namespace", Local: "lang"}
|
||||
|
||||
func xmlLang(s xml.StartElement, d string) string {
|
||||
for _, attr := range s.Attr {
|
||||
if attr.Name == xmlLangName {
|
||||
return attr.Value
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
Generated
Vendored
+201
@@ -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 ocdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
ocdaverrors "github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
|
||||
)
|
||||
|
||||
// PublicFileHandler handles requests on a shared file. it needs to be wrapped in a collection
|
||||
type PublicFileHandler struct {
|
||||
namespace string
|
||||
}
|
||||
|
||||
func (h *PublicFileHandler) init(ns string) error {
|
||||
h.namespace = path.Join("/", ns)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handler handles requests
|
||||
func (h *PublicFileHandler) Handler(s *svc) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
log := appctx.GetLogger(ctx)
|
||||
token, relativePath := router.ShiftPath(r.URL.Path)
|
||||
|
||||
base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), token)
|
||||
ctx = context.WithValue(ctx, net.CtxKeyBaseURI, base)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
log.Debug().Str("relativePath", relativePath).Msg("PublicFileHandler func")
|
||||
|
||||
if relativePath != "" && relativePath != "/" {
|
||||
// accessing the file
|
||||
|
||||
switch r.Method {
|
||||
case MethodPropfind:
|
||||
s.handlePropfindOnToken(w, r, h.namespace, false)
|
||||
case http.MethodGet:
|
||||
s.handlePathGet(w, r, h.namespace)
|
||||
case http.MethodOptions:
|
||||
s.handleOptions(w, r)
|
||||
case http.MethodHead:
|
||||
s.handlePathHead(w, r, h.namespace)
|
||||
case http.MethodPut:
|
||||
s.handlePathPut(w, r, h.namespace)
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
} else {
|
||||
// accessing the virtual parent folder
|
||||
switch r.Method {
|
||||
case MethodPropfind:
|
||||
s.handlePropfindOnToken(w, r, h.namespace, true)
|
||||
case http.MethodOptions:
|
||||
s.handleOptions(w, r)
|
||||
case http.MethodHead:
|
||||
s.handlePathHead(w, r, h.namespace)
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ns is the namespace that is prefixed to the path in the cs3 namespace
|
||||
func (s *svc) handlePropfindOnToken(w http.ResponseWriter, r *http.Request, ns string, onContainer bool) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "token_propfind")
|
||||
defer span.End()
|
||||
|
||||
tokenStatInfo, ok := TokenStatInfoFromContext(ctx)
|
||||
if !ok {
|
||||
span.RecordError(ocdaverrors.ErrTokenStatInfoMissing)
|
||||
span.SetStatus(codes.Error, ocdaverrors.ErrTokenStatInfoMissing.Error())
|
||||
span.SetAttributes(semconv.HTTPStatusCodeKey.Int(http.StatusInternalServerError))
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
b, err := ocdaverrors.Marshal(http.StatusInternalServerError, ocdaverrors.ErrTokenStatInfoMissing.Error(), "", "")
|
||||
ocdaverrors.HandleWebdavError(appctx.GetLogger(ctx), w, b, err)
|
||||
return
|
||||
}
|
||||
sublog := appctx.GetLogger(ctx).With().Interface("tokenStatInfo", tokenStatInfo).Logger()
|
||||
sublog.Debug().Msg("handlePropfindOnToken")
|
||||
|
||||
dh := r.Header.Get(net.HeaderDepth)
|
||||
depth, err := net.ParseDepth(dh)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, "Invalid Depth header value")
|
||||
span.SetAttributes(semconv.HTTPStatusCodeKey.Int(http.StatusBadRequest))
|
||||
sublog.Debug().Str("depth", dh).Msg(err.Error())
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
m := fmt.Sprintf("Invalid Depth header value: %v", dh)
|
||||
b, err := ocdaverrors.Marshal(http.StatusBadRequest, m, "", "")
|
||||
ocdaverrors.HandleWebdavError(&sublog, w, b, err)
|
||||
return
|
||||
}
|
||||
|
||||
if depth == net.DepthInfinity && !s.c.AllowPropfindDepthInfinitiy {
|
||||
span.RecordError(ocdaverrors.ErrInvalidDepth)
|
||||
span.SetStatus(codes.Error, "DEPTH: infinity is not supported")
|
||||
span.SetAttributes(semconv.HTTPStatusCodeKey.Int(http.StatusBadRequest))
|
||||
sublog.Debug().Str("depth", dh).Msg(ocdaverrors.ErrInvalidDepth.Error())
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
m := fmt.Sprintf("Invalid Depth header value: %v", dh)
|
||||
b, err := ocdaverrors.Marshal(http.StatusBadRequest, m, "", "")
|
||||
ocdaverrors.HandleWebdavError(&sublog, w, b, err)
|
||||
return
|
||||
}
|
||||
|
||||
pf, status, err := propfind.ReadPropfind(r.Body)
|
||||
if err != nil {
|
||||
sublog.Debug().Err(err).Msg("error reading propfind request")
|
||||
w.WriteHeader(status)
|
||||
return
|
||||
}
|
||||
|
||||
infos := s.getPublicFileInfos(onContainer, depth == net.DepthZero, tokenStatInfo)
|
||||
|
||||
prefer := net.ParsePrefer(r.Header.Get("prefer"))
|
||||
returnMinimal := prefer[net.HeaderPreferReturn] == "minimal"
|
||||
|
||||
propRes, err := propfind.MultistatusResponse(ctx, &pf, infos, s.c.PublicURL, ns, nil, returnMinimal)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error formatting propfind")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol")
|
||||
w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8")
|
||||
w.Header().Set(net.HeaderVary, net.HeaderPrefer)
|
||||
if returnMinimal {
|
||||
w.Header().Set(net.HeaderPreferenceApplied, "return=minimal")
|
||||
}
|
||||
w.WriteHeader(http.StatusMultiStatus)
|
||||
if _, err := w.Write(propRes); err != nil {
|
||||
sublog.Err(err).Msg("error writing response")
|
||||
}
|
||||
}
|
||||
|
||||
// there are only two possible entries
|
||||
// 1. the non existing collection
|
||||
// 2. the shared file
|
||||
func (s *svc) getPublicFileInfos(onContainer, onlyRoot bool, i *provider.ResourceInfo) []*provider.ResourceInfo {
|
||||
infos := []*provider.ResourceInfo{}
|
||||
if onContainer {
|
||||
// copy link-share data if present
|
||||
// we don't copy everything because the checksum should not be present
|
||||
var o *typesv1beta1.Opaque
|
||||
if i.Opaque != nil && i.Opaque.Map != nil && i.Opaque.Map["link-share"] != nil {
|
||||
o = &typesv1beta1.Opaque{
|
||||
Map: map[string]*typesv1beta1.OpaqueEntry{
|
||||
"link-share": i.Opaque.Map["link-share"],
|
||||
},
|
||||
}
|
||||
}
|
||||
// always add collection
|
||||
infos = append(infos, &provider.ResourceInfo{
|
||||
// Opaque carries the link-share data we need when rendering the collection root href
|
||||
Opaque: o,
|
||||
Path: path.Dir(i.Path),
|
||||
Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER,
|
||||
})
|
||||
if onlyRoot {
|
||||
return infos
|
||||
}
|
||||
}
|
||||
|
||||
// add the file info
|
||||
infos = append(infos, i)
|
||||
|
||||
return infos
|
||||
}
|
||||
Generated
Vendored
+456
@@ -0,0 +1,456 @@
|
||||
// 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 ocdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "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/datagateway"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
|
||||
"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/storagespace"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/rs/zerolog"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
)
|
||||
|
||||
func sufferMacOSFinder(r *http.Request) bool {
|
||||
return r.Header.Get(net.HeaderExpectedEntityLength) != ""
|
||||
}
|
||||
|
||||
func handleMacOSFinder(w http.ResponseWriter, r *http.Request) error {
|
||||
/*
|
||||
Many webservers will not cooperate well with Finder PUT requests,
|
||||
because it uses 'Chunked' transfer encoding for the request body.
|
||||
The symptom of this problem is that Finder sends files to the
|
||||
server, but they arrive as 0-length files.
|
||||
If we don't do anything, the user might think they are uploading
|
||||
files successfully, but they end up empty on the server. Instead,
|
||||
we throw back an error if we detect this.
|
||||
The reason Finder uses Chunked, is because it thinks the files
|
||||
might change as it's being uploaded, and therefore the
|
||||
Content-Length can vary.
|
||||
Instead it sends the X-Expected-Entity-Length header with the size
|
||||
of the file at the very start of the request. If this header is set,
|
||||
but we don't get a request body we will fail the request to
|
||||
protect the end-user.
|
||||
*/
|
||||
|
||||
log := appctx.GetLogger(r.Context())
|
||||
content := r.Header.Get(net.HeaderContentLength)
|
||||
expected := r.Header.Get(net.HeaderExpectedEntityLength)
|
||||
log.Warn().Str("content-length", content).Str("x-expected-entity-length", expected).Msg("Mac OS Finder corner-case detected")
|
||||
|
||||
// The best mitigation to this problem is to tell users to not use crappy Finder.
|
||||
// Another possible mitigation is to change the use the value of X-Expected-Entity-Length header in the Content-Length header.
|
||||
expectedInt, err := strconv.ParseInt(expected, 10, 64)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error parsing expected length")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return err
|
||||
}
|
||||
r.ContentLength = expectedInt
|
||||
return nil
|
||||
}
|
||||
|
||||
func isContentRange(r *http.Request) bool {
|
||||
/*
|
||||
Content-Range is dangerous for PUT requests: PUT per definition
|
||||
stores a full resource. draft-ietf-httpbis-p2-semantics-15 says
|
||||
in section 7.6:
|
||||
An origin server SHOULD reject any PUT request that contains a
|
||||
Content-Range header field, since it might be misinterpreted as
|
||||
partial content (or might be partial content that is being mistakenly
|
||||
PUT as a full representation). Partial content updates are possible
|
||||
by targeting a separately identified resource with state that
|
||||
overlaps a portion of the larger resource, or by using a different
|
||||
method that has been specifically defined for partial updates (for
|
||||
example, the PATCH method defined in [RFC5789]).
|
||||
This clarifies RFC2616 section 9.6:
|
||||
The recipient of the entity MUST NOT ignore any Content-*
|
||||
(e.g. Content-Range) headers that it does not understand or implement
|
||||
and MUST return a 501 (Not Implemented) response in such cases.
|
||||
OTOH is a PUT request with a Content-Range currently the only way to
|
||||
continue an aborted upload request and is supported by curl, mod_dav,
|
||||
Tomcat and others. Since some clients do use this feature which results
|
||||
in unexpected behaviour (cf PEAR::HTTP_WebDAV_Client 1.0.1), we reject
|
||||
all PUT requests with a Content-Range for now.
|
||||
*/
|
||||
return r.Header.Get(net.HeaderContentRange) != ""
|
||||
}
|
||||
|
||||
func (s *svc) handlePathPut(w http.ResponseWriter, r *http.Request, ns string) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "put")
|
||||
defer span.End()
|
||||
|
||||
fn := path.Join(ns, r.URL.Path)
|
||||
sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger()
|
||||
|
||||
if err := ValidateName(filename(r.URL.Path), s.nameValidators); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
b, err := errors.Marshal(http.StatusBadRequest, err.Error(), "", "")
|
||||
errors.HandleWebdavError(&sublog, w, b, err)
|
||||
return
|
||||
}
|
||||
|
||||
space, status, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, fn)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Str("path", fn).Msg("failed to look up storage space")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if status.Code != rpc.Code_CODE_OK {
|
||||
errors.HandleErrorStatus(&sublog, w, status)
|
||||
return
|
||||
}
|
||||
|
||||
s.handlePut(ctx, w, r, spacelookup.MakeRelativeReference(space, fn, false), sublog)
|
||||
}
|
||||
|
||||
func (s *svc) handlePut(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, log zerolog.Logger) {
|
||||
if !checkPreconditions(w, r, log) {
|
||||
// checkPreconditions handles error returns
|
||||
return
|
||||
}
|
||||
|
||||
length, err := getContentLength(r)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error getting the content length")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error selecting next gateway client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Test if the target is a secret filedrop
|
||||
tokenStatInfo, ok := TokenStatInfoFromContext(ctx)
|
||||
// We assume that when the uploader can create containers, but is not allowed to list them, it is a secret file drop
|
||||
if ok && tokenStatInfo.GetPermissionSet().CreateContainer && !tokenStatInfo.GetPermissionSet().ListContainer {
|
||||
// TODO we can skip this stat if the tokenStatInfo is the direct parent
|
||||
sReq := &provider.StatRequest{
|
||||
Ref: ref,
|
||||
}
|
||||
sRes, err := client.Stat(ctx, sReq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error sending grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// We also need to continue if we are not allowed to stat a resource. We may not have stat permission. That still means it exists and we need to find a new filename.
|
||||
switch sRes.Status.Code {
|
||||
case rpc.Code_CODE_OK, rpc.Code_CODE_PERMISSION_DENIED:
|
||||
// find next filename
|
||||
newName, status, err := FindName(ctx, client, filepath.Base(ref.Path), sRes.GetInfo().GetParentId())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error sending grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if status.Code != rpc.Code_CODE_OK {
|
||||
log.Error().Interface("status", status).Msg("error listing file")
|
||||
errors.HandleErrorStatus(&log, w, status)
|
||||
return
|
||||
}
|
||||
ref.Path = utils.MakeRelativePath(filepath.Join(filepath.Dir(ref.GetPath()), newName))
|
||||
case rpc.Code_CODE_NOT_FOUND:
|
||||
// just continue with normal upload
|
||||
default:
|
||||
log.Error().Interface("status", sRes.Status).Msg("error stating file")
|
||||
errors.HandleErrorStatus(&log, w, sRes.Status)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
opaque := &typespb.Opaque{}
|
||||
if mtime := r.Header.Get(net.HeaderOCMtime); mtime != "" {
|
||||
utils.AppendPlainToOpaque(opaque, net.HeaderOCMtime, mtime)
|
||||
|
||||
// TODO: find a way to check if the storage really accepted the value
|
||||
w.Header().Set(net.HeaderOCMtime, "accepted")
|
||||
}
|
||||
if length == 0 {
|
||||
tfRes, err := client.TouchFile(ctx, &provider.TouchFileRequest{
|
||||
Opaque: opaque,
|
||||
Ref: ref,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error sending grpc touch file request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if tfRes.Status.Code == rpc.Code_CODE_OK {
|
||||
sRes, err := client.Stat(ctx, &provider.StatRequest{
|
||||
Ref: ref,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error sending grpc touch file request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if sRes.Status.Code != rpc.Code_CODE_OK {
|
||||
log.Error().Interface("status", sRes.Status).Msg("error touching file")
|
||||
errors.HandleErrorStatus(&log, w, sRes.Status)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set(net.HeaderETag, sRes.Info.Etag)
|
||||
w.Header().Set(net.HeaderOCETag, sRes.Info.Etag)
|
||||
w.Header().Set(net.HeaderOCFileID, storagespace.FormatResourceID(sRes.Info.Id))
|
||||
w.Header().Set(net.HeaderLastModified, net.RFC1123Z(sRes.Info.Mtime))
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
return
|
||||
}
|
||||
|
||||
if tfRes.Status.Code != rpc.Code_CODE_ALREADY_EXISTS {
|
||||
log.Error().Interface("status", tfRes.Status).Msg("error touching file")
|
||||
errors.HandleErrorStatus(&log, w, tfRes.Status)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
utils.AppendPlainToOpaque(opaque, net.HeaderUploadLength, strconv.FormatInt(length, 10))
|
||||
|
||||
// curl -X PUT https://demo.owncloud.com/remote.php/webdav/testcs.bin -u demo:demo -d '123' -v -H 'OC-Checksum: SHA1:40bd001563085fc35165329ea1ff5c5ecbdbbeef'
|
||||
|
||||
var cparts []string
|
||||
// TUS Upload-Checksum header takes precedence
|
||||
if checksum := r.Header.Get(net.HeaderUploadChecksum); checksum != "" {
|
||||
cparts = strings.SplitN(checksum, " ", 2)
|
||||
if len(cparts) != 2 {
|
||||
log.Debug().Str("upload-checksum", checksum).Msg("invalid Upload-Checksum format, expected '[algorithm] [checksum]'")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Then try owncloud header
|
||||
} else if checksum := r.Header.Get(net.HeaderOCChecksum); checksum != "" {
|
||||
cparts = strings.SplitN(checksum, ":", 2)
|
||||
if len(cparts) != 2 {
|
||||
log.Debug().Str("oc-checksum", checksum).Msg("invalid OC-Checksum format, expected '[algorithm]:[checksum]'")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
// we do not check the algorithm here, because it might depend on the storage
|
||||
if len(cparts) == 2 {
|
||||
// Translate into TUS style Upload-Checksum header
|
||||
// algorithm is always lowercase, checksum is separated by space
|
||||
utils.AppendPlainToOpaque(opaque, net.HeaderUploadChecksum, strings.ToLower(cparts[0])+" "+cparts[1])
|
||||
}
|
||||
|
||||
uReq := &provider.InitiateFileUploadRequest{
|
||||
Ref: ref,
|
||||
Opaque: opaque,
|
||||
LockId: requestLockToken(r),
|
||||
}
|
||||
if ifMatch := r.Header.Get(net.HeaderIfMatch); ifMatch != "" {
|
||||
uReq.Options = &provider.InitiateFileUploadRequest_IfMatch{IfMatch: ifMatch}
|
||||
}
|
||||
|
||||
// where to upload the file?
|
||||
uRes, err := client.InitiateFileUpload(ctx, uReq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error initiating file upload")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if uRes.Status.Code != rpc.Code_CODE_OK {
|
||||
if r.ProtoMajor == 1 {
|
||||
// drain body to avoid `connection closed` errors
|
||||
_, _ = io.Copy(io.Discard, r.Body)
|
||||
}
|
||||
switch uRes.Status.Code {
|
||||
case rpc.Code_CODE_PERMISSION_DENIED:
|
||||
status := http.StatusForbidden
|
||||
m := uRes.Status.Message
|
||||
// check if user has access to parent
|
||||
sRes, err := client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{
|
||||
ResourceId: ref.ResourceId,
|
||||
Path: utils.MakeRelativePath(path.Dir(ref.Path)),
|
||||
}})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error performing stat grpc request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if sRes.Status.Code != rpc.Code_CODE_OK {
|
||||
// return not found error so we do not leak existence of a file
|
||||
// TODO hide permission failed for users without access in every kind of request
|
||||
// TODO should this be done in the driver?
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
if status == http.StatusNotFound {
|
||||
m = "Resource not found" // mimic the oc10 error message
|
||||
}
|
||||
w.WriteHeader(status)
|
||||
b, err := errors.Marshal(status, m, "", "")
|
||||
errors.HandleWebdavError(&log, w, b, err)
|
||||
case rpc.Code_CODE_ABORTED:
|
||||
w.WriteHeader(http.StatusPreconditionFailed)
|
||||
case rpc.Code_CODE_FAILED_PRECONDITION:
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
default:
|
||||
errors.HandleErrorStatus(&log, w, uRes.Status)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ony send actual PUT request if file has bytes. Otherwise the initiate file upload request creates the file
|
||||
if length != 0 {
|
||||
var ep, token string
|
||||
for _, p := range uRes.Protocols {
|
||||
if p.Protocol == "simple" {
|
||||
ep, token = p.UploadEndpoint, p.Token
|
||||
}
|
||||
}
|
||||
|
||||
httpReq, err := rhttp.NewRequest(ctx, http.MethodPut, ep, r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
Propagator.Inject(ctx, propagation.HeaderCarrier(httpReq.Header))
|
||||
httpReq.Header.Set(datagateway.TokenTransportHeader, token)
|
||||
httpReq.ContentLength = length
|
||||
|
||||
httpRes, err := s.client.Do(httpReq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error doing PUT request to data service")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer httpRes.Body.Close()
|
||||
if httpRes.StatusCode != http.StatusOK {
|
||||
if httpRes.StatusCode == http.StatusPartialContent {
|
||||
w.WriteHeader(http.StatusPartialContent)
|
||||
return
|
||||
}
|
||||
if httpRes.StatusCode == errtypes.StatusChecksumMismatch {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
b, err := errors.Marshal(http.StatusBadRequest, "The computed checksum does not match the one received from the client.", "", "")
|
||||
errors.HandleWebdavError(&log, w, b, err)
|
||||
return
|
||||
}
|
||||
log.Error().Err(err).Msg("PUT request to data server failed")
|
||||
w.WriteHeader(httpRes.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
// copy headers if they are present
|
||||
if httpRes.Header.Get(net.HeaderETag) != "" {
|
||||
w.Header().Set(net.HeaderETag, httpRes.Header.Get(net.HeaderETag))
|
||||
}
|
||||
if httpRes.Header.Get(net.HeaderOCETag) != "" {
|
||||
w.Header().Set(net.HeaderOCETag, httpRes.Header.Get(net.HeaderOCETag))
|
||||
}
|
||||
if httpRes.Header.Get(net.HeaderOCFileID) != "" {
|
||||
w.Header().Set(net.HeaderOCFileID, httpRes.Header.Get(net.HeaderOCFileID))
|
||||
}
|
||||
if httpRes.Header.Get(net.HeaderLastModified) != "" {
|
||||
w.Header().Set(net.HeaderLastModified, httpRes.Header.Get(net.HeaderLastModified))
|
||||
}
|
||||
}
|
||||
|
||||
// file was new
|
||||
// FIXME make created flag a property on the InitiateFileUploadResponse
|
||||
if created := utils.ReadPlainFromOpaque(uRes.Opaque, "created"); created == "true" {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
return
|
||||
}
|
||||
|
||||
// overwrite
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *svc) handleSpacesPut(w http.ResponseWriter, r *http.Request, spaceID string) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "spaces_put")
|
||||
defer span.End()
|
||||
|
||||
sublog := appctx.GetLogger(ctx).With().Str("spaceid", spaceID).Str("path", r.URL.Path).Logger()
|
||||
|
||||
ref, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if ref.GetResourceId().GetOpaqueId() != "" && ref.GetResourceId().GetSpaceId() != ref.GetResourceId().GetOpaqueId() && r.URL.Path == "/" {
|
||||
s.handlePut(ctx, w, r, &ref, sublog)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ValidateName(filename(ref.Path), s.nameValidators); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
b, err := errors.Marshal(http.StatusBadRequest, err.Error(), "", "")
|
||||
errors.HandleWebdavError(&sublog, w, b, err)
|
||||
return
|
||||
}
|
||||
|
||||
s.handlePut(ctx, w, r, &ref, sublog)
|
||||
}
|
||||
|
||||
func checkPreconditions(w http.ResponseWriter, r *http.Request, log zerolog.Logger) bool {
|
||||
if isContentRange(r) {
|
||||
log.Debug().Msg("Content-Range not supported for PUT")
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
return false
|
||||
}
|
||||
|
||||
if sufferMacOSFinder(r) {
|
||||
err := handleMacOSFinder(w, r)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("error handling Mac OS corner-case")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getContentLength(r *http.Request) (int64, error) {
|
||||
length, err := strconv.ParseInt(r.Header.Get(net.HeaderContentLength), 10, 64)
|
||||
if err != nil {
|
||||
// Fallback to Upload-Length
|
||||
length, err = strconv.ParseInt(r.Header.Get(net.HeaderUploadLength), 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return length, nil
|
||||
}
|
||||
Generated
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
// 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 ocdav
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
)
|
||||
|
||||
func (s *svc) handleLegacyPath(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
dir := query.Get("dir")
|
||||
url := s.c.PublicURL + path.Join("#", "/files/list/all", url.PathEscape(dir))
|
||||
http.Redirect(w, r, url, http.StatusMovedPermanently)
|
||||
}
|
||||
Generated
Vendored
+199
@@ -0,0 +1,199 @@
|
||||
// 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 ocdav
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/permission"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
elementNameSearchFiles = "search-files"
|
||||
elementNameFilterFiles = "filter-files"
|
||||
)
|
||||
|
||||
func (s *svc) handleReport(w http.ResponseWriter, r *http.Request, ns string) {
|
||||
ctx := r.Context()
|
||||
log := appctx.GetLogger(ctx)
|
||||
// fn := path.Join(ns, r.URL.Path)
|
||||
|
||||
rep, status, err := readReport(r.Body)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error reading report")
|
||||
w.WriteHeader(status)
|
||||
return
|
||||
}
|
||||
if rep.SearchFiles != nil {
|
||||
s.doSearchFiles(w, r, rep.SearchFiles)
|
||||
return
|
||||
}
|
||||
|
||||
if rep.FilterFiles != nil {
|
||||
s.doFilterFiles(w, r, rep.FilterFiles, ns)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(jfd): implement report
|
||||
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
func (s *svc) doSearchFiles(w http.ResponseWriter, r *http.Request, sf *reportSearchFiles) {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
func (s *svc) doFilterFiles(w http.ResponseWriter, r *http.Request, ff *reportFilterFiles, namespace string) {
|
||||
ctx := r.Context()
|
||||
log := appctx.GetLogger(ctx)
|
||||
|
||||
if ff.Rules.Favorite {
|
||||
// List the users favorite resources.
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error selecting next gateway client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
currentUser := ctxpkg.ContextMustGetUser(ctx)
|
||||
ok, err := utils.CheckPermission(ctx, permission.ListFavorites, client)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error checking permission")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
log.Info().Interface("user", currentUser).Msg("user not allowed to list favorites")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
favorites, err := s.favoritesManager.ListFavorites(ctx, currentUser.Id)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error getting favorites")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
infos := make([]*provider.ResourceInfo, 0, len(favorites))
|
||||
for i := range favorites {
|
||||
statRes, err := client.Stat(ctx, &providerv1beta1.StatRequest{Ref: &providerv1beta1.Reference{ResourceId: favorites[i]}})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error getting resource info")
|
||||
continue
|
||||
}
|
||||
if statRes.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
log.Error().Interface("stat_response", statRes).Msg("error getting resource info")
|
||||
continue
|
||||
}
|
||||
infos = append(infos, statRes.Info)
|
||||
}
|
||||
|
||||
prefer := net.ParsePrefer(r.Header.Get("prefer"))
|
||||
returnMinimal := prefer[net.HeaderPreferReturn] == "minimal"
|
||||
|
||||
responsesXML, err := propfind.MultistatusResponse(ctx, &propfind.XML{Prop: ff.Prop}, infos, s.c.PublicURL, namespace, nil, returnMinimal)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error formatting propfind")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol")
|
||||
w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8")
|
||||
w.Header().Set(net.HeaderVary, net.HeaderPrefer)
|
||||
if returnMinimal {
|
||||
w.Header().Set(net.HeaderPreferenceApplied, "return=minimal")
|
||||
}
|
||||
w.WriteHeader(http.StatusMultiStatus)
|
||||
if _, err := w.Write(responsesXML); err != nil {
|
||||
log.Err(err).Msg("error writing response")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type report struct {
|
||||
SearchFiles *reportSearchFiles
|
||||
// FilterFiles TODO add this for tag based search
|
||||
FilterFiles *reportFilterFiles `xml:"filter-files"`
|
||||
}
|
||||
type reportSearchFiles struct {
|
||||
XMLName xml.Name `xml:"search-files"`
|
||||
Lang string `xml:"xml:lang,attr,omitempty"`
|
||||
Prop propfind.Props `xml:"DAV: prop"`
|
||||
Search reportSearchFilesSearch `xml:"search"`
|
||||
}
|
||||
type reportSearchFilesSearch struct {
|
||||
Pattern string `xml:"search"`
|
||||
Limit int `xml:"limit"`
|
||||
Offset int `xml:"offset"`
|
||||
}
|
||||
|
||||
type reportFilterFiles struct {
|
||||
XMLName xml.Name `xml:"filter-files"`
|
||||
Lang string `xml:"xml:lang,attr,omitempty"`
|
||||
Prop propfind.Props `xml:"DAV: prop"`
|
||||
Rules reportFilterFilesRules `xml:"filter-rules"`
|
||||
}
|
||||
|
||||
type reportFilterFilesRules struct {
|
||||
Favorite bool `xml:"favorite"`
|
||||
SystemTag int `xml:"systemtag"`
|
||||
}
|
||||
|
||||
func readReport(r io.Reader) (rep *report, status int, err error) {
|
||||
decoder := xml.NewDecoder(r)
|
||||
rep = &report{}
|
||||
for {
|
||||
t, err := decoder.Token()
|
||||
if err == io.EOF {
|
||||
// io.EOF is a successful end
|
||||
return rep, 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
if v, ok := t.(xml.StartElement); ok {
|
||||
if v.Name.Local == elementNameSearchFiles {
|
||||
var repSF reportSearchFiles
|
||||
err = decoder.DecodeElement(&repSF, &v)
|
||||
if err != nil {
|
||||
return nil, http.StatusBadRequest, err
|
||||
}
|
||||
rep.SearchFiles = &repSF
|
||||
} else if v.Name.Local == elementNameFilterFiles {
|
||||
var repFF reportFilterFiles
|
||||
err = decoder.DecodeElement(&repFF, &v)
|
||||
if err != nil {
|
||||
return nil, http.StatusBadRequest, err
|
||||
}
|
||||
rep.FilterFiles = &repFF
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+191
@@ -0,0 +1,191 @@
|
||||
// Copyright 2018-2022 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 spacelookup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
storageProvider "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/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/utils"
|
||||
"google.golang.org/protobuf/types/known/fieldmaskpb"
|
||||
)
|
||||
|
||||
// LookupReferenceForPath returns:
|
||||
// a reference with root and relative path
|
||||
// the status and error for the lookup
|
||||
func LookupReferenceForPath(ctx context.Context, selector pool.Selectable[gateway.GatewayAPIClient], path string) (*storageProvider.Reference, *rpc.Status, error) {
|
||||
space, cs3Status, err := LookUpStorageSpaceForPath(ctx, selector, path)
|
||||
if err != nil || cs3Status.Code != rpc.Code_CODE_OK {
|
||||
return nil, cs3Status, err
|
||||
}
|
||||
spacePath := string(space.Opaque.Map["path"].Value) // FIXME error checks
|
||||
return &storageProvider.Reference{
|
||||
ResourceId: space.Root,
|
||||
Path: utils.MakeRelativePath(strings.TrimPrefix(path, spacePath)),
|
||||
}, cs3Status, nil
|
||||
}
|
||||
|
||||
// LookUpStorageSpaceForPath returns:
|
||||
// the storage spaces responsible for a path
|
||||
// the status and error for the lookup
|
||||
func LookUpStorageSpaceForPath(ctx context.Context, selector pool.Selectable[gateway.GatewayAPIClient], path string) (*storageProvider.StorageSpace, *rpc.Status, error) {
|
||||
// TODO add filter to only fetch spaces changed in the last 30 sec?
|
||||
// TODO cache space information, invalidate after ... 5min? so we do not need to fetch all spaces?
|
||||
// TODO use ListContainerStream to listen for changes
|
||||
// retrieve a specific storage space
|
||||
lSSReq := &storageProvider.ListStorageSpacesRequest{
|
||||
Opaque: &typesv1beta1.Opaque{
|
||||
Map: map[string]*typesv1beta1.OpaqueEntry{
|
||||
"path": {
|
||||
Decoder: "plain",
|
||||
Value: []byte(path),
|
||||
},
|
||||
"unique": {
|
||||
Decoder: "plain",
|
||||
Value: []byte(strconv.FormatBool(true)),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
client, err := selector.Next()
|
||||
if err != nil {
|
||||
return nil, status.NewInternal(ctx, "could not select next client"), err
|
||||
}
|
||||
|
||||
lSSRes, err := client.ListStorageSpaces(ctx, lSSReq)
|
||||
if err != nil || lSSRes.Status.Code != rpc.Code_CODE_OK {
|
||||
status := status.NewStatusFromErrType(ctx, "failed to lookup storage spaces", err)
|
||||
if lSSRes != nil {
|
||||
status = lSSRes.Status
|
||||
}
|
||||
return nil, status, err
|
||||
}
|
||||
switch len(lSSRes.StorageSpaces) {
|
||||
case 0:
|
||||
return nil, status.NewNotFound(ctx, "no space found"), nil
|
||||
case 1:
|
||||
return lSSRes.StorageSpaces[0], lSSRes.Status, nil
|
||||
}
|
||||
|
||||
return nil, status.NewInternal(ctx, "too many spaces returned"), nil
|
||||
}
|
||||
|
||||
// LookUpStorageSpacesForPathWithChildren returns:
|
||||
// the list of storage spaces responsible for a path
|
||||
// the status and error for the lookup
|
||||
func LookUpStorageSpacesForPathWithChildren(ctx context.Context, client gateway.GatewayAPIClient, path string) ([]*storageProvider.StorageSpace, *rpc.Status, error) {
|
||||
// TODO add filter to only fetch spaces changed in the last 30 sec?
|
||||
// TODO cache space information, invalidate after ... 5min? so we do not need to fetch all spaces?
|
||||
// TODO use ListContainerStream to listen for changes
|
||||
// retrieve a specific storage space
|
||||
lSSReq := &storageProvider.ListStorageSpacesRequest{
|
||||
// get all fields, including root_info
|
||||
FieldMask: &fieldmaskpb.FieldMask{Paths: []string{"*"}},
|
||||
}
|
||||
// list all providers at or below the given path
|
||||
lSSReq.Opaque = utils.AppendPlainToOpaque(lSSReq.Opaque, "path", path)
|
||||
// we want to get all metadata? really? when looking up the space roots we actually only want etag, mtime and type so we can construct a child ...
|
||||
lSSReq.Opaque = utils.AppendPlainToOpaque(lSSReq.Opaque, "metadata", "*")
|
||||
|
||||
lSSRes, err := client.ListStorageSpaces(ctx, lSSReq)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if lSSRes.Status.GetCode() != rpc.Code_CODE_OK {
|
||||
return nil, lSSRes.Status, err
|
||||
}
|
||||
|
||||
return lSSRes.StorageSpaces, lSSRes.Status, nil
|
||||
}
|
||||
|
||||
// LookUpStorageSpaceByID find a space by ID
|
||||
func LookUpStorageSpaceByID(ctx context.Context, client gateway.GatewayAPIClient, spaceID string) (*storageProvider.StorageSpace, *rpc.Status, error) {
|
||||
// retrieve a specific storage space
|
||||
lSSReq := &storageProvider.ListStorageSpacesRequest{
|
||||
Opaque: &typesv1beta1.Opaque{},
|
||||
Filters: []*storageProvider.ListStorageSpacesRequest_Filter{
|
||||
{
|
||||
Type: storageProvider.ListStorageSpacesRequest_Filter_TYPE_ID,
|
||||
Term: &storageProvider.ListStorageSpacesRequest_Filter_Id{
|
||||
Id: &storageProvider.StorageSpaceId{
|
||||
OpaqueId: spaceID,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
lSSRes, err := client.ListStorageSpaces(ctx, lSSReq)
|
||||
if err != nil || lSSRes.Status.Code != rpc.Code_CODE_OK {
|
||||
return nil, lSSRes.Status, err
|
||||
}
|
||||
|
||||
switch len(lSSRes.StorageSpaces) {
|
||||
case 0:
|
||||
return nil, &rpc.Status{Code: rpc.Code_CODE_NOT_FOUND}, nil // since the caller only expects a single space return not found status
|
||||
case 1:
|
||||
return lSSRes.StorageSpaces[0], lSSRes.Status, nil
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("unexpected number of spaces %d", len(lSSRes.StorageSpaces))
|
||||
}
|
||||
}
|
||||
|
||||
// MakeStorageSpaceReference find a space by id and returns a relative reference
|
||||
func MakeStorageSpaceReference(spaceID string, relativePath string) (storageProvider.Reference, error) {
|
||||
resourceID, err := storagespace.ParseID(spaceID)
|
||||
if err != nil {
|
||||
return storageProvider.Reference{}, err
|
||||
}
|
||||
// be tolerant about missing sharesstorageprovider id
|
||||
if resourceID.StorageId == "" && resourceID.SpaceId == utils.ShareStorageSpaceID {
|
||||
resourceID.StorageId = utils.ShareStorageProviderID
|
||||
}
|
||||
return storageProvider.Reference{
|
||||
ResourceId: &resourceID,
|
||||
Path: utils.MakeRelativePath(relativePath),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MakeRelativeReference returns a relative reference for the given space and path
|
||||
func MakeRelativeReference(space *storageProvider.StorageSpace, relativePath string, spacesDavRequest bool) *storageProvider.Reference {
|
||||
if space.Opaque == nil || space.Opaque.Map == nil || space.Opaque.Map["path"] == nil || space.Opaque.Map["path"].Decoder != "plain" {
|
||||
return nil // not mounted
|
||||
}
|
||||
spacePath := string(space.Opaque.Map["path"].Value)
|
||||
relativeSpacePath := "."
|
||||
if strings.HasPrefix(relativePath, spacePath) {
|
||||
relativeSpacePath = utils.MakeRelativePath(strings.TrimPrefix(relativePath, spacePath))
|
||||
} else if spacesDavRequest {
|
||||
relativeSpacePath = utils.MakeRelativePath(relativePath)
|
||||
}
|
||||
return &storageProvider.Reference{
|
||||
ResourceId: space.Root,
|
||||
Path: relativeSpacePath,
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+188
@@ -0,0 +1,188 @@
|
||||
// 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 ocdav
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/config"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"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"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// SpacesHandler handles trashbin requests
|
||||
type SpacesHandler struct {
|
||||
gatewaySvc string
|
||||
namespace string
|
||||
useLoggedInUserNS bool
|
||||
}
|
||||
|
||||
func (h *SpacesHandler) init(c *config.Config) error {
|
||||
h.gatewaySvc = c.GatewaySvc
|
||||
h.namespace = path.Join("/", c.WebdavNamespace)
|
||||
h.useLoggedInUserNS = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handler handles requests
|
||||
func (h *SpacesHandler) Handler(s *svc, trashbinHandler *TrashbinHandler) http.Handler {
|
||||
config := s.Config()
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// ctx := r.Context()
|
||||
// log := appctx.GetLogger(ctx)
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
s.handleOptions(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
var segment string
|
||||
segment, r.URL.Path = router.ShiftPath(r.URL.Path)
|
||||
if segment == "" {
|
||||
// listing is disabled, no auth will change that
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if segment == _trashbinPath {
|
||||
h.handleSpacesTrashbin(w, r, s, trashbinHandler)
|
||||
return
|
||||
}
|
||||
|
||||
spaceID := segment
|
||||
|
||||
// TODO initialize status with http.StatusBadRequest
|
||||
// TODO initialize err with errors.ErrUnsupportedMethod
|
||||
var status int // status 0 means the handler already sent the response
|
||||
var err error
|
||||
switch r.Method {
|
||||
case MethodPropfind:
|
||||
p := propfind.NewHandler(config.PublicURL, s.gatewaySelector, config)
|
||||
p.HandleSpacesPropfind(w, r, spaceID)
|
||||
case MethodProppatch:
|
||||
status, err = s.handleSpacesProppatch(w, r, spaceID)
|
||||
case MethodLock:
|
||||
status, err = s.handleSpacesLock(w, r, spaceID)
|
||||
case MethodUnlock:
|
||||
status, err = s.handleSpaceUnlock(w, r, spaceID)
|
||||
case MethodMkcol:
|
||||
status, err = s.handleSpacesMkCol(w, r, spaceID)
|
||||
case MethodMove:
|
||||
s.handleSpacesMove(w, r, spaceID)
|
||||
case MethodCopy:
|
||||
s.handleSpacesCopy(w, r, spaceID)
|
||||
case MethodReport:
|
||||
s.handleReport(w, r, spaceID)
|
||||
case http.MethodGet:
|
||||
s.handleSpacesGet(w, r, spaceID)
|
||||
case http.MethodPut:
|
||||
s.handleSpacesPut(w, r, spaceID)
|
||||
case http.MethodPost:
|
||||
s.handleSpacesTusPost(w, r, spaceID)
|
||||
case http.MethodOptions:
|
||||
s.handleOptions(w, r)
|
||||
case http.MethodHead:
|
||||
s.handleSpacesHead(w, r, spaceID)
|
||||
case http.MethodDelete:
|
||||
status, err = s.handleSpacesDelete(w, r, spaceID)
|
||||
default:
|
||||
http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
if status != 0 { // 0 means the handler already sent the response
|
||||
w.WriteHeader(status)
|
||||
if status != http.StatusNoContent {
|
||||
var b []byte
|
||||
if b, err = errors.Marshal(status, err.Error(), "", ""); err == nil {
|
||||
_, err = w.Write(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
appctx.GetLogger(r.Context()).Error().Err(err).Msg(err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (h *SpacesHandler) handleSpacesTrashbin(w http.ResponseWriter, r *http.Request, s *svc, trashbinHandler *TrashbinHandler) {
|
||||
ctx := r.Context()
|
||||
log := appctx.GetLogger(ctx)
|
||||
|
||||
spaceID, key := splitSpaceAndKey(r.URL.Path)
|
||||
if spaceID == "" {
|
||||
// listing is disabled, no auth will change that
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
ref, err := storagespace.ParseReference(spaceID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case MethodPropfind:
|
||||
trashbinHandler.listTrashbin(w, r, s, &ref, path.Join(_trashbinPath, spaceID), key)
|
||||
case MethodMove:
|
||||
if key == "" {
|
||||
http.Error(w, "501 Not implemented", http.StatusNotImplemented)
|
||||
break
|
||||
}
|
||||
// find path in url relative to trash base
|
||||
baseURI := ctx.Value(net.CtxKeyBaseURI).(string)
|
||||
baseURI = path.Join(baseURI, spaceID)
|
||||
|
||||
dh := r.Header.Get(net.HeaderDestination)
|
||||
dst, err := net.ParseDestination(baseURI, dh)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
log.Debug().Str("key", key).Str("dst", dst).Msg("spaces restore")
|
||||
|
||||
dstRef := proto.Clone(&ref).(*provider.Reference)
|
||||
dstRef.Path = utils.MakeRelativePath(dst)
|
||||
|
||||
trashbinHandler.restore(w, r, s, &ref, dstRef, key)
|
||||
case http.MethodDelete:
|
||||
trashbinHandler.delete(w, r, s, &ref, key)
|
||||
default:
|
||||
http.Error(w, "501 Not implemented", http.StatusNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
func splitSpaceAndKey(p string) (space, key string) {
|
||||
p = strings.TrimPrefix(p, "/")
|
||||
parts := strings.SplitN(p, "/", 2)
|
||||
space = parts[0]
|
||||
if len(parts) > 1 {
|
||||
key = parts[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
Generated
Vendored
+54
@@ -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 ocdav
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/owncloud/ocs"
|
||||
)
|
||||
|
||||
func (s *svc) doStatus(w http.ResponseWriter, r *http.Request) {
|
||||
log := appctx.GetLogger(r.Context())
|
||||
status := &ocs.Status{
|
||||
Installed: true,
|
||||
Maintenance: false,
|
||||
NeedsDBUpgrade: false,
|
||||
Version: s.c.Version,
|
||||
VersionString: s.c.VersionString,
|
||||
Edition: s.c.Edition,
|
||||
ProductName: s.c.ProductName,
|
||||
ProductVersion: s.c.ProductVersion,
|
||||
Product: s.c.Product,
|
||||
}
|
||||
|
||||
statusJSON, err := json.MarshalIndent(status, "", " ")
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if _, err := w.Write(statusJSON); err != nil {
|
||||
log.Err(err).Msg("error writing response")
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+422
@@ -0,0 +1,422 @@
|
||||
// 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 ocdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
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"
|
||||
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/datagateway"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
"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/opencloud-eu/reva/v2/pkg/rhttp"
|
||||
)
|
||||
|
||||
const (
|
||||
// PerfMarkerResponseTime corresponds to the interval at which a performance marker is sent back to the TPC client
|
||||
PerfMarkerResponseTime float64 = 5
|
||||
)
|
||||
|
||||
// PerfResponse provides a single chunk of permormance marker response
|
||||
type PerfResponse struct {
|
||||
Timestamp time.Time
|
||||
Bytes uint64
|
||||
Index int
|
||||
Count int
|
||||
}
|
||||
|
||||
func (p *PerfResponse) getPerfResponseString() string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Perf Marker\n")
|
||||
sb.WriteString("Timestamp: " + strconv.FormatInt(p.Timestamp.Unix(), 10) + "\n")
|
||||
sb.WriteString("Stripe Bytes Transferred: " + strconv.FormatUint(p.Bytes, 10) + "\n")
|
||||
sb.WriteString("Strip Index: " + strconv.Itoa(p.Index) + "\n")
|
||||
sb.WriteString("Total Stripe Count: " + strconv.Itoa(p.Count) + "\n")
|
||||
sb.WriteString("End\n")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// WriteCounter counts the number of bytes transferred and reports
|
||||
// back to the TPC client about the progress of the transfer
|
||||
// through the performance marker response stream.
|
||||
type WriteCounter struct {
|
||||
Total uint64
|
||||
PrevTime time.Time
|
||||
w http.ResponseWriter
|
||||
}
|
||||
|
||||
// SendPerfMarker flushes a single chunk (performance marker) as
|
||||
// part of the chunked transfer encoding scheme.
|
||||
func (wc *WriteCounter) SendPerfMarker(size uint64) {
|
||||
flusher, ok := wc.w.(http.Flusher)
|
||||
if !ok {
|
||||
panic("expected http.ResponseWriter to be an http.Flusher")
|
||||
}
|
||||
perfResp := PerfResponse{time.Now(), size, 0, 1}
|
||||
pString := perfResp.getPerfResponseString()
|
||||
fmt.Fprintln(wc.w, pString)
|
||||
flusher.Flush()
|
||||
}
|
||||
|
||||
func (wc *WriteCounter) Write(p []byte) (int, error) {
|
||||
|
||||
n := len(p)
|
||||
wc.Total += uint64(n)
|
||||
NowTime := time.Now()
|
||||
|
||||
diff := NowTime.Sub(wc.PrevTime).Seconds()
|
||||
if diff >= PerfMarkerResponseTime {
|
||||
wc.SendPerfMarker(wc.Total)
|
||||
wc.PrevTime = NowTime
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
//
|
||||
// An example of an HTTP TPC Pull
|
||||
//
|
||||
// +-----------------+ GET +----------------+
|
||||
// | Src server | <---------------- | Dest server |
|
||||
// | (Remote) | ----------------> | (Reva) |
|
||||
// +-----------------+ Data +----------------+
|
||||
// ^
|
||||
// |
|
||||
// | COPY
|
||||
// |
|
||||
// +----------+
|
||||
// | Client |
|
||||
// +----------+
|
||||
|
||||
// handleTPCPull performs a GET request on the remote site and upload it
|
||||
// the requested reva endpoint.
|
||||
func (s *svc) handleTPCPull(ctx context.Context, w http.ResponseWriter, r *http.Request, ns string) {
|
||||
src := r.Header.Get("Source")
|
||||
dst := path.Join(ns, r.URL.Path)
|
||||
sublog := appctx.GetLogger(ctx).With().Str("src", src).Str("dst", dst).Logger()
|
||||
|
||||
oh := r.Header.Get(net.HeaderOverwrite)
|
||||
overwrite, err := net.ParseOverwrite(oh)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
m := fmt.Sprintf("Overwrite header is set to incorrect value %v", overwrite)
|
||||
sublog.Warn().Msgf("HTTP TPC Pull: %s", m)
|
||||
b, err := errors.Marshal(http.StatusBadRequest, m, "", "")
|
||||
errors.HandleWebdavError(&sublog, w, b, err)
|
||||
return
|
||||
}
|
||||
sublog.Debug().Bool("overwrite", overwrite).Msg("TPC Pull")
|
||||
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error selecting next gateway client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// check if destination exists
|
||||
ref := &provider.Reference{Path: dst}
|
||||
dstStatReq := &provider.StatRequest{Ref: ref}
|
||||
dstStatRes, err := client.Stat(ctx, dstStatReq)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error sending grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if dstStatRes.Status.Code != rpc.Code_CODE_OK && dstStatRes.Status.Code != rpc.Code_CODE_NOT_FOUND {
|
||||
errors.HandleErrorStatus(&sublog, w, dstStatRes.Status)
|
||||
return
|
||||
}
|
||||
if dstStatRes.Status.Code == rpc.Code_CODE_OK && oh == "F" {
|
||||
sublog.Warn().Bool("overwrite", overwrite).Msg("Destination already exists")
|
||||
w.WriteHeader(http.StatusPreconditionFailed) // 412, see https://tools.ietf.org/html/rfc4918#section-9.8.5
|
||||
return
|
||||
}
|
||||
|
||||
err = s.performHTTPPull(ctx, s.gatewaySelector, r, w, ns)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error performing TPC Pull")
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "success: Created")
|
||||
}
|
||||
|
||||
func (s *svc) performHTTPPull(ctx context.Context, selector pool.Selectable[gateway.GatewayAPIClient], r *http.Request, w http.ResponseWriter, ns string) error {
|
||||
src := r.Header.Get("Source")
|
||||
dst := path.Join(ns, r.URL.Path)
|
||||
sublog := appctx.GetLogger(ctx)
|
||||
sublog.Debug().Str("src", src).Str("dst", dst).Msg("Performing HTTP Pull")
|
||||
|
||||
// get http client for remote
|
||||
httpClient := &http.Client{}
|
||||
|
||||
req, err := http.NewRequest("GET", src, nil)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return err
|
||||
}
|
||||
|
||||
// add authentication header
|
||||
bearerHeader := r.Header.Get(net.HeaderTransferAuth)
|
||||
req.Header.Add("Authorization", bearerHeader)
|
||||
|
||||
// do download
|
||||
httpDownloadRes, err := httpClient.Do(req) // lgtm[go/request-forgery]
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return err
|
||||
}
|
||||
defer httpDownloadRes.Body.Close()
|
||||
|
||||
if httpDownloadRes.StatusCode == http.StatusNotImplemented {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return errtypes.NotSupported("Third-Party copy not supported, source might be a folder")
|
||||
}
|
||||
if httpDownloadRes.StatusCode != http.StatusOK {
|
||||
w.WriteHeader(httpDownloadRes.StatusCode)
|
||||
return errtypes.InternalError(fmt.Sprintf("Remote GET returned status code %d", httpDownloadRes.StatusCode))
|
||||
}
|
||||
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error selecting next gateway client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return errtypes.InternalError(err.Error())
|
||||
}
|
||||
// get upload url
|
||||
uReq := &provider.InitiateFileUploadRequest{
|
||||
Ref: &provider.Reference{Path: dst},
|
||||
Opaque: &typespb.Opaque{
|
||||
Map: map[string]*typespb.OpaqueEntry{
|
||||
"sizedeferred": {
|
||||
Value: []byte("true"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
uRes, err := client.InitiateFileUpload(ctx, uReq)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return err
|
||||
}
|
||||
|
||||
if uRes.Status.Code != rpc.Code_CODE_OK {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return fmt.Errorf("status code %d", uRes.Status.Code)
|
||||
}
|
||||
|
||||
var uploadEP, uploadToken string
|
||||
for _, p := range uRes.Protocols {
|
||||
if p.Protocol == "simple" {
|
||||
uploadEP, uploadToken = p.UploadEndpoint, p.Token
|
||||
}
|
||||
}
|
||||
|
||||
// send performance markers periodically every PerfMarkerResponseTime (5 seconds unless configured)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
wc := WriteCounter{0, time.Now(), w}
|
||||
tempReader := io.TeeReader(httpDownloadRes.Body, &wc)
|
||||
|
||||
// do Upload
|
||||
httpUploadReq, err := rhttp.NewRequest(ctx, "PUT", uploadEP, tempReader)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return err
|
||||
}
|
||||
httpUploadReq.Header.Set(datagateway.TokenTransportHeader, uploadToken)
|
||||
httpUploadRes, err := s.client.Do(httpUploadReq)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return err
|
||||
}
|
||||
|
||||
defer httpUploadRes.Body.Close()
|
||||
if httpUploadRes.StatusCode != http.StatusOK {
|
||||
w.WriteHeader(httpUploadRes.StatusCode)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// An example of an HTTP TPC Push
|
||||
//
|
||||
// +-----------------+ PUT +----------------+
|
||||
// | Dest server | <---------------- | Src server |
|
||||
// | (Remote) | ----------------> | (Reva) |
|
||||
// +-----------------+ Done +----------------+
|
||||
// ^
|
||||
// |
|
||||
// | COPY
|
||||
// |
|
||||
// +----------+
|
||||
// | Client |
|
||||
// +----------+
|
||||
|
||||
// handleTPCPush performs a PUT request on the remote site and while downloading
|
||||
// data from the requested reva endpoint.
|
||||
func (s *svc) handleTPCPush(ctx context.Context, w http.ResponseWriter, r *http.Request, ns string) {
|
||||
src := path.Join(ns, r.URL.Path)
|
||||
dst := r.Header.Get("Destination")
|
||||
sublog := appctx.GetLogger(ctx).With().Str("src", src).Str("dst", dst).Logger()
|
||||
|
||||
oh := r.Header.Get(net.HeaderOverwrite)
|
||||
overwrite, err := net.ParseOverwrite(oh)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
m := fmt.Sprintf("Overwrite header is set to incorrect value %v", overwrite)
|
||||
sublog.Warn().Msgf("HTTP TPC Push: %s", m)
|
||||
b, err := errors.Marshal(http.StatusBadRequest, m, "", "")
|
||||
errors.HandleWebdavError(&sublog, w, b, err)
|
||||
return
|
||||
}
|
||||
|
||||
sublog.Debug().Bool("overwrite", overwrite).Msg("TPC Push")
|
||||
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error selecting next gateway client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
ref := &provider.Reference{Path: src}
|
||||
srcStatReq := &provider.StatRequest{Ref: ref}
|
||||
srcStatRes, err := client.Stat(ctx, srcStatReq)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error sending grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if srcStatRes.Status.Code != rpc.Code_CODE_OK && srcStatRes.Status.Code != rpc.Code_CODE_NOT_FOUND {
|
||||
errors.HandleErrorStatus(&sublog, w, srcStatRes.Status)
|
||||
return
|
||||
}
|
||||
if srcStatRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
|
||||
sublog.Error().Msg("Third-Party copy of a folder is not supported")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = s.performHTTPPush(ctx, r, w, srcStatRes.Info, ns)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error performing TPC Push")
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "success: Created")
|
||||
}
|
||||
|
||||
func (s *svc) performHTTPPush(ctx context.Context, r *http.Request, w http.ResponseWriter, srcInfo *provider.ResourceInfo, ns string) error {
|
||||
src := path.Join(ns, r.URL.Path)
|
||||
dst := r.Header.Get("Destination")
|
||||
|
||||
sublog := appctx.GetLogger(ctx)
|
||||
sublog.Debug().Str("src", src).Str("dst", dst).Msg("Performing HTTP Push")
|
||||
|
||||
// get download url
|
||||
dReq := &provider.InitiateFileDownloadRequest{
|
||||
Ref: &provider.Reference{Path: src},
|
||||
}
|
||||
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error selecting next gateway client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return err
|
||||
}
|
||||
dRes, err := client.InitiateFileDownload(ctx, dReq)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return err
|
||||
}
|
||||
|
||||
if dRes.Status.Code != rpc.Code_CODE_OK {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return fmt.Errorf("status code %d", dRes.Status.Code)
|
||||
}
|
||||
|
||||
var downloadEP, downloadToken string
|
||||
for _, p := range dRes.Protocols {
|
||||
if p.Protocol == "simple" {
|
||||
downloadEP, downloadToken = p.DownloadEndpoint, p.Token
|
||||
}
|
||||
}
|
||||
|
||||
// do download
|
||||
httpDownloadReq, err := rhttp.NewRequest(ctx, "GET", downloadEP, nil)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return err
|
||||
}
|
||||
httpDownloadReq.Header.Set(datagateway.TokenTransportHeader, downloadToken)
|
||||
|
||||
httpDownloadRes, err := s.client.Do(httpDownloadReq)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return err
|
||||
}
|
||||
defer httpDownloadRes.Body.Close()
|
||||
if httpDownloadRes.StatusCode != http.StatusOK {
|
||||
w.WriteHeader(httpDownloadRes.StatusCode)
|
||||
return fmt.Errorf("Remote PUT returned status code %d", httpDownloadRes.StatusCode)
|
||||
}
|
||||
|
||||
// send performance markers periodically every PerfMarkerResponseTime (5 seconds unless configured)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
wc := WriteCounter{0, time.Now(), w}
|
||||
tempReader := io.TeeReader(httpDownloadRes.Body, &wc)
|
||||
|
||||
// get http client for a remote call
|
||||
httpClient := &http.Client{}
|
||||
req, err := http.NewRequest("PUT", dst, tempReader)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return err
|
||||
}
|
||||
|
||||
// add authentication header and content length
|
||||
bearerHeader := r.Header.Get(net.HeaderTransferAuth)
|
||||
req.Header.Add("Authorization", bearerHeader)
|
||||
req.ContentLength = int64(srcInfo.GetSize())
|
||||
|
||||
// do Upload
|
||||
httpUploadRes, err := httpClient.Do(req) // lgtm[go/request-forgery]
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return err
|
||||
}
|
||||
defer httpUploadRes.Body.Close()
|
||||
|
||||
if httpUploadRes.StatusCode != http.StatusOK {
|
||||
w.WriteHeader(httpUploadRes.StatusCode)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Generated
Vendored
+665
@@ -0,0 +1,665 @@
|
||||
// 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 ocdav
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/config"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/prop"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
|
||||
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
rstatus "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
// TrashbinHandler handles trashbin requests
|
||||
type TrashbinHandler struct {
|
||||
gatewaySvc string
|
||||
namespace string
|
||||
allowPropfindDepthInfinitiy bool
|
||||
}
|
||||
|
||||
func (h *TrashbinHandler) init(c *config.Config) error {
|
||||
h.gatewaySvc = c.GatewaySvc
|
||||
h.namespace = path.Join("/", c.FilesNamespace)
|
||||
h.allowPropfindDepthInfinitiy = c.AllowPropfindDepthInfinitiy
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handler handles requests
|
||||
func (h *TrashbinHandler) Handler(s *svc) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
log := appctx.GetLogger(ctx)
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
s.handleOptions(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
var username string
|
||||
username, r.URL.Path = splitSpaceAndKey(r.URL.Path)
|
||||
if username == "" {
|
||||
// listing is disabled, no auth will change that
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
user, ok := ctxpkg.ContextGetUser(ctx)
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if user.Username != username {
|
||||
log.Debug().Str("username", username).Interface("user", user).Msg("trying to read another users trash")
|
||||
// listing other users trash is forbidden, no auth will change that
|
||||
// do not leak existence of space and return 404
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
b, err := errors.Marshal(http.StatusNotFound, "not found", "", "")
|
||||
if err != nil {
|
||||
log.Error().Msgf("error marshaling xml response: %s", b)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_, err = w.Write(b)
|
||||
if err != nil {
|
||||
log.Error().Msgf("error writing xml response: %s", b)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
useLoggedInUser := true
|
||||
ns, newPath, err := s.ApplyLayout(ctx, h.namespace, useLoggedInUser, r.URL.Path)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
b, err := errors.Marshal(http.StatusNotFound, fmt.Sprintf("could not get storage for %s", r.URL.Path), "", "")
|
||||
errors.HandleWebdavError(appctx.GetLogger(r.Context()), w, b, err)
|
||||
}
|
||||
r.URL.Path = newPath
|
||||
|
||||
basePath := path.Join(ns, newPath)
|
||||
space, rpcstatus, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, basePath)
|
||||
switch {
|
||||
case err != nil:
|
||||
log.Error().Err(err).Str("path", basePath).Msg("failed to look up storage space")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
case rpcstatus.Code != rpc.Code_CODE_OK:
|
||||
httpStatus := rstatus.HTTPStatusFromCode(rpcstatus.Code)
|
||||
w.WriteHeader(httpStatus)
|
||||
b, err := errors.Marshal(httpStatus, rpcstatus.Message, "", "")
|
||||
errors.HandleWebdavError(log, w, b, err)
|
||||
return
|
||||
}
|
||||
ref := spacelookup.MakeRelativeReference(space, ".", false)
|
||||
|
||||
// key will be a base64 encoded cs3 path, it uniquely identifies a trash item with an opaque id and an optional path
|
||||
key := r.URL.Path
|
||||
|
||||
switch r.Method {
|
||||
case MethodPropfind:
|
||||
h.listTrashbin(w, r, s, ref, user.Username, key)
|
||||
case MethodMove:
|
||||
if key == "" {
|
||||
http.Error(w, "501 Not implemented", http.StatusNotImplemented)
|
||||
break
|
||||
}
|
||||
// find path in url relative to trash base
|
||||
trashBase := ctx.Value(net.CtxKeyBaseURI).(string)
|
||||
baseURI := path.Join(path.Dir(trashBase), "files", username)
|
||||
|
||||
dh := r.Header.Get(net.HeaderDestination)
|
||||
dst, err := net.ParseDestination(baseURI, dh)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
p := path.Join(ns, dst)
|
||||
// The destination can be in another space. E.g. the 'Shares Jail'.
|
||||
space, rpcstatus, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, p)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("path", p).Msg("failed to look up destination storage space")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if rpcstatus.Code != rpc.Code_CODE_OK {
|
||||
httpStatus := rstatus.HTTPStatusFromCode(rpcstatus.Code)
|
||||
w.WriteHeader(httpStatus)
|
||||
b, err := errors.Marshal(httpStatus, rpcstatus.Message, "", "")
|
||||
errors.HandleWebdavError(log, w, b, err)
|
||||
return
|
||||
}
|
||||
dstRef := spacelookup.MakeRelativeReference(space, p, false)
|
||||
|
||||
log.Debug().Str("key", key).Str("dst", dst).Msg("restore")
|
||||
h.restore(w, r, s, ref, dstRef, key)
|
||||
case http.MethodDelete:
|
||||
h.delete(w, r, s, ref, key)
|
||||
default:
|
||||
http.Error(w, "501 Not implemented", http.StatusNotImplemented)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (h *TrashbinHandler) getDepth(r *http.Request) (net.Depth, error) {
|
||||
dh := r.Header.Get(net.HeaderDepth)
|
||||
depth, err := net.ParseDepth(dh)
|
||||
if err != nil || depth == net.DepthInfinity && !h.allowPropfindDepthInfinitiy {
|
||||
return "", errors.ErrInvalidDepth
|
||||
}
|
||||
return depth, nil
|
||||
}
|
||||
|
||||
func (h *TrashbinHandler) listTrashbin(w http.ResponseWriter, r *http.Request, s *svc, ref *provider.Reference, refBase, key string) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "list_trashbin")
|
||||
defer span.End()
|
||||
|
||||
sublog := appctx.GetLogger(ctx).With().Logger()
|
||||
|
||||
depth, err := h.getDepth(r)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, "Invalid Depth header value")
|
||||
span.SetAttributes(semconv.HTTPStatusCodeKey.Int(http.StatusBadRequest))
|
||||
sublog.Debug().Str("depth", r.Header.Get(net.HeaderDepth)).Msg(err.Error())
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
m := fmt.Sprintf("Invalid Depth header value: %v", r.Header.Get(net.HeaderDepth))
|
||||
b, err := errors.Marshal(http.StatusBadRequest, m, "", "")
|
||||
errors.HandleWebdavError(&sublog, w, b, err)
|
||||
return
|
||||
}
|
||||
|
||||
pf, status, err := propfind.ReadPropfind(r.Body)
|
||||
if err != nil {
|
||||
sublog.Debug().Err(err).Msg("error reading propfind request")
|
||||
w.WriteHeader(status)
|
||||
return
|
||||
}
|
||||
|
||||
if key == "" && depth == net.DepthZero {
|
||||
// we are listing the trash root, but without children
|
||||
// so we just fake a root element without actually querying the gateway
|
||||
rootHref := path.Join(refBase, key)
|
||||
propRes, err := h.formatTrashPropfind(ctx, s, ref.ResourceId.SpaceId, refBase, rootHref, &pf, nil, true)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error formatting propfind")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol")
|
||||
w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8")
|
||||
w.WriteHeader(http.StatusMultiStatus)
|
||||
_, err = w.Write(propRes)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error writing body")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if depth == net.DepthOne && key != "" && !strings.HasSuffix(key, "/") {
|
||||
// when a key is provided and the depth is 1 we need to append a / to the key to list the children
|
||||
key += "/"
|
||||
}
|
||||
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error selecting next gateway client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// ask gateway for recycle items
|
||||
getRecycleRes, err := client.ListRecycle(ctx, &provider.ListRecycleRequest{Ref: ref, Key: key})
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error calling ListRecycle")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if getRecycleRes.Status.Code != rpc.Code_CODE_OK {
|
||||
httpStatus := rstatus.HTTPStatusFromCode(getRecycleRes.Status.Code)
|
||||
w.WriteHeader(httpStatus)
|
||||
b, err := errors.Marshal(httpStatus, getRecycleRes.Status.Message, "", "")
|
||||
errors.HandleWebdavError(&sublog, w, b, err)
|
||||
return
|
||||
}
|
||||
|
||||
items := getRecycleRes.RecycleItems
|
||||
|
||||
if depth == net.DepthInfinity {
|
||||
var stack []string
|
||||
// check sub-containers in reverse order and add them to the stack
|
||||
// the reversed order here will produce a more logical sorting of results
|
||||
for i := len(items) - 1; i >= 0; i-- {
|
||||
// for i := range res.Infos {
|
||||
if items[i].Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
|
||||
stack = append(stack, items[i].Key+"/") // fetch children of the item
|
||||
}
|
||||
}
|
||||
|
||||
for len(stack) > 0 {
|
||||
key := stack[len(stack)-1]
|
||||
getRecycleRes, err := client.ListRecycle(ctx, &provider.ListRecycleRequest{Ref: ref, Key: key})
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error calling ListRecycle")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if getRecycleRes.Status.Code != rpc.Code_CODE_OK {
|
||||
httpStatus := rstatus.HTTPStatusFromCode(getRecycleRes.Status.Code)
|
||||
w.WriteHeader(httpStatus)
|
||||
b, err := errors.Marshal(httpStatus, getRecycleRes.Status.Message, "", "")
|
||||
errors.HandleWebdavError(&sublog, w, b, err)
|
||||
return
|
||||
}
|
||||
items = append(items, getRecycleRes.RecycleItems...)
|
||||
|
||||
stack = stack[:len(stack)-1]
|
||||
// check sub-containers in reverse order and add them to the stack
|
||||
// the reversed order here will produce a more logical sorting of results
|
||||
for i := len(getRecycleRes.RecycleItems) - 1; i >= 0; i-- {
|
||||
// for i := range res.Infos {
|
||||
if getRecycleRes.RecycleItems[i].Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
|
||||
stack = append(stack, getRecycleRes.RecycleItems[i].Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rootHref := path.Join(refBase, key)
|
||||
propRes, err := h.formatTrashPropfind(ctx, s, ref.ResourceId.SpaceId, refBase, rootHref, &pf, items, depth != net.DepthZero)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error formatting propfind")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol")
|
||||
w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8")
|
||||
w.WriteHeader(http.StatusMultiStatus)
|
||||
_, err = w.Write(propRes)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error writing body")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *TrashbinHandler) formatTrashPropfind(ctx context.Context, s *svc, spaceID, refBase, rootHref string, pf *propfind.XML, items []*provider.RecycleItem, fakeRoot bool) ([]byte, error) {
|
||||
responses := make([]*propfind.ResponseXML, 0, len(items)+1)
|
||||
if fakeRoot {
|
||||
responses = append(responses, &propfind.ResponseXML{
|
||||
Href: net.EncodePath(path.Join(ctx.Value(net.CtxKeyBaseURI).(string), rootHref) + "/"), // url encode response.Href TODO
|
||||
Propstat: []propfind.PropstatXML{
|
||||
{
|
||||
Status: "HTTP/1.1 200 OK",
|
||||
Prop: []prop.PropertyXML{
|
||||
prop.Raw("d:resourcetype", "<d:collection/>"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Status: "HTTP/1.1 404 Not Found",
|
||||
Prop: []prop.PropertyXML{
|
||||
prop.NotFound("oc:trashbin-original-filename"),
|
||||
prop.NotFound("oc:trashbin-original-location"),
|
||||
prop.NotFound("oc:trashbin-delete-datetime"),
|
||||
prop.NotFound("d:getcontentlength"),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for i := range items {
|
||||
res, err := h.itemToPropResponse(ctx, s, spaceID, refBase, pf, items[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
responses = append(responses, res)
|
||||
}
|
||||
responsesXML, err := xml.Marshal(&responses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(`<?xml version="1.0" encoding="utf-8"?><d:multistatus xmlns:d="DAV:" `)
|
||||
buf.WriteString(`xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns">`)
|
||||
buf.Write(responsesXML)
|
||||
buf.WriteString(`</d:multistatus>`)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// itemToPropResponse needs to create a listing that contains a key and destination
|
||||
// the key is the name of an entry in the trash listing
|
||||
// for now we need to limit trash to the users home, so we can expect all trash keys to have the home storage as the opaque id
|
||||
func (h *TrashbinHandler) itemToPropResponse(ctx context.Context, s *svc, spaceID, refBase string, pf *propfind.XML, item *provider.RecycleItem) (*propfind.ResponseXML, error) {
|
||||
|
||||
baseURI := ctx.Value(net.CtxKeyBaseURI).(string)
|
||||
ref := path.Join(baseURI, refBase, item.GetKey())
|
||||
if item.GetType() == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
|
||||
ref += "/"
|
||||
}
|
||||
|
||||
response := propfind.ResponseXML{
|
||||
Href: net.EncodePath(ref), // url encode response.Href
|
||||
Propstat: []propfind.PropstatXML{},
|
||||
}
|
||||
|
||||
// TODO(jfd): if the path we list here is taken from the ListRecycle request we rely on the gateway to prefix it with the mount point
|
||||
|
||||
t := utils.TSToTime(item.GetDeletionTime()).UTC()
|
||||
dTime := t.Format(time.RFC1123Z)
|
||||
size := strconv.FormatUint(item.GetSize(), 10)
|
||||
|
||||
// when allprops has been requested
|
||||
if pf.Allprop != nil {
|
||||
// return all known properties
|
||||
propstatOK := propfind.PropstatXML{
|
||||
Status: "HTTP/1.1 200 OK",
|
||||
Prop: []prop.PropertyXML{},
|
||||
}
|
||||
// yes this is redundant, can be derived from oc:trashbin-original-location which contains the full path, clients should not fetch it
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-original-filename", path.Base(item.GetRef().GetPath())))
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-original-location", strings.TrimPrefix(item.GetRef().GetPath(), "/")))
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-delete-timestamp", strconv.FormatUint(item.GetDeletionTime().GetSeconds(), 10)))
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-delete-datetime", dTime))
|
||||
if item.GetType() == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Raw("d:resourcetype", "<d:collection/>"))
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Raw("oc:size", size))
|
||||
} else {
|
||||
propstatOK.Prop = append(propstatOK.Prop,
|
||||
prop.Escaped("d:resourcetype", ""),
|
||||
prop.Escaped("d:getcontentlength", size),
|
||||
)
|
||||
}
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:spaceid", spaceID))
|
||||
response.Propstat = append(response.Propstat, propstatOK)
|
||||
} else {
|
||||
// otherwise return only the requested properties
|
||||
propstatOK := propfind.PropstatXML{
|
||||
Status: "HTTP/1.1 200 OK",
|
||||
Prop: []prop.PropertyXML{},
|
||||
}
|
||||
propstatNotFound := propfind.PropstatXML{
|
||||
Status: "HTTP/1.1 404 Not Found",
|
||||
Prop: []prop.PropertyXML{},
|
||||
}
|
||||
for i := range pf.Prop {
|
||||
switch pf.Prop[i].Space {
|
||||
case net.NsOwncloud:
|
||||
switch pf.Prop[i].Local {
|
||||
case "oc:size":
|
||||
if item.GetType() == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:size", size))
|
||||
} else {
|
||||
propstatNotFound.Prop = append(propstatNotFound.Prop, prop.NotFound("oc:size"))
|
||||
}
|
||||
case "trashbin-original-filename":
|
||||
// yes this is redundant, can be derived from oc:trashbin-original-location which contains the full path, clients should not fetch it
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-original-filename", path.Base(item.GetRef().GetPath())))
|
||||
case "trashbin-original-location":
|
||||
// TODO (jfd) double check and clarify the cs3 spec what the Key is about and if Path is only the folder that contains the file or if it includes the filename
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-original-location", strings.TrimPrefix(item.GetRef().GetPath(), "/")))
|
||||
case "trashbin-delete-datetime":
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-delete-datetime", dTime))
|
||||
case "trashbin-delete-timestamp":
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-delete-timestamp", strconv.FormatUint(item.GetDeletionTime().GetSeconds(), 10)))
|
||||
case "spaceid":
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:spaceid", spaceID))
|
||||
default:
|
||||
propstatNotFound.Prop = append(propstatNotFound.Prop, prop.NotFound("oc:"+pf.Prop[i].Local))
|
||||
}
|
||||
case net.NsDav:
|
||||
switch pf.Prop[i].Local {
|
||||
case "getcontentlength":
|
||||
if item.GetType() == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
|
||||
propstatNotFound.Prop = append(propstatNotFound.Prop, prop.NotFound("d:getcontentlength"))
|
||||
} else {
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("d:getcontentlength", size))
|
||||
}
|
||||
case "resourcetype":
|
||||
if item.GetType() == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Raw("d:resourcetype", "<d:collection/>"))
|
||||
} else {
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Raw("d:resourcetype", ""))
|
||||
// redirectref is another option
|
||||
}
|
||||
case "getcontenttype":
|
||||
if item.GetType() == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
|
||||
propstatOK.Prop = append(propstatOK.Prop, prop.Raw("d:getcontenttype", "httpd/unix-directory"))
|
||||
} else {
|
||||
propstatNotFound.Prop = append(propstatNotFound.Prop, prop.NotFound("d:getcontenttype"))
|
||||
}
|
||||
default:
|
||||
propstatNotFound.Prop = append(propstatNotFound.Prop, prop.NotFound("d:"+pf.Prop[i].Local))
|
||||
}
|
||||
default:
|
||||
// TODO (jfd) lookup shortname for unknown namespaces?
|
||||
propstatNotFound.Prop = append(propstatNotFound.Prop, prop.NotFound(pf.Prop[i].Space+":"+pf.Prop[i].Local))
|
||||
}
|
||||
}
|
||||
response.Propstat = append(response.Propstat, propstatOK, propstatNotFound)
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func (h *TrashbinHandler) restore(w http.ResponseWriter, r *http.Request, s *svc, ref, dst *provider.Reference, key string) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "restore")
|
||||
defer span.End()
|
||||
|
||||
sublog := appctx.GetLogger(ctx).With().Logger()
|
||||
|
||||
oh := r.Header.Get(net.HeaderOverwrite)
|
||||
|
||||
overwrite, err := net.ParseOverwrite(oh)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error selecting next gateway client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
dstStatReq := &provider.StatRequest{Ref: dst}
|
||||
dstStatRes, err := client.Stat(ctx, dstStatReq)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error sending grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if dstStatRes.Status.Code != rpc.Code_CODE_OK && dstStatRes.Status.Code != rpc.Code_CODE_NOT_FOUND {
|
||||
errors.HandleErrorStatus(&sublog, w, dstStatRes.Status)
|
||||
return
|
||||
}
|
||||
|
||||
// Restoring to a non-existent location is not supported by the WebDAV spec. The following block ensures the target
|
||||
// restore location exists, and if it doesn't returns a conflict error code.
|
||||
if dstStatRes.Status.Code == rpc.Code_CODE_NOT_FOUND && isNested(dst.Path) {
|
||||
parentRef := &provider.Reference{ResourceId: dst.ResourceId, Path: utils.MakeRelativePath(path.Dir(dst.Path))}
|
||||
parentStatReq := &provider.StatRequest{Ref: parentRef}
|
||||
|
||||
parentStatResponse, err := client.Stat(ctx, parentStatReq)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error sending grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if parentStatResponse.Status.Code == rpc.Code_CODE_NOT_FOUND {
|
||||
// 409 if intermediate dir is missing, see https://tools.ietf.org/html/rfc4918#section-9.8.5
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
successCode := http.StatusCreated // 201 if new resource was created, see https://tools.ietf.org/html/rfc4918#section-9.9.4
|
||||
if dstStatRes.Status.Code == rpc.Code_CODE_OK {
|
||||
successCode = http.StatusNoContent // 204 if target already existed, see https://tools.ietf.org/html/rfc4918#section-9.9.4
|
||||
|
||||
if !overwrite {
|
||||
sublog.Warn().Bool("overwrite", overwrite).Msg("dst already exists")
|
||||
w.WriteHeader(http.StatusPreconditionFailed) // 412, see https://tools.ietf.org/html/rfc4918#section-9.9.4
|
||||
b, err := errors.Marshal(
|
||||
http.StatusPreconditionFailed,
|
||||
"The destination node already exists, and the overwrite header is set to false",
|
||||
net.HeaderOverwrite,
|
||||
"",
|
||||
)
|
||||
errors.HandleWebdavError(&sublog, w, b, err)
|
||||
return
|
||||
}
|
||||
// delete existing tree
|
||||
delReq := &provider.DeleteRequest{Ref: dst}
|
||||
delRes, err := client.Delete(ctx, delReq)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error sending grpc delete request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if delRes.Status.Code != rpc.Code_CODE_OK && delRes.Status.Code != rpc.Code_CODE_NOT_FOUND {
|
||||
errors.HandleErrorStatus(&sublog, w, delRes.Status)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
req := &provider.RestoreRecycleItemRequest{
|
||||
Ref: ref,
|
||||
Key: key,
|
||||
RestoreRef: dst,
|
||||
}
|
||||
|
||||
res, err := client.RestoreRecycleItem(ctx, req)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error sending a grpc restore recycle item request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if res.Status.Code != rpc.Code_CODE_OK {
|
||||
if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
b, err := errors.Marshal(http.StatusForbidden, "Permission denied to restore", "", "")
|
||||
errors.HandleWebdavError(&sublog, w, b, err)
|
||||
}
|
||||
errors.HandleErrorStatus(&sublog, w, res.Status)
|
||||
return
|
||||
}
|
||||
|
||||
dstStatRes, err = client.Stat(ctx, dstStatReq)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error sending grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if dstStatRes.Status.Code != rpc.Code_CODE_OK {
|
||||
errors.HandleErrorStatus(&sublog, w, dstStatRes.Status)
|
||||
return
|
||||
}
|
||||
|
||||
info := dstStatRes.Info
|
||||
w.Header().Set(net.HeaderContentType, info.MimeType)
|
||||
w.Header().Set(net.HeaderETag, info.Etag)
|
||||
w.Header().Set(net.HeaderOCFileID, storagespace.FormatResourceID(info.Id))
|
||||
w.Header().Set(net.HeaderOCETag, info.Etag)
|
||||
|
||||
w.WriteHeader(successCode)
|
||||
}
|
||||
|
||||
// delete has only a key
|
||||
func (h *TrashbinHandler) delete(w http.ResponseWriter, r *http.Request, s *svc, ref *provider.Reference, key string) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "erase")
|
||||
defer span.End()
|
||||
|
||||
sublog := appctx.GetLogger(ctx).With().Interface("reference", ref).Str("key", key).Logger()
|
||||
|
||||
req := &provider.PurgeRecycleRequest{
|
||||
Ref: ref,
|
||||
Key: key,
|
||||
}
|
||||
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error selecting next gateway client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
res, err := client.PurgeRecycle(ctx, req)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error sending a grpc restore recycle item request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
switch res.Status.Code {
|
||||
case rpc.Code_CODE_OK:
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
case rpc.Code_CODE_NOT_FOUND:
|
||||
sublog.Debug().Interface("status", res.Status).Msg("resource not found")
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
m := fmt.Sprintf("key %s not found", key)
|
||||
b, err := errors.Marshal(http.StatusConflict, m, "", "")
|
||||
errors.HandleWebdavError(&sublog, w, b, err)
|
||||
case rpc.Code_CODE_PERMISSION_DENIED:
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
var m string
|
||||
if key == "" {
|
||||
m = "Permission denied to purge recycle"
|
||||
} else {
|
||||
m = "Permission denied to delete"
|
||||
}
|
||||
b, err := errors.Marshal(http.StatusForbidden, m, "", "")
|
||||
errors.HandleWebdavError(&sublog, w, b, err)
|
||||
default:
|
||||
errors.HandleErrorStatus(&sublog, w, res.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func isNested(p string) bool {
|
||||
dir, _ := path.Split(p)
|
||||
return dir != "/" && dir != "./"
|
||||
}
|
||||
Generated
Vendored
+393
@@ -0,0 +1,393 @@
|
||||
// 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 ocdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
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"
|
||||
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/conversions"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/rs/zerolog"
|
||||
tusd "github.com/tus/tusd/v2/pkg/handler"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
)
|
||||
|
||||
// Propagator ensures the importer module uses the same trace propagation strategy.
|
||||
var Propagator = propagation.NewCompositeTextMapPropagator(
|
||||
propagation.Baggage{},
|
||||
propagation.TraceContext{},
|
||||
)
|
||||
|
||||
func (s *svc) handlePathTusPost(w http.ResponseWriter, r *http.Request, ns string) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "tus-post")
|
||||
defer span.End()
|
||||
|
||||
// read filename from metadata
|
||||
meta := tusd.ParseMetadataHeader(r.Header.Get(net.HeaderUploadMetadata))
|
||||
|
||||
// append filename to current dir
|
||||
ref := &provider.Reference{
|
||||
// a path based request has no resource id, so we can only provide a path. The gateway has te figure out which provider is responsible
|
||||
Path: path.Join(ns, r.URL.Path, meta["filename"]),
|
||||
}
|
||||
|
||||
sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Str("filename", meta["filename"]).Logger()
|
||||
|
||||
s.handleTusPost(ctx, w, r, meta, ref, sublog)
|
||||
}
|
||||
|
||||
func (s *svc) handleSpacesTusPost(w http.ResponseWriter, r *http.Request, spaceID string) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "spaces-tus-post")
|
||||
defer span.End()
|
||||
|
||||
// read filename from metadata
|
||||
meta := tusd.ParseMetadataHeader(r.Header.Get(net.HeaderUploadMetadata))
|
||||
|
||||
ref, err := spacelookup.MakeStorageSpaceReference(spaceID, path.Join(r.URL.Path, meta["filename"]))
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sublog := appctx.GetLogger(ctx).With().Str("spaceid", spaceID).Str("path", r.URL.Path).Str("filename", meta["filename"]).Logger()
|
||||
|
||||
s.handleTusPost(ctx, w, r, meta, &ref, sublog)
|
||||
}
|
||||
|
||||
func (s *svc) handleTusPost(ctx context.Context, w http.ResponseWriter, r *http.Request, meta map[string]string, ref *provider.Reference, log zerolog.Logger) {
|
||||
w.Header().Add(net.HeaderAccessControlAllowHeaders, strings.Join([]string{net.HeaderTusResumable, net.HeaderUploadLength, net.HeaderUploadMetadata, net.HeaderIfMatch}, ", "))
|
||||
w.Header().Add(net.HeaderAccessControlExposeHeaders, strings.Join([]string{net.HeaderTusResumable, net.HeaderUploadOffset, net.HeaderLocation}, ", "))
|
||||
w.Header().Set(net.HeaderTusExtension, "creation,creation-with-upload,checksum,expiration")
|
||||
|
||||
w.Header().Set(net.HeaderTusResumable, "1.0.0")
|
||||
|
||||
// Test if the version sent by the client is supported
|
||||
// GET methods are not checked since a browser may visit this URL and does
|
||||
// not include this header. This request is not part of the specification.
|
||||
if r.Header.Get(net.HeaderTusResumable) != "1.0.0" {
|
||||
w.WriteHeader(http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
if r.Header.Get(net.HeaderUploadLength) == "" {
|
||||
w.WriteHeader(http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
if err := ValidateName(filename(meta["filename"]), s.nameValidators); err != nil {
|
||||
w.WriteHeader(http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
|
||||
// Test if the target is a secret filedrop
|
||||
var isSecretFileDrop bool
|
||||
tokenStatInfo, ok := TokenStatInfoFromContext(ctx)
|
||||
// We assume that when the uploader can create containers, but is not allowed to list them, it is a secret file drop
|
||||
if ok && tokenStatInfo.GetPermissionSet().CreateContainer && !tokenStatInfo.GetPermissionSet().ListContainer {
|
||||
isSecretFileDrop = true
|
||||
}
|
||||
|
||||
// r.Header.Get(net.HeaderOCChecksum)
|
||||
// TODO must be SHA1, ADLER32 or MD5 ... in capital letters????
|
||||
// curl -X PUT https://demo.owncloud.com/remote.php/webdav/testcs.bin -u demo:demo -d '123' -v -H 'OC-Checksum: SHA1:40bd001563085fc35165329ea1ff5c5ecbdbbeef'
|
||||
|
||||
// TODO check Expect: 100-continue
|
||||
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
sReq := &provider.StatRequest{
|
||||
Ref: ref,
|
||||
}
|
||||
sRes, err := client.Stat(ctx, sReq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error sending grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if sRes.Status.Code != rpc.Code_CODE_OK && sRes.Status.Code != rpc.Code_CODE_NOT_FOUND {
|
||||
errors.HandleErrorStatus(&log, w, sRes.Status)
|
||||
return
|
||||
}
|
||||
|
||||
info := sRes.Info
|
||||
if info != nil && info.Type != provider.ResourceType_RESOURCE_TYPE_FILE {
|
||||
log.Warn().Msg("resource is not a file")
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
if info != nil {
|
||||
clientETag := r.Header.Get(net.HeaderIfMatch)
|
||||
serverETag := info.Etag
|
||||
if clientETag != "" {
|
||||
if clientETag != serverETag {
|
||||
log.Warn().Str("client-etag", clientETag).Str("server-etag", serverETag).Msg("etags mismatch")
|
||||
w.WriteHeader(http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
}
|
||||
if isSecretFileDrop {
|
||||
// find next filename
|
||||
newName, status, err := FindName(ctx, client, filepath.Base(ref.Path), sRes.GetInfo().GetParentId())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error sending grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if status.GetCode() != rpc.Code_CODE_OK {
|
||||
log.Error().Interface("status", status).Msg("error listing file")
|
||||
errors.HandleErrorStatus(&log, w, status)
|
||||
return
|
||||
}
|
||||
ref.Path = filepath.Join(filepath.Dir(ref.GetPath()), newName)
|
||||
sRes.GetInfo().Name = newName
|
||||
}
|
||||
}
|
||||
|
||||
uploadLength, err := strconv.ParseInt(r.Header.Get(net.HeaderUploadLength), 10, 64)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("wrong request")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if uploadLength == 0 {
|
||||
tfRes, err := client.TouchFile(ctx, &provider.TouchFileRequest{
|
||||
Ref: ref,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error sending grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
switch tfRes.Status.Code {
|
||||
case rpc.Code_CODE_OK:
|
||||
w.Header().Set(net.HeaderLocation, "")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
return
|
||||
case rpc.Code_CODE_ALREADY_EXISTS:
|
||||
// Fall through to the tus case
|
||||
default:
|
||||
log.Error().Interface("status", tfRes.Status).Msg("error touching file")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
opaqueMap := map[string]*typespb.OpaqueEntry{
|
||||
net.HeaderUploadLength: {
|
||||
Decoder: "plain",
|
||||
Value: []byte(r.Header.Get(net.HeaderUploadLength)),
|
||||
},
|
||||
}
|
||||
|
||||
mtime := meta["mtime"]
|
||||
if mtime != "" {
|
||||
opaqueMap[net.HeaderOCMtime] = &typespb.OpaqueEntry{
|
||||
Decoder: "plain",
|
||||
Value: []byte(mtime),
|
||||
}
|
||||
}
|
||||
|
||||
// initiateUpload
|
||||
uReq := &provider.InitiateFileUploadRequest{
|
||||
Ref: ref,
|
||||
Opaque: &typespb.Opaque{
|
||||
Map: opaqueMap,
|
||||
},
|
||||
}
|
||||
|
||||
uRes, err := client.InitiateFileUpload(ctx, uReq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error initiating file upload")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if uRes.Status.Code != rpc.Code_CODE_OK {
|
||||
if r.ProtoMajor == 1 {
|
||||
// drain body to avoid `connection closed` errors
|
||||
_, _ = io.Copy(io.Discard, r.Body)
|
||||
}
|
||||
if uRes.Status.Code == rpc.Code_CODE_NOT_FOUND {
|
||||
w.WriteHeader(http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
errors.HandleErrorStatus(&log, w, uRes.Status)
|
||||
return
|
||||
}
|
||||
|
||||
var ep, token string
|
||||
for _, p := range uRes.Protocols {
|
||||
if p.Protocol == "tus" {
|
||||
ep, token = p.UploadEndpoint, p.Token
|
||||
}
|
||||
}
|
||||
|
||||
// TUS clients don't understand the reva transfer token. We need to append it to the upload endpoint.
|
||||
// The DataGateway has to take care of pulling it back into the request header upon request arrival.
|
||||
if token != "" {
|
||||
if !strings.HasSuffix(ep, "/") {
|
||||
ep += "/"
|
||||
}
|
||||
ep += token
|
||||
}
|
||||
|
||||
w.Header().Set(net.HeaderLocation, ep)
|
||||
|
||||
// for creation-with-upload extension forward bytes to dataprovider
|
||||
// TODO check this really streams
|
||||
if r.Header.Get(net.HeaderContentType) == "application/offset+octet-stream" {
|
||||
finishUpload := true
|
||||
if uploadLength > 0 {
|
||||
var httpRes *http.Response
|
||||
|
||||
httpReq, err := rhttp.NewRequest(ctx, http.MethodPatch, ep, r.Body)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("wrong request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
Propagator.Inject(ctx, propagation.HeaderCarrier(httpReq.Header))
|
||||
|
||||
httpReq.Header.Set(net.HeaderContentType, r.Header.Get(net.HeaderContentType))
|
||||
httpReq.Header.Set(net.HeaderContentLength, r.Header.Get(net.HeaderContentLength))
|
||||
if r.Header.Get(net.HeaderUploadOffset) != "" {
|
||||
httpReq.Header.Set(net.HeaderUploadOffset, r.Header.Get(net.HeaderUploadOffset))
|
||||
} else {
|
||||
httpReq.Header.Set(net.HeaderUploadOffset, "0")
|
||||
}
|
||||
httpReq.Header.Set(net.HeaderTusResumable, r.Header.Get(net.HeaderTusResumable))
|
||||
|
||||
httpRes, err = s.client.Do(httpReq)
|
||||
if err != nil || httpRes == nil {
|
||||
log.Error().Err(err).Msg("error doing PATCH request to data gateway")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer httpRes.Body.Close()
|
||||
|
||||
if httpRes.StatusCode != http.StatusNoContent {
|
||||
w.WriteHeader(httpRes.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set(net.HeaderUploadOffset, httpRes.Header.Get(net.HeaderUploadOffset))
|
||||
w.Header().Set(net.HeaderTusResumable, httpRes.Header.Get(net.HeaderTusResumable))
|
||||
w.Header().Set(net.HeaderTusUploadExpires, httpRes.Header.Get(net.HeaderTusUploadExpires))
|
||||
if httpRes.Header.Get(net.HeaderOCMtime) != "" {
|
||||
w.Header().Set(net.HeaderOCMtime, httpRes.Header.Get(net.HeaderOCMtime))
|
||||
}
|
||||
|
||||
if strings.HasPrefix(uReq.GetRef().GetPath(), "/public") && uReq.GetRef().GetResourceId() == nil {
|
||||
// Use the path based request for the public link
|
||||
sReq.Ref.Path = uReq.Ref.GetPath()
|
||||
sReq.Ref.ResourceId = nil
|
||||
} else {
|
||||
if resid, err := storagespace.ParseID(httpRes.Header.Get(net.HeaderOCFileID)); err == nil {
|
||||
sReq.Ref = &provider.Reference{
|
||||
ResourceId: &resid,
|
||||
}
|
||||
}
|
||||
}
|
||||
finishUpload = httpRes.Header.Get(net.HeaderUploadOffset) == r.Header.Get(net.HeaderUploadLength)
|
||||
}
|
||||
|
||||
// check if upload was fully completed
|
||||
if uploadLength == 0 || finishUpload {
|
||||
// get uploaded file metadata
|
||||
|
||||
sRes, err := client.Stat(ctx, sReq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error sending grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if sRes.Status.Code != rpc.Code_CODE_OK && sRes.Status.Code != rpc.Code_CODE_NOT_FOUND {
|
||||
if sRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
|
||||
// the token expired during upload, so the stat failed
|
||||
// and we can't do anything about it.
|
||||
// the clients will handle this gracefully by doing a propfind on the file
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
errors.HandleErrorStatus(&log, w, sRes.Status)
|
||||
return
|
||||
}
|
||||
|
||||
info := sRes.Info
|
||||
if info == nil {
|
||||
log.Error().Msg("No info found for uploaded file")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// get WebDav permissions for file
|
||||
isPublic := false
|
||||
if info.Opaque != nil && info.Opaque.Map != nil {
|
||||
if info.Opaque.Map["link-share"] != nil && info.Opaque.Map["link-share"].Decoder == "json" {
|
||||
ls := &link.PublicShare{}
|
||||
_ = json.Unmarshal(info.Opaque.Map["link-share"].Value, ls)
|
||||
isPublic = ls != nil
|
||||
}
|
||||
}
|
||||
isShared := !net.IsCurrentUserOwnerOrManager(ctx, info.Owner, info)
|
||||
role := conversions.RoleFromResourcePermissions(info.PermissionSet, isPublic)
|
||||
permissions := role.WebDAVPermissions(
|
||||
info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER,
|
||||
isShared,
|
||||
false,
|
||||
isPublic,
|
||||
)
|
||||
|
||||
w.Header().Set(net.HeaderContentType, info.MimeType)
|
||||
w.Header().Set(net.HeaderOCFileID, storagespace.FormatResourceID(info.Id))
|
||||
w.Header().Set(net.HeaderOCETag, info.Etag)
|
||||
w.Header().Set(net.HeaderETag, info.Etag)
|
||||
w.Header().Set(net.HeaderOCPermissions, permissions)
|
||||
|
||||
t := utils.TSToTime(info.Mtime).UTC()
|
||||
lastModifiedString := t.Format(time.RFC1123Z)
|
||||
w.Header().Set(net.HeaderLastModified, lastModifiedString)
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
Generated
Vendored
+79
@@ -0,0 +1,79 @@
|
||||
package ocdav
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/config"
|
||||
)
|
||||
|
||||
// Validator validates strings
|
||||
type Validator func(string) error
|
||||
|
||||
// ValidatorsFromConfig returns the configured Validators
|
||||
func ValidatorsFromConfig(c *config.Config) []Validator {
|
||||
// we always want to exclude empty names
|
||||
vals := []Validator{notEmpty()}
|
||||
|
||||
// forbidden characters
|
||||
vals = append(vals, doesNotContain(c.NameValidation.InvalidChars))
|
||||
|
||||
// max length
|
||||
vals = append(vals, isShorterThan(c.NameValidation.MaxLength))
|
||||
|
||||
return vals
|
||||
}
|
||||
|
||||
// ValidateName will validate a file or folder name, returning an error when it is not accepted
|
||||
func ValidateName(name string, validators []Validator) error {
|
||||
return ValidateDestination(name, append(validators, notReserved()))
|
||||
}
|
||||
|
||||
// ValidateDestination will validate a file or folder destination name (which can be . or ..), returning an error when it is not accepted
|
||||
func ValidateDestination(name string, validators []Validator) error {
|
||||
for _, v := range validators {
|
||||
if err := v(name); err != nil {
|
||||
return fmt.Errorf("name validation failed: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func notReserved() Validator {
|
||||
return func(s string) error {
|
||||
if s == ".." || s == "." {
|
||||
return errors.New(". and .. are reserved names")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func notEmpty() Validator {
|
||||
return func(s string) error {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return errors.New("must not be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func doesNotContain(bad []string) Validator {
|
||||
return func(s string) error {
|
||||
for _, b := range bad {
|
||||
if strings.Contains(s, b) {
|
||||
return fmt.Errorf("must not contain %s", b)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func isShorterThan(maxLength int) Validator {
|
||||
return func(s string) error {
|
||||
if len(s) > maxLength {
|
||||
return fmt.Errorf("must be shorter than %d", maxLength)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+258
@@ -0,0 +1,258 @@
|
||||
// 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 ocdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/config"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
|
||||
)
|
||||
|
||||
// VersionsHandler handles version requests
|
||||
type VersionsHandler struct {
|
||||
}
|
||||
|
||||
func (h *VersionsHandler) init(c *config.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handler handles requests
|
||||
// versions can be listed with a PROPFIND to /remote.php/dav/meta/<fileid>/v
|
||||
// a version is identified by a timestamp, eg. /remote.php/dav/meta/<fileid>/v/1561410426
|
||||
func (h *VersionsHandler) Handler(s *svc, rid *provider.ResourceId) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
if rid == nil {
|
||||
http.Error(w, "404 Not Found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// baseURI is encoded as part of the response payload in href field
|
||||
baseURI := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), storagespace.FormatResourceID(rid))
|
||||
ctx = context.WithValue(ctx, net.CtxKeyBaseURI, baseURI)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
var key string
|
||||
key, r.URL.Path = router.ShiftPath(r.URL.Path)
|
||||
if r.Method == http.MethodOptions {
|
||||
s.handleOptions(w, r)
|
||||
return
|
||||
}
|
||||
if key == "" && r.Method == MethodPropfind {
|
||||
h.doListVersions(w, r, s, rid)
|
||||
return
|
||||
}
|
||||
if key != "" {
|
||||
switch r.Method {
|
||||
case MethodCopy:
|
||||
// TODO(jfd) cs3api has no delete file version call
|
||||
// TODO(jfd) restore version to given Destination, but cs3api has no destination
|
||||
h.doRestore(w, r, s, rid, key)
|
||||
return
|
||||
case http.MethodHead:
|
||||
log := appctx.GetLogger(ctx)
|
||||
ref := &provider.Reference{
|
||||
ResourceId: &provider.ResourceId{
|
||||
StorageId: rid.StorageId,
|
||||
SpaceId: rid.SpaceId,
|
||||
OpaqueId: key,
|
||||
},
|
||||
Path: utils.MakeRelativePath(r.URL.Path),
|
||||
}
|
||||
s.handleHead(ctx, w, r, ref, *log)
|
||||
return
|
||||
case http.MethodGet:
|
||||
log := appctx.GetLogger(ctx)
|
||||
ref := &provider.Reference{
|
||||
ResourceId: &provider.ResourceId{
|
||||
StorageId: rid.StorageId,
|
||||
SpaceId: rid.SpaceId,
|
||||
OpaqueId: key,
|
||||
},
|
||||
Path: utils.MakeRelativePath(r.URL.Path),
|
||||
}
|
||||
s.handleGet(ctx, w, r, ref, "spaces", *log)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.Error(w, "501 Forbidden", http.StatusNotImplemented)
|
||||
})
|
||||
}
|
||||
|
||||
func (h *VersionsHandler) doListVersions(w http.ResponseWriter, r *http.Request, s *svc, rid *provider.ResourceId) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "listVersions")
|
||||
defer span.End()
|
||||
|
||||
sublog := appctx.GetLogger(ctx).With().Interface("resourceid", rid).Logger()
|
||||
|
||||
pf, status, err := propfind.ReadPropfind(r.Body)
|
||||
if err != nil {
|
||||
sublog.Debug().Err(err).Msg("error reading propfind request")
|
||||
w.WriteHeader(status)
|
||||
return
|
||||
}
|
||||
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error selecting next gateway client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
ref := &provider.Reference{ResourceId: rid}
|
||||
res, err := client.Stat(ctx, &provider.StatRequest{Ref: ref})
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error sending a grpc stat request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if res.Status.Code != rpc.Code_CODE_OK {
|
||||
if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED || res.Status.Code == rpc.Code_CODE_NOT_FOUND {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
b, err := errors.Marshal(http.StatusNotFound, "Resource not found", "", "")
|
||||
errors.HandleWebdavError(&sublog, w, b, err)
|
||||
return
|
||||
}
|
||||
errors.HandleErrorStatus(&sublog, w, res.Status)
|
||||
return
|
||||
}
|
||||
|
||||
info := res.Info
|
||||
|
||||
lvRes, err := client.ListFileVersions(ctx, &provider.ListFileVersionsRequest{Ref: ref})
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error sending list container grpc request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if lvRes.Status.Code != rpc.Code_CODE_OK {
|
||||
if lvRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
b, err := errors.Marshal(http.StatusForbidden, "You have no permission to list file versions on this resource", "", "")
|
||||
errors.HandleWebdavError(&sublog, w, b, err)
|
||||
return
|
||||
}
|
||||
errors.HandleErrorStatus(&sublog, w, lvRes.Status)
|
||||
return
|
||||
}
|
||||
|
||||
versions := lvRes.GetVersions()
|
||||
infos := make([]*provider.ResourceInfo, 0, len(versions)+1)
|
||||
// add version dir . entry, derived from file info
|
||||
infos = append(infos, &provider.ResourceInfo{
|
||||
Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER,
|
||||
})
|
||||
|
||||
for i := range versions {
|
||||
vi := &provider.ResourceInfo{
|
||||
// TODO(jfd) we cannot access version content, this will be a problem when trying to fetch version thumbnails
|
||||
// Opaque
|
||||
Type: provider.ResourceType_RESOURCE_TYPE_FILE,
|
||||
Id: &provider.ResourceId{
|
||||
StorageId: "versions",
|
||||
OpaqueId: info.Id.OpaqueId + "@" + versions[i].GetKey(),
|
||||
},
|
||||
// Checksum
|
||||
Etag: versions[i].Etag,
|
||||
// MimeType
|
||||
Mtime: &types.Timestamp{
|
||||
Seconds: versions[i].Mtime,
|
||||
// TODO cs3apis FileVersion should use types.Timestamp instead of uint64
|
||||
},
|
||||
Path: path.Join("v", versions[i].Key),
|
||||
// PermissionSet
|
||||
Size: versions[i].Size,
|
||||
Owner: info.Owner,
|
||||
}
|
||||
infos = append(infos, vi)
|
||||
}
|
||||
|
||||
prefer := net.ParsePrefer(r.Header.Get("prefer"))
|
||||
returnMinimal := prefer[net.HeaderPreferReturn] == "minimal"
|
||||
|
||||
propRes, err := propfind.MultistatusResponse(ctx, &pf, infos, s.c.PublicURL, "", nil, returnMinimal)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error formatting propfind")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol")
|
||||
w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8")
|
||||
w.Header().Set(net.HeaderVary, net.HeaderPrefer)
|
||||
if returnMinimal {
|
||||
w.Header().Set(net.HeaderPreferenceApplied, "return=minimal")
|
||||
}
|
||||
w.WriteHeader(http.StatusMultiStatus)
|
||||
_, err = w.Write(propRes)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error writing body")
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (h *VersionsHandler) doRestore(w http.ResponseWriter, r *http.Request, s *svc, rid *provider.ResourceId, key string) {
|
||||
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "restore")
|
||||
defer span.End()
|
||||
|
||||
sublog := appctx.GetLogger(ctx).With().Interface("resourceid", rid).Str("key", key).Logger()
|
||||
|
||||
req := &provider.RestoreFileVersionRequest{
|
||||
Ref: &provider.Reference{ResourceId: rid},
|
||||
Key: key,
|
||||
}
|
||||
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error selecting next gateway client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
res, err := client.RestoreFileVersion(ctx, req)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error sending a grpc restore version request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if res.Status.Code != rpc.Code_CODE_OK {
|
||||
if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
b, err := errors.Marshal(http.StatusForbidden, "You have no permission to restore versions on this resource", "", "")
|
||||
errors.HandleWebdavError(&sublog, w, b, err)
|
||||
return
|
||||
}
|
||||
errors.HandleErrorStatus(&sublog, w, res.Status)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
Generated
Vendored
+120
@@ -0,0 +1,120 @@
|
||||
// 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 ocdav
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
)
|
||||
|
||||
// Common Webdav methods.
|
||||
//
|
||||
// Unless otherwise noted, these are defined in RFC 4918 section 9.
|
||||
const (
|
||||
MethodPropfind = "PROPFIND"
|
||||
MethodLock = "LOCK"
|
||||
MethodUnlock = "UNLOCK"
|
||||
MethodProppatch = "PROPPATCH"
|
||||
MethodMkcol = "MKCOL"
|
||||
MethodMove = "MOVE"
|
||||
MethodCopy = "COPY"
|
||||
MethodReport = "REPORT"
|
||||
)
|
||||
|
||||
// WebDavHandler implements a dav endpoint
|
||||
type WebDavHandler struct {
|
||||
namespace string
|
||||
useLoggedInUserNS bool
|
||||
}
|
||||
|
||||
func (h *WebDavHandler) init(ns string, useLoggedInUserNS bool) error {
|
||||
h.namespace = path.Join("/", ns)
|
||||
h.useLoggedInUserNS = useLoggedInUserNS
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handler handles requests
|
||||
func (h *WebDavHandler) Handler(s *svc) http.Handler {
|
||||
config := s.Config()
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ns, newPath, err := s.ApplyLayout(r.Context(), h.namespace, h.useLoggedInUserNS, r.URL.Path)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
b, err := errors.Marshal(http.StatusNotFound, fmt.Sprintf("could not get storage for %s", r.URL.Path), "", "")
|
||||
errors.HandleWebdavError(appctx.GetLogger(r.Context()), w, b, err)
|
||||
return
|
||||
}
|
||||
r.URL.Path = newPath
|
||||
|
||||
// TODO initialize status with http.StatusBadRequest
|
||||
// TODO initialize err with errors.ErrUnsupportedMethod
|
||||
var status int // status 0 means the handler already sent the response
|
||||
switch r.Method {
|
||||
case MethodPropfind:
|
||||
p := propfind.NewHandler(config.PublicURL, s.gatewaySelector, config)
|
||||
p.HandlePathPropfind(w, r, ns)
|
||||
case MethodLock:
|
||||
status, err = s.handleLock(w, r, ns)
|
||||
case MethodUnlock:
|
||||
status, err = s.handleUnlock(w, r, ns)
|
||||
case MethodProppatch:
|
||||
status, err = s.handlePathProppatch(w, r, ns)
|
||||
case MethodMkcol:
|
||||
status, err = s.handlePathMkcol(w, r, ns)
|
||||
case MethodMove:
|
||||
s.handlePathMove(w, r, ns)
|
||||
case MethodCopy:
|
||||
s.handlePathCopy(w, r, ns)
|
||||
case MethodReport:
|
||||
s.handleReport(w, r, ns)
|
||||
case http.MethodGet:
|
||||
s.handlePathGet(w, r, ns)
|
||||
case http.MethodPut:
|
||||
s.handlePathPut(w, r, ns)
|
||||
case http.MethodPost:
|
||||
s.handlePathTusPost(w, r, ns)
|
||||
case http.MethodOptions:
|
||||
s.handleOptions(w, r)
|
||||
case http.MethodHead:
|
||||
s.handlePathHead(w, r, ns)
|
||||
case http.MethodDelete:
|
||||
status, err = s.handlePathDelete(w, r, ns)
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
||||
if status != 0 { // 0 means the handler already sent the response
|
||||
w.WriteHeader(status)
|
||||
if status != http.StatusNoContent {
|
||||
var b []byte
|
||||
if b, err = errors.Marshal(status, err.Error(), "", ""); err == nil {
|
||||
_, err = w.Write(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
appctx.GetLogger(r.Context()).Error().Err(err).Msg(err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+73
@@ -0,0 +1,73 @@
|
||||
// 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 ocs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func (s *svc) cacheWarmup(w http.ResponseWriter, r *http.Request) {
|
||||
if s.warmupCacheTracker != nil {
|
||||
u, ok1 := ctxpkg.ContextGetUser(r.Context())
|
||||
tkn, ok2 := ctxpkg.ContextGetToken(r.Context())
|
||||
if !ok1 || !ok2 {
|
||||
return
|
||||
}
|
||||
|
||||
log := appctx.GetLogger(r.Context())
|
||||
|
||||
// We make a copy of the context because the original one comes with its cancel channel,
|
||||
// so once the initial request is finished, this ctx gets cancelled as well.
|
||||
// And in most of the cases, the warmup takes a longer amount of time to complete than the original request.
|
||||
// TODO: Check if we can come up with a better solution, eg, https://stackoverflow.com/a/54132324
|
||||
ctx := context.Background()
|
||||
ctx = appctx.WithLogger(ctx, log)
|
||||
ctx = ctxpkg.ContextSetUser(ctx, u)
|
||||
ctx = ctxpkg.ContextSetToken(ctx, tkn)
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, tkn)
|
||||
|
||||
req, _ := http.NewRequest("GET", "", nil)
|
||||
req = req.WithContext(ctx)
|
||||
req.URL = r.URL
|
||||
|
||||
id := u.Id.OpaqueId
|
||||
if _, err := s.warmupCacheTracker.Get(id); err != nil {
|
||||
p := httptest.NewRecorder()
|
||||
_ = s.warmupCacheTracker.Set(id, true)
|
||||
|
||||
log.Info().Msgf("cache warmup getting created shares for user %s", id)
|
||||
req.URL.Path = "/v1.php/apps/files_sharing/api/v1/shares"
|
||||
s.router.ServeHTTP(p, req)
|
||||
|
||||
log.Info().Msgf("cache warmup getting received shares for user %s", id)
|
||||
req.URL.Path = "/v1.php/apps/files_sharing/api/v1/shares"
|
||||
q := req.URL.Query()
|
||||
q.Set("shared_with_me", "true")
|
||||
q.Set("state", "all")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
s.router.ServeHTTP(p, req)
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+81
@@ -0,0 +1,81 @@
|
||||
// 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 config
|
||||
|
||||
import (
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/data"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/owncloud/ocs"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/cache"
|
||||
)
|
||||
|
||||
// Config holds the config options that need to be passed down to all ocs handlers
|
||||
type Config struct {
|
||||
Prefix string `mapstructure:"prefix"`
|
||||
Config data.ConfigData `mapstructure:"config"`
|
||||
Capabilities ocs.CapabilitiesData `mapstructure:"capabilities"`
|
||||
GatewaySvc string `mapstructure:"gatewaysvc"`
|
||||
StorageregistrySvc string `mapstructure:"storage_registry_svc"`
|
||||
DefaultUploadProtocol string `mapstructure:"default_upload_protocol"`
|
||||
UserAgentChunkingMap map[string]string `mapstructure:"user_agent_chunking_map"`
|
||||
SharePrefix string `mapstructure:"share_prefix"`
|
||||
HomeNamespace string `mapstructure:"home_namespace"`
|
||||
AdditionalInfoAttribute string `mapstructure:"additional_info_attribute"`
|
||||
CacheWarmupDriver string `mapstructure:"cache_warmup_driver"`
|
||||
CacheWarmupDrivers map[string]map[string]interface{} `mapstructure:"cache_warmup_drivers"`
|
||||
StatCacheConfig cache.Config `mapstructure:"stat_cache_config"`
|
||||
UserIdentifierCacheTTL int `mapstructure:"user_identifier_cache_ttl"`
|
||||
MachineAuthAPIKey string `mapstructure:"machine_auth_apikey"`
|
||||
SkipUpdatingExistingSharesMountpoints bool `mapstructure:"skip_updating_existing_shares_mountpoint"`
|
||||
EnableDenials bool `mapstructure:"enable_denials"`
|
||||
OCMMountPoint string `mapstructure:"ocm_mount_point"`
|
||||
ListOCMShares bool `mapstructure:"list_ocm_shares"`
|
||||
Notifications map[string]interface{} `mapstructure:"notifications"`
|
||||
IncludeOCMSharees bool `mapstructure:"include_ocm_sharees"`
|
||||
ShowEmailInResults bool `mapstructure:"show_email_in_results"`
|
||||
}
|
||||
|
||||
// Init sets sane defaults
|
||||
func (c *Config) Init() {
|
||||
if c.Prefix == "" {
|
||||
c.Prefix = "ocs"
|
||||
}
|
||||
|
||||
if c.SharePrefix == "" {
|
||||
c.SharePrefix = "/Shares"
|
||||
}
|
||||
|
||||
if c.DefaultUploadProtocol == "" {
|
||||
c.DefaultUploadProtocol = "tus"
|
||||
}
|
||||
|
||||
if c.HomeNamespace == "" {
|
||||
c.HomeNamespace = "/users/{{.Id.OpaqueId}}"
|
||||
}
|
||||
|
||||
if c.AdditionalInfoAttribute == "" {
|
||||
c.AdditionalInfoAttribute = "{{.Mail}}"
|
||||
}
|
||||
|
||||
if c.UserIdentifierCacheTTL == 0 {
|
||||
c.UserIdentifierCacheTTL = 60
|
||||
}
|
||||
|
||||
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
|
||||
}
|
||||
Generated
Vendored
+28
@@ -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 data
|
||||
|
||||
// ConfigData holds basic config
|
||||
type ConfigData struct {
|
||||
Version string `json:"version" xml:"version"`
|
||||
Website string `json:"website" xml:"website"`
|
||||
Host string `json:"host" xml:"host"`
|
||||
Contact string `json:"contact" xml:"contact"`
|
||||
SSL string `json:"ssl" xml:"ssl"`
|
||||
}
|
||||
Generated
Vendored
+201
@@ -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 sharees
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/conversions"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/config"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/templates"
|
||||
)
|
||||
|
||||
// Handler implements the ownCloud sharing API
|
||||
type Handler struct {
|
||||
gatewayAddr string
|
||||
additionalInfoAttribute string
|
||||
includeOCMSharees bool
|
||||
showUserEmailInResults bool
|
||||
}
|
||||
|
||||
// Init initializes this and any contained handlers
|
||||
func (h *Handler) Init(c *config.Config) {
|
||||
h.gatewayAddr = c.GatewaySvc
|
||||
h.additionalInfoAttribute = c.AdditionalInfoAttribute
|
||||
h.includeOCMSharees = c.IncludeOCMSharees
|
||||
h.showUserEmailInResults = c.ShowEmailInResults
|
||||
}
|
||||
|
||||
// FindSharees implements the /apps/files_sharing/api/v1/sharees endpoint
|
||||
func (h *Handler) FindSharees(w http.ResponseWriter, r *http.Request) {
|
||||
log := appctx.GetLogger(r.Context())
|
||||
term := r.URL.Query().Get("search")
|
||||
|
||||
if term == "" {
|
||||
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "search must not be empty", nil)
|
||||
return
|
||||
}
|
||||
|
||||
gwc, err := pool.GetGatewayServiceClient(h.gatewayAddr)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting gateway grpc client", err)
|
||||
return
|
||||
}
|
||||
usersRes, err := gwc.FindUsers(r.Context(), &userpb.FindUsersRequest{Filter: term, SkipFetchingUserGroups: true})
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error searching users", err)
|
||||
return
|
||||
}
|
||||
log.Debug().Int("count", len(usersRes.GetUsers())).Str("search", term).Msg("users found")
|
||||
|
||||
userMatches := make([]*conversions.MatchData, 0, len(usersRes.GetUsers()))
|
||||
exactUserMatches := make([]*conversions.MatchData, 0)
|
||||
for _, user := range usersRes.GetUsers() {
|
||||
match := h.userAsMatch(user)
|
||||
log.Debug().Interface("user", user).Interface("match", match).Msg("mapped")
|
||||
if h.isExactMatch(match, term) {
|
||||
exactUserMatches = append(exactUserMatches, match)
|
||||
} else {
|
||||
userMatches = append(userMatches, match)
|
||||
}
|
||||
}
|
||||
|
||||
if h.includeOCMSharees {
|
||||
remoteUsersRes, err := gwc.FindAcceptedUsers(r.Context(), &invitepb.FindAcceptedUsersRequest{Filter: term})
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error searching remote users", err)
|
||||
return
|
||||
}
|
||||
if remoteUsersRes.Status.Code != rpc.Code_CODE_OK {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error searching remote users", nil)
|
||||
return
|
||||
}
|
||||
for _, user := range remoteUsersRes.GetAcceptedUsers() {
|
||||
match := h.userAsMatch(user)
|
||||
log.Debug().Interface("user", user).Interface("match", match).Msg("mapped")
|
||||
if h.isExactMatch(match, term) {
|
||||
exactUserMatches = append(exactUserMatches, match)
|
||||
} else {
|
||||
userMatches = append(userMatches, match)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groupsRes, err := gwc.FindGroups(r.Context(), &grouppb.FindGroupsRequest{Filter: term, SkipFetchingMembers: true})
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error searching groups", err)
|
||||
return
|
||||
}
|
||||
log.Debug().Int("count", len(groupsRes.GetGroups())).Str("search", term).Msg("groups found")
|
||||
|
||||
groupMatches := make([]*conversions.MatchData, 0, len(groupsRes.GetGroups()))
|
||||
exactGroupMatches := make([]*conversions.MatchData, 0)
|
||||
for _, g := range groupsRes.GetGroups() {
|
||||
match := h.groupAsMatch(g)
|
||||
log.Debug().Interface("group", g).Interface("match", match).Msg("mapped")
|
||||
if h.isExactMatch(match, term) {
|
||||
exactGroupMatches = append(exactGroupMatches, match)
|
||||
} else {
|
||||
groupMatches = append(groupMatches, match)
|
||||
}
|
||||
}
|
||||
|
||||
if !h.showUserEmailInResults {
|
||||
for _, m := range userMatches {
|
||||
m.Value.ShareWithAdditionalInfo = m.Value.ShareWith
|
||||
}
|
||||
for _, m := range exactUserMatches {
|
||||
m.Value.ShareWithAdditionalInfo = m.Value.ShareWith
|
||||
}
|
||||
for _, m := range groupMatches {
|
||||
m.Value.ShareWithAdditionalInfo = m.Value.ShareWith
|
||||
}
|
||||
for _, m := range exactGroupMatches {
|
||||
m.Value.ShareWithAdditionalInfo = m.Value.ShareWith
|
||||
}
|
||||
}
|
||||
|
||||
response.WriteOCSSuccess(w, r, &conversions.ShareeData{
|
||||
Exact: &conversions.ExactMatchesData{
|
||||
Users: exactUserMatches,
|
||||
Groups: exactGroupMatches,
|
||||
Remotes: []*conversions.MatchData{},
|
||||
},
|
||||
Users: userMatches,
|
||||
Groups: groupMatches,
|
||||
Remotes: []*conversions.MatchData{},
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) userAsMatch(u *userpb.User) *conversions.MatchData {
|
||||
data := &conversions.MatchValueData{
|
||||
ShareType: int(conversions.ShareTypeUser),
|
||||
// api compatibility with oc10: mark guest users in share invite dialogue
|
||||
UserType: 0,
|
||||
// api compatibility with oc10: always use the username
|
||||
ShareWith: u.Username,
|
||||
ShareWithAdditionalInfo: h.getAdditionalInfoAttribute(u),
|
||||
}
|
||||
|
||||
switch u.Id.Type {
|
||||
case userpb.UserType_USER_TYPE_GUEST, userpb.UserType_USER_TYPE_LIGHTWEIGHT:
|
||||
data.UserType = 1
|
||||
case userpb.UserType_USER_TYPE_FEDERATED:
|
||||
data.ShareType = int(conversions.ShareTypeFederatedCloudShare)
|
||||
data.ShareWith = u.Id.OpaqueId
|
||||
data.ShareWithProvider = u.Id.Idp
|
||||
}
|
||||
|
||||
return &conversions.MatchData{
|
||||
Label: u.DisplayName,
|
||||
Value: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) groupAsMatch(g *grouppb.Group) *conversions.MatchData {
|
||||
return &conversions.MatchData{
|
||||
Label: g.DisplayName,
|
||||
Value: &conversions.MatchValueData{
|
||||
ShareType: int(conversions.ShareTypeGroup),
|
||||
ShareWith: g.GroupName,
|
||||
ShareWithAdditionalInfo: g.Mail,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) getAdditionalInfoAttribute(u *userpb.User) string {
|
||||
return templates.WithUser(u, h.additionalInfoAttribute)
|
||||
}
|
||||
|
||||
func (h *Handler) isExactMatch(match *conversions.MatchData, term string) bool {
|
||||
if match == nil || match.Value == nil {
|
||||
return false
|
||||
}
|
||||
return strings.EqualFold(match.Value.ShareWith, term) || strings.EqualFold(match.Value.ShareWithAdditionalInfo, term) ||
|
||||
strings.EqualFold(match.Label, term)
|
||||
}
|
||||
Generated
Vendored
+169
@@ -0,0 +1,169 @@
|
||||
// Copyright 2018-2022 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 sharees
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/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/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
|
||||
"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/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
// TokenInfo handles http requests regarding tokens
|
||||
func (h *Handler) TokenInfo(protected bool) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
log := appctx.GetLogger(r.Context())
|
||||
tkn := path.Base(r.URL.Path)
|
||||
_, pw, _ := r.BasicAuth()
|
||||
|
||||
c, err := pool.GetGatewayServiceClient(h.gatewayAddr)
|
||||
if err != nil {
|
||||
// endpoint public - don't exponse information
|
||||
log.Error().Err(err).Msg("error getting gateway client")
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
t, err := handleGetToken(r.Context(), tkn, pw, c, protected)
|
||||
if err != nil {
|
||||
// endpoint public - don't exponse information
|
||||
log.Error().Err(err).Msg("error while handling GET TokenInfo")
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteOCSSuccess(w, r, t)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetToken(ctx context.Context, tkn string, pw string, c gateway.GatewayAPIClient, protected bool) (conversions.TokenInfo, error) {
|
||||
user, token, passwordProtected, err := getInfoForToken(tkn, pw, c)
|
||||
if err != nil {
|
||||
return conversions.TokenInfo{}, err
|
||||
}
|
||||
|
||||
t, err := buildTokenInfo(user, tkn, token, passwordProtected, c)
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
|
||||
if protected && !t.PasswordProtected {
|
||||
space, status, err := spacelookup.LookUpStorageSpaceByID(ctx, c, storagespace.FormatResourceID(&provider.ResourceId{StorageId: t.StorageID, SpaceId: t.SpaceID, OpaqueId: t.OpaqueID}))
|
||||
// add info only if user is able to stat
|
||||
if err == nil && status.Code == rpc.Code_CODE_OK {
|
||||
t.SpacePath = utils.ReadPlainFromOpaque(space.Opaque, "path")
|
||||
t.SpaceAlias = utils.ReadPlainFromOpaque(space.Opaque, "spaceAlias")
|
||||
t.SpaceURL = path.Join(t.SpaceAlias, t.OpaqueID, t.Path)
|
||||
t.SpaceType = space.SpaceType
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func buildTokenInfo(owner *user.User, tkn string, token string, passProtected bool, c gateway.GatewayAPIClient) (conversions.TokenInfo, error) {
|
||||
t := conversions.TokenInfo{Token: tkn, LinkURL: "/s/" + tkn}
|
||||
if passProtected {
|
||||
t.PasswordProtected = true
|
||||
return t, nil
|
||||
}
|
||||
|
||||
ctx := ctxpkg.ContextSetToken(context.TODO(), token)
|
||||
ctx = ctxpkg.ContextSetUser(ctx, owner)
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, token)
|
||||
|
||||
sRes, err := getPublicShare(ctx, c, tkn)
|
||||
if err != nil || sRes.Status.Code != rpc.Code_CODE_OK {
|
||||
return t, fmt.Errorf("can't stat resource. %+v %s", sRes, err)
|
||||
}
|
||||
|
||||
t.ID = storagespace.FormatResourceID(sRes.Share.GetResourceId())
|
||||
t.StorageID = sRes.Share.ResourceId.GetStorageId()
|
||||
t.SpaceID = sRes.Share.ResourceId.GetSpaceId()
|
||||
t.OpaqueID = sRes.Share.ResourceId.GetOpaqueId()
|
||||
|
||||
role := conversions.RoleFromResourcePermissions(sRes.Share.Permissions.GetPermissions(), true)
|
||||
t.Aliaslink = role.OCSPermissions() == 0
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func getInfoForToken(tkn string, pw string, c gateway.GatewayAPIClient) (owner *user.User, token string, passwordProtected bool, err error) {
|
||||
ctx := context.Background()
|
||||
|
||||
res, err := handleBasicAuth(ctx, c, tkn, pw)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch res.Status.Code {
|
||||
case rpc.Code_CODE_OK:
|
||||
// nothing to do
|
||||
case rpc.Code_CODE_PERMISSION_DENIED:
|
||||
if res.Status.Message != "wrong password" {
|
||||
err = errors.New("not found")
|
||||
return
|
||||
}
|
||||
|
||||
passwordProtected = true
|
||||
return
|
||||
default:
|
||||
err = fmt.Errorf("authentication returned unsupported status code '%d'", res.Status.Code)
|
||||
return
|
||||
}
|
||||
|
||||
return res.User, res.Token, false, nil
|
||||
}
|
||||
|
||||
func handleBasicAuth(ctx context.Context, c gateway.GatewayAPIClient, token, pw string) (*gateway.AuthenticateResponse, error) {
|
||||
authenticateRequest := gateway.AuthenticateRequest{
|
||||
Type: "publicshares",
|
||||
ClientId: token,
|
||||
ClientSecret: "password|" + pw,
|
||||
}
|
||||
|
||||
return c.Authenticate(ctx, &authenticateRequest)
|
||||
}
|
||||
|
||||
func getPublicShare(ctx context.Context, client gateway.GatewayAPIClient, token string) (*link.GetPublicShareResponse, error) {
|
||||
return client.GetPublicShare(ctx, &link.GetPublicShareRequest{
|
||||
Ref: &link.PublicShareReference{
|
||||
Spec: &link.PublicShareReference_Token{
|
||||
Token: token,
|
||||
},
|
||||
}})
|
||||
}
|
||||
Generated
Vendored
+100
@@ -0,0 +1,100 @@
|
||||
// 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 shares
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/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"
|
||||
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/conversions"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
|
||||
)
|
||||
|
||||
func (h *Handler) createGroupShare(w http.ResponseWriter, r *http.Request, statInfo *provider.ResourceInfo, role *conversions.Role, roleVal []byte) (*collaboration.Share, *ocsError) {
|
||||
ctx := r.Context()
|
||||
c, err := h.getClient()
|
||||
if err != nil {
|
||||
return nil, &ocsError{
|
||||
Code: response.MetaServerError.StatusCode,
|
||||
Message: "error getting grpc gateway client",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
shareWith := r.FormValue("shareWith")
|
||||
if shareWith == "" {
|
||||
return nil, &ocsError{
|
||||
Code: response.MetaBadRequest.StatusCode,
|
||||
Message: "missing shareWith",
|
||||
}
|
||||
}
|
||||
|
||||
groupRes, err := c.GetGroupByClaim(ctx, &grouppb.GetGroupByClaimRequest{
|
||||
Claim: "group_name",
|
||||
Value: shareWith,
|
||||
SkipFetchingMembers: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, &ocsError{
|
||||
Code: response.MetaServerError.StatusCode,
|
||||
Message: "error searching recipient",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
if groupRes.Status.Code != rpc.Code_CODE_OK {
|
||||
return nil, &ocsError{
|
||||
Code: response.MetaNotFound.StatusCode,
|
||||
Message: "group not found",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
createShareReq := &collaboration.CreateShareRequest{
|
||||
Opaque: &types.Opaque{
|
||||
Map: map[string]*types.OpaqueEntry{
|
||||
"role": {
|
||||
Decoder: "json",
|
||||
Value: roleVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
ResourceInfo: statInfo,
|
||||
Grant: &collaboration.ShareGrant{
|
||||
Grantee: &provider.Grantee{
|
||||
Type: provider.GranteeType_GRANTEE_TYPE_GROUP,
|
||||
Id: &provider.Grantee_GroupId{GroupId: groupRes.Group.GetId()},
|
||||
},
|
||||
Permissions: &collaboration.SharePermissions{
|
||||
Permissions: role.CS3ResourcePermissions(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
share, ocsErr := h.createCs3Share(ctx, w, r, c, createShareReq)
|
||||
if ocsErr != nil {
|
||||
return nil, ocsErr
|
||||
}
|
||||
|
||||
return share, nil
|
||||
}
|
||||
Generated
Vendored
+377
@@ -0,0 +1,377 @@
|
||||
// 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 shares
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
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"
|
||||
ocmv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/protobuf/types/known/fieldmaskpb"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/internal/grpc/services/usershareprovider"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/conversions"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
)
|
||||
|
||||
const (
|
||||
// shareidkey is the key user to obtain the id of the share to update. It is present in the request URL.
|
||||
shareidkey string = "shareid"
|
||||
)
|
||||
|
||||
// AcceptReceivedShare handles Post Requests on /apps/files_sharing/api/v1/shares/{shareid}
|
||||
func (h *Handler) AcceptReceivedShare(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
shareID := chi.URLParam(r, shareidkey)
|
||||
|
||||
if h.isFederatedReceivedShare(r, shareID) {
|
||||
h.updateReceivedFederatedShare(w, r, shareID, false)
|
||||
return
|
||||
}
|
||||
|
||||
client, err := h.getClient()
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err)
|
||||
return
|
||||
}
|
||||
|
||||
receivedShare, ocsResponse := getReceivedShareFromID(ctx, client, shareID)
|
||||
if ocsResponse != nil {
|
||||
response.WriteOCSResponse(w, r, *ocsResponse, nil)
|
||||
return
|
||||
}
|
||||
|
||||
sharedResource, ocsResponse := getSharedResource(ctx, client, receivedShare.Share.ResourceId)
|
||||
if ocsResponse != nil {
|
||||
response.WriteOCSResponse(w, r, *ocsResponse, nil)
|
||||
return
|
||||
}
|
||||
mount, unmountedShares, err := usershareprovider.GetMountpointAndUnmountedShares(
|
||||
ctx,
|
||||
client,
|
||||
sharedResource.GetInfo().GetId(),
|
||||
sharedResource.GetInfo().GetName(),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "could not determine mountpoint", err)
|
||||
return
|
||||
}
|
||||
|
||||
// first update the requested share
|
||||
receivedShare.State = collaboration.ShareState_SHARE_STATE_ACCEPTED
|
||||
// we need to add a path to the share
|
||||
receivedShare.MountPoint = &provider.Reference{
|
||||
Path: mount,
|
||||
}
|
||||
|
||||
updateMask := &fieldmaskpb.FieldMask{Paths: []string{"state", "mount_point"}}
|
||||
data, meta, err := h.updateReceivedShare(r.Context(), receivedShare, updateMask)
|
||||
if err != nil {
|
||||
// we log an error for affected shares, for the actual share we return an error
|
||||
response.WriteOCSData(w, r, meta, data, err)
|
||||
return
|
||||
}
|
||||
response.WriteOCSSuccess(w, r, []*conversions.ShareData{data})
|
||||
|
||||
// then update other unmounted shares to the same resource
|
||||
for _, rs := range unmountedShares {
|
||||
if rs.GetShare().GetId().GetOpaqueId() == shareID {
|
||||
// we already updated this one
|
||||
continue
|
||||
}
|
||||
|
||||
rs.State = collaboration.ShareState_SHARE_STATE_ACCEPTED
|
||||
// set the same mountpoint as for the requested received share
|
||||
rs.MountPoint = &provider.Reference{
|
||||
Path: mount,
|
||||
}
|
||||
|
||||
_, _, err := h.updateReceivedShare(r.Context(), rs, updateMask)
|
||||
if err != nil {
|
||||
// we log an error for affected shares, the actual share was successful
|
||||
appctx.GetLogger(ctx).Error().Err(err).Str("received_share", shareID).Str("affected_share", rs.GetShare().GetId().GetOpaqueId()).Msg("could not update affected received share")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RejectReceivedShare handles DELETE Requests on /apps/files_sharing/api/v1/shares/{shareid}
|
||||
func (h *Handler) RejectReceivedShare(w http.ResponseWriter, r *http.Request) {
|
||||
shareID := chi.URLParam(r, "shareid")
|
||||
|
||||
if h.isFederatedReceivedShare(r, shareID) {
|
||||
h.updateReceivedFederatedShare(w, r, shareID, true)
|
||||
return
|
||||
}
|
||||
|
||||
// we need to add a path to the share
|
||||
receivedShare := &collaboration.ReceivedShare{
|
||||
Share: &collaboration.Share{
|
||||
Id: &collaboration.ShareId{OpaqueId: shareID},
|
||||
},
|
||||
State: collaboration.ShareState_SHARE_STATE_REJECTED,
|
||||
}
|
||||
updateMask := &fieldmaskpb.FieldMask{Paths: []string{"state"}}
|
||||
|
||||
data, meta, err := h.updateReceivedShare(r.Context(), receivedShare, updateMask)
|
||||
if err != nil {
|
||||
response.WriteOCSData(w, r, meta, nil, err)
|
||||
}
|
||||
response.WriteOCSSuccess(w, r, []*conversions.ShareData{data})
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateReceivedShare(w http.ResponseWriter, r *http.Request) {
|
||||
shareID := chi.URLParam(r, "shareid")
|
||||
hideFlag, _ := strconv.ParseBool(r.URL.Query().Get("hidden"))
|
||||
|
||||
// unfortunately we need to get the share first to read the state
|
||||
client, err := h.getClient()
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err)
|
||||
return
|
||||
}
|
||||
|
||||
// we need to add a path to the share
|
||||
receivedShare := &collaboration.ReceivedShare{
|
||||
Share: &collaboration.Share{
|
||||
Id: &collaboration.ShareId{OpaqueId: shareID},
|
||||
},
|
||||
Hidden: hideFlag,
|
||||
}
|
||||
updateMask := &fieldmaskpb.FieldMask{Paths: []string{"state", "hidden"}}
|
||||
|
||||
rs, _ := getReceivedShareFromID(r.Context(), client, shareID)
|
||||
if rs != nil && rs.Share != nil {
|
||||
receivedShare.State = rs.State
|
||||
}
|
||||
|
||||
data, meta, err := h.updateReceivedShare(r.Context(), receivedShare, updateMask)
|
||||
if err != nil {
|
||||
response.WriteOCSData(w, r, meta, nil, err)
|
||||
}
|
||||
response.WriteOCSSuccess(w, r, []*conversions.ShareData{data})
|
||||
}
|
||||
|
||||
func (h *Handler) updateReceivedShare(ctx context.Context, receivedShare *collaboration.ReceivedShare, fieldMask *fieldmaskpb.FieldMask) (*conversions.ShareData, response.Meta, error) {
|
||||
logger := appctx.GetLogger(ctx)
|
||||
|
||||
updateShareRequest := &collaboration.UpdateReceivedShareRequest{
|
||||
Share: receivedShare,
|
||||
UpdateMask: fieldMask,
|
||||
}
|
||||
|
||||
client, err := h.getClient()
|
||||
if err != nil {
|
||||
return nil, response.MetaServerError, errors.Wrap(err, "error getting grpc gateway client")
|
||||
}
|
||||
|
||||
shareRes, err := client.UpdateReceivedShare(ctx, updateShareRequest)
|
||||
if err != nil {
|
||||
return nil, response.MetaServerError, errors.Wrap(err, "grpc update received share request failed")
|
||||
}
|
||||
|
||||
if shareRes.Status.Code != rpc.Code_CODE_OK {
|
||||
if shareRes.Status.Code == rpc.Code_CODE_NOT_FOUND {
|
||||
return nil, response.MetaNotFound, errors.New(shareRes.Status.Message)
|
||||
}
|
||||
return nil, response.MetaServerError, errors.Errorf("grpc update received share request failed: code: %d, message: %s", shareRes.Status.Code, shareRes.Status.Message)
|
||||
}
|
||||
|
||||
rs := shareRes.GetShare()
|
||||
|
||||
info, status, err := h.getResourceInfoByID(ctx, client, rs.Share.ResourceId)
|
||||
if err != nil || status.Code != rpc.Code_CODE_OK {
|
||||
h.logProblems(logger, status, err, "could not stat, skipping")
|
||||
return nil, response.MetaServerError, errors.Errorf("grpc get resource info failed: code: %d, message: %s", status.Code, status.Message)
|
||||
}
|
||||
|
||||
data := conversions.CS3Share2ShareData(ctx, rs.Share)
|
||||
|
||||
data.State = mapState(rs.GetState())
|
||||
data.Hidden = rs.GetHidden()
|
||||
|
||||
h.addFileInfo(ctx, data, info)
|
||||
h.mapUserIds(ctx, client, data)
|
||||
|
||||
if data.State == ocsStateAccepted {
|
||||
// Needed because received shares can be jailed in a folder in the users home
|
||||
data.Path = path.Join(h.sharePrefix, path.Base(info.Path))
|
||||
}
|
||||
|
||||
return data, response.MetaOK, nil
|
||||
}
|
||||
|
||||
func (h *Handler) updateReceivedFederatedShare(w http.ResponseWriter, r *http.Request, shareID string, rejectShare bool) {
|
||||
ctx := r.Context()
|
||||
|
||||
client, err := h.getClient()
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err)
|
||||
return
|
||||
}
|
||||
|
||||
share, err := client.GetReceivedOCMShare(ctx, &ocmv1beta1.GetReceivedOCMShareRequest{
|
||||
Ref: &ocmv1beta1.ShareReference{
|
||||
Spec: &ocmv1beta1.ShareReference_Id{
|
||||
Id: &ocmv1beta1.ShareId{
|
||||
OpaqueId: shareID,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc update received share request failed", err)
|
||||
return
|
||||
}
|
||||
if share.Status.Code != rpc.Code_CODE_OK {
|
||||
if share.Status.Code == rpc.Code_CODE_NOT_FOUND {
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "not found", nil)
|
||||
return
|
||||
}
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc update received share request failed", errors.Errorf("code: %d, message: %s", share.Status.Code, share.Status.Message))
|
||||
return
|
||||
}
|
||||
|
||||
req := &ocmv1beta1.UpdateReceivedOCMShareRequest{
|
||||
Share: &ocmv1beta1.ReceivedShare{
|
||||
Id: &ocmv1beta1.ShareId{
|
||||
OpaqueId: shareID,
|
||||
},
|
||||
},
|
||||
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"state"}},
|
||||
}
|
||||
if rejectShare {
|
||||
req.Share.State = ocmv1beta1.ShareState_SHARE_STATE_REJECTED
|
||||
} else {
|
||||
req.Share.State = ocmv1beta1.ShareState_SHARE_STATE_ACCEPTED
|
||||
}
|
||||
|
||||
updateRes, err := client.UpdateReceivedOCMShare(ctx, req)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc update received share request failed", err)
|
||||
return
|
||||
}
|
||||
|
||||
if updateRes.Status.Code != rpc.Code_CODE_OK {
|
||||
if updateRes.Status.Code == rpc.Code_CODE_NOT_FOUND {
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "not found", nil)
|
||||
return
|
||||
}
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc update received share request failed", errors.Errorf("code: %d, message: %s", updateRes.Status.Code, updateRes.Status.Message))
|
||||
return
|
||||
}
|
||||
|
||||
data, err := conversions.ReceivedOCMShare2ShareData(share.Share, h.ocmLocalMount(share.Share))
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc update received share request failed", err)
|
||||
return
|
||||
}
|
||||
h.mapUserIdsReceivedFederatedShare(ctx, client, data)
|
||||
data.State = mapOCMState(req.Share.State)
|
||||
response.WriteOCSSuccess(w, r, []*conversions.ShareData{data})
|
||||
}
|
||||
|
||||
// getReceivedShareFromID uses a client to the gateway to fetch a share based on its ID.
|
||||
func getReceivedShareFromID(ctx context.Context, client gateway.GatewayAPIClient, shareID string) (*collaboration.ReceivedShare, *response.Response) {
|
||||
s, err := client.GetReceivedShare(ctx, &collaboration.GetReceivedShareRequest{
|
||||
Ref: &collaboration.ShareReference{
|
||||
Spec: &collaboration.ShareReference_Id{
|
||||
Id: &collaboration.ShareId{
|
||||
OpaqueId: shareID,
|
||||
}},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
e := errors.Wrap(err, fmt.Sprintf("could not get share with ID: `%s`", shareID))
|
||||
return nil, arbitraryOcsResponse(response.MetaServerError.StatusCode, e.Error())
|
||||
}
|
||||
|
||||
if s.Status.Code != rpc.Code_CODE_OK {
|
||||
if s.Status.Code == rpc.Code_CODE_NOT_FOUND {
|
||||
e := fmt.Errorf("share not found")
|
||||
return nil, arbitraryOcsResponse(response.MetaNotFound.StatusCode, e.Error())
|
||||
}
|
||||
|
||||
e := fmt.Errorf("invalid share: %s", s.GetStatus().GetMessage())
|
||||
return nil, arbitraryOcsResponse(response.MetaBadRequest.StatusCode, e.Error())
|
||||
}
|
||||
|
||||
return s.Share, nil
|
||||
}
|
||||
|
||||
// getSharedResource attempts to get a shared resource from the storage from the resource reference.
|
||||
func getSharedResource(ctx context.Context, client gateway.GatewayAPIClient, resID *provider.ResourceId) (*provider.StatResponse, *response.Response) {
|
||||
res, err := client.Stat(ctx, &provider.StatRequest{
|
||||
Ref: &provider.Reference{
|
||||
ResourceId: resID,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, arbitraryOcsResponse(response.MetaServerError.StatusCode, "could not get reference")
|
||||
}
|
||||
|
||||
if res.Status.Code != rpc.Code_CODE_OK {
|
||||
if res.Status.Code == rpc.Code_CODE_NOT_FOUND {
|
||||
return nil, arbitraryOcsResponse(response.MetaNotFound.StatusCode, "not found")
|
||||
}
|
||||
return nil, arbitraryOcsResponse(response.MetaServerError.StatusCode, res.GetStatus().GetMessage())
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// listReceivedShares list all received shares for the current user.
|
||||
func listReceivedShares(ctx context.Context, client gateway.GatewayAPIClient) ([]*collaboration.ReceivedShare, error) {
|
||||
res, err := client.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{})
|
||||
if err != nil {
|
||||
return nil, errtypes.InternalError("grpc list received shares request failed")
|
||||
}
|
||||
|
||||
if err := errtypes.NewErrtypeFromStatus(res.Status); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res.Shares, nil
|
||||
}
|
||||
|
||||
// arbitraryOcsResponse abstracts the boilerplate that is creating a response.Response struct.
|
||||
func arbitraryOcsResponse(statusCode int, message string) *response.Response {
|
||||
r := response.Response{
|
||||
OCS: &response.Payload{
|
||||
XMLName: struct{}{},
|
||||
Meta: response.Meta{},
|
||||
Data: nil,
|
||||
},
|
||||
}
|
||||
|
||||
r.OCS.Meta.StatusCode = statusCode
|
||||
r.OCS.Meta.Message = message
|
||||
return &r
|
||||
}
|
||||
Generated
Vendored
+736
@@ -0,0 +1,736 @@
|
||||
// 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 shares
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/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"
|
||||
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/huandu/xstrings"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/conversions"
|
||||
"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/utils"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/publicshare"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var _defaultPublicLinkPermission = 1
|
||||
|
||||
func (h *Handler) createPublicLinkShare(w http.ResponseWriter, r *http.Request, statInfo *provider.ResourceInfo) (*link.PublicShare, *ocsError) {
|
||||
ctx := r.Context()
|
||||
log := appctx.GetLogger(ctx)
|
||||
|
||||
c, err := h.getClient()
|
||||
if err != nil {
|
||||
return nil, &ocsError{
|
||||
Code: response.MetaServerError.StatusCode,
|
||||
Message: "error getting grpc gateway client",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
permKey, err := permKeyFromRequest(r, h)
|
||||
if err != nil {
|
||||
return nil, &ocsError{
|
||||
Code: response.MetaBadRequest.StatusCode,
|
||||
Message: "Could not read permission from request",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: one is allowed to create an internal link without the `Publink.Write` permission
|
||||
if permKey != nil && *permKey != 0 {
|
||||
ok, err := utils.CheckPermission(ctx, permission.WritePublicLink, c)
|
||||
if err != nil {
|
||||
return nil, &ocsError{
|
||||
Code: response.MetaServerError.StatusCode,
|
||||
Message: "failed to check user permission",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return nil, &ocsError{
|
||||
Code: response.MetaForbidden.StatusCode,
|
||||
Message: "user is not allowed to create a public link",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = r.ParseForm()
|
||||
if err != nil {
|
||||
return nil, &ocsError{
|
||||
Code: response.MetaBadRequest.StatusCode,
|
||||
Message: "Could not parse form from request",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// check if a quicklink should be created
|
||||
quick, _ := strconv.ParseBool(r.FormValue("quicklink")) // no need to check the error - defaults to zero value!
|
||||
if quick {
|
||||
f := []*link.ListPublicSharesRequest_Filter{publicshare.ResourceIDFilter(statInfo.Id)}
|
||||
req := link.ListPublicSharesRequest{Filters: f}
|
||||
res, err := c.ListPublicShares(ctx, &req)
|
||||
if err != nil {
|
||||
return nil, &ocsError{
|
||||
Code: response.MetaServerError.StatusCode,
|
||||
Message: "could not list public links",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
if res.Status.Code != rpc.Code_CODE_OK {
|
||||
return nil, &ocsError{
|
||||
Code: int(res.Status.GetCode()),
|
||||
Message: "could not list public links",
|
||||
}
|
||||
}
|
||||
|
||||
for _, l := range res.GetShare() {
|
||||
if l.Quicklink {
|
||||
return l, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// default perms: read-only
|
||||
// TODO: the default might change depending on allowed permissions and configs
|
||||
if permKey == nil {
|
||||
permKey = &_defaultPublicLinkPermission
|
||||
}
|
||||
permissions, err := ocPublicPermToCs3(permKey)
|
||||
if err != nil {
|
||||
return nil, &ocsError{
|
||||
Code: response.MetaBadRequest.StatusCode,
|
||||
Message: "Could not create permission from permission key",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
password := r.FormValue("password")
|
||||
if h.enforcePassword(permKey) && len(password) == 0 {
|
||||
return nil, &ocsError{
|
||||
Code: response.MetaBadRequest.StatusCode,
|
||||
Message: "missing required password",
|
||||
Error: errors.New("missing required password"),
|
||||
}
|
||||
}
|
||||
if len(password) > 0 {
|
||||
if err := h.passwordValidator.Validate(password); err != nil {
|
||||
return nil, &ocsError{
|
||||
Code: response.MetaBadRequest.StatusCode,
|
||||
Message: xstrings.FirstRuneToUpper(err.Error()),
|
||||
Error: fmt.Errorf("password validation failed: %w", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if statInfo != nil && statInfo.Type == provider.ResourceType_RESOURCE_TYPE_FILE {
|
||||
// Single file shares should never have delete or create permissions
|
||||
role := conversions.RoleFromResourcePermissions(permissions, true)
|
||||
p := role.OCSPermissions()
|
||||
p &^= conversions.PermissionCreate
|
||||
p &^= conversions.PermissionDelete
|
||||
permissions = conversions.RoleFromOCSPermissions(p, statInfo).CS3ResourcePermissions()
|
||||
}
|
||||
|
||||
if !sufficientPermissions(statInfo.PermissionSet, permissions, true) {
|
||||
response.WriteOCSError(w, r, http.StatusForbidden, "no share permission", nil)
|
||||
return nil, &ocsError{
|
||||
Code: http.StatusForbidden,
|
||||
Message: "Cannot set the requested share permissions",
|
||||
Error: errors.New("cannot set the requested share permissions"),
|
||||
}
|
||||
}
|
||||
|
||||
req := link.CreatePublicShareRequest{
|
||||
ResourceInfo: statInfo,
|
||||
Grant: &link.Grant{
|
||||
Permissions: &link.PublicSharePermissions{
|
||||
Permissions: permissions,
|
||||
},
|
||||
Password: password,
|
||||
},
|
||||
}
|
||||
|
||||
expireTimeString, ok := r.Form["expireDate"]
|
||||
if ok {
|
||||
if expireTimeString[0] != "" {
|
||||
expireTime, err := conversions.ParseTimestamp(expireTimeString[0])
|
||||
if err != nil {
|
||||
return nil, &ocsError{
|
||||
Code: response.MetaBadRequest.StatusCode,
|
||||
Message: err.Error(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
if expireTime != nil {
|
||||
req.Grant.Expiration = expireTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set displayname and password protected as arbitrary metadata
|
||||
req.ResourceInfo.ArbitraryMetadata = &provider.ArbitraryMetadata{
|
||||
Metadata: map[string]string{
|
||||
"name": r.FormValue("name"),
|
||||
"quicklink": r.FormValue("quicklink"),
|
||||
},
|
||||
}
|
||||
|
||||
createRes, err := c.CreatePublicShare(ctx, &req)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Str("createShare", "shares").Msgf("error creating a public share to resource id: %v", statInfo.GetId())
|
||||
return nil, &ocsError{
|
||||
Code: response.MetaServerError.StatusCode,
|
||||
Message: "error creating public share",
|
||||
Error: fmt.Errorf("error creating a public share to resource id: %v", statInfo.GetId()),
|
||||
}
|
||||
}
|
||||
|
||||
if createRes.Status.Code != rpc.Code_CODE_OK {
|
||||
log.Debug().Err(errors.New("create public share failed")).Str("shares", "createShare").Msgf("create public share failed with status code: %v", createRes.Status.Code.String())
|
||||
return nil, &ocsError{
|
||||
Code: response.MetaServerError.StatusCode,
|
||||
Message: "grpc create public share request failed",
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
return createRes.Share, nil
|
||||
}
|
||||
|
||||
func (h *Handler) listPublicShares(r *http.Request, filters []*link.ListPublicSharesRequest_Filter) ([]*conversions.ShareData, *rpc.Status, error) {
|
||||
ctx := r.Context()
|
||||
log := appctx.GetLogger(ctx)
|
||||
|
||||
ocsDataPayload := make([]*conversions.ShareData, 0)
|
||||
// TODO(refs) why is this guard needed? Are we moving towards a gateway only for service discovery? without a gateway this is dead code.
|
||||
if h.gatewayAddr != "" {
|
||||
client, err := pool.GetGatewayServiceClient(h.gatewayAddr)
|
||||
if err != nil {
|
||||
return ocsDataPayload, nil, err
|
||||
}
|
||||
|
||||
req := link.ListPublicSharesRequest{
|
||||
Filters: filters,
|
||||
}
|
||||
|
||||
res, err := client.ListPublicShares(ctx, &req)
|
||||
if err != nil {
|
||||
return ocsDataPayload, nil, err
|
||||
}
|
||||
if res.Status.Code != rpc.Code_CODE_OK {
|
||||
return ocsDataPayload, res.Status, nil
|
||||
}
|
||||
|
||||
for _, share := range res.GetShare() {
|
||||
info, status, err := h.getResourceInfoByID(ctx, client, share.ResourceId)
|
||||
if err != nil || status.Code != rpc.Code_CODE_OK {
|
||||
log.Debug().Interface("share", share).Interface("status", status).Err(err).Msg("could not stat share, skipping")
|
||||
continue
|
||||
}
|
||||
|
||||
sData := conversions.PublicShare2ShareData(share, r, h.publicURL)
|
||||
|
||||
sData.Name = share.DisplayName
|
||||
|
||||
h.addFileInfo(ctx, sData, info)
|
||||
h.mapUserIds(ctx, client, sData)
|
||||
|
||||
log.Debug().Interface("share", share).Interface("info", info).Interface("shareData", share).Msg("mapped")
|
||||
|
||||
ocsDataPayload = append(ocsDataPayload, sData)
|
||||
|
||||
}
|
||||
|
||||
return ocsDataPayload, nil, nil
|
||||
}
|
||||
|
||||
return ocsDataPayload, nil, errors.New("bad request")
|
||||
}
|
||||
|
||||
func (h *Handler) isPublicShare(r *http.Request, oid string) (*link.PublicShare, bool) {
|
||||
logger := appctx.GetLogger(r.Context())
|
||||
client, err := h.getClient()
|
||||
if err != nil {
|
||||
logger.Err(err)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
psRes, err := client.GetPublicShare(r.Context(), &link.GetPublicShareRequest{
|
||||
Ref: &link.PublicShareReference{
|
||||
Spec: &link.PublicShareReference_Id{
|
||||
Id: &link.PublicShareId{
|
||||
OpaqueId: oid,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
switch {
|
||||
case err != nil:
|
||||
log.Err(err).Send()
|
||||
case psRes.Status.Code == rpc.Code_CODE_OK:
|
||||
return psRes.GetShare(), psRes.GetShare() != nil
|
||||
case psRes.Status.Code == rpc.Code_CODE_INTERNAL:
|
||||
log.Error().Str("message", psRes.GetStatus().GetMessage()).Str("code", psRes.GetStatus().GetCode().String()).Msg("isPublicShare received internal error")
|
||||
default:
|
||||
log.Debug().Str("message", psRes.GetStatus().GetMessage()).Str("code", psRes.GetStatus().GetCode().String()).Msg("isPublicShare received unexpected status")
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (h *Handler) updatePublicShare(w http.ResponseWriter, r *http.Request, share *link.PublicShare) {
|
||||
updates := []*link.UpdatePublicShareRequest_Update{}
|
||||
logger := appctx.GetLogger(r.Context())
|
||||
|
||||
gwC, err := h.getClient()
|
||||
if err != nil {
|
||||
log.Err(err).Str("shareID", share.GetId().GetOpaqueId()).Msg("updatePublicShare")
|
||||
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "error getting a connection to the gateway service", nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
|
||||
permKey, err := permKeyFromRequest(r, h)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "invalid permissions", err)
|
||||
return
|
||||
}
|
||||
|
||||
createdByUser := publicshare.IsCreatedByUser(share, user)
|
||||
|
||||
// NOTE: you are allowed to update a link TO a public link without the `PublicLink.Write` permission if you created it yourself
|
||||
if (permKey != nil && *permKey != 0) || !createdByUser {
|
||||
ok, err := utils.CheckPermission(ctx, permission.WritePublicLink, gwC)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "failed to check user permission", err)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
response.WriteOCSError(w, r, response.MetaForbidden.StatusCode, "user is not allowed to update the public link", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !createdByUser {
|
||||
sRes, err := gwC.Stat(r.Context(), &provider.StatRequest{Ref: &provider.Reference{ResourceId: share.ResourceId}})
|
||||
if err != nil {
|
||||
log.Err(err).Interface("resource_id", share.ResourceId).Msg("failed to stat shared resource")
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "failed to get public share", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if sRes.Info == nil || !sRes.Info.GetPermissionSet().UpdateGrant {
|
||||
response.WriteOCSError(w, r, response.MetaUnauthorized.StatusCode, "missing permissions to update share", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = r.ParseForm()
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "Could not parse form from request", err)
|
||||
return
|
||||
}
|
||||
|
||||
// indicates whether values to update were found,
|
||||
// to check if the request was valid,
|
||||
// not whether an actual update has been performed
|
||||
updatesFound := false
|
||||
|
||||
newName, ok := r.Form["name"]
|
||||
if ok {
|
||||
updatesFound = true
|
||||
if newName[0] != share.DisplayName {
|
||||
updates = append(updates, &link.UpdatePublicShareRequest_Update{
|
||||
Type: link.UpdatePublicShareRequest_Update_TYPE_DISPLAYNAME,
|
||||
DisplayName: newName[0],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Permissions
|
||||
newPermissions, err := ocPublicPermToCs3(permKey)
|
||||
logger.Debug().Interface("newPermissions", newPermissions).Msg("Parsed permissions")
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "invalid permissions", err)
|
||||
return
|
||||
}
|
||||
|
||||
// update permissions if given
|
||||
if newPermissions != nil {
|
||||
updatesFound = true
|
||||
publicSharePermissions := &link.PublicSharePermissions{
|
||||
Permissions: newPermissions,
|
||||
}
|
||||
beforePerm, _ := json.Marshal(share.Permissions)
|
||||
afterPerm, _ := json.Marshal(publicSharePermissions)
|
||||
if string(beforePerm) != string(afterPerm) {
|
||||
logger.Info().Str("shares", "update").Msgf("updating permissions from %v to: %v", string(beforePerm), string(afterPerm))
|
||||
updates = append(updates, &link.UpdatePublicShareRequest_Update{
|
||||
Type: link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS,
|
||||
Grant: &link.Grant{
|
||||
Permissions: publicSharePermissions,
|
||||
Password: r.FormValue("password"),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
statReq := provider.StatRequest{Ref: &provider.Reference{ResourceId: share.ResourceId}}
|
||||
statRes, err := gwC.Stat(r.Context(), &statReq)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Str("shares", "update public share").Msg("error during stat")
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "missing resource information", fmt.Errorf("error getting resource information"))
|
||||
return
|
||||
}
|
||||
if statRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
if statRes.GetStatus().GetCode() == rpc.Code_CODE_NOT_FOUND {
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "update public share: resource not found", err)
|
||||
return
|
||||
}
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc stat request failed for stat when updating a public share", err)
|
||||
return
|
||||
}
|
||||
|
||||
// empty permissions mean internal link here - NOT denial. Hence we need an extra check
|
||||
if newPermissions != nil {
|
||||
if !sufficientPermissions(statRes.GetInfo().GetPermissionSet(), newPermissions, true) {
|
||||
response.WriteOCSError(w, r, http.StatusForbidden, "no share permission", nil)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
statRes.GetInfo().GetPermissionSet()
|
||||
p := decreasePermissionsIfNecessary(int(conversions.RoleFromResourcePermissions(statRes.GetInfo().GetPermissionSet(), false).OCSPermissions()))
|
||||
permKey = &p
|
||||
}
|
||||
|
||||
// ExpireDate
|
||||
expireTimeString, ok := r.Form["expireDate"]
|
||||
// check if value is set and must be updated or cleared
|
||||
if ok {
|
||||
updatesFound = true
|
||||
var newExpiration *types.Timestamp
|
||||
if expireTimeString[0] != "" {
|
||||
newExpiration, err = conversions.ParseTimestamp(expireTimeString[0])
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "invalid datetime format", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
beforeExpiration, _ := json.Marshal(share.Expiration)
|
||||
afterExpiration, _ := json.Marshal(newExpiration)
|
||||
if string(afterExpiration) != string(beforeExpiration) {
|
||||
logger.Debug().Str("shares", "update").Msgf("updating expire date from %v to: %v", string(beforeExpiration), string(afterExpiration))
|
||||
updates = append(updates, &link.UpdatePublicShareRequest_Update{
|
||||
Type: link.UpdatePublicShareRequest_Update_TYPE_EXPIRATION,
|
||||
Grant: &link.Grant{
|
||||
Expiration: newExpiration,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Password
|
||||
newPassword, ok := r.Form["password"]
|
||||
// enforcePassword
|
||||
if h.enforcePassword(permKey) {
|
||||
p, err := conversions.NewPermissions(decreasePermissionsIfNecessary(*permKey))
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "failed to check permissions from request", err)
|
||||
return
|
||||
}
|
||||
if !ok && !share.PasswordProtected || ok && len(newPassword[0]) == 0 {
|
||||
if h.checkPasswordEnforcement(ctx, user, p, w, r) != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update or clear password
|
||||
if ok {
|
||||
// skip validation if the clear password scenario
|
||||
if len(newPassword[0]) > 0 {
|
||||
if err := h.passwordValidator.Validate(newPassword[0]); err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, xstrings.FirstRuneToUpper(err.Error()), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
updatesFound = true
|
||||
logger.Info().Str("shares", "update").Msg("password updated")
|
||||
updates = append(updates, &link.UpdatePublicShareRequest_Update{
|
||||
Type: link.UpdatePublicShareRequest_Update_TYPE_PASSWORD,
|
||||
Grant: &link.Grant{
|
||||
Password: newPassword[0],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Updates are atomical. See: https://github.com/cs3org/cs3apis/pull/67#issuecomment-617651428 so in order to get the latest updated version
|
||||
if len(updates) > 0 {
|
||||
uRes := &link.UpdatePublicShareResponse{Share: share}
|
||||
for k := range updates {
|
||||
uRes, err = gwC.UpdatePublicShare(r.Context(), &link.UpdatePublicShareRequest{
|
||||
Ref: &link.PublicShareReference{
|
||||
Spec: &link.PublicShareReference_Id{
|
||||
Id: &link.PublicShareId{
|
||||
OpaqueId: share.Id.OpaqueId,
|
||||
},
|
||||
},
|
||||
},
|
||||
Update: updates[k],
|
||||
})
|
||||
if err != nil {
|
||||
log.Err(err).Str("shareID", share.Id.OpaqueId).Msg("sending update request to public link provider")
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "Error sending update request to public link provider", err)
|
||||
return
|
||||
}
|
||||
if uRes.Status.Code != rpc.Code_CODE_OK {
|
||||
log.Debug().Str("shareID", share.Id.OpaqueId).Msgf("sending update request to public link provider failed: %s", uRes.Status.Message)
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, fmt.Sprintf("Error sending update request to public link provider: %s", uRes.Status.Message), nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
share = uRes.Share
|
||||
} else if !updatesFound {
|
||||
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "No updates specified in request", nil)
|
||||
return
|
||||
}
|
||||
|
||||
s := conversions.PublicShare2ShareData(share, r, h.publicURL)
|
||||
h.addFileInfo(r.Context(), s, statRes.Info)
|
||||
h.mapUserIds(r.Context(), gwC, s)
|
||||
|
||||
response.WriteOCSSuccess(w, r, s)
|
||||
}
|
||||
|
||||
func (h *Handler) removePublicShare(w http.ResponseWriter, r *http.Request, share *link.PublicShare) {
|
||||
ctx := r.Context()
|
||||
|
||||
c, err := h.getClient()
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err)
|
||||
return
|
||||
}
|
||||
|
||||
u := ctxpkg.ContextMustGetUser(ctx)
|
||||
if !publicshare.IsCreatedByUser(share, u) {
|
||||
sRes, err := c.Stat(r.Context(), &provider.StatRequest{Ref: &provider.Reference{ResourceId: share.ResourceId}})
|
||||
if err != nil {
|
||||
log.Err(err).Interface("resource_id", share.ResourceId).Msg("failed to stat shared resource")
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "failed to get public share", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if !sRes.Info.PermissionSet.RemoveGrant {
|
||||
response.WriteOCSError(w, r, response.MetaUnauthorized.StatusCode, "missing permissions to remove share", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
req := &link.RemovePublicShareRequest{
|
||||
Ref: &link.PublicShareReference{
|
||||
Spec: &link.PublicShareReference_Id{
|
||||
Id: &link.PublicShareId{
|
||||
OpaqueId: share.GetId().GetOpaqueId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
res, err := c.RemovePublicShare(ctx, req)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc delete share request", err)
|
||||
return
|
||||
}
|
||||
if res.Status.Code != rpc.Code_CODE_OK {
|
||||
if res.Status.Code == rpc.Code_CODE_NOT_FOUND {
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "not found", nil)
|
||||
return
|
||||
}
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc delete share request failed", err)
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteOCSSuccess(w, r, nil)
|
||||
}
|
||||
|
||||
// enforcePassword validate Password enforce based on configuration
|
||||
// read_only: 1
|
||||
// read_write: 3 or 5
|
||||
// read_write_delete: 15
|
||||
// upload_only: 4
|
||||
func (h *Handler) enforcePassword(pk *int) bool {
|
||||
if pk == nil {
|
||||
return false
|
||||
}
|
||||
p, err := conversions.NewPermissions(decreasePermissionsIfNecessary(*pk))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if h.publicPasswordEnforced.EnforcedForReadOnly &&
|
||||
p == conversions.PermissionRead {
|
||||
return true
|
||||
}
|
||||
if h.publicPasswordEnforced.EnforcedForReadWrite &&
|
||||
(p == conversions.PermissionRead|conversions.PermissionWrite ||
|
||||
p == conversions.PermissionRead|conversions.PermissionCreate) {
|
||||
return true
|
||||
}
|
||||
if h.publicPasswordEnforced.EnforcedForReadWriteDelete &&
|
||||
p == conversions.PermissionRead|conversions.PermissionWrite|conversions.PermissionCreate|conversions.PermissionDelete {
|
||||
return true
|
||||
}
|
||||
if h.publicPasswordEnforced.EnforcedForUploadOnly &&
|
||||
p == conversions.PermissionCreate {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// for public links oc10 api decreases all permissions to read: stay compatible!
|
||||
func decreasePermissionsIfNecessary(perm int) int {
|
||||
if perm == int(conversions.PermissionAll) {
|
||||
perm = int(conversions.PermissionRead)
|
||||
}
|
||||
return perm
|
||||
}
|
||||
|
||||
func ocPublicPermToCs3(pk *int) (*provider.ResourcePermissions, error) {
|
||||
if pk == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
permKey := decreasePermissionsIfNecessary(*pk)
|
||||
|
||||
// TODO refactor this ocPublicPermToRole[permKey] check into a conversions.NewPublicSharePermissions?
|
||||
// not all permissions are possible for public shares
|
||||
_, ok := ocPublicPermToRole[permKey]
|
||||
if !ok {
|
||||
log.Error().Str("ocPublicPermToCs3", "shares").Int("perm", permKey).Msg("invalid public share permission")
|
||||
return nil, fmt.Errorf("invalid public share permission: %d", permKey)
|
||||
}
|
||||
|
||||
perm, err := conversions.NewPermissions(permKey)
|
||||
if err != nil && err != conversions.ErrZeroPermission { // we allow empty permissions for public links
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conversions.RoleFromOCSPermissions(perm, nil).CS3ResourcePermissions(), nil
|
||||
}
|
||||
|
||||
// pointer will be nil if no permission is set
|
||||
func permKeyFromRequest(r *http.Request, h *Handler) (*int, error) {
|
||||
var err error
|
||||
// phoenix sends: {"permissions": 15}. See ocPublicPermToRole struct for mapping
|
||||
|
||||
permKey := 1
|
||||
|
||||
// note: "permissions" value has higher priority than "publicUpload"
|
||||
|
||||
// handle legacy "publicUpload" arg that overrides permissions differently depending on the scenario
|
||||
// https://github.com/owncloud/core/blob/v10.4.0/apps/files_sharing/lib/Controller/Share20OcsController.php#L447
|
||||
publicUploadString := r.FormValue("publicUpload")
|
||||
if publicUploadString != "" {
|
||||
publicUploadFlag, err := strconv.ParseBool(publicUploadString)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("publicUpload", publicUploadString).Msg("could not parse publicUpload argument")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if publicUploadFlag {
|
||||
// all perms except reshare
|
||||
permKey = 15
|
||||
}
|
||||
} else {
|
||||
permissionsString := r.FormValue("permissions")
|
||||
if permissionsString == "" {
|
||||
// no permission values given
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
permKey, err = strconv.Atoi(permissionsString)
|
||||
if err != nil {
|
||||
log.Error().Str("permissionFromRequest", "shares").Msgf("invalid type: %T", permKey)
|
||||
return nil, fmt.Errorf("invalid type: %T", permKey)
|
||||
}
|
||||
}
|
||||
|
||||
return &permKey, nil
|
||||
}
|
||||
|
||||
// checkPasswordEnforcement checks if the password needs to be set for a link
|
||||
// some users can opt out of the enforcement based on a user permission
|
||||
func (h *Handler) checkPasswordEnforcement(ctx context.Context, user *userv1beta1.User, perm conversions.Permissions, w http.ResponseWriter, r *http.Request) error {
|
||||
// Non-read-only links
|
||||
if perm != conversions.PermissionRead {
|
||||
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "missing required password", nil)
|
||||
return errors.New("missing required password")
|
||||
}
|
||||
// Check if the user is allowed to opt out of the password enforcement
|
||||
// for read-only links
|
||||
gwC, err := h.getClient()
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "could not check permission", err)
|
||||
return errors.New("could not check permission")
|
||||
}
|
||||
ok, err := utils.CheckPermission(ctx, permission.DeleteReadOnlyPassword, gwC)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "failed to check user permission", err)
|
||||
return errors.New("failed to check user permission")
|
||||
}
|
||||
if !ok {
|
||||
response.WriteOCSError(w, r, response.MetaForbidden.StatusCode, "user is not allowed to delete the password from the public link", nil)
|
||||
return errors.New("user is not allowed to delete the password from the public link")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: add mapping for user share permissions to role
|
||||
|
||||
// Maps oc10 public link permissions to roles
|
||||
var ocPublicPermToRole = map[int]string{
|
||||
// Recipients can do nothing
|
||||
0: "none",
|
||||
// Recipients can view and download contents.
|
||||
1: "viewer",
|
||||
// Recipients can view, download and edit single files.
|
||||
3: "file-editor",
|
||||
// Recipients can view, download, edit, delete and upload contents
|
||||
15: "editor",
|
||||
// Recipients can upload but existing contents are not revealed
|
||||
4: "uploader",
|
||||
// Recipients can view, download and upload contents
|
||||
5: "contributor",
|
||||
}
|
||||
Generated
Vendored
+300
@@ -0,0 +1,300 @@
|
||||
// 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 shares
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
|
||||
gatewayv1beta1 "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"
|
||||
providerpb "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"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/conversions"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/ocm/share"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (h *Handler) createFederatedCloudShare(w http.ResponseWriter, r *http.Request, resource *provider.ResourceInfo, role *conversions.Role, roleVal []byte) {
|
||||
ctx := r.Context()
|
||||
|
||||
c, err := pool.GetGatewayServiceClient(h.gatewayAddr)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err)
|
||||
return
|
||||
}
|
||||
|
||||
shareWithUser, shareWithProvider := r.FormValue("shareWith"), r.FormValue("shareWithProvider")
|
||||
if shareWithUser == "" || shareWithProvider == "" {
|
||||
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "missing shareWith parameters", nil)
|
||||
return
|
||||
}
|
||||
|
||||
providerInfoResp, err := c.GetInfoByDomain(ctx, &providerpb.GetInfoByDomainRequest{
|
||||
Domain: shareWithProvider,
|
||||
})
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc get invite by domain info request", err)
|
||||
return
|
||||
}
|
||||
|
||||
if providerInfoResp.Status.Code != rpc.Code_CODE_OK {
|
||||
// return proper error
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error from provider info response", errors.New(providerInfoResp.Status.Message))
|
||||
return
|
||||
}
|
||||
|
||||
remoteUserRes, err := c.GetAcceptedUser(ctx, &invitepb.GetAcceptedUserRequest{
|
||||
RemoteUserId: &userpb.UserId{OpaqueId: shareWithUser, Idp: shareWithProvider, Type: userpb.UserType_USER_TYPE_FEDERATED},
|
||||
})
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error searching recipient", err)
|
||||
return
|
||||
}
|
||||
if remoteUserRes.Status.Code != rpc.Code_CODE_OK {
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "user not found", err)
|
||||
return
|
||||
}
|
||||
|
||||
createShareResponse, err := c.CreateOCMShare(ctx, &ocm.CreateOCMShareRequest{
|
||||
ResourceId: resource.Id,
|
||||
Grantee: &provider.Grantee{
|
||||
Type: provider.GranteeType_GRANTEE_TYPE_USER,
|
||||
Id: &provider.Grantee_UserId{
|
||||
UserId: remoteUserRes.RemoteUser.Id,
|
||||
},
|
||||
},
|
||||
RecipientMeshProvider: providerInfoResp.ProviderInfo,
|
||||
AccessMethods: []*ocm.AccessMethod{
|
||||
share.NewWebDavAccessMethod(role.CS3ResourcePermissions()),
|
||||
share.NewWebappAccessMethod(getViewModeFromRole(role)),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc create ocm share request", err)
|
||||
return
|
||||
}
|
||||
if createShareResponse.Status.Code != rpc.Code_CODE_OK {
|
||||
if createShareResponse.Status.Code == rpc.Code_CODE_NOT_FOUND {
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "not found", nil)
|
||||
return
|
||||
}
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc create ocm share request failed", err)
|
||||
return
|
||||
}
|
||||
|
||||
s := createShareResponse.Share
|
||||
data, err := conversions.OCMShare2ShareData(s)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error converting share", err)
|
||||
return
|
||||
}
|
||||
h.mapUserIdsFederatedShare(ctx, c, data)
|
||||
|
||||
info, status, err := h.getResourceInfoByID(ctx, c, s.ResourceId)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error statting resource id", err)
|
||||
return
|
||||
}
|
||||
if status.Code != rpc.Code_CODE_OK {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error statting resource id", errors.New(status.Message))
|
||||
return
|
||||
}
|
||||
|
||||
h.addFileInfo(ctx, data, info)
|
||||
|
||||
response.WriteOCSSuccess(w, r, data)
|
||||
}
|
||||
|
||||
func getViewModeFromRole(role *conversions.Role) providerv1beta1.ViewMode {
|
||||
switch role.Name {
|
||||
case conversions.RoleViewer:
|
||||
return providerv1beta1.ViewMode_VIEW_MODE_READ_ONLY
|
||||
case conversions.RoleEditor:
|
||||
return providerv1beta1.ViewMode_VIEW_MODE_READ_WRITE
|
||||
}
|
||||
return providerv1beta1.ViewMode_VIEW_MODE_INVALID
|
||||
}
|
||||
|
||||
// GetFederatedShare handles GET requests on /apps/files_sharing/api/v1/shares/remote_shares/{shareid}.
|
||||
func (h *Handler) GetFederatedShare(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: Implement response with HAL schemating
|
||||
ctx := r.Context()
|
||||
|
||||
shareID := chi.URLParam(r, "shareid")
|
||||
gatewayClient, err := pool.GetGatewayServiceClient(h.gatewayAddr)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err)
|
||||
return
|
||||
}
|
||||
|
||||
listOCMSharesRequest := &ocm.GetOCMShareRequest{
|
||||
Ref: &ocm.ShareReference{
|
||||
Spec: &ocm.ShareReference_Id{
|
||||
Id: &ocm.ShareId{
|
||||
OpaqueId: shareID,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
ocmShareResponse, err := gatewayClient.GetOCMShare(ctx, listOCMSharesRequest)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc get ocm share request", err)
|
||||
return
|
||||
}
|
||||
|
||||
share := ocmShareResponse.GetShare()
|
||||
if share == nil {
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "share not found", err)
|
||||
return
|
||||
}
|
||||
response.WriteOCSSuccess(w, r, share)
|
||||
}
|
||||
|
||||
// ListFederatedShares handles GET requests on /apps/files_sharing/api/v1/shares/remote_shares.
|
||||
func (h *Handler) ListFederatedShares(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO Implement pagination.
|
||||
// TODO Implement response with HAL schemating
|
||||
}
|
||||
|
||||
func (h *Handler) listReceivedFederatedShares(ctx context.Context, gw gatewayv1beta1.GatewayAPIClient, state ocm.ShareState) ([]*conversions.ShareData, error) {
|
||||
listRes, err := gw.ListReceivedOCMShares(ctx, &ocm.ListReceivedOCMSharesRequest{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
shares := []*conversions.ShareData{}
|
||||
for _, s := range listRes.Shares {
|
||||
if state != ocsStateUnknown && s.State != state {
|
||||
continue
|
||||
}
|
||||
sd, err := conversions.ReceivedOCMShare2ShareData(s, h.ocmLocalMount(s))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
h.mapUserIdsReceivedFederatedShare(ctx, gw, sd)
|
||||
sd.State = mapOCMState(s.State)
|
||||
shares = append(shares, sd)
|
||||
}
|
||||
return shares, nil
|
||||
}
|
||||
|
||||
func (h *Handler) ocmLocalMount(share *ocm.ReceivedShare) string {
|
||||
return filepath.Join("/", h.ocmMountPoint, share.Id.OpaqueId)
|
||||
}
|
||||
|
||||
func (h *Handler) mapUserIdsReceivedFederatedShare(ctx context.Context, gw gatewayv1beta1.GatewayAPIClient, sd *conversions.ShareData) {
|
||||
if sd.ShareWith != "" {
|
||||
user := h.mustGetIdentifiers(ctx, gw, sd.ShareWith, false)
|
||||
sd.ShareWith = user.Username
|
||||
sd.ShareWithDisplayname = user.DisplayName
|
||||
}
|
||||
|
||||
if sd.UIDOwner != "" {
|
||||
user := h.mustGetRemoteUser(ctx, gw, sd.UIDOwner)
|
||||
sd.DisplaynameOwner = user.DisplayName
|
||||
}
|
||||
|
||||
if sd.UIDFileOwner != "" {
|
||||
user := h.mustGetRemoteUser(ctx, gw, sd.UIDFileOwner)
|
||||
sd.DisplaynameFileOwner = user.DisplayName
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) mapUserIdsFederatedShare(ctx context.Context, gw gatewayv1beta1.GatewayAPIClient, sd *conversions.ShareData) {
|
||||
if sd.ShareWith != "" {
|
||||
user := h.mustGetRemoteUser(ctx, gw, sd.ShareWith)
|
||||
sd.ShareWith = user.Username
|
||||
sd.ShareWithDisplayname = user.DisplayName
|
||||
}
|
||||
|
||||
if sd.UIDOwner != "" {
|
||||
user := h.mustGetIdentifiers(ctx, gw, sd.UIDOwner, false)
|
||||
sd.DisplaynameOwner = user.DisplayName
|
||||
}
|
||||
|
||||
if sd.UIDFileOwner != "" {
|
||||
user := h.mustGetIdentifiers(ctx, gw, sd.UIDFileOwner, false)
|
||||
sd.DisplaynameFileOwner = user.DisplayName
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) mustGetRemoteUser(ctx context.Context, gw gatewayv1beta1.GatewayAPIClient, id string) *userIdentifiers {
|
||||
s := strings.SplitN(id, "@", 2)
|
||||
opaqueID, idp := s[0], s[1]
|
||||
userRes, err := gw.GetAcceptedUser(ctx, &invitepb.GetAcceptedUserRequest{
|
||||
RemoteUserId: &userpb.UserId{
|
||||
Idp: idp,
|
||||
OpaqueId: opaqueID,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return &userIdentifiers{}
|
||||
}
|
||||
if userRes.Status.Code != rpc.Code_CODE_OK {
|
||||
return &userIdentifiers{}
|
||||
}
|
||||
|
||||
user := userRes.RemoteUser
|
||||
return &userIdentifiers{
|
||||
DisplayName: user.DisplayName,
|
||||
Username: user.Username,
|
||||
Mail: user.Mail,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) listOutcomingFederatedShares(ctx context.Context, gw gatewayv1beta1.GatewayAPIClient, filters []*ocm.ListOCMSharesRequest_Filter) ([]*conversions.ShareData, error) {
|
||||
listRes, err := gw.ListOCMShares(ctx, &ocm.ListOCMSharesRequest{
|
||||
Filters: filters,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
shares := []*conversions.ShareData{}
|
||||
for _, s := range listRes.Shares {
|
||||
sd, err := conversions.OCMShare2ShareData(s)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
h.mapUserIdsFederatedShare(ctx, gw, sd)
|
||||
|
||||
info, status, err := h.getResourceInfoByID(ctx, gw, s.ResourceId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if status.Code != rpc.Code_CODE_OK {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h.addFileInfo(ctx, sd, info)
|
||||
shares = append(shares, sd)
|
||||
}
|
||||
return shares, nil
|
||||
}
|
||||
Generated
Vendored
+1719
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+391
@@ -0,0 +1,391 @@
|
||||
// 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 shares
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
groupv1beta1 "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"
|
||||
collaborationv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
|
||||
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/conversions"
|
||||
"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"
|
||||
sdk "github.com/opencloud-eu/reva/v2/pkg/sdk/common"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/protobuf/types/known/fieldmaskpb"
|
||||
)
|
||||
|
||||
func (h *Handler) getGrantee(ctx context.Context, name string) (provider.Grantee, error) {
|
||||
log := appctx.GetLogger(ctx)
|
||||
client, err := h.getClient()
|
||||
if err != nil {
|
||||
return provider.Grantee{}, err
|
||||
}
|
||||
userRes, err := client.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{
|
||||
Claim: "username",
|
||||
Value: name,
|
||||
})
|
||||
if err == nil && userRes.Status.Code == rpc.Code_CODE_OK {
|
||||
return provider.Grantee{
|
||||
Type: provider.GranteeType_GRANTEE_TYPE_USER,
|
||||
Id: &provider.Grantee_UserId{UserId: userRes.User.Id},
|
||||
}, nil
|
||||
}
|
||||
log.Debug().Str("name", name).Msg("no user found")
|
||||
|
||||
groupRes, err := client.GetGroupByClaim(ctx, &groupv1beta1.GetGroupByClaimRequest{
|
||||
Claim: "group_name",
|
||||
Value: name,
|
||||
SkipFetchingMembers: true,
|
||||
})
|
||||
if err == nil && groupRes.Status.Code == rpc.Code_CODE_OK {
|
||||
return provider.Grantee{
|
||||
Type: provider.GranteeType_GRANTEE_TYPE_GROUP,
|
||||
Id: &provider.Grantee_GroupId{GroupId: groupRes.Group.Id},
|
||||
}, nil
|
||||
}
|
||||
log.Debug().Str("name", name).Msg("no group found")
|
||||
|
||||
return provider.Grantee{}, fmt.Errorf("no grantee found with name %s", name)
|
||||
}
|
||||
|
||||
func (h *Handler) addSpaceMember(w http.ResponseWriter, r *http.Request, info *provider.ResourceInfo, role *conversions.Role, roleVal []byte) {
|
||||
ctx := r.Context()
|
||||
|
||||
if info.Space.SpaceType == "personal" {
|
||||
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "can not add members to personal spaces", nil)
|
||||
return
|
||||
}
|
||||
|
||||
shareWith := r.FormValue("shareWith")
|
||||
if shareWith == "" {
|
||||
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "missing shareWith", nil)
|
||||
return
|
||||
}
|
||||
|
||||
grantee, err := h.getGrantee(ctx, shareWith)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "error getting grantee", err)
|
||||
return
|
||||
}
|
||||
|
||||
client, err := h.getClient()
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "error getting gateway client", err)
|
||||
return
|
||||
}
|
||||
|
||||
permissions := role.CS3ResourcePermissions()
|
||||
// All members of a space should be able to list shares inside that space.
|
||||
// The viewer role doesn't have the ListGrants permission so we set it here.
|
||||
permissions.ListGrants = true
|
||||
|
||||
fieldmask := []string{}
|
||||
expireDate := r.PostFormValue("expireDate")
|
||||
var expirationTs *types.Timestamp
|
||||
fieldmask = append(fieldmask, "expiration")
|
||||
if expireDate != "" {
|
||||
expiration, err := time.Parse(_iso8601, expireDate)
|
||||
if err != nil {
|
||||
// Web sends different formats when adding and when editing a space membership...
|
||||
// We need to fix this in a separate PR.
|
||||
expiration, err = time.Parse(time.RFC3339, expireDate)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "could not parse expireDate", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
expirationTs = &types.Timestamp{
|
||||
Seconds: uint64(expiration.UnixNano() / int64(time.Second)),
|
||||
Nanos: uint32(expiration.UnixNano() % int64(time.Second)),
|
||||
}
|
||||
fieldmask = append(fieldmask, "expiration")
|
||||
}
|
||||
|
||||
ref := provider.Reference{ResourceId: info.GetId()}
|
||||
p, err := h.findProvider(ctx, &ref)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "error getting storage provider", err)
|
||||
return
|
||||
}
|
||||
|
||||
providerClient, err := h.getStorageProviderClient(p)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "error getting storage provider client", err)
|
||||
return
|
||||
}
|
||||
|
||||
lgRes, err := providerClient.ListGrants(ctx, &provider.ListGrantsRequest{Ref: &ref})
|
||||
if err != nil || lgRes.Status.Code != rpc.Code_CODE_OK {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error listing space grants", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !isSpaceManagerRemaining(lgRes.Grants, &grantee) {
|
||||
response.WriteOCSError(w, r, http.StatusForbidden, "the space must have at least one manager", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// we have to send the update request to the gateway to give it a chance to invalidate its cache
|
||||
// TODO the gateway no longer should cache stuff because invalidation is to expensive. The decomposedfs already has a better cache.
|
||||
if granteeExists(lgRes.Grants, &grantee) {
|
||||
if permissions != nil {
|
||||
fieldmask = append(fieldmask, "permissions")
|
||||
}
|
||||
updateShareReq := &collaborationv1beta1.UpdateShareRequest{
|
||||
// TODO: change CS3 APIs
|
||||
Opaque: &types.Opaque{
|
||||
Map: map[string]*types.OpaqueEntry{
|
||||
"spacegrant": {},
|
||||
},
|
||||
},
|
||||
Share: &collaborationv1beta1.Share{
|
||||
ResourceId: ref.GetResourceId(),
|
||||
Permissions: &collaborationv1beta1.SharePermissions{
|
||||
Permissions: permissions,
|
||||
},
|
||||
Grantee: &grantee,
|
||||
Expiration: expirationTs,
|
||||
},
|
||||
UpdateMask: &fieldmaskpb.FieldMask{
|
||||
Paths: fieldmask,
|
||||
},
|
||||
}
|
||||
updateShareReq.Opaque = utils.AppendPlainToOpaque(updateShareReq.Opaque, "spacetype", info.GetSpace().GetSpaceType())
|
||||
updateShareRes, err := client.UpdateShare(ctx, updateShareReq)
|
||||
if err != nil || updateShareRes.Status.Code != rpc.Code_CODE_OK {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "could not update space member grant", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
createShareRes, err := client.CreateShare(ctx, &collaborationv1beta1.CreateShareRequest{
|
||||
ResourceInfo: info,
|
||||
Grant: &collaborationv1beta1.ShareGrant{
|
||||
Permissions: &collaborationv1beta1.SharePermissions{
|
||||
Permissions: permissions,
|
||||
},
|
||||
Grantee: &grantee,
|
||||
Expiration: expirationTs,
|
||||
},
|
||||
})
|
||||
if err != nil || createShareRes.Status.Code != rpc.Code_CODE_OK {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "could not add space member grant", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.WriteOCSSuccess(w, r, nil)
|
||||
}
|
||||
|
||||
func (h *Handler) isSpaceShare(r *http.Request, spaceID string) (*registry.ProviderInfo, bool) {
|
||||
ref, err := storagespace.ParseReference(spaceID)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if ref.ResourceId.OpaqueId == "" {
|
||||
ref.ResourceId.OpaqueId = ref.ResourceId.SpaceId
|
||||
}
|
||||
|
||||
p, err := h.findProvider(r.Context(), &ref)
|
||||
return p, err == nil
|
||||
}
|
||||
|
||||
func (h *Handler) removeSpaceMember(w http.ResponseWriter, r *http.Request, spaceID string, prov *registry.ProviderInfo) {
|
||||
ctx := r.Context()
|
||||
|
||||
shareWith := r.URL.Query().Get("shareWith")
|
||||
if shareWith == "" {
|
||||
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "missing shareWith", nil)
|
||||
return
|
||||
}
|
||||
|
||||
grantee, err := h.getGrantee(ctx, shareWith)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "error getting grantee", err)
|
||||
return
|
||||
}
|
||||
|
||||
ref, err := storagespace.ParseReference(spaceID)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "could not parse space id", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ref.ResourceId.OpaqueId == "" {
|
||||
ref.ResourceId.OpaqueId = ref.ResourceId.SpaceId
|
||||
}
|
||||
|
||||
providerClient, err := h.getStorageProviderClient(prov)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "error getting storage provider client", err)
|
||||
return
|
||||
}
|
||||
|
||||
lgRes, err := providerClient.ListGrants(ctx, &provider.ListGrantsRequest{Ref: &ref})
|
||||
if err != nil || lgRes.Status.Code != rpc.Code_CODE_OK {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error listing space grants", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(lgRes.Grants) == 1 || !isSpaceManagerRemaining(lgRes.Grants, &grantee) {
|
||||
response.WriteOCSError(w, r, http.StatusForbidden, "can't remove the last manager", nil)
|
||||
return
|
||||
}
|
||||
|
||||
gatewayClient, err := h.getClient()
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "error getting gateway client", err)
|
||||
return
|
||||
}
|
||||
|
||||
removeShareRes, err := gatewayClient.RemoveShare(ctx, &collaborationv1beta1.RemoveShareRequest{
|
||||
Ref: &collaborationv1beta1.ShareReference{
|
||||
Spec: &collaborationv1beta1.ShareReference_Key{
|
||||
Key: &collaborationv1beta1.ShareKey{
|
||||
ResourceId: ref.ResourceId,
|
||||
Grantee: &grantee,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error removing grant", err)
|
||||
return
|
||||
}
|
||||
if removeShareRes.Status.Code != rpc.Code_CODE_OK {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error removing grant", err)
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteOCSSuccess(w, r, nil)
|
||||
}
|
||||
|
||||
func (h *Handler) getStorageProviderClient(p *registry.ProviderInfo) (provider.ProviderAPIClient, error) {
|
||||
c, err := pool.GetStorageProviderServiceClient(p.Address)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "shares spaces: error getting a storage provider client")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (h *Handler) findProvider(ctx context.Context, ref *provider.Reference) (*registry.ProviderInfo, error) {
|
||||
c, err := pool.GetStorageRegistryClient(h.storageRegistryAddr)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "shares spaces: error getting storage registry client")
|
||||
}
|
||||
|
||||
filters := map[string]string{}
|
||||
if ref.Path != "" {
|
||||
filters["path"] = ref.Path
|
||||
}
|
||||
if ref.ResourceId != nil {
|
||||
filters["storage_id"] = ref.ResourceId.StorageId
|
||||
filters["space_id"] = ref.ResourceId.SpaceId
|
||||
filters["opaque_id"] = ref.ResourceId.OpaqueId
|
||||
}
|
||||
|
||||
listReq := ®istry.ListStorageProvidersRequest{
|
||||
Opaque: &types.Opaque{},
|
||||
}
|
||||
sdk.EncodeOpaqueMap(listReq.Opaque, filters)
|
||||
|
||||
res, err := c.ListStorageProviders(ctx, listReq)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "shares spaces: error calling ListStorageProviders")
|
||||
}
|
||||
|
||||
if res.Status.Code != rpc.Code_CODE_OK {
|
||||
switch res.Status.Code {
|
||||
case rpc.Code_CODE_NOT_FOUND:
|
||||
return nil, errtypes.NotFound("shares spaces: storage provider not found for reference:" + ref.String())
|
||||
case rpc.Code_CODE_PERMISSION_DENIED:
|
||||
return nil, errtypes.PermissionDenied("shares spaces: " + res.Status.Message + " for " + ref.String() + " with code " + res.Status.Code.String())
|
||||
case rpc.Code_CODE_INVALID_ARGUMENT, rpc.Code_CODE_FAILED_PRECONDITION, rpc.Code_CODE_OUT_OF_RANGE:
|
||||
return nil, errtypes.BadRequest("shares spaces: " + res.Status.Message + " for " + ref.String() + " with code " + res.Status.Code.String())
|
||||
case rpc.Code_CODE_UNIMPLEMENTED:
|
||||
return nil, errtypes.NotSupported("shares spaces: " + res.Status.Message + " for " + ref.String() + " with code " + res.Status.Code.String())
|
||||
default:
|
||||
return nil, status.NewErrorFromCode(res.Status.Code, "shares spaces")
|
||||
}
|
||||
}
|
||||
|
||||
if len(res.Providers) < 1 {
|
||||
return nil, errtypes.NotFound("shares spaces: no provider found")
|
||||
}
|
||||
|
||||
return res.Providers[0], nil
|
||||
}
|
||||
|
||||
func isSpaceManagerRemaining(grants []*provider.Grant, grantee *provider.Grantee) bool {
|
||||
for _, g := range grants {
|
||||
// RemoveGrant is currently the way to check for the manager role
|
||||
// If it is not set than the current grant is not for a manager and
|
||||
// we can just continue with the next one.
|
||||
if g.Permissions.RemoveGrant && !isEqualGrantee(g.Grantee, grantee) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func granteeExists(grants []*provider.Grant, grantee *provider.Grantee) bool {
|
||||
for _, g := range grants {
|
||||
if isEqualGrantee(g.Grantee, grantee) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isEqualGrantee(a, b *provider.Grantee) bool {
|
||||
// Ideally we would want to use utils.GranteeEqual()
|
||||
// but the grants stored in the decomposedfs aren't complete (missing usertype and idp)
|
||||
// because of that the check would fail so we can only check the ... for now.
|
||||
if a.Type != b.Type {
|
||||
return false
|
||||
}
|
||||
|
||||
var aID, bID string
|
||||
switch a.Type {
|
||||
case provider.GranteeType_GRANTEE_TYPE_GROUP:
|
||||
aID = a.GetGroupId().OpaqueId
|
||||
bID = b.GetGroupId().OpaqueId
|
||||
case provider.GranteeType_GRANTEE_TYPE_USER:
|
||||
aID = a.GetUserId().OpaqueId
|
||||
bID = b.GetUserId().OpaqueId
|
||||
}
|
||||
return aID == bID
|
||||
}
|
||||
Generated
Vendored
+421
@@ -0,0 +1,421 @@
|
||||
// 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 shares
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
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"
|
||||
ocmpb "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
|
||||
"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/permission"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
_iso8601 = "2006-01-02T15:04:05Z0700"
|
||||
)
|
||||
|
||||
func (h *Handler) createUserShare(w http.ResponseWriter, r *http.Request, statInfo *provider.ResourceInfo, role *conversions.Role, roleVal []byte) (*collaboration.Share, *ocsError) {
|
||||
ctx := r.Context()
|
||||
c, err := h.getClient()
|
||||
if err != nil {
|
||||
return nil, &ocsError{
|
||||
Code: response.MetaServerError.StatusCode,
|
||||
Message: "error getting grpc gateway client",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
shareWith := r.FormValue("shareWith")
|
||||
if shareWith == "" {
|
||||
return nil, &ocsError{
|
||||
Code: response.MetaBadRequest.StatusCode,
|
||||
Message: "missing shareWith",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
userRes, err := c.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{
|
||||
Claim: "username",
|
||||
Value: shareWith,
|
||||
SkipFetchingUserGroups: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, &ocsError{
|
||||
Code: response.MetaServerError.StatusCode,
|
||||
Message: "error searching recipient",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
if userRes.Status.Code != rpc.Code_CODE_OK {
|
||||
return nil, &ocsError{
|
||||
Code: response.MetaNotFound.StatusCode,
|
||||
Message: "user not found",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
expireDate := r.PostFormValue("expireDate")
|
||||
var expirationTs *types.Timestamp
|
||||
if expireDate != "" {
|
||||
// FIXME: the web ui sends the RFC3339 format when updating a share but
|
||||
// initially on creating a share the format ISO 8601 is used.
|
||||
// OC10 uses RFC3339 in both cases so we should fix the web ui and change it here.
|
||||
expiration, err := time.Parse(_iso8601, expireDate)
|
||||
if err != nil {
|
||||
return nil, &ocsError{
|
||||
Code: response.MetaBadRequest.StatusCode,
|
||||
Message: "could not parse expireDate",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
expirationTs = &types.Timestamp{
|
||||
Seconds: uint64(expiration.UnixNano() / int64(time.Second)),
|
||||
Nanos: uint32(expiration.UnixNano() % int64(time.Second)),
|
||||
}
|
||||
}
|
||||
|
||||
createShareReq := &collaboration.CreateShareRequest{
|
||||
Opaque: &types.Opaque{
|
||||
Map: map[string]*types.OpaqueEntry{
|
||||
"role": {
|
||||
Decoder: "json",
|
||||
Value: roleVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
ResourceInfo: statInfo,
|
||||
Grant: &collaboration.ShareGrant{
|
||||
Grantee: &provider.Grantee{
|
||||
Type: provider.GranteeType_GRANTEE_TYPE_USER,
|
||||
Id: &provider.Grantee_UserId{UserId: userRes.User.GetId()},
|
||||
},
|
||||
Permissions: &collaboration.SharePermissions{
|
||||
Permissions: role.CS3ResourcePermissions(),
|
||||
},
|
||||
Expiration: expirationTs,
|
||||
},
|
||||
}
|
||||
|
||||
share, ocsErr := h.createCs3Share(ctx, w, r, c, createShareReq)
|
||||
if ocsErr != nil {
|
||||
return nil, ocsErr
|
||||
}
|
||||
return share, nil
|
||||
}
|
||||
|
||||
func (h *Handler) isUserShare(r *http.Request, oid string) (*collaboration.Share, bool) {
|
||||
log := appctx.GetLogger(r.Context())
|
||||
client, err := pool.GetGatewayServiceClient(h.gatewayAddr)
|
||||
if err != nil {
|
||||
log.Err(err).Send()
|
||||
}
|
||||
|
||||
getShareRes, err := client.GetShare(r.Context(), &collaboration.GetShareRequest{
|
||||
Ref: &collaboration.ShareReference{
|
||||
Spec: &collaboration.ShareReference_Id{
|
||||
Id: &collaboration.ShareId{
|
||||
OpaqueId: oid,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
switch {
|
||||
case err != nil:
|
||||
log.Err(err).Send()
|
||||
case getShareRes.Status.Code == rpc.Code_CODE_OK:
|
||||
return getShareRes.GetShare(), true
|
||||
case getShareRes.Status.Code == rpc.Code_CODE_INTERNAL:
|
||||
log.Error().Str("message", getShareRes.GetStatus().GetMessage()).Str("code", getShareRes.GetStatus().GetCode().String()).Msg("isUserShare received internal error")
|
||||
default:
|
||||
log.Debug().Str("message", getShareRes.GetStatus().GetMessage()).Str("code", getShareRes.GetStatus().GetCode().String()).Msg("isUserShare received unexpected status")
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (h *Handler) isFederatedShare(r *http.Request, shareID string) bool {
|
||||
log := appctx.GetLogger(r.Context())
|
||||
client, err := pool.GetGatewayServiceClient(h.gatewayAddr)
|
||||
if err != nil {
|
||||
log.Err(err).Send()
|
||||
return false
|
||||
}
|
||||
|
||||
getShareRes, err := client.GetOCMShare(r.Context(), &ocmpb.GetOCMShareRequest{
|
||||
Ref: &ocmpb.ShareReference{
|
||||
Spec: &ocmpb.ShareReference_Id{
|
||||
Id: &ocmpb.ShareId{
|
||||
OpaqueId: shareID,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
switch {
|
||||
case err != nil:
|
||||
log.Err(err).Send()
|
||||
case getShareRes.Status.Code == rpc.Code_CODE_OK:
|
||||
return true
|
||||
case getShareRes.Status.Code == rpc.Code_CODE_INTERNAL:
|
||||
log.Error().Str("message", getShareRes.GetStatus().GetMessage()).Str("code", getShareRes.GetStatus().GetCode().String()).Msg("isFederatedShare received internal error")
|
||||
default:
|
||||
log.Debug().Str("message", getShareRes.GetStatus().GetMessage()).Str("code", getShareRes.GetStatus().GetCode().String()).Msg("isFederatedShare received unexpected status")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *Handler) removeFederatedShare(w http.ResponseWriter, r *http.Request, shareID string) {
|
||||
ctx := r.Context()
|
||||
|
||||
client, err := pool.GetGatewayServiceClient(h.gatewayAddr)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err)
|
||||
return
|
||||
}
|
||||
|
||||
shareRef := &ocmpb.ShareReference_Id{Id: &ocmpb.ShareId{OpaqueId: shareID}}
|
||||
// Get the share, so that we can include it in the response.
|
||||
getShareResp, err := client.GetOCMShare(ctx, &ocmpb.GetOCMShareRequest{Ref: &ocmpb.ShareReference{Spec: shareRef}})
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc delete share request", err)
|
||||
return
|
||||
}
|
||||
if getShareResp.Status.Code != rpc.Code_CODE_OK {
|
||||
if getShareResp.Status.Code == rpc.Code_CODE_NOT_FOUND {
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "not found", nil)
|
||||
return
|
||||
}
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "deleting share failed", err)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := conversions.OCMShare2ShareData(getShareResp.Share)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "deleting share failed", err)
|
||||
return
|
||||
}
|
||||
// A deleted share should not have an ID.
|
||||
data.ID = ""
|
||||
|
||||
uRes, err := client.RemoveOCMShare(ctx, &ocmpb.RemoveOCMShareRequest{Ref: &ocmpb.ShareReference{Spec: shareRef}})
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc delete share request", err)
|
||||
return
|
||||
}
|
||||
|
||||
if uRes.Status.Code != rpc.Code_CODE_OK {
|
||||
if uRes.Status.Code == rpc.Code_CODE_NOT_FOUND {
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "not found", nil)
|
||||
return
|
||||
}
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc delete share request failed", err)
|
||||
return
|
||||
}
|
||||
response.WriteOCSSuccess(w, r, data)
|
||||
}
|
||||
|
||||
func (h *Handler) isFederatedReceivedShare(r *http.Request, shareID string) bool {
|
||||
log := appctx.GetLogger(r.Context())
|
||||
client, err := pool.GetGatewayServiceClient(h.gatewayAddr)
|
||||
if err != nil {
|
||||
log.Err(err).Send()
|
||||
return false
|
||||
}
|
||||
|
||||
getShareRes, err := client.GetReceivedOCMShare(r.Context(), &ocmpb.GetReceivedOCMShareRequest{
|
||||
Ref: &ocmpb.ShareReference{
|
||||
Spec: &ocmpb.ShareReference_Id{
|
||||
Id: &ocmpb.ShareId{
|
||||
OpaqueId: shareID,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
switch {
|
||||
case err != nil:
|
||||
log.Err(err).Send()
|
||||
case getShareRes.Status.Code == rpc.Code_CODE_OK:
|
||||
return true
|
||||
case getShareRes.Status.Code == rpc.Code_CODE_INTERNAL:
|
||||
log.Error().Str("message", getShareRes.GetStatus().GetMessage()).Str("code", getShareRes.GetStatus().GetCode().String()).Msg("isFederatedReceivedShare received internal error")
|
||||
default:
|
||||
log.Debug().Str("message", getShareRes.GetStatus().GetMessage()).Str("code", getShareRes.GetStatus().GetCode().String()).Msg("isFederatedReceivedShare received unexpected status")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *Handler) removeUserShare(w http.ResponseWriter, r *http.Request, share *collaboration.Share) {
|
||||
ctx := r.Context()
|
||||
|
||||
uClient, err := h.getClient()
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: should we use Share.Delete here?
|
||||
ok, err := utils.CheckPermission(ctx, permission.WriteShare, uClient)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error checking user permissions", err)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
response.WriteOCSError(w, r, response.MetaForbidden.StatusCode, "permission denied", nil)
|
||||
return
|
||||
}
|
||||
|
||||
shareRef := &collaboration.ShareReference{
|
||||
Spec: &collaboration.ShareReference_Id{
|
||||
Id: share.Id,
|
||||
},
|
||||
}
|
||||
|
||||
data := conversions.CS3Share2ShareData(ctx, share)
|
||||
// A deleted share should not have an ID.
|
||||
data.ID = ""
|
||||
|
||||
uReq := &collaboration.RemoveShareRequest{Ref: shareRef}
|
||||
uRes, err := uClient.RemoveShare(ctx, uReq)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc delete share request", err)
|
||||
return
|
||||
}
|
||||
|
||||
if uRes.Status.Code != rpc.Code_CODE_OK {
|
||||
switch uRes.Status.Code {
|
||||
case rpc.Code_CODE_NOT_FOUND:
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "not found", nil)
|
||||
return
|
||||
case rpc.Code_CODE_LOCKED:
|
||||
response.WriteOCSError(w, r, response.MetaLocked.StatusCode, uRes.GetStatus().GetMessage(), nil)
|
||||
return
|
||||
}
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc delete share request failed", err)
|
||||
return
|
||||
}
|
||||
if currentUser, ok := ctxpkg.ContextGetUser(ctx); ok {
|
||||
h.statCache.RemoveStat(currentUser.Id, share.ResourceId)
|
||||
}
|
||||
response.WriteOCSSuccess(w, r, data)
|
||||
}
|
||||
|
||||
func (h *Handler) listUserShares(r *http.Request, filters []*collaboration.Filter) ([]*conversions.ShareData, *rpc.Status, error) {
|
||||
ctx := r.Context()
|
||||
log := appctx.GetLogger(ctx)
|
||||
|
||||
lsUserSharesRequest := collaboration.ListSharesRequest{
|
||||
Filters: filters,
|
||||
}
|
||||
|
||||
ocsDataPayload := make([]*conversions.ShareData, 0)
|
||||
if h.gatewayAddr != "" {
|
||||
// get a connection to the users share provider
|
||||
client, err := h.getClient()
|
||||
if err != nil {
|
||||
return ocsDataPayload, nil, err
|
||||
}
|
||||
|
||||
// do list shares request. filtered
|
||||
lsUserSharesResponse, err := client.ListShares(ctx, &lsUserSharesRequest)
|
||||
if err != nil {
|
||||
return ocsDataPayload, nil, err
|
||||
}
|
||||
if lsUserSharesResponse.Status.Code != rpc.Code_CODE_OK {
|
||||
return ocsDataPayload, lsUserSharesResponse.Status, nil
|
||||
}
|
||||
|
||||
// build OCS response payload
|
||||
for _, s := range lsUserSharesResponse.Shares {
|
||||
data := conversions.CS3Share2ShareData(ctx, s)
|
||||
|
||||
info, status, err := h.getResourceInfoByID(ctx, client, s.ResourceId)
|
||||
if err != nil || status.Code != rpc.Code_CODE_OK {
|
||||
log.Debug().Interface("share", s).Interface("status", status).Interface("shareData", data).Err(err).Msg("could not stat share, skipping")
|
||||
continue
|
||||
}
|
||||
u := ctxpkg.ContextMustGetUser(ctx)
|
||||
// check if the user has the permission to list all shares on the resource
|
||||
if !utils.UserEqual(s.Creator, u.Id) && !info.GetPermissionSet().ListGrants {
|
||||
log.Debug().Interface("share", s).Interface("user", u).Msg("user has no permission to list all grants and is not the creator of this share")
|
||||
continue
|
||||
}
|
||||
|
||||
h.addFileInfo(ctx, data, info)
|
||||
h.mapUserIds(ctx, client, data)
|
||||
// Filter out a share if ShareWith is not found because the user or group already deleted
|
||||
if data.ShareWith == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debug().Interface("share", s).Interface("info", info).Interface("shareData", data).Msg("mapped")
|
||||
ocsDataPayload = append(ocsDataPayload, data)
|
||||
}
|
||||
|
||||
if h.listOCMShares {
|
||||
// include the ocm shares
|
||||
ocmShares, err := h.listOutcomingFederatedShares(ctx, client, convertToOCMFilters(filters))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ocsDataPayload = append(ocsDataPayload, ocmShares...)
|
||||
}
|
||||
}
|
||||
|
||||
return ocsDataPayload, nil, nil
|
||||
}
|
||||
|
||||
func convertToOCMFilters(filters []*collaboration.Filter) []*ocmpb.ListOCMSharesRequest_Filter {
|
||||
ocmfilters := []*ocmpb.ListOCMSharesRequest_Filter{}
|
||||
for _, f := range filters {
|
||||
switch v := f.Term.(type) {
|
||||
case *collaboration.Filter_ResourceId:
|
||||
ocmfilters = append(ocmfilters, &ocmpb.ListOCMSharesRequest_Filter{
|
||||
Type: ocmpb.ListOCMSharesRequest_Filter_TYPE_RESOURCE_ID,
|
||||
Term: &ocmpb.ListOCMSharesRequest_Filter_ResourceId{
|
||||
ResourceId: v.ResourceId,
|
||||
},
|
||||
})
|
||||
case *collaboration.Filter_Creator:
|
||||
ocmfilters = append(ocmfilters, &ocmpb.ListOCMSharesRequest_Filter{
|
||||
Type: ocmpb.ListOCMSharesRequest_Filter_TYPE_CREATOR,
|
||||
Term: &ocmpb.ListOCMSharesRequest_Filter_Creator{
|
||||
Creator: v.Creator,
|
||||
},
|
||||
})
|
||||
case *collaboration.Filter_Owner:
|
||||
ocmfilters = append(ocmfilters, &ocmpb.ListOCMSharesRequest_Filter{
|
||||
Type: ocmpb.ListOCMSharesRequest_Filter_TYPE_OWNER,
|
||||
Term: &ocmpb.ListOCMSharesRequest_Filter_Owner{
|
||||
Owner: v.Owner,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return ocmfilters
|
||||
}
|
||||
Generated
Vendored
+238
@@ -0,0 +1,238 @@
|
||||
// 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 capabilities
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/config"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/owncloud/ocs"
|
||||
)
|
||||
|
||||
// Handler renders the capability endpoint
|
||||
type Handler struct {
|
||||
c ocs.CapabilitiesData
|
||||
defaultUploadProtocol string
|
||||
userAgentChunkingMap map[string]string
|
||||
}
|
||||
|
||||
// Init initializes this and any contained handlers
|
||||
func (h *Handler) Init(c *config.Config) {
|
||||
h.c = c.Capabilities
|
||||
h.defaultUploadProtocol = c.DefaultUploadProtocol
|
||||
h.userAgentChunkingMap = c.UserAgentChunkingMap
|
||||
|
||||
// capabilities
|
||||
if h.c.Capabilities == nil {
|
||||
h.c.Capabilities = &ocs.Capabilities{}
|
||||
}
|
||||
|
||||
// core
|
||||
|
||||
if h.c.Capabilities.Core == nil {
|
||||
h.c.Capabilities.Core = &ocs.CapabilitiesCore{}
|
||||
}
|
||||
if h.c.Capabilities.Core.PollInterval == 0 {
|
||||
h.c.Capabilities.Core.PollInterval = 60
|
||||
}
|
||||
if h.c.Capabilities.Core.WebdavRoot == "" {
|
||||
h.c.Capabilities.Core.WebdavRoot = "remote.php/webdav"
|
||||
}
|
||||
// h.c.Capabilities.Core.SupportURLSigning is boolean
|
||||
|
||||
if h.c.Capabilities.Core.Status == nil {
|
||||
h.c.Capabilities.Core.Status = &ocs.Status{}
|
||||
}
|
||||
// h.c.Capabilities.Core.Status.Installed is boolean
|
||||
// h.c.Capabilities.Core.Status.Maintenance is boolean
|
||||
// h.c.Capabilities.Core.Status.NeedsDBUpgrade is boolean
|
||||
if h.c.Capabilities.Core.Status.Version == "" {
|
||||
h.c.Capabilities.Core.Status.Version = "10.0.11.5" // TODO make build determined
|
||||
}
|
||||
if h.c.Capabilities.Core.Status.VersionString == "" {
|
||||
h.c.Capabilities.Core.Status.VersionString = "10.0.11" // TODO make build determined
|
||||
}
|
||||
if h.c.Capabilities.Core.Status.Edition == "" {
|
||||
h.c.Capabilities.Core.Status.Edition = "community" // TODO make build determined
|
||||
}
|
||||
if h.c.Capabilities.Core.Status.ProductName == "" {
|
||||
h.c.Capabilities.Core.Status.ProductName = "reva" // TODO make build determined
|
||||
}
|
||||
if h.c.Capabilities.Core.Status.Product == "" {
|
||||
h.c.Capabilities.Core.Status.Product = "reva" // TODO make build determined
|
||||
}
|
||||
if h.c.Capabilities.Core.Status.Hostname == "" {
|
||||
h.c.Capabilities.Core.Status.Hostname = "" // TODO get from context?
|
||||
}
|
||||
|
||||
// checksums
|
||||
|
||||
if h.c.Capabilities.Checksums == nil {
|
||||
h.c.Capabilities.Checksums = &ocs.CapabilitiesChecksums{}
|
||||
}
|
||||
if h.c.Capabilities.Checksums.SupportedTypes == nil {
|
||||
h.c.Capabilities.Checksums.SupportedTypes = []string{"SHA256"}
|
||||
}
|
||||
if h.c.Capabilities.Checksums.PreferredUploadType == "" {
|
||||
h.c.Capabilities.Checksums.PreferredUploadType = "SHA1"
|
||||
}
|
||||
|
||||
// files
|
||||
|
||||
if h.c.Capabilities.Files == nil {
|
||||
h.c.Capabilities.Files = &ocs.CapabilitiesFiles{}
|
||||
}
|
||||
|
||||
if h.c.Capabilities.Files.BlacklistedFiles == nil {
|
||||
h.c.Capabilities.Files.BlacklistedFiles = []string{}
|
||||
}
|
||||
// h.c.Capabilities.Files.Undelete is boolean
|
||||
// h.c.Capabilities.Files.Versioning is boolean
|
||||
// h.c.Capabilities.Files.Favorites is boolean
|
||||
|
||||
if h.c.Capabilities.Files.Archivers == nil {
|
||||
h.c.Capabilities.Files.Archivers = []*ocs.CapabilitiesArchiver{}
|
||||
}
|
||||
|
||||
if h.c.Capabilities.Files.AppProviders == nil {
|
||||
h.c.Capabilities.Files.AppProviders = []*ocs.CapabilitiesAppProvider{}
|
||||
}
|
||||
|
||||
// dav
|
||||
|
||||
if h.c.Capabilities.Dav == nil {
|
||||
h.c.Capabilities.Dav = &ocs.CapabilitiesDav{}
|
||||
}
|
||||
if h.c.Capabilities.Dav.Trashbin == "" {
|
||||
h.c.Capabilities.Dav.Trashbin = "1.0"
|
||||
}
|
||||
if h.c.Capabilities.Dav.Reports == nil {
|
||||
h.c.Capabilities.Dav.Reports = []string{}
|
||||
}
|
||||
|
||||
// sharing
|
||||
|
||||
if h.c.Capabilities.FilesSharing == nil {
|
||||
h.c.Capabilities.FilesSharing = &ocs.CapabilitiesFilesSharing{}
|
||||
}
|
||||
|
||||
// h.c.Capabilities.FilesSharing.APIEnabled is boolean
|
||||
|
||||
if h.c.Capabilities.FilesSharing.Public == nil {
|
||||
h.c.Capabilities.FilesSharing.Public = &ocs.CapabilitiesFilesSharingPublic{}
|
||||
}
|
||||
|
||||
// h.c.Capabilities.FilesSharing.IsPublic.Enabled is boolean
|
||||
h.c.Capabilities.FilesSharing.Public.Enabled = true
|
||||
|
||||
if h.c.Capabilities.FilesSharing.Public.Password == nil {
|
||||
h.c.Capabilities.FilesSharing.Public.Password = &ocs.CapabilitiesFilesSharingPublicPassword{}
|
||||
}
|
||||
|
||||
if h.c.Capabilities.FilesSharing.Public.Password.EnforcedFor == nil {
|
||||
h.c.Capabilities.FilesSharing.Public.Password.EnforcedFor = &ocs.CapabilitiesFilesSharingPublicPasswordEnforcedFor{}
|
||||
}
|
||||
|
||||
// h.c.Capabilities.FilesSharing.IsPublic.Password.EnforcedFor.ReadOnly is boolean
|
||||
// h.c.Capabilities.FilesSharing.IsPublic.Password.EnforcedFor.ReadWrite is boolean
|
||||
// h.c.Capabilities.FilesSharing.IsPublic.Password.EnforcedFor.ReadWriteDelete is boolean
|
||||
// h.c.Capabilities.FilesSharing.IsPublic.Password.EnforcedFor.UploadOnly is boolean
|
||||
|
||||
// h.c.Capabilities.FilesSharing.IsPublic.Password.Enforced is boolean
|
||||
|
||||
if h.c.Capabilities.FilesSharing.Public.ExpireDate == nil {
|
||||
h.c.Capabilities.FilesSharing.Public.ExpireDate = &ocs.CapabilitiesFilesSharingPublicExpireDate{}
|
||||
}
|
||||
// h.c.Capabilities.FilesSharing.IsPublic.ExpireDate.Enabled is boolean
|
||||
|
||||
// h.c.Capabilities.FilesSharing.IsPublic.SendMail is boolean
|
||||
// h.c.Capabilities.FilesSharing.IsPublic.SocialShare is boolean
|
||||
// h.c.Capabilities.FilesSharing.IsPublic.Upload is boolean
|
||||
// h.c.Capabilities.FilesSharing.IsPublic.Multiple is boolean
|
||||
// h.c.Capabilities.FilesSharing.IsPublic.SupportsUploadOnly is boolean
|
||||
|
||||
if h.c.Capabilities.FilesSharing.User == nil {
|
||||
h.c.Capabilities.FilesSharing.User = &ocs.CapabilitiesFilesSharingUser{}
|
||||
}
|
||||
|
||||
// h.c.Capabilities.FilesSharing.User.SendMail is boolean
|
||||
|
||||
// h.c.Capabilities.FilesSharing.Resharing is boolean
|
||||
// h.c.Capabilities.FilesSharing.GroupSharing is boolean
|
||||
// h.c.Capabilities.FilesSharing.SharingRoles is boolean
|
||||
// h.c.Capabilities.FilesSharing.AutoAcceptShare is boolean
|
||||
// h.c.Capabilities.FilesSharing.ShareWithGroupMembersOnly is boolean
|
||||
// h.c.Capabilities.FilesSharing.ShareWithMembershipGroupsOnly is boolean
|
||||
|
||||
if h.c.Capabilities.FilesSharing.UserEnumeration == nil {
|
||||
h.c.Capabilities.FilesSharing.UserEnumeration = &ocs.CapabilitiesFilesSharingUserEnumeration{}
|
||||
}
|
||||
|
||||
// h.c.Capabilities.FilesSharing.UserEnumeration.Enabled is boolean
|
||||
// h.c.Capabilities.FilesSharing.UserEnumeration.GroupMembersOnly is boolean
|
||||
|
||||
if h.c.Capabilities.FilesSharing.DefaultPermissions == 0 {
|
||||
h.c.Capabilities.FilesSharing.DefaultPermissions = 31
|
||||
}
|
||||
if h.c.Capabilities.FilesSharing.Federation == nil {
|
||||
h.c.Capabilities.FilesSharing.Federation = &ocs.CapabilitiesFilesSharingFederation{}
|
||||
}
|
||||
|
||||
// h.c.Capabilities.FilesSharing.Federation.Outgoing is boolean
|
||||
// h.c.Capabilities.FilesSharing.Federation.Incoming is boolean
|
||||
|
||||
if h.c.Capabilities.FilesSharing.SearchMinLength == 0 {
|
||||
h.c.Capabilities.FilesSharing.SearchMinLength = 2
|
||||
}
|
||||
|
||||
// notifications
|
||||
|
||||
// if h.c.Capabilities.Notifications == nil {
|
||||
// h.c.Capabilities.Notifications = &ocs.CapabilitiesNotifications{}
|
||||
// }
|
||||
// if h.c.Capabilities.Notifications.Endpoints == nil {
|
||||
// h.c.Capabilities.Notifications.Endpoints = []string{"list", "get", "delete"}
|
||||
// }
|
||||
|
||||
// version
|
||||
|
||||
if h.c.Version == nil {
|
||||
h.c.Version = &ocs.Version{
|
||||
// TODO get from build env
|
||||
Major: 10,
|
||||
Minor: 0,
|
||||
Micro: 11,
|
||||
String: "10.0.11",
|
||||
Edition: "community",
|
||||
Product: "reva",
|
||||
ProductVersion: "",
|
||||
}
|
||||
}
|
||||
|
||||
// upload protocol-specific details
|
||||
setCapabilitiesForChunkProtocol(chunkProtocol(h.defaultUploadProtocol), &h.c)
|
||||
|
||||
}
|
||||
|
||||
// Handler renders the capabilities
|
||||
func (h *Handler) GetCapabilities(w http.ResponseWriter, r *http.Request) {
|
||||
c := h.getCapabilitiesForUserAgent(r.UserAgent())
|
||||
response.WriteOCSSuccess(w, r, c)
|
||||
}
|
||||
Generated
Vendored
+79
@@ -0,0 +1,79 @@
|
||||
// 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 capabilities
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/owncloud/ocs"
|
||||
)
|
||||
|
||||
type chunkProtocol string
|
||||
|
||||
var (
|
||||
chunkV1 chunkProtocol = "v1"
|
||||
chunkNG chunkProtocol = "ng"
|
||||
chunkTUS chunkProtocol = "tus"
|
||||
)
|
||||
|
||||
func (h *Handler) getCapabilitiesForUserAgent(userAgent string) ocs.CapabilitiesData {
|
||||
if userAgent != "" {
|
||||
for k, v := range h.userAgentChunkingMap {
|
||||
// we could also use a regexp for pattern matching
|
||||
if strings.Contains(userAgent, k) {
|
||||
// Creating a copy of the capabilities struct is less expensive than taking a lock
|
||||
c := h.c
|
||||
setCapabilitiesForChunkProtocol(chunkProtocol(v), &c)
|
||||
return c
|
||||
}
|
||||
}
|
||||
}
|
||||
return h.c
|
||||
}
|
||||
|
||||
func setCapabilitiesForChunkProtocol(cp chunkProtocol, c *ocs.CapabilitiesData) {
|
||||
switch cp {
|
||||
case chunkV1:
|
||||
// 2.7+ will use Chunking V1 if "capabilities > files > bigfilechunking" is "true" AND "capabilities > dav > chunking" is not there
|
||||
c.Capabilities.Files.BigFileChunking = true
|
||||
c.Capabilities.Dav = nil
|
||||
c.Capabilities.Files.TusSupport = nil
|
||||
|
||||
case chunkNG:
|
||||
// 2.7+ will use Chunking NG if "capabilities > files > bigfilechunking" is "true" AND "capabilities > dav > chunking" = 1.0
|
||||
c.Capabilities.Files.BigFileChunking = true
|
||||
c.Capabilities.Dav.Chunking = "1.0"
|
||||
c.Capabilities.Files.TusSupport = nil
|
||||
|
||||
case chunkTUS:
|
||||
// 2.7+ will use TUS if "capabilities > files > bigfilechunking" is "false" AND "capabilities > dav > chunking" = "" AND "capabilities > files > tus_support" has proper entries.
|
||||
c.Capabilities.Files.BigFileChunking = false
|
||||
c.Capabilities.Dav.Chunking = ""
|
||||
|
||||
// TODO: infer from various TUS handlers from all known storages
|
||||
// until now we take the manually configured tus options
|
||||
// c.Capabilities.Files.TusSupport = &data.CapabilitiesFilesTusSupport{
|
||||
// Version: "1.0.0",
|
||||
// Resumable: "1.0.0",
|
||||
// Extension: "creation,creation-with-upload",
|
||||
// MaxChunkSize: 0,
|
||||
// HTTPMethodOverride: "",
|
||||
// }
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+59
@@ -0,0 +1,59 @@
|
||||
// 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 user
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/conversions"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
)
|
||||
|
||||
// The Handler renders the user endpoint
|
||||
type Handler struct {
|
||||
}
|
||||
|
||||
// GetSelf handles GET requests on /cloud/user
|
||||
func (h *Handler) GetSelf(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// TODO move user to handler parameter?
|
||||
u, ok := ctxpkg.ContextGetUser(ctx)
|
||||
if !ok {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "missing user in context", fmt.Errorf("missing user in context"))
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteOCSSuccess(w, r, &User{
|
||||
ID: u.Username,
|
||||
DisplayName: u.DisplayName,
|
||||
Email: u.Mail,
|
||||
UserType: conversions.UserTypeString(u.Id.Type),
|
||||
})
|
||||
}
|
||||
|
||||
// User holds user data
|
||||
type User struct {
|
||||
ID string `json:"id" xml:"id"` // UserID in ocs is the owncloud internal username
|
||||
DisplayName string `json:"display-name" xml:"display-name"` // is used in ocs/v(1|2).php/cloud/user - yes this is different from the users endpoint
|
||||
Email string `json:"email" xml:"email"`
|
||||
UserType string `json:"user-type" xml:"user-type"`
|
||||
}
|
||||
Generated
Vendored
+230
@@ -0,0 +1,230 @@
|
||||
// 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 users
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
cs3gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
cs3identity "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
cs3storage "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/config"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
|
||||
"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/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
// Handler renders user data for the user id given in the url path
|
||||
type Handler struct {
|
||||
gatewayAddr string
|
||||
}
|
||||
|
||||
// Init initializes this and any contained handlers
|
||||
func (h *Handler) Init(c *config.Config) {
|
||||
h.gatewayAddr = c.GatewaySvc
|
||||
}
|
||||
|
||||
// GetGroups handles GET requests on /cloud/users/groups
|
||||
// TODO: implement
|
||||
func (h *Handler) GetGroups(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
user := chi.URLParam(r, "userid")
|
||||
// FIXME use ldap to fetch user info
|
||||
u, ok := ctxpkg.ContextGetUser(ctx)
|
||||
if !ok {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "missing user in context", fmt.Errorf("missing user in context"))
|
||||
return
|
||||
}
|
||||
if user != u.Username {
|
||||
// FIXME allow fetching other users info? only for admins
|
||||
response.WriteOCSError(w, r, http.StatusForbidden, "user id mismatch", fmt.Errorf("%s tried to access %s user info endpoint", u.Id.OpaqueId, user))
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteOCSSuccess(w, r, &Groups{Groups: u.Groups})
|
||||
}
|
||||
|
||||
// Quota holds quota information
|
||||
type Quota struct {
|
||||
Free int64 `json:"free,omitempty" xml:"free,omitempty"`
|
||||
Used int64 `json:"used,omitempty" xml:"used,omitempty"`
|
||||
Total int64 `json:"total,omitempty" xml:"total,omitempty"`
|
||||
Relative float32 `json:"relative,omitempty" xml:"relative,omitempty"`
|
||||
Definition string `json:"definition,omitempty" xml:"definition,omitempty"`
|
||||
}
|
||||
|
||||
// User holds user data
|
||||
type User struct {
|
||||
Enabled string `json:"enabled" xml:"enabled"`
|
||||
Quota *Quota `json:"quota,omitempty" xml:"quota,omitempty"`
|
||||
Email string `json:"email" xml:"email"`
|
||||
DisplayName string `json:"displayname" xml:"displayname"` // is used in ocs/v(1|2).php/cloud/users/{username} - yes this is different from the /user endpoint
|
||||
UserType string `json:"user-type" xml:"user-type"`
|
||||
UIDNumber int64 `json:"uidnumber,omitempty" xml:"uidnumber,omitempty"`
|
||||
GIDNumber int64 `json:"gidnumber,omitempty" xml:"gidnumber,omitempty"`
|
||||
// FIXME home should never be exposed ... even in oc 10, well only the admin can call this endpoint ...
|
||||
// Home string `json:"home" xml:"home"`
|
||||
TwoFactorAuthEnabled bool `json:"two_factor_auth_enabled" xml:"two_factor_auth_enabled"`
|
||||
LastLogin int64 `json:"last_login" xml:"last_login"`
|
||||
}
|
||||
|
||||
// Groups holds group data
|
||||
type Groups struct {
|
||||
Groups []string `json:"groups" xml:"groups>element"`
|
||||
}
|
||||
|
||||
// GetUsers handles GET requests on /cloud/users
|
||||
// Only allow self-read currently. TODO: List Users and Get on other users (both require
|
||||
// administrative privileges)
|
||||
func (h *Handler) GetUsers(w http.ResponseWriter, r *http.Request) {
|
||||
userid := chi.URLParam(r, "userid")
|
||||
userid, err := url.PathUnescape(userid)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "could not unescape username", err)
|
||||
return
|
||||
}
|
||||
|
||||
currentUser, ok := ctxpkg.ContextGetUser(r.Context())
|
||||
if !ok {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "missing user in context", fmt.Errorf("missing user in context"))
|
||||
return
|
||||
}
|
||||
|
||||
var user *cs3identity.User
|
||||
switch {
|
||||
case userid == "":
|
||||
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "missing username", fmt.Errorf("missing username"))
|
||||
return
|
||||
case userid == currentUser.Username:
|
||||
user = currentUser
|
||||
default:
|
||||
// FIXME allow fetching other users info? only for admins
|
||||
response.WriteOCSError(w, r, http.StatusForbidden, "user id mismatch", fmt.Errorf("%s tried to access %s user info endpoint", currentUser.Id.OpaqueId, user))
|
||||
return
|
||||
}
|
||||
|
||||
d := &User{
|
||||
Enabled: "true", // TODO include in response only when admin?
|
||||
DisplayName: user.DisplayName,
|
||||
Email: user.Mail,
|
||||
UserType: conversions.UserTypeString(user.Id.Type),
|
||||
Quota: &Quota{},
|
||||
}
|
||||
// TODO how do we fill lastlogin of a user when another user (with the necessary permissions) looks up the user?
|
||||
// TODO someone needs to fill last-login
|
||||
if lastLogin := utils.ReadPlainFromOpaque(user.Opaque, "last-login"); lastLogin != "" {
|
||||
d.LastLogin, _ = strconv.ParseInt(lastLogin, 10, 64)
|
||||
}
|
||||
|
||||
// lightweight and federated users don't have access to their storage space
|
||||
if currentUser.Id.Type != cs3identity.UserType_USER_TYPE_LIGHTWEIGHT && currentUser.Id.Type != cs3identity.UserType_USER_TYPE_FEDERATED {
|
||||
h.fillPersonalQuota(r.Context(), d, user)
|
||||
}
|
||||
|
||||
response.WriteOCSSuccess(w, r, d)
|
||||
}
|
||||
|
||||
func (h Handler) fillPersonalQuota(ctx context.Context, d *User, u *cs3identity.User) {
|
||||
|
||||
sublog := appctx.GetLogger(ctx)
|
||||
|
||||
gc, err := pool.GetGatewayServiceClient(h.gatewayAddr)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error getting gateway client")
|
||||
return
|
||||
}
|
||||
|
||||
res, err := gc.ListStorageSpaces(ctx, &cs3storage.ListStorageSpacesRequest{
|
||||
Filters: []*cs3storage.ListStorageSpacesRequest_Filter{
|
||||
{
|
||||
Type: cs3storage.ListStorageSpacesRequest_Filter_TYPE_OWNER,
|
||||
Term: &cs3storage.ListStorageSpacesRequest_Filter_Owner{
|
||||
Owner: u.Id,
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: cs3storage.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE,
|
||||
Term: &cs3storage.ListStorageSpacesRequest_Filter_SpaceType{
|
||||
SpaceType: "personal",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error calling ListStorageSpaces")
|
||||
return
|
||||
}
|
||||
if res.Status.Code != cs3rpc.Code_CODE_OK {
|
||||
return
|
||||
}
|
||||
|
||||
if len(res.StorageSpaces) == 0 {
|
||||
sublog.Error().Err(err).Msg("list spaces returned empty list")
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
getQuotaRes, err := gc.GetQuota(ctx, &cs3gateway.GetQuotaRequest{Ref: &cs3storage.Reference{
|
||||
ResourceId: res.StorageSpaces[0].Root,
|
||||
Path: ".",
|
||||
}})
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error calling GetQuota")
|
||||
return
|
||||
}
|
||||
if res.Status.Code != cs3rpc.Code_CODE_OK {
|
||||
sublog.Debug().Interface("status", res.Status).Msg("GetQuota returned non OK result")
|
||||
return
|
||||
}
|
||||
|
||||
total := getQuotaRes.TotalBytes
|
||||
used := getQuotaRes.UsedBytes
|
||||
|
||||
d.Quota = &Quota{
|
||||
Used: int64(used),
|
||||
// TODO support negative values or flags for the quota to carry special meaning: -1 = uncalculated, -2 = unknown, -3 = unlimited
|
||||
// for now we can only report total and used
|
||||
Total: int64(total),
|
||||
// we cannot differentiate between `default` or a human readable `1 GB` definition.
|
||||
// The web UI can create a human readable string from the actual total if it is set. Otherwise it has to leave out relative and total anyway.
|
||||
// Definition: "default",
|
||||
}
|
||||
|
||||
if raw := utils.ReadPlainFromOpaque(getQuotaRes.Opaque, "remaining"); raw != "" {
|
||||
d.Quota.Free, _ = strconv.ParseInt(raw, 10, 64)
|
||||
}
|
||||
|
||||
// only calculate free and relative when total is available
|
||||
if total > 0 {
|
||||
d.Quota.Free = int64(total - used)
|
||||
d.Quota.Relative = float32((float64(used) / float64(total)) * 100.0)
|
||||
} else {
|
||||
d.Quota.Definition = "none" // this indicates no quota / unlimited to the ui
|
||||
}
|
||||
}
|
||||
vendor/github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/handlers/config/config.go
Generated
Vendored
+64
@@ -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 ocs
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/config"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/data"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
|
||||
)
|
||||
|
||||
// Handler renders the config endpoint
|
||||
type Handler struct {
|
||||
c data.ConfigData
|
||||
}
|
||||
|
||||
// Init initializes this and any contained handlers
|
||||
func (h *Handler) Init(c *config.Config) {
|
||||
h.c = c.Config
|
||||
// config
|
||||
if h.c.Version == "" {
|
||||
h.c.Version = "1.7"
|
||||
}
|
||||
if h.c.Website == "" {
|
||||
h.c.Website = "reva"
|
||||
}
|
||||
if h.c.Host == "" {
|
||||
h.c.Host = "" // TODO get from context?
|
||||
}
|
||||
if h.c.Contact == "" {
|
||||
h.c.Contact = ""
|
||||
}
|
||||
if h.c.SSL == "" {
|
||||
h.c.SSL = "false" // TODO get from context?
|
||||
}
|
||||
|
||||
// ensure that host has no protocol
|
||||
if url, err := url.Parse(h.c.Host); err == nil {
|
||||
h.c.Host = url.Host + url.Path
|
||||
}
|
||||
}
|
||||
|
||||
// Handler renders the config
|
||||
func (h *Handler) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
response.WriteOCSSuccess(w, r, h.c)
|
||||
}
|
||||
Generated
Vendored
+178
@@ -0,0 +1,178 @@
|
||||
// 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 ocs
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/jellydator/ttlcache/v2"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/config"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/handlers/apps/sharing/sharees"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/handlers/cloud/capabilities"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/handlers/cloud/user"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/handlers/cloud/users"
|
||||
configHandler "github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/handlers/config"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
|
||||
"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("ocs", New)
|
||||
}
|
||||
|
||||
type svc struct {
|
||||
c *config.Config
|
||||
router *chi.Mux
|
||||
warmupCacheTracker *ttlcache.Cache
|
||||
}
|
||||
|
||||
// New initializes the service
|
||||
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
|
||||
conf := &config.Config{}
|
||||
if err := mapstructure.Decode(m, conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf.Init()
|
||||
|
||||
r := chi.NewRouter()
|
||||
s := &svc{
|
||||
c: conf,
|
||||
router: r,
|
||||
}
|
||||
|
||||
if err := s.routerInit(log); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if conf.CacheWarmupDriver == "first-request" && conf.StatCacheConfig.Store != "noop" {
|
||||
s.warmupCacheTracker = ttlcache.NewCache()
|
||||
_ = s.warmupCacheTracker.SetTTL(conf.StatCacheConfig.TTL)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *svc) Prefix() string {
|
||||
return s.c.Prefix
|
||||
}
|
||||
|
||||
func (s *svc) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *svc) Unprotected() []string {
|
||||
return []string{
|
||||
"/v1.php/config",
|
||||
"/v2.php/config",
|
||||
"/v1.php/apps/files_sharing/api/v1/tokeninfo/unprotected",
|
||||
"/v2.php/apps/files_sharing/api/v1/tokeninfo/unprotected",
|
||||
"/v1.php/cloud/capabilities",
|
||||
"/v2.php/cloud/capabilities",
|
||||
}
|
||||
}
|
||||
|
||||
func (s *svc) routerInit(log *zerolog.Logger) error {
|
||||
capabilitiesHandler := new(capabilities.Handler)
|
||||
userHandler := new(user.Handler)
|
||||
usersHandler := new(users.Handler)
|
||||
configHandler := new(configHandler.Handler)
|
||||
sharesHandler := new(shares.Handler)
|
||||
shareesHandler := new(sharees.Handler)
|
||||
capabilitiesHandler.Init(s.c)
|
||||
usersHandler.Init(s.c)
|
||||
configHandler.Init(s.c)
|
||||
err := sharesHandler.Init(s.c)
|
||||
if err != nil {
|
||||
log.Fatal().Msg(err.Error())
|
||||
}
|
||||
shareesHandler.Init(s.c)
|
||||
|
||||
s.router.Route("/v{version:(1|2)}.php", func(r chi.Router) {
|
||||
r.Use(response.VersionCtx)
|
||||
r.Route("/apps/files_sharing/api/v1", func(r chi.Router) {
|
||||
r.Route("/shares", func(r chi.Router) {
|
||||
r.Get("/", sharesHandler.ListShares)
|
||||
r.Options("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
r.Post("/", sharesHandler.CreateShare)
|
||||
r.Route("/pending/{shareid}", func(r chi.Router) {
|
||||
r.Post("/", sharesHandler.AcceptReceivedShare)
|
||||
r.Delete("/", sharesHandler.RejectReceivedShare)
|
||||
r.Put("/", sharesHandler.UpdateReceivedShare)
|
||||
})
|
||||
r.Route("/remote_shares", func(r chi.Router) {
|
||||
r.Get("/", sharesHandler.ListFederatedShares)
|
||||
r.Get("/{shareid}", sharesHandler.GetFederatedShare)
|
||||
})
|
||||
r.Get("/{shareid}", sharesHandler.GetShare)
|
||||
r.Put("/{shareid}", sharesHandler.UpdateShare)
|
||||
r.Delete("/{shareid}", sharesHandler.RemoveShare)
|
||||
})
|
||||
r.Get("/sharees", shareesHandler.FindSharees)
|
||||
r.Route("/tokeninfo", func(r chi.Router) {
|
||||
r.Get("/protected/{tkn}", shareesHandler.TokenInfo(true))
|
||||
r.Get("/unprotected/{tkn}", shareesHandler.TokenInfo(false))
|
||||
})
|
||||
})
|
||||
|
||||
// placeholder for notifications
|
||||
r.Get("/apps/notifications/api/v1/notifications", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
r.Get("/config", configHandler.GetConfig)
|
||||
|
||||
r.Route("/cloud", func(r chi.Router) {
|
||||
r.Get("/capabilities", capabilitiesHandler.GetCapabilities)
|
||||
r.Get("/user", userHandler.GetSelf)
|
||||
r.Route("/users", func(r chi.Router) {
|
||||
r.Get("/{userid}", usersHandler.GetUsers)
|
||||
r.Get("/{userid}/groups", usersHandler.GetGroups)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
_ = chi.Walk(s.router, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
||||
log.Debug().Str("service", "ocs").Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint")
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *svc) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log := appctx.GetLogger(r.Context())
|
||||
log.Debug().Str("path", r.URL.Path).Msg("ocs routing")
|
||||
|
||||
// Warmup the share cache for the user
|
||||
go s.cacheWarmup(w, r)
|
||||
|
||||
// unset raw path, otherwise chi uses it to route and then fails to match percent encoded path segments
|
||||
r.URL.RawPath = ""
|
||||
s.router.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+308
@@ -0,0 +1,308 @@
|
||||
// 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 response
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
)
|
||||
|
||||
type key int
|
||||
|
||||
const (
|
||||
apiVersionKey key = 1
|
||||
)
|
||||
|
||||
var (
|
||||
defaultStatusCodeMapper = OcsV2StatusCodes
|
||||
)
|
||||
|
||||
// Response is the top level response structure
|
||||
type Response struct {
|
||||
OCS *Payload `json:"ocs"`
|
||||
}
|
||||
|
||||
// Payload combines response metadata and data
|
||||
type Payload struct {
|
||||
XMLName struct{} `json:"-" xml:"ocs"`
|
||||
Meta Meta `json:"meta" xml:"meta"`
|
||||
Data interface{} `json:"data,omitempty" xml:"data,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
elementStartElement = xml.StartElement{Name: xml.Name{Local: "element"}}
|
||||
metaStartElement = xml.StartElement{Name: xml.Name{Local: "meta"}}
|
||||
ocsName = xml.Name{Local: "ocs"}
|
||||
dataName = xml.Name{Local: "data"}
|
||||
)
|
||||
|
||||
// MarshalXML handles ocs specific wrapping of array members in 'element' tags for the data
|
||||
func (p Payload) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
|
||||
// first the easy part
|
||||
// use ocs as the surrounding tag
|
||||
start.Name = ocsName
|
||||
if err = e.EncodeToken(start); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// encode the meta tag
|
||||
if err = e.EncodeElement(p.Meta, metaStartElement); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// we need to use reflection to determine if p.Data is an array or a slice
|
||||
rt := reflect.TypeOf(p.Data)
|
||||
if rt != nil && (rt.Kind() == reflect.Array || rt.Kind() == reflect.Slice) {
|
||||
// this is how to wrap the data elements in their own <element> tag
|
||||
v := reflect.ValueOf(p.Data)
|
||||
if err = e.EncodeToken(xml.StartElement{Name: dataName}); err != nil {
|
||||
return
|
||||
}
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
if err = e.EncodeElement(v.Index(i).Interface(), elementStartElement); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err = e.EncodeToken(xml.EndElement{Name: dataName}); err != nil {
|
||||
return
|
||||
}
|
||||
} else if err = e.EncodeElement(p.Data, xml.StartElement{Name: dataName}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// write the closing <ocs> tag
|
||||
if err = e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Meta holds response metadata
|
||||
type Meta struct {
|
||||
Status string `json:"status" xml:"status"`
|
||||
StatusCode int `json:"statuscode" xml:"statuscode"`
|
||||
Message string `json:"message" xml:"message"`
|
||||
TotalItems string `json:"totalitems,omitempty" xml:"totalitems,omitempty"`
|
||||
ItemsPerPage string `json:"itemsperpage,omitempty" xml:"itemsperpage,omitempty"`
|
||||
}
|
||||
|
||||
// MetaOK is the default ok response
|
||||
var MetaOK = Meta{Status: "ok", StatusCode: 100, Message: "OK"}
|
||||
|
||||
// MetaFailure is a failure response with code 101
|
||||
var MetaFailure = Meta{Status: "", StatusCode: 101, Message: "Failure"}
|
||||
|
||||
// MetaInvalidInput is an error response with code 102
|
||||
var MetaInvalidInput = Meta{Status: "", StatusCode: 102, Message: "Invalid Input"}
|
||||
|
||||
// MetaForbidden is an error response with code 104
|
||||
var MetaForbidden = Meta{Status: "", StatusCode: 104, Message: "Forbidden"}
|
||||
|
||||
// MetaBadRequest is used for unknown errors
|
||||
var MetaBadRequest = Meta{Status: "error", StatusCode: 400, Message: "Bad Request"}
|
||||
|
||||
// MetaPathNotFound is returned when trying to share not existing resources
|
||||
var MetaPathNotFound = Meta{Status: "failure", StatusCode: 404, Message: MessagePathNotFound}
|
||||
|
||||
// MetaLocked is returned when trying to share not existing resources
|
||||
var MetaLocked = Meta{Status: "failure", StatusCode: 423, Message: "The file is locked"}
|
||||
|
||||
// MetaServerError is returned on server errors
|
||||
var MetaServerError = Meta{Status: "error", StatusCode: 996, Message: "Server Error"}
|
||||
|
||||
// MetaUnauthorized is returned on unauthorized requests
|
||||
var MetaUnauthorized = Meta{Status: "error", StatusCode: 997, Message: "Unauthorised"}
|
||||
|
||||
// MetaNotFound is returned when trying to access not existing resources
|
||||
var MetaNotFound = Meta{Status: "error", StatusCode: 998, Message: "Not Found"}
|
||||
|
||||
// MetaUnknownError is used for unknown errors
|
||||
var MetaUnknownError = Meta{Status: "error", StatusCode: 999, Message: "Unknown Error"}
|
||||
|
||||
// MessageUserNotFound is used when a user can not be found
|
||||
var MessageUserNotFound = "The requested user could not be found"
|
||||
|
||||
// MessageGroupNotFound is used when a group can not be found
|
||||
var MessageGroupNotFound = "The requested group could not be found"
|
||||
|
||||
// MessagePathNotFound is used when a file or folder can not be found
|
||||
var MessagePathNotFound = "Wrong path, file/folder doesn't exist"
|
||||
|
||||
// MessageShareExists is used when a user tries to create a new share for the same user
|
||||
var MessageShareExists = "A share for the recipient already exists"
|
||||
|
||||
// MessageLockedForSharing is used when a user tries to create a new share until the file is in use by at least one user
|
||||
var MessageLockedForSharing = "The file is locked until the file is in use by at least one user"
|
||||
|
||||
// WriteOCSSuccess handles writing successful ocs response data
|
||||
func WriteOCSSuccess(w http.ResponseWriter, r *http.Request, d interface{}) {
|
||||
WriteOCSData(w, r, MetaOK, d, nil)
|
||||
}
|
||||
|
||||
// WriteOCSError handles writing error ocs responses
|
||||
func WriteOCSError(w http.ResponseWriter, r *http.Request, c int, m string, err error) {
|
||||
WriteOCSData(w, r, Meta{Status: "error", StatusCode: c, Message: m}, nil, err)
|
||||
}
|
||||
|
||||
// WriteOCSData handles writing ocs data in json and xml
|
||||
func WriteOCSData(w http.ResponseWriter, r *http.Request, m Meta, d interface{}, err error) {
|
||||
WriteOCSResponse(w, r, Response{
|
||||
OCS: &Payload{
|
||||
Meta: m,
|
||||
Data: d,
|
||||
},
|
||||
}, err)
|
||||
}
|
||||
|
||||
// WriteOCSResponse handles writing ocs responses in json and xml
|
||||
func WriteOCSResponse(w http.ResponseWriter, r *http.Request, res Response, err error) {
|
||||
if err != nil {
|
||||
appctx.GetLogger(r.Context()).
|
||||
Debug().
|
||||
Err(err).
|
||||
Str("ocs_msg", res.OCS.Meta.Message).
|
||||
Msg("writing ocs error response")
|
||||
}
|
||||
|
||||
version := APIVersion(r.Context())
|
||||
m := statusCodeMapper(version)
|
||||
statusCode := m(res.OCS.Meta)
|
||||
if version == "2" && statusCode == http.StatusOK {
|
||||
res.OCS.Meta.StatusCode = statusCode
|
||||
}
|
||||
|
||||
var encoder func(Response) ([]byte, error)
|
||||
if r.URL.Query().Get("format") == "json" {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
encoder = encodeJSON
|
||||
} else {
|
||||
w.Header().Set("Content-Type", "text/xml; charset=utf-8")
|
||||
encoder = encodeXML
|
||||
}
|
||||
w.WriteHeader(statusCode)
|
||||
encoded, err := encoder(res)
|
||||
if err != nil {
|
||||
appctx.GetLogger(r.Context()).Error().Err(err).Msg("error encoding ocs response")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = w.Write(encoded)
|
||||
if err != nil {
|
||||
appctx.GetLogger(r.Context()).Error().Err(err).Msg("error writing ocs response")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeXML(res Response) ([]byte, error) {
|
||||
marshalled, err := xml.Marshal(res.OCS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := new(bytes.Buffer)
|
||||
b.WriteString(xml.Header)
|
||||
b.Write(marshalled)
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func encodeJSON(res Response) ([]byte, error) {
|
||||
return json.Marshal(res)
|
||||
}
|
||||
|
||||
// OcsV1StatusCodes returns the http status codes for the OCS API v1.
|
||||
func OcsV1StatusCodes(meta Meta) int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
// OcsV2StatusCodes maps the OCS codes to http status codes for the ocs API v2.
|
||||
func OcsV2StatusCodes(meta Meta) int {
|
||||
sc := meta.StatusCode
|
||||
switch sc {
|
||||
case MetaNotFound.StatusCode:
|
||||
return http.StatusNotFound
|
||||
case MetaUnknownError.StatusCode:
|
||||
fallthrough
|
||||
case MetaServerError.StatusCode:
|
||||
return http.StatusInternalServerError
|
||||
case MetaUnauthorized.StatusCode:
|
||||
return http.StatusUnauthorized
|
||||
case MetaOK.StatusCode:
|
||||
meta.StatusCode = http.StatusOK
|
||||
return http.StatusOK
|
||||
case MetaForbidden.StatusCode:
|
||||
return http.StatusForbidden
|
||||
}
|
||||
// any 2xx, 4xx and 5xx will be used as is
|
||||
if sc >= 200 && sc < 600 {
|
||||
return sc
|
||||
}
|
||||
|
||||
// any error codes > 100 are treated as client errors
|
||||
if sc > 100 && sc < 200 {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
// TODO change this status code?
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
// WithAPIVersion puts the api version in the context.
|
||||
func VersionCtx(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
version := chi.URLParam(r, "version")
|
||||
if version == "" {
|
||||
WriteOCSError(w, r, MetaBadRequest.StatusCode, "unknown ocs api version", nil)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Ocs-Api-Version", version)
|
||||
|
||||
// store version in context so handlers can access it
|
||||
ctx := context.WithValue(r.Context(), apiVersionKey, version)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// APIVersion retrieves the api version from the context.
|
||||
func APIVersion(ctx context.Context) string {
|
||||
value := ctx.Value(apiVersionKey)
|
||||
if value != nil {
|
||||
return value.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func statusCodeMapper(version string) func(Meta) int {
|
||||
var mapper func(Meta) int
|
||||
switch version {
|
||||
case "1":
|
||||
mapper = OcsV1StatusCodes
|
||||
case "2":
|
||||
mapper = OcsV2StatusCodes
|
||||
default:
|
||||
mapper = defaultStatusCodeMapper
|
||||
}
|
||||
return mapper
|
||||
}
|
||||
Generated
Vendored
+218
@@ -0,0 +1,218 @@
|
||||
// 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 (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
preferences "github.com/cs3org/go-cs3apis/cs3/preferences/v1beta1"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"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/rs/zerolog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
global.Register("preferences", New)
|
||||
}
|
||||
|
||||
// Config holds the config options that for the preferences HTTP service
|
||||
type Config struct {
|
||||
Prefix string `mapstructure:"prefix"`
|
||||
GatewaySvc string `mapstructure:"gatewaysvc"`
|
||||
}
|
||||
|
||||
func (c *Config) init() {
|
||||
if c.Prefix == "" {
|
||||
c.Prefix = "preferences"
|
||||
}
|
||||
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(log); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *svc) routerInit(log *zerolog.Logger) error {
|
||||
s.router.Get("/", s.handleGet)
|
||||
s.router.Post("/", s.handlePost)
|
||||
|
||||
_ = chi.Walk(s.router, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
||||
log.Debug().Str("service", "preferences").Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint")
|
||||
return nil
|
||||
})
|
||||
|
||||
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{}
|
||||
}
|
||||
|
||||
func (s *svc) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
s.router.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *svc) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
log := appctx.GetLogger(ctx)
|
||||
|
||||
key := r.URL.Query().Get("key")
|
||||
ns := r.URL.Query().Get("ns")
|
||||
|
||||
if key == "" || ns == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
if _, err := w.Write([]byte("key or namespace query missing")); err != nil {
|
||||
log.Error().Err(err).Msg("error writing to response")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error getting grpc gateway client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := client.GetKey(ctx, &preferences.GetKeyRequest{
|
||||
Key: &preferences.PreferenceKey{
|
||||
Namespace: ns,
|
||||
Key: key,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error retrieving key")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if res.Status.Code != rpc.Code_CODE_OK {
|
||||
if res.Status.Code == rpc.Code_CODE_NOT_FOUND {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
log.Error().Interface("status", res.Status).Msg("error retrieving key")
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(map[string]interface{}{
|
||||
"namespace": ns,
|
||||
"key": key,
|
||||
"value": res.Val,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error marshalling response")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if _, err = w.Write(js); err != nil {
|
||||
log.Error().Err(err).Msg("error writing JSON response")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *svc) handlePost(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
log := appctx.GetLogger(ctx)
|
||||
|
||||
key := r.FormValue("key")
|
||||
ns := r.FormValue("ns")
|
||||
val := r.FormValue("value")
|
||||
|
||||
if key == "" || ns == "" || val == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
if _, err := w.Write([]byte("key, namespace or value parameter missing")); err != nil {
|
||||
log.Error().Err(err).Msg("error writing to response")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error getting grpc gateway client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := client.SetKey(ctx, &preferences.SetKeyRequest{
|
||||
Key: &preferences.PreferenceKey{
|
||||
Namespace: ns,
|
||||
Key: key,
|
||||
},
|
||||
Val: val,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error setting key")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if res.Status.Code != rpc.Code_CODE_OK {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Error().Interface("status", res.Status).Msg("error setting key")
|
||||
return
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+87
@@ -0,0 +1,87 @@
|
||||
// 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"
|
||||
|
||||
"contrib.go.opencensus.io/exporter/prometheus"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"go.opencensus.io/stats/view"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
|
||||
)
|
||||
|
||||
func init() {
|
||||
global.Register("prometheus", New)
|
||||
}
|
||||
|
||||
// New returns a new prometheus 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()
|
||||
|
||||
pe, err := prometheus.NewExporter(prometheus.Options{
|
||||
Namespace: "revad",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "prometheus: error creating exporter")
|
||||
}
|
||||
|
||||
view.RegisterExporter(pe)
|
||||
return &svc{prefix: conf.Prefix, h: pe}, nil
|
||||
}
|
||||
|
||||
type config struct {
|
||||
Prefix string `mapstructure:"prefix"`
|
||||
}
|
||||
|
||||
func (c *config) init() {
|
||||
if c.Prefix == "" {
|
||||
c.Prefix = "metrics"
|
||||
}
|
||||
}
|
||||
|
||||
type svc struct {
|
||||
prefix string
|
||||
h http.Handler
|
||||
}
|
||||
|
||||
func (s *svc) Prefix() string {
|
||||
return s.prefix
|
||||
}
|
||||
|
||||
func (s *svc) Handler() http.Handler {
|
||||
return s.h
|
||||
}
|
||||
|
||||
func (s *svc) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *svc) Unprotected() []string {
|
||||
// TODO(labkode): all prometheus endpoints are public?
|
||||
return []string{"/"}
|
||||
}
|
||||
+84
@@ -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 reqres
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
)
|
||||
|
||||
// APIErrorCode stores the type of error encountered.
|
||||
type APIErrorCode string
|
||||
|
||||
// The various types of errors that can be expected to occur.
|
||||
const (
|
||||
APIErrorNotFound APIErrorCode = "RESOURCE_NOT_FOUND"
|
||||
APIErrorUnauthenticated APIErrorCode = "UNAUTHENTICATED"
|
||||
APIErrorUntrustedService APIErrorCode = "UNTRUSTED_SERVICE"
|
||||
APIErrorUnimplemented APIErrorCode = "FUNCTION_NOT_IMPLEMENTED"
|
||||
APIErrorInvalidParameter APIErrorCode = "INVALID_PARAMETER"
|
||||
APIErrorProviderError APIErrorCode = "PROVIDER_ERROR"
|
||||
APIErrorAlreadyExist APIErrorCode = "ALREADY_EXIST"
|
||||
APIErrorServerError APIErrorCode = "SERVER_ERROR"
|
||||
)
|
||||
|
||||
// APIErrorCodeMapping stores the HTTP error code mapping for various APIErrorCodes.
|
||||
var APIErrorCodeMapping = map[APIErrorCode]int{
|
||||
APIErrorNotFound: http.StatusNotFound,
|
||||
APIErrorUnauthenticated: http.StatusUnauthorized,
|
||||
APIErrorUntrustedService: http.StatusForbidden,
|
||||
APIErrorUnimplemented: http.StatusNotImplemented,
|
||||
APIErrorInvalidParameter: http.StatusBadRequest,
|
||||
APIErrorProviderError: http.StatusBadGateway,
|
||||
APIErrorAlreadyExist: http.StatusConflict,
|
||||
APIErrorServerError: http.StatusInternalServerError,
|
||||
}
|
||||
|
||||
// APIError encompasses the error type and message.
|
||||
type APIError struct {
|
||||
Code APIErrorCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// WriteError handles writing error responses.
|
||||
func WriteError(w http.ResponseWriter, r *http.Request, code APIErrorCode, message string, e error) {
|
||||
if e != nil {
|
||||
appctx.GetLogger(r.Context()).Error().Err(e).Msg(message)
|
||||
}
|
||||
|
||||
var encoded []byte
|
||||
var err error
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
encoded, err = json.MarshalIndent(APIError{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(APIErrorCodeMapping[code])
|
||||
_, err = w.Write(encoded)
|
||||
if err != nil {
|
||||
appctx.GetLogger(r.Context()).Error().Err(err).Msg("error writing response")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+124
@@ -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 reverseproxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
global.Register("reverseproxy", New)
|
||||
}
|
||||
|
||||
type proxyRule struct {
|
||||
Endpoint string `mapstructure:"endpoint" json:"endpoint"`
|
||||
Backend string `mapstructure:"backend" json:"backend"`
|
||||
}
|
||||
|
||||
type config struct {
|
||||
ProxyRulesJSON string `mapstructure:"proxy_rules_json"`
|
||||
}
|
||||
|
||||
func (c *config) init() {
|
||||
if c.ProxyRulesJSON == "" {
|
||||
c.ProxyRulesJSON = "/etc/revad/proxy_rules.json"
|
||||
}
|
||||
}
|
||||
|
||||
type svc struct {
|
||||
router *chi.Mux
|
||||
}
|
||||
|
||||
// New returns an instance of the reverse proxy 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()
|
||||
|
||||
f, err := os.ReadFile(conf.ProxyRulesJSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rules []proxyRule
|
||||
err = json.Unmarshal(f, &rules)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
for _, rule := range rules {
|
||||
remote, err := url.Parse(rule.Backend)
|
||||
if err != nil {
|
||||
// Skip the rule if the backend is not a valid URL
|
||||
continue
|
||||
}
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(remote)
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Host = remote.Host
|
||||
if token, ok := ctxpkg.ContextGetToken(r.Context()); ok {
|
||||
r.Header.Set(ctxpkg.TokenHeader, token)
|
||||
}
|
||||
proxy.ServeHTTP(w, r)
|
||||
})
|
||||
r.Mount(rule.Endpoint, handler)
|
||||
}
|
||||
|
||||
_ = chi.Walk(r, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
||||
log.Debug().Str("service", "reverseproxy").Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint")
|
||||
return nil
|
||||
})
|
||||
|
||||
return &svc{router: r}, nil
|
||||
}
|
||||
|
||||
func (s *svc) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *svc) Prefix() string {
|
||||
// This service will be served at root
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *svc) Unprotected() []string {
|
||||
// TODO: If the services which will be served via the reverse proxy have unprotected endpoints,
|
||||
// we won't be able to support those at the moment.
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (s *svc) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
s.router.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+143
@@ -0,0 +1,143 @@
|
||||
// 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 sciencemesh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
ocmpb "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/reqres"
|
||||
"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/router"
|
||||
)
|
||||
|
||||
type appsHandler struct {
|
||||
gatewaySelector *pool.Selector[gateway.GatewayAPIClient]
|
||||
ocmMountPoint string
|
||||
}
|
||||
|
||||
func (h *appsHandler) init(c *config) error {
|
||||
var err error
|
||||
h.gatewaySelector, err = pool.GatewaySelector(c.GatewaySvc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.ocmMountPoint = c.OCMMountPoint
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *appsHandler) shareInfo(p string) (*ocmpb.ShareId, string) {
|
||||
p = strings.TrimPrefix(p, h.ocmMountPoint)
|
||||
shareID, rel := router.ShiftPath(p)
|
||||
if len(rel) > 0 {
|
||||
rel = rel[1:]
|
||||
}
|
||||
return &ocmpb.ShareId{OpaqueId: shareID}, rel
|
||||
}
|
||||
|
||||
func (h *appsHandler) OpenInApp(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "parameters could not be parsed", nil)
|
||||
return
|
||||
}
|
||||
|
||||
path := r.Form.Get("file")
|
||||
if path == "" {
|
||||
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "missing file", nil)
|
||||
return
|
||||
}
|
||||
|
||||
shareID, rel := h.shareInfo(path)
|
||||
|
||||
template, err := h.webappTemplate(ctx, shareID)
|
||||
if err != nil {
|
||||
var e errtypes.NotFound
|
||||
if errors.As(err, &e) {
|
||||
reqres.WriteError(w, r, reqres.APIErrorNotFound, e.Error(), nil)
|
||||
}
|
||||
reqres.WriteError(w, r, reqres.APIErrorServerError, err.Error(), err)
|
||||
return
|
||||
}
|
||||
|
||||
url := resolveTemplate(template, rel)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(map[string]any{
|
||||
"app_url": url,
|
||||
}); err != nil {
|
||||
reqres.WriteError(w, r, reqres.APIErrorServerError, "error marshalling JSON response", err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *appsHandler) webappTemplate(ctx context.Context, id *ocmpb.ShareId) (string, error) {
|
||||
gc, err := h.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res, err := gc.GetReceivedOCMShare(ctx, &ocmpb.GetReceivedOCMShareRequest{
|
||||
Ref: &ocmpb.ShareReference{
|
||||
Spec: &ocmpb.ShareReference_Id{
|
||||
Id: id,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if res.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
if res.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND {
|
||||
return "", errtypes.NotFound(res.Status.Message)
|
||||
}
|
||||
return "", errtypes.InternalError(res.Status.Message)
|
||||
}
|
||||
|
||||
webapp, ok := getWebappProtocol(res.Share.Protocols)
|
||||
if !ok {
|
||||
return "", errtypes.BadRequest("share does not contain webapp protocol")
|
||||
}
|
||||
|
||||
return webapp.UriTemplate, nil
|
||||
}
|
||||
|
||||
func getWebappProtocol(protocols []*ocmpb.Protocol) (*ocmpb.WebappProtocol, bool) {
|
||||
for _, p := range protocols {
|
||||
if t, ok := p.Term.(*ocmpb.Protocol_WebappOptions); ok {
|
||||
return t.WebappOptions, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func resolveTemplate(template string, rel string) string {
|
||||
// the template is of type "https://open-cloud-mesh.org/s/share-hash/{relative-path-to-shared-resource}"
|
||||
return strings.Replace(template, "{relative-path-to-shared-resource}", rel, 1)
|
||||
}
|
||||
Generated
Vendored
+92
@@ -0,0 +1,92 @@
|
||||
// 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 sciencemesh
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
providerpb "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/reqres"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
)
|
||||
|
||||
type providersHandler struct {
|
||||
gatewaySelector *pool.Selector[gateway.GatewayAPIClient]
|
||||
}
|
||||
|
||||
func (h *providersHandler) init(c *config) error {
|
||||
var err error
|
||||
h.gatewaySelector, err = pool.GatewaySelector(c.GatewaySvc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type provider struct {
|
||||
FullName string `json:"full_name"`
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
// ListProviders lists all the providers filtering by the `search` query parameter.
|
||||
func (h *providersHandler) ListProviders(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
term := strings.ToLower(r.URL.Query().Get("search"))
|
||||
|
||||
gc, err := h.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
reqres.WriteError(w, r, reqres.APIErrorServerError, "error selecting gateway client", err)
|
||||
return
|
||||
}
|
||||
listRes, err := gc.ListAllProviders(ctx, &providerpb.ListAllProvidersRequest{})
|
||||
if err != nil {
|
||||
reqres.WriteError(w, r, reqres.APIErrorServerError, "error listing all providers", err)
|
||||
return
|
||||
}
|
||||
|
||||
if listRes.Status.Code != rpc.Code_CODE_OK {
|
||||
reqres.WriteError(w, r, reqres.APIErrorServerError, listRes.Status.Message, errors.New(listRes.Status.Message))
|
||||
return
|
||||
}
|
||||
|
||||
filtered := []*provider{}
|
||||
for _, p := range listRes.Providers {
|
||||
if strings.Contains(strings.ToLower(p.FullName), term) ||
|
||||
strings.Contains(strings.ToLower(p.Domain), term) {
|
||||
filtered = append(filtered, &provider{
|
||||
FullName: p.FullName,
|
||||
Domain: p.Domain,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(filtered); err != nil {
|
||||
reqres.WriteError(w, r, reqres.APIErrorServerError, "error encoding response in json", err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
Generated
Vendored
+145
@@ -0,0 +1,145 @@
|
||||
// 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 sciencemesh
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils/cfg"
|
||||
)
|
||||
|
||||
func init() {
|
||||
global.Register("sciencemesh", New)
|
||||
}
|
||||
|
||||
// New returns a new sciencemesh service.
|
||||
func New(m map[string]interface{}, _ *zerolog.Logger) (global.Service, error) {
|
||||
var c config
|
||||
if err := cfg.Decode(m, &c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
s := &svc{
|
||||
conf: &c,
|
||||
router: r,
|
||||
}
|
||||
|
||||
if err := s.routerInit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Close performs cleanup.
|
||||
func (s *svc) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type config struct {
|
||||
Prefix string `mapstructure:"prefix"`
|
||||
GatewaySvc string `mapstructure:"gatewaysvc" validate:"required"`
|
||||
ProviderDomain string `mapstructure:"provider_domain" validate:"required"`
|
||||
MeshDirectoryURL string `mapstructure:"mesh_directory_url"`
|
||||
OCMMountPoint string `mapstructure:"ocm_mount_point"`
|
||||
Events EventOptions `mapstructure:"events"`
|
||||
}
|
||||
|
||||
// EventOptions are the configurable options for events
|
||||
type EventOptions struct {
|
||||
Endpoint string `mapstructure:"natsaddress"`
|
||||
Cluster string `mapstructure:"natsclusterid"`
|
||||
TLSInsecure bool `mapstructure:"tlsinsecure"`
|
||||
TLSRootCACertificate string `mapstructure:"tlsrootcacertificate"`
|
||||
EnableTLS bool `mapstructure:"enabletls"`
|
||||
AuthUsername string `mapstructure:"authusername"`
|
||||
AuthPassword string `mapstructure:"authpassword"`
|
||||
}
|
||||
|
||||
func (c *config) ApplyDefaults() {
|
||||
if c.Prefix == "" {
|
||||
c.Prefix = "sciencemesh"
|
||||
}
|
||||
if c.OCMMountPoint == "" {
|
||||
c.OCMMountPoint = "/ocm"
|
||||
}
|
||||
|
||||
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
|
||||
}
|
||||
|
||||
type svc struct {
|
||||
conf *config
|
||||
router chi.Router
|
||||
}
|
||||
|
||||
func (s *svc) routerInit() error {
|
||||
tokenHandler := new(tokenHandler)
|
||||
if err := tokenHandler.init(s.conf); err != nil {
|
||||
return err
|
||||
}
|
||||
providersHandler := new(providersHandler)
|
||||
if err := providersHandler.init(s.conf); err != nil {
|
||||
return err
|
||||
}
|
||||
sharesHandler := new(sharesHandler)
|
||||
if err := sharesHandler.init(s.conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
appsHandler := new(appsHandler)
|
||||
if err := appsHandler.init(s.conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.router.Post("/generate-invite", tokenHandler.Generate)
|
||||
s.router.Get("/list-invite", tokenHandler.ListInvite)
|
||||
s.router.Post("/accept-invite", tokenHandler.AcceptInvite)
|
||||
s.router.Get("/find-accepted-users", tokenHandler.FindAccepted)
|
||||
s.router.Delete("/delete-accepted-user", tokenHandler.DeleteAccepted)
|
||||
s.router.Get("/list-providers", providersHandler.ListProviders)
|
||||
s.router.Post("/create-share", sharesHandler.CreateShare)
|
||||
s.router.Post("/open-in-app", appsHandler.OpenInApp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *svc) Prefix() string {
|
||||
return s.conf.Prefix
|
||||
}
|
||||
|
||||
func (s *svc) Unprotected() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *svc) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log := appctx.GetLogger(r.Context())
|
||||
log.Debug().Str("path", r.URL.Path).Msg("sciencemesh routing")
|
||||
|
||||
// unset raw path, otherwise chi uses it to route and then fails to match percent encoded path segments
|
||||
r.URL.RawPath = ""
|
||||
s.router.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user