Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
// 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 app
import (
"context"
appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
registry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
)
// Registry is the interface that application registries implement
// for discovering application providers
type Registry interface {
FindProviders(ctx context.Context, mimeType string) ([]*registry.ProviderInfo, error)
ListProviders(ctx context.Context) ([]*registry.ProviderInfo, error)
ListSupportedMimeTypes(ctx context.Context) ([]*registry.MimeTypeInfo, error)
AddProvider(ctx context.Context, p *registry.ProviderInfo) error
GetDefaultProviderForMimeType(ctx context.Context, mimeType string) (*registry.ProviderInfo, error)
SetDefaultProviderForMimeType(ctx context.Context, mimeType string, p *registry.ProviderInfo) error
}
// Provider is the interface that application providers implement
// for interacting with external apps that serve the requested resource.
type Provider interface {
GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.ViewMode, token, language string) (*appprovider.OpenInAppURL, error)
GetAppProviderInfo(ctx context.Context) (*registry.ProviderInfo, error)
}
@@ -0,0 +1,76 @@
// 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 demo
import (
"context"
"fmt"
appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/app"
"github.com/opencloud-eu/reva/v2/pkg/app/provider/registry"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
)
func init() {
registry.Register("demo", New)
}
type demoProvider struct {
iframeUIProvider string
}
func (p *demoProvider) GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.ViewMode, token, language string) (*appprovider.OpenInAppURL, error) {
url := fmt.Sprintf("<iframe src=%s/open/%s?view-mode=%s&access-token=%s />", p.iframeUIProvider, storagespace.FormatResourceID(resource.Id), viewMode.String(), token)
return &appprovider.OpenInAppURL{
AppUrl: url,
Method: "GET",
}, nil
}
func (p *demoProvider) GetAppProviderInfo(ctx context.Context) (*appregistry.ProviderInfo, error) {
return &appregistry.ProviderInfo{
Name: "demo-app",
}, nil
}
type config struct {
IFrameUIProvider string `mapstructure:"iframe_ui_provider"`
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
return c, nil
}
// New returns an implementation to of the app.Provider interface that
// connects to an application in the backend.
func New(m map[string]interface{}) (app.Provider, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
return &demoProvider{iframeUIProvider: c.IFrameUIProvider}, nil
}
@@ -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 application providers.
_ "github.com/opencloud-eu/reva/v2/pkg/app/provider/demo"
_ "github.com/opencloud-eu/reva/v2/pkg/app/provider/wopi"
// Add your own here
)
@@ -0,0 +1,34 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package registry
import "github.com/opencloud-eu/reva/v2/pkg/app"
// NewFunc is the function that app provider implementations
// should register to at init time.
type NewFunc func(map[string]interface{}) (app.Provider, error)
// NewFuncs is a map containing all the registered app providers.
var NewFuncs = map[string]NewFunc{}
// Register registers a new app provider new function.
// Not safe for concurrent use. Safe for use from package init.
func Register(name string, f NewFunc) {
NewFuncs[name] = f
}
@@ -0,0 +1,492 @@
// 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 wopi
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/beevik/etree"
appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/golang-jwt/jwt/v5"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/app"
"github.com/opencloud-eu/reva/v2/pkg/app/provider/registry"
"github.com/opencloud-eu/reva/v2/pkg/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/mime"
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/templates"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/pkg/errors"
)
func init() {
registry.Register("wopi", New)
}
type config struct {
IOPSecret string `mapstructure:"iop_secret" docs:";The IOP secret used to connect to the wopiserver."`
WopiURL string `mapstructure:"wopi_url" docs:";The wopiserver's URL."`
WopiFolderURLBaseURL string `mapstructure:"wopi_folder_url_base_url" docs:";The base URL to generate links to navigate back to the containing folder."`
WopiFolderURLPathTemplate string `mapstructure:"wopi_folder_url_path_template" docs:";The template to generate the folderurl path segments."`
AppName string `mapstructure:"app_name" docs:";The App user-friendly name."`
AppIconURI string `mapstructure:"app_icon_uri" docs:";A URI to a static asset which represents the app icon."`
AppURL string `mapstructure:"app_url" docs:";The App URL."`
AppIntURL string `mapstructure:"app_int_url" docs:";The internal app URL in case of dockerized deployments. Defaults to AppURL"`
AppAPIKey string `mapstructure:"app_api_key" docs:";The API key used by the app, if applicable."`
JWTSecret string `mapstructure:"jwt_secret" docs:";The JWT secret to be used to retrieve the token TTL."`
AppDesktopOnly bool `mapstructure:"app_desktop_only" docs:"false;Specifies if the app can be opened only on desktop."`
InsecureConnections bool `mapstructure:"insecure_connections"`
AppDisableChat bool `mapstructure:"app_disable_chat"`
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
return c, nil
}
type wopiProvider struct {
conf *config
wopiClient *http.Client
appURLs map[string]map[string]string // map[viewMode]map[extension]appURL
}
// New returns an implementation of the app.Provider interface that
// connects to an application in the backend.
func New(m map[string]interface{}) (app.Provider, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
if c.AppIntURL == "" {
c.AppIntURL = c.AppURL
}
if c.IOPSecret == "" {
c.IOPSecret = os.Getenv("REVA_APPPROVIDER_IOPSECRET")
}
c.JWTSecret = sharedconf.GetJWTSecret(c.JWTSecret)
appURLs, err := getAppURLs(c)
if err != nil {
return nil, err
}
wopiClient := rhttp.GetHTTPClient(
rhttp.Timeout(time.Duration(5*int64(time.Second))),
rhttp.Insecure(c.InsecureConnections),
)
wopiClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
return &wopiProvider{
conf: c,
wopiClient: wopiClient,
appURLs: appURLs,
}, nil
}
func (p *wopiProvider) GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.ViewMode, token, language string) (*appprovider.OpenInAppURL, error) {
log := appctx.GetLogger(ctx)
ext := path.Ext(resource.Path)
wopiurl, err := url.Parse(p.conf.WopiURL)
if err != nil {
return nil, err
}
wopiurl.Path = path.Join(wopiurl.Path, "/wopi/iop/openinapp")
httpReq, err := rhttp.NewRequest(ctx, "GET", wopiurl.String(), nil)
if err != nil {
return nil, err
}
q := httpReq.URL.Query()
q.Add("endpoint", storagespace.FormatStorageID(resource.GetId().GetStorageId(), resource.GetId().GetSpaceId()))
q.Add("fileid", resource.GetId().OpaqueId)
q.Add("viewmode", viewMode.String())
folderURLPath := templates.WithResourceInfo(resource, p.conf.WopiFolderURLPathTemplate)
folderURLBaseURL, err := url.Parse(p.conf.WopiFolderURLBaseURL)
if err != nil {
return nil, err
}
if folderURLPath != "" {
folderURLBaseURL.Path = path.Join(folderURLBaseURL.Path, folderURLPath)
q.Add("folderurl", folderURLBaseURL.String())
}
u, ok := ctxpkg.ContextGetUser(ctx)
if ok { // else defaults to "Guest xyz"
if u.Id.Type == userpb.UserType_USER_TYPE_LIGHTWEIGHT || u.Id.Type == userpb.UserType_USER_TYPE_FEDERATED {
q.Add("userid", resource.Owner.OpaqueId+"@"+resource.Owner.Idp)
} else {
q.Add("userid", u.Id.OpaqueId+"@"+u.Id.Idp)
}
var isPublicShare bool
if u.Opaque != nil {
if _, ok := u.Opaque.Map["public-share-role"]; ok {
isPublicShare = true
}
}
if !isPublicShare {
q.Add("username", u.DisplayName)
}
}
q.Add("appname", p.conf.AppName)
var viewAppURL string
if viewAppURLs, ok := p.appURLs["view"]; ok {
if viewAppURL, ok = viewAppURLs[ext]; ok {
q.Add("appviewurl", viewAppURL)
}
}
access := "edit"
if resource.GetSize() == 0 {
if _, ok := p.appURLs["editnew"]; ok {
access = "editnew"
}
}
if editAppURLs, ok := p.appURLs[access]; ok {
if editAppURL, ok := editAppURLs[ext]; ok {
q.Add("appurl", editAppURL)
}
}
if q.Get("appurl") == "" {
// assuming that a view action is always available in the /hosting/discovery manifest
// eg. Collabora does support viewing jpgs but no editing
// eg. OnlyOffice does support viewing pdfs but no editing
// there is no known case of supporting edit only without view
q.Add("appurl", viewAppURL)
}
if q.Get("appurl") == "" && q.Get("appviewurl") == "" {
return nil, errors.New("wopi: neither edit nor view app url found")
}
if p.conf.AppIntURL != "" {
q.Add("appinturl", p.conf.AppIntURL)
}
httpReq.URL.RawQuery = q.Encode()
if p.conf.AppAPIKey != "" {
httpReq.Header.Set("ApiKey", p.conf.AppAPIKey)
}
httpReq.Header.Set("Authorization", "Bearer "+p.conf.IOPSecret)
httpReq.Header.Set("TokenHeader", token)
// Call the WOPI server and parse the response (body will always contain a payload)
openRes, err := p.wopiClient.Do(httpReq)
if err != nil {
return nil, errors.Wrap(err, "wopi: error performing open request to WOPI server")
}
defer openRes.Body.Close()
body, err := io.ReadAll(openRes.Body)
if err != nil {
return nil, err
}
if openRes.StatusCode != http.StatusOK {
// WOPI returned failure: body contains a user-friendly error message (yet perform a sanity check)
sbody := ""
if body != nil {
sbody = string(body)
}
log.Warn().Msg(fmt.Sprintf("wopi: WOPI server returned HTTP %s to request %s, error was: %s", openRes.Status, httpReq.URL.String(), sbody))
return nil, errors.New(sbody)
}
var result map[string]interface{}
err = json.Unmarshal(body, &result)
if err != nil {
return nil, err
}
tokenTTL, err := p.getAccessTokenTTL(ctx)
if err != nil {
return nil, err
}
url, err := url.Parse(result["app-url"].(string))
if err != nil {
return nil, err
}
urlQuery := url.Query()
if language != "" {
urlQuery.Set("ui", language) // OnlyOffice
urlQuery.Set("lang", covertLangTag(language)) // Collabora, Impact on the default document language of OnlyOffice
urlQuery.Set("UI_LLCC", language) // Office365
}
if p.conf.AppDisableChat {
urlQuery.Set("dchat", "1") // OnlyOffice disable chat
}
url.RawQuery = urlQuery.Encode()
appFullURL := url.String()
// Depending on whether wopi server returned any form parameters or not,
// we decide whether the request method is POST or GET
var formParams map[string]string
method := "GET"
if form, ok := result["form-parameters"].(map[string]interface{}); ok {
if tkn, ok := form["access_token"].(string); ok {
formParams = map[string]string{
"access_token": tkn,
"access_token_ttl": tokenTTL,
}
method = "POST"
}
}
log.Info().Msg(fmt.Sprintf("wopi: returning app URL %s", appFullURL))
return &appprovider.OpenInAppURL{
AppUrl: appFullURL,
Method: method,
FormParameters: formParams,
}, nil
}
func (p *wopiProvider) GetAppProviderInfo(ctx context.Context) (*appregistry.ProviderInfo, error) {
// Initially we store the mime types in a map to avoid duplicates
mimeTypesMap := make(map[string]bool)
for _, extensions := range p.appURLs {
for ext := range extensions {
m := mime.Detect(false, ext)
mimeTypesMap[m] = true
}
}
mimeTypes := make([]string, 0, len(mimeTypesMap))
for m := range mimeTypesMap {
mimeTypes = append(mimeTypes, m)
}
return &appregistry.ProviderInfo{
Name: p.conf.AppName,
Icon: p.conf.AppIconURI,
DesktopOnly: p.conf.AppDesktopOnly,
MimeTypes: mimeTypes,
}, nil
}
func getAppURLs(c *config) (map[string]map[string]string, error) {
// Initialize WOPI URLs by discovery
httpcl := rhttp.GetHTTPClient(
rhttp.Timeout(time.Duration(5*int64(time.Second))),
rhttp.Insecure(c.InsecureConnections),
)
appurl, err := url.Parse(c.AppIntURL)
if err != nil {
return nil, err
}
appurl.Path = path.Join(appurl.Path, "/hosting/discovery")
discReq, err := http.NewRequest("GET", appurl.String(), nil)
if err != nil {
return nil, err
}
discRes, err := httpcl.Do(discReq)
if err != nil {
return nil, err
}
defer discRes.Body.Close()
var appURLs map[string]map[string]string
switch discRes.StatusCode {
case http.StatusOK:
appURLs, err = parseWopiDiscovery(discRes.Body)
if err != nil {
return nil, errors.Wrap(err, "error parsing wopi discovery response")
}
case http.StatusNotFound:
// this may be a bridge-supported app
discReq, err = http.NewRequest("GET", c.AppIntURL, nil)
if err != nil {
return nil, err
}
discRes, err = httpcl.Do(discReq)
if err != nil {
return nil, err
}
defer discRes.Body.Close()
buf := new(bytes.Buffer)
_, err = buf.ReadFrom(discRes.Body)
if err != nil {
return nil, err
}
// scrape app's home page to find the appname
if !strings.Contains(buf.String(), c.AppName) {
return nil, errors.New("Application server at " + c.AppURL + " does not match this AppProvider for " + c.AppName)
}
// register the supported mimetypes in the AppRegistry: this is hardcoded for the time being
// TODO(lopresti) move to config
switch c.AppName {
case "CodiMD":
appURLs = getCodimdExtensions(c.AppURL)
case "Etherpad":
appURLs = getEtherpadExtensions(c.AppURL)
default:
return nil, errors.New("Application server " + c.AppName + " running at " + c.AppURL + " is unsupported")
}
}
return appURLs, nil
}
func (p *wopiProvider) getAccessTokenTTL(ctx context.Context) (string, error) {
tkn := ctxpkg.ContextMustGetToken(ctx)
token, err := jwt.ParseWithClaims(tkn, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(p.conf.JWTSecret), nil
})
if err != nil {
return "", err
}
if claims, ok := token.Claims.(*jwt.RegisteredClaims); ok && token.Valid {
// milliseconds since Jan 1, 1970 UTC as required in https://wopi.readthedocs.io/projects/wopirest/en/latest/concepts.html?highlight=access_token_ttl#term-access-token-ttl
return strconv.FormatInt(claims.ExpiresAt.Unix()*1000, 10), nil
}
return "", errtypes.InvalidCredentials("wopi: invalid token present in ctx")
}
func parseWopiDiscovery(body io.Reader) (map[string]map[string]string, error) {
appURLs := make(map[string]map[string]string)
doc := etree.NewDocument()
if _, err := doc.ReadFrom(body); err != nil {
return nil, err
}
root := doc.SelectElement("wopi-discovery")
if root == nil {
return nil, errors.New("wopi-discovery response malformed")
}
for _, netzone := range root.SelectElements("net-zone") {
if strings.Contains(netzone.SelectAttrValue("name", ""), "external") {
for _, app := range netzone.SelectElements("app") {
for _, action := range app.SelectElements("action") {
access := action.SelectAttrValue("name", "")
if access == "view" || access == "edit" || access == "editnew" {
ext := action.SelectAttrValue("ext", "")
urlString := action.SelectAttrValue("urlsrc", "")
if ext == "" || urlString == "" {
continue
}
u, err := url.Parse(urlString)
if err != nil {
// it sucks we cannot log here because this function is run
// on init without any context.
// TODO(labkode): add logging when we'll have static logging in boot phase.
continue
}
// remove any malformed query parameter from discovery urls
q := u.Query()
for k := range q {
if strings.Contains(k, "<") || strings.Contains(k, ">") {
q.Del(k)
}
}
u.RawQuery = q.Encode()
if _, ok := appURLs[access]; !ok {
appURLs[access] = make(map[string]string)
}
appURLs[access]["."+ext] = u.String()
}
}
}
}
}
return appURLs, nil
}
func getCodimdExtensions(appURL string) map[string]map[string]string {
// Register custom mime types
mime.RegisterMime(".zmd", "application/compressed-markdown")
appURLs := make(map[string]map[string]string)
appURLs["edit"] = map[string]string{
".txt": appURL,
".md": appURL,
".zmd": appURL,
}
return appURLs
}
func getEtherpadExtensions(appURL string) map[string]map[string]string {
appURLs := make(map[string]map[string]string)
appURLs["edit"] = map[string]string{
".epd": appURL,
}
return appURLs
}
// TODO Find better solution
// This conversion was made because no other way to set the default document language to OnlyOffice was found.
func covertLangTag(lang string) string {
switch lang {
case "cs":
return "cs-CZ"
case "de":
return "de-DE"
case "es":
return "es-ES"
case "fr":
return "fr-FR"
case "it":
return "it-IT"
default:
return "en"
}
}
@@ -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 app registry drivers.
_ "github.com/opencloud-eu/reva/v2/pkg/app/registry/micro"
_ "github.com/opencloud-eu/reva/v2/pkg/app/registry/static"
// Add your own here
)
@@ -0,0 +1,40 @@
// 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 micro
import "github.com/mitchellh/mapstructure"
type config struct {
Namespace string `mapstructure:"namespace"`
MimeTypes []*mimeTypeConfig `mapstructure:"mime_types"`
}
func (c *config) init() {
if c.Namespace == "" {
c.Namespace = "cs3"
}
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
return c, nil
}
@@ -0,0 +1,265 @@
// 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 micro
import (
"context"
"sort"
"strconv"
"sync"
"time"
registrypb "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
oreg "github.com/qsfera/server/pkg/registry"
"github.com/opencloud-eu/reva/v2/pkg/app"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/rs/zerolog/log"
mreg "go-micro.dev/v4/registry"
)
type manager struct {
namespace string
sync.RWMutex
cancelFunc context.CancelFunc
mimeTypes map[string][]*registrypb.ProviderInfo
providers []*registrypb.ProviderInfo
config *config
}
// New returns an implementation of the app.Registry interface.
func New(m map[string]interface{}) (app.Registry, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
ctx, cancelFunc := context.WithCancel(context.Background())
newManager := manager{
namespace: c.Namespace,
cancelFunc: cancelFunc,
config: c,
}
err = newManager.updateProvidersFromMicroRegistry()
if err != nil {
if _, ok := err.(errtypes.NotFound); !ok {
return nil, err
}
}
t := time.NewTicker(time.Second * 30)
go func() {
for {
select {
case <-t.C:
log.Debug().Msg("app provider tick, updating local app list")
err = newManager.updateProvidersFromMicroRegistry()
if err != nil {
log.Error().Err(err).Msg("could not update the local provider cache")
continue
}
case <-ctx.Done():
log.Debug().Msg("app provider stopped")
t.Stop()
}
}
}()
return &newManager, nil
}
// AddProvider does not do anything for this registry, it is a placeholder to satisfy the interface
func (m *manager) AddProvider(ctx context.Context, p *registrypb.ProviderInfo) error {
log := appctx.GetLogger(ctx)
log.Info().Interface("provider", p).Msg("Tried to register through cs3 api, make sure the provider registers directly through go-micro")
return nil
}
// FindProvider returns all providers that can provide an app for the given mimeType
func (m *manager) FindProviders(ctx context.Context, mimeType string) ([]*registrypb.ProviderInfo, error) {
m.RLock()
defer m.RUnlock()
if len(m.mimeTypes[mimeType]) < 1 {
return nil, mreg.ErrNotFound
}
return m.mimeTypes[mimeType], nil
}
// GetDefaultProviderForMimeType returns the default provider for the given mimeType
func (m *manager) GetDefaultProviderForMimeType(ctx context.Context, mimeType string) (*registrypb.ProviderInfo, error) {
m.RLock()
defer m.RUnlock()
for _, mt := range m.config.MimeTypes {
if mt.MimeType != mimeType {
continue
}
for _, p := range m.mimeTypes[mimeType] {
if p.Name == mt.DefaultApp {
return p, nil
}
}
}
return nil, mreg.ErrNotFound
}
// ListProviders lists all registered Providers
func (m *manager) ListProviders(ctx context.Context) ([]*registrypb.ProviderInfo, error) {
return m.providers, nil
}
// ListSupportedMimeTypes lists all supported mimeTypes
func (m *manager) ListSupportedMimeTypes(ctx context.Context) ([]*registrypb.MimeTypeInfo, error) {
m.RLock()
defer m.RUnlock()
res := make([]*registrypb.MimeTypeInfo, 0, len(m.config.MimeTypes))
for _, mime := range m.config.MimeTypes {
res = append(res, &registrypb.MimeTypeInfo{
MimeType: mime.MimeType,
Ext: mime.Extension,
Name: mime.Name,
Description: mime.Description,
Icon: mime.Icon,
AppProviders: m.mimeTypes[mime.MimeType],
AllowCreation: mime.AllowCreation,
DefaultApplication: mime.DefaultApp,
})
}
return res, nil
}
// SetDefaultProviderForMimeType sets the default provider for the given mimeType
func (m *manager) SetDefaultProviderForMimeType(ctx context.Context, mimeType string, p *registrypb.ProviderInfo) error {
m.Lock()
defer m.Unlock()
// NOTE: this is a dirty workaround:
for _, mt := range m.config.MimeTypes {
if mt.MimeType == mimeType {
mt.DefaultApp = p.Name
return nil
}
}
log.Info().Msgf("default provider for app is not set through the provider, but defined for the app")
return mreg.ErrNotFound
}
func (m *manager) getProvidersFromMicroRegistry(ctx context.Context) ([]*registrypb.ProviderInfo, error) {
reg := oreg.GetRegistry()
services, err := reg.GetService(m.namespace+".api.app-provider", mreg.GetContext(ctx))
if err != nil {
log.Warn().Err(err).Msg("getProvidersFromMicroRegistry")
}
if len(services) == 0 {
return nil, errtypes.NotFound("no application provider service registered")
}
if len(services) > 1 {
return nil, errtypes.InternalError("more than one application provider services registered")
}
providers := make([]*registrypb.ProviderInfo, 0, len(services[0].Nodes))
for _, node := range services[0].Nodes {
p := m.providerFromMetadata(node.Metadata)
p.Address = node.Address
providers = append(providers, p)
}
return providers, nil
}
func (m *manager) providerFromMetadata(metadata map[string]string) *registrypb.ProviderInfo {
p := &registrypb.ProviderInfo{
MimeTypes: splitMimeTypes(metadata[m.namespace+".app-provider.mime_type"]),
// Address: node.Address,
Name: metadata[m.namespace+".app-provider.name"],
Description: metadata[m.namespace+".app-provider.description"],
Icon: metadata[m.namespace+".app-provider.icon"],
DesktopOnly: metadata[m.namespace+".app-provider.desktop_only"] == "true",
Capability: registrypb.ProviderInfo_Capability(registrypb.ProviderInfo_Capability_value[metadata[m.namespace+".app-provider.capability"]]),
}
if metadata[m.namespace+".app-provider.priority"] != "" {
p.Opaque = &typesv1beta1.Opaque{Map: map[string]*typesv1beta1.OpaqueEntry{
"priority": {
Decoder: "plain",
Value: []byte(metadata[m.namespace+".app-provider.priority"]),
},
}}
}
return p
}
func (m *manager) updateProvidersFromMicroRegistry() error {
lst, err := m.getProvidersFromMicroRegistry(context.Background())
ma := map[string][]*registrypb.ProviderInfo{}
if err != nil {
return err
}
sortByPriority(lst)
for _, outer := range lst {
for _, inner := range outer.MimeTypes {
ma[inner] = append(ma[inner], outer)
}
}
m.Lock()
defer m.Unlock()
m.mimeTypes = ma
m.providers = lst
return nil
}
func equalsProviderInfo(p1, p2 *registrypb.ProviderInfo) bool {
sameName := p1.Name == p2.Name
sameAddress := p1.Address == p2.Address
if sameName && sameAddress {
return true
}
return false
}
func getPriority(p *registrypb.ProviderInfo) string {
if p.Opaque != nil && len(p.Opaque.Map) != 0 {
if priority, ok := p.Opaque.Map["priority"]; ok {
return string(priority.GetValue())
}
}
return defaultPriority
}
func sortByPriority(providers []*registrypb.ProviderInfo) {
less := func(i, j int) bool {
prioI, _ := strconv.ParseInt(getPriority(providers[i]), 10, 64)
prioJ, _ := strconv.ParseInt(getPriority(providers[j]), 10, 64)
return prioI < prioJ
}
sort.Slice(providers, less)
}
@@ -0,0 +1,50 @@
// 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 micro
import (
"strings"
"github.com/opencloud-eu/reva/v2/pkg/app/registry/registry"
)
const defaultPriority = "0"
func init() {
registry.Register("micro", New)
}
type mimeTypeConfig struct {
MimeType string `mapstructure:"mime_type"`
Extension string `mapstructure:"extension"`
Name string `mapstructure:"name"`
Description string `mapstructure:"description"`
Icon string `mapstructure:"icon"`
DefaultApp string `mapstructure:"default_app"`
AllowCreation bool `mapstructure:"allow_creation"`
}
// use the UTF-8 record separator
func splitMimeTypes(s string) []string {
return strings.Split(s, "␞")
}
func joinMimeTypes(mimetypes []string) string {
return strings.Join(mimetypes, "␞")
}
@@ -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/app"
// NewFunc is the function that app provider implementations
// should register to at init time.
type NewFunc func(map[string]interface{}) (app.Registry, error)
// NewFuncs is a map containing all the registered app registry backends.
var NewFuncs = map[string]NewFunc{}
// Register registers a new app registry new function.
// Not safe for concurrent use. Safe for use from package init.
func Register(name string, f NewFunc) {
NewFuncs[name] = f
}
@@ -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 static
import (
"container/heap"
"context"
"fmt"
"strconv"
"strings"
"sync"
registrypb "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/app"
"github.com/opencloud-eu/reva/v2/pkg/app/registry/registry"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/rs/zerolog/log"
orderedmap "github.com/wk8/go-ordered-map"
)
func init() {
registry.Register("static", New)
}
const defaultPriority = 0
type mimeTypeConfig struct {
MimeType string `mapstructure:"mime_type"`
Extension string `mapstructure:"extension"`
Name string `mapstructure:"name"`
Description string `mapstructure:"description"`
Icon string `mapstructure:"icon"`
DefaultApp string `mapstructure:"default_app"`
AllowCreation bool `mapstructure:"allow_creation"`
apps providerHeap
}
type config struct {
Providers []*registrypb.ProviderInfo `mapstructure:"providers"`
MimeTypes []*mimeTypeConfig `mapstructure:"mime_types"`
}
func (c *config) init() {
if len(c.Providers) == 0 {
c.Providers = []*registrypb.ProviderInfo{}
}
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
return c, nil
}
type manager struct {
providers map[string]*registrypb.ProviderInfo
mimetypes *orderedmap.OrderedMap // map[string]*mimeTypeConfig -> map the mime type to the addresses of the corresponding providers
sync.RWMutex
}
// New returns an implementation of the app.Registry interface.
func New(m map[string]interface{}) (app.Registry, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
mimetypes := orderedmap.New()
for _, mime := range c.MimeTypes {
mimetypes.Set(mime.MimeType, mime)
}
providerMap := make(map[string]*registrypb.ProviderInfo)
for _, p := range c.Providers {
providerMap[p.Address] = p
}
// register providers configured manually from the config
// (different from the others that are registering themselves -
// dinamically added invoking the AddProvider function)
for _, p := range c.Providers {
if p != nil {
for _, m := range p.MimeTypes {
if v, ok := mimetypes.Get(m); ok {
mtc := v.(*mimeTypeConfig)
registerProvider(p, mtc)
} else {
return nil, errtypes.NotFound(fmt.Sprintf("mimetype %s not found in the configuration", m))
}
}
}
}
newManager := manager{
providers: providerMap,
mimetypes: mimetypes,
}
return &newManager, nil
}
// remove a provider from the provider list in a mime type
// it's a no-op if the provider is not in the list of providers in the mime type
func unregisterProvider(p *registrypb.ProviderInfo, mime *mimeTypeConfig) {
if index, in := getIndex(mime.apps, p); in {
// remove the provider from the list
heap.Remove(&mime.apps, index)
}
}
func registerProvider(p *registrypb.ProviderInfo, mime *mimeTypeConfig) {
// the app provider could be previously registered to the same mime type list
// so we will remove it
unregisterProvider(p, mime)
heap.Push(&mime.apps, providerWithPriority{
provider: p,
priority: getPriority(p),
})
}
func getPriority(p *registrypb.ProviderInfo) uint64 {
if p.Opaque != nil && len(p.Opaque.Map) != 0 {
if priority, ok := p.Opaque.Map["priority"]; ok {
if pr, err := strconv.ParseUint(string(priority.GetValue()), 10, 64); err == nil {
return pr
}
}
}
return defaultPriority
}
func (m *manager) FindProviders(ctx context.Context, mimeType string) ([]*registrypb.ProviderInfo, error) {
// find longest match
var match string
m.RLock()
defer m.RUnlock()
for pair := m.mimetypes.Oldest(); pair != nil; pair = pair.Next() {
prefix := pair.Key.(string)
if strings.HasPrefix(mimeType, prefix) && len(prefix) > len(match) {
match = prefix
}
}
if match == "" {
return nil, errtypes.NotFound("application provider not found for mime type " + mimeType)
}
mimeInterface, _ := m.mimetypes.Get(match)
mimeMatch := mimeInterface.(*mimeTypeConfig)
var providers = make([]*registrypb.ProviderInfo, 0, len(mimeMatch.apps))
for _, p := range mimeMatch.apps {
providers = append(providers, m.providers[p.provider.Address])
}
return providers, nil
}
func (m *manager) AddProvider(ctx context.Context, p *registrypb.ProviderInfo) error {
m.Lock()
defer m.Unlock()
// check if the provider was already registered
// if it's the case, we have to unregister it
// from all the old mime types
if oldP, ok := m.providers[p.Address]; ok {
oldMimeTypes := oldP.MimeTypes
for _, mimeName := range oldMimeTypes {
mimeIf, ok := m.mimetypes.Get(mimeName)
if !ok {
continue
}
mime := mimeIf.(*mimeTypeConfig)
unregisterProvider(p, mime)
}
}
m.providers[p.Address] = p
for _, mime := range p.MimeTypes {
if mimeTypeInterface, ok := m.mimetypes.Get(mime); ok {
mimeType := mimeTypeInterface.(*mimeTypeConfig)
registerProvider(p, mimeType)
} else {
// the mime type should be already registered as config in the AppRegistry
// we will create a new entry fot the mimetype, but leaving a warning for
// future log inspection for weird behaviour
// log.Warn().Msgf("config for mimetype '%s' not found while adding a new AppProvider", m)
m.mimetypes.Set(mime, dummyMimeType(mime, []*registrypb.ProviderInfo{p}))
}
}
return nil
}
func (m *manager) ListProviders(ctx context.Context) ([]*registrypb.ProviderInfo, error) {
m.RLock()
defer m.RUnlock()
providers := make([]*registrypb.ProviderInfo, 0, len(m.providers))
for _, p := range m.providers {
providers = append(providers, p)
}
return providers, nil
}
func (m *manager) ListSupportedMimeTypes(ctx context.Context) ([]*registrypb.MimeTypeInfo, error) {
m.RLock()
defer m.RUnlock()
res := make([]*registrypb.MimeTypeInfo, 0, m.mimetypes.Len())
for pair := m.mimetypes.Oldest(); pair != nil; pair = pair.Next() {
mime := pair.Value.(*mimeTypeConfig)
res = append(res, &registrypb.MimeTypeInfo{
MimeType: mime.MimeType,
Ext: mime.Extension,
Name: mime.Name,
Description: mime.Description,
Icon: mime.Icon,
AppProviders: mime.apps.getOrderedProviderByPriority(),
AllowCreation: mime.AllowCreation,
DefaultApplication: mime.DefaultApp,
})
}
return res, nil
}
func (h providerHeap) getOrderedProviderByPriority() []*registrypb.ProviderInfo {
providers := make([]*registrypb.ProviderInfo, 0, h.Len())
for _, pp := range h {
providers = append(providers, pp.provider)
}
return providers
}
func getIndex(h providerHeap, s *registrypb.ProviderInfo) (int, bool) {
for i, e := range h {
if equalsProviderInfo(e.provider, s) {
return i, true
}
}
return -1, false
}
func (m *manager) SetDefaultProviderForMimeType(ctx context.Context, mimeType string, p *registrypb.ProviderInfo) error {
m.Lock()
defer m.Unlock()
mimeInterface, ok := m.mimetypes.Get(mimeType)
if ok {
mime := mimeInterface.(*mimeTypeConfig)
mime.DefaultApp = p.Address
registerProvider(p, mime)
} else {
// the mime type should be already registered as config in the AppRegistry
// we will create a new entry fot the mimetype, but leaving a warning for
// future log inspection for weird behaviour
log.Warn().Msgf("config for mimetype '%s' not found while setting a new default AppProvider", mimeType)
m.mimetypes.Set(mimeType, dummyMimeType(mimeType, []*registrypb.ProviderInfo{p}))
}
return nil
}
func dummyMimeType(m string, apps []*registrypb.ProviderInfo) *mimeTypeConfig {
appsHeap := providerHeap{}
for _, p := range apps {
heap.Push(&appsHeap, providerWithPriority{
provider: p,
priority: getPriority(p),
})
}
return &mimeTypeConfig{
MimeType: m,
apps: appsHeap,
//Extension: "", // there is no meaningful general extension, so omit it
//Name: "", // there is no meaningful general name, so omit it
//Description: "", // there is no meaningful general description, so omit it
}
}
func (m *manager) GetDefaultProviderForMimeType(ctx context.Context, mimeType string) (*registrypb.ProviderInfo, error) {
m.RLock()
defer m.RUnlock()
mimeInterface, ok := m.mimetypes.Get(mimeType)
if ok {
mime := mimeInterface.(*mimeTypeConfig)
// default by provider address
if p, ok := m.providers[mime.DefaultApp]; ok {
return p, nil
}
// default by provider name
for _, p := range m.providers {
if p.Name == mime.DefaultApp {
return p, nil
}
}
}
return nil, errtypes.NotFound("default application provider not set for mime type " + mimeType)
}
func equalsProviderInfo(p1, p2 *registrypb.ProviderInfo) bool {
return p1.Name == p2.Name
}
// check that all providers in the two lists are equals
func providersEquals(l1, l2 []*registrypb.ProviderInfo) bool {
if len(l1) != len(l2) {
return false
}
for i := 0; i < len(l1); i++ {
if !equalsProviderInfo(l1[i], l2[i]) {
return false
}
}
return true
}
type providerWithPriority struct {
provider *registrypb.ProviderInfo
priority uint64
}
type providerHeap []providerWithPriority
func (h providerHeap) Len() int {
return len(h)
}
func (h providerHeap) Less(i, j int) bool {
return h[i].priority > h[j].priority
}
func (h providerHeap) Swap(i, j int) {
h[i], h[j] = h[j], h[i]
}
func (h *providerHeap) Push(x interface{}) {
*h = append(*h, x.(providerWithPriority))
}
func (h *providerHeap) Pop() interface{} {
last := len(*h) - 1
x := (*h)[last]
*h = (*h)[:last]
return x
}
+41
View File
@@ -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 appauth
import (
"context"
apppb "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
)
// Manager is the interface that manages application authentication mechanisms.
type Manager interface {
// GenerateAppPassword creates a password with specified scope to be used by
// third-party applications.
GenerateAppPassword(ctx context.Context, scope map[string]*authpb.Scope, label string, expiration *typespb.Timestamp) (*apppb.AppPassword, error)
// ListAppPasswords lists the application passwords created by a user.
ListAppPasswords(ctx context.Context) ([]*apppb.AppPassword, error)
// InvalidateAppPassword invalidates a generated password.
InvalidateAppPassword(ctx context.Context, secret string) error
// GetAppPassword retrieves the password information by the combination of username and password.
GetAppPassword(ctx context.Context, user *userpb.UserId, secret string) (*apppb.AppPassword, error)
}
@@ -0,0 +1,255 @@
// 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 json
import (
"context"
"encoding/json"
"io"
"os"
"sync"
"time"
apppb "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appauth"
"github.com/opencloud-eu/reva/v2/pkg/appauth/manager/registry"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/pkg/errors"
"github.com/sethvargo/go-password/password"
"golang.org/x/crypto/bcrypt"
"google.golang.org/protobuf/proto"
)
func init() {
registry.Register("json", New)
}
type config struct {
File string `mapstructure:"file"`
TokenStrength int `mapstructure:"token_strength"`
PasswordHashCost int `mapstructure:"password_hash_cost"`
}
type jsonManager struct {
sync.Mutex
config *config
// map[userid][password]AppPassword
passwords map[string]map[string]*apppb.AppPassword
}
// New returns a new mgr.
func New(m map[string]interface{}) (appauth.Manager, error) {
c, err := parseConfig(m)
if err != nil {
return nil, errors.Wrap(err, "error creating a new manager")
}
c.init()
// load or create file
manager, err := loadOrCreate(c.File)
if err != nil {
return nil, errors.Wrap(err, "error loading the file containing the application passwords")
}
manager.config = c
return manager, nil
}
func (c *config) init() {
if c.File == "" {
c.File = "/var/tmp/reva/appauth.json"
}
if c.TokenStrength == 0 {
c.TokenStrength = 16
}
if c.PasswordHashCost == 0 {
c.PasswordHashCost = 11
}
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
return c, nil
}
func loadOrCreate(file string) (*jsonManager, error) {
stat, err := os.Stat(file)
if os.IsNotExist(err) || stat.Size() == 0 {
if err = os.WriteFile(file, []byte("{}"), 0644); err != nil {
return nil, errors.Wrapf(err, "error creating the file %s", file)
}
}
fd, err := os.OpenFile(file, os.O_RDONLY, 0)
if err != nil {
return nil, errors.Wrapf(err, "error opening the file %s", file)
}
defer fd.Close()
data, err := io.ReadAll(fd)
if err != nil {
return nil, errors.Wrapf(err, "error reading the file %s", file)
}
m := &jsonManager{}
if err = json.Unmarshal(data, &m.passwords); err != nil {
return nil, errors.Wrapf(err, "error parsing the file %s", file)
}
if m.passwords == nil {
m.passwords = make(map[string]map[string]*apppb.AppPassword)
}
return m, nil
}
func (mgr *jsonManager) GenerateAppPassword(ctx context.Context, scope map[string]*authpb.Scope, label string, expiration *typespb.Timestamp) (*apppb.AppPassword, error) {
token, err := password.Generate(mgr.config.TokenStrength, mgr.config.TokenStrength/2, 0, false, false)
if err != nil {
return nil, errors.Wrap(err, "error creating new token")
}
tokenHashed, err := bcrypt.GenerateFromPassword([]byte(token), mgr.config.PasswordHashCost)
if err != nil {
return nil, errors.Wrap(err, "error creating new token")
}
userID := ctxpkg.ContextMustGetUser(ctx).GetId()
ctime := now()
password := string(tokenHashed)
appPass := &apppb.AppPassword{
Password: password,
TokenScope: scope,
Label: label,
Expiration: expiration,
Ctime: ctime,
Utime: ctime,
User: userID,
}
mgr.Lock()
defer mgr.Unlock()
// check if user has some previous password
if _, ok := mgr.passwords[userID.String()]; !ok {
mgr.passwords[userID.String()] = make(map[string]*apppb.AppPassword)
}
mgr.passwords[userID.String()][password] = appPass
err = mgr.save()
if err != nil {
return nil, errors.Wrap(err, "error saving new token")
}
clonedAppPass := proto.Clone(appPass).(*apppb.AppPassword)
clonedAppPass.Password = token
return clonedAppPass, nil
}
func (mgr *jsonManager) ListAppPasswords(ctx context.Context) ([]*apppb.AppPassword, error) {
userID := ctxpkg.ContextMustGetUser(ctx).GetId()
mgr.Lock()
defer mgr.Unlock()
appPasswords := make([]*apppb.AppPassword, 0, len(mgr.passwords[userID.String()]))
for _, pw := range mgr.passwords[userID.String()] {
appPasswords = append(appPasswords, pw)
}
return appPasswords, nil
}
func (mgr *jsonManager) InvalidateAppPassword(ctx context.Context, password string) error {
userID := ctxpkg.ContextMustGetUser(ctx).GetId()
mgr.Lock()
defer mgr.Unlock()
// see if user has a list of passwords
appPasswords, ok := mgr.passwords[userID.String()]
if !ok || len(appPasswords) == 0 {
return errtypes.NotFound("password not found")
}
if _, ok := appPasswords[password]; !ok {
return errtypes.NotFound("password not found")
}
delete(mgr.passwords[userID.String()], password)
// if user has 0 passwords, delete user key from state map
if len(mgr.passwords[userID.String()]) == 0 {
delete(mgr.passwords, userID.String())
}
return mgr.save()
}
func (mgr *jsonManager) GetAppPassword(ctx context.Context, userID *userpb.UserId, password string) (*apppb.AppPassword, error) {
mgr.Lock()
defer mgr.Unlock()
appPassword, ok := mgr.passwords[userID.String()]
if !ok {
return nil, errtypes.NotFound("password not found")
}
for hash, pw := range appPassword {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
if err == nil {
// password found
if pw.Expiration != nil && pw.Expiration.Seconds != 0 && uint64(time.Now().Unix()) > pw.Expiration.Seconds {
// password expired
return nil, errtypes.NotFound("password not found")
}
// password not expired
// update last used time
pw.Utime = now()
if err := mgr.save(); err != nil {
return nil, errors.Wrap(err, "error saving file")
}
return pw, nil
}
}
return nil, errtypes.NotFound("password not found")
}
func now() *typespb.Timestamp {
return &typespb.Timestamp{Seconds: uint64(time.Now().Unix())}
}
func (mgr *jsonManager) save() error {
data, err := json.Marshal(mgr.passwords)
if err != nil {
return errors.Wrap(err, "error encoding json file")
}
if err = os.WriteFile(mgr.config.File, data, 0644); err != nil {
return errors.Wrapf(err, "error writing to file %s", mgr.config.File)
}
return nil
}
@@ -0,0 +1,557 @@
package jsoncs3
import (
"context"
"encoding/json"
"fmt"
"math/rand"
"strings"
"sync"
"time"
"github.com/alexedwards/argon2id"
apppb "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/google/uuid"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appauth"
"github.com/opencloud-eu/reva/v2/pkg/appauth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/pkg/errors"
"github.com/sethvargo/go-diceware/diceware"
"github.com/sethvargo/go-password/password"
"go.opentelemetry.io/otel/codes"
)
type PasswordGenerator interface {
GeneratePassword() (string, error)
}
func init() {
registry.Register("jsoncs3", New)
}
type manager struct {
sync.RWMutex // for lazy initialization
mds metadata.Storage
generator PasswordGenerator
uTimeUpdateInterval time.Duration
updateRetryCount int
initialized bool
}
type config struct {
ProviderAddr string `mapstructure:"provider_addr"`
ServiceUserID string `mapstructure:"service_user_id"`
ServiceUserIdp string `mapstructure:"service_user_idp"`
MachineAuthAPIKey string `mapstructure:"machine_auth_apikey"`
Generator string `mapstructure:"password_generator"`
GeneratorConfig map[string]any `mapstructure:"generator_config"`
// Time interval in seconds to update the UTime of a token when calling GetAppPassword. Default is 5 min.
// For testing set this -1 to disable automatic updates.
UTimeUpdateInterval int `mapstructure:"utime_update_interval_seconds"`
UpdateRetryCount int `mapstructure:"update_retry_count"`
}
type updaterFunc func(map[string]*apppb.AppPassword) (map[string]*apppb.AppPassword, error)
const tracerName = "jsoncs3"
func New(m map[string]any) (appauth.Manager, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error creating a new manager")
return nil, err
}
if c.ProviderAddr == "" {
return nil, fmt.Errorf("appauth jsoncs3 manager: provider_addr not set")
}
if c.ServiceUserID == "" {
return nil, fmt.Errorf("appauth jsoncs3 manager: service_user_id not set")
}
if c.ServiceUserIdp == "" {
return nil, fmt.Errorf("appauth jsoncs3 manager: service_user_idp not set")
}
if c.MachineAuthAPIKey == "" {
return nil, fmt.Errorf("appauth jsoncs3 manager: machine_auth_apikey not set")
}
if c.Generator == "" {
c.Generator = "diceware"
}
if c.UpdateRetryCount <= 0 {
c.UpdateRetryCount = 5
}
var updateInterval time.Duration
switch c.UTimeUpdateInterval {
case -1:
updateInterval = 0
case 0:
updateInterval = 5 * time.Minute
default:
updateInterval = time.Duration(c.UTimeUpdateInterval) * time.Second
}
var pwgen PasswordGenerator
var err error
switch c.Generator {
case "diceware":
pwgen, err = NewDicewareGenerator(c.GeneratorConfig)
case "random":
pwgen, err = NewRandGenerator(c.GeneratorConfig)
default:
return nil, fmt.Errorf("appauth jsoncs3 manager: unknown generator: %s", c.Generator)
}
if err != nil {
return nil, fmt.Errorf("appauth jsoncs3 manager: failed initialize password generator: %w", err)
}
cs3, err := metadata.NewCS3Storage(c.ProviderAddr, c.ProviderAddr, c.ServiceUserID, c.ServiceUserIdp, c.MachineAuthAPIKey)
if err != nil {
return nil, err
}
return NewWithOptions(cs3, pwgen, updateInterval, c.UpdateRetryCount)
}
func NewWithOptions(mds metadata.Storage, generator PasswordGenerator, uTimeUpdateInterval time.Duration, updateRetries int) (*manager, error) {
return &manager{
mds: mds,
generator: generator,
uTimeUpdateInterval: uTimeUpdateInterval,
updateRetryCount: updateRetries,
}, nil
}
// GenerateAppPassword creates a password with specified scope to be used by
// third-party applications.
func (m *manager) GenerateAppPassword(ctx context.Context, scope map[string]*authpb.Scope, label string, expiration *typespb.Timestamp) (*apppb.AppPassword, error) {
logger := appctx.GetLogger(ctx)
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "GenerateAppPassword")
defer span.End()
if err := m.initialize(ctx); err != nil {
logger.Error().Err(err).Msg("initializing appauth manager failed")
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
}
token, err := m.generator.GeneratePassword()
if err != nil {
logger.Debug().Err(err).Msg("error generating new password")
return nil, errors.Wrap(err, "error creating new token")
}
tokenHashed, err := argon2id.CreateHash(token, argon2id.DefaultParams)
if err != nil {
logger.Debug().Err(err).Msg("error generating password hash")
return nil, errors.Wrap(err, "error creating new token")
}
var userID *userpb.UserId
if user, ok := ctxpkg.ContextGetUser(ctx); ok {
userID = user.GetId()
} else {
logger.Debug().Err(err).Msg("no user in context")
return nil, errtypes.BadRequest("no user in context")
}
cTime := &typespb.Timestamp{Seconds: uint64(time.Now().Unix())}
// For persisting we use the hashed password, since we don't
// want to store it in cleartext
appPass := &apppb.AppPassword{
Password: tokenHashed,
TokenScope: scope,
Label: label,
Expiration: expiration,
Ctime: cTime,
Utime: cTime,
User: userID,
}
id := uuid.New().String()
err = m.updateWithRetry(ctx, m.updateRetryCount, true, userID, func(a map[string]*apppb.AppPassword) (map[string]*apppb.AppPassword, error) {
a[id] = appPass
return a, nil
})
if err != nil {
logger.Debug().Err(err).Msg("failed to store new app password")
return nil, err
}
// Here we need to resplace the hash with the cleartext password again since
// the requestor needs to know the cleartext value.
appPass.Password = token
return appPass, nil
}
// ListAppPasswords lists the application passwords created by a user.
func (m *manager) ListAppPasswords(ctx context.Context) ([]*apppb.AppPassword, error) {
log := appctx.GetLogger(ctx)
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "ListAppPasswords")
defer span.End()
if err := m.initialize(ctx); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
}
var userID *userpb.UserId
if user, ok := ctxpkg.ContextGetUser(ctx); ok {
userID = user.GetId()
} else {
return nil, errtypes.BadRequest("no user in context")
}
_, userAppPasswords, err := m.getUserAppPasswords(ctx, userID)
if err != nil {
if _, ok := err.(errtypes.NotFound); ok {
return []*apppb.AppPassword{}, nil
}
log.Error().Err(err).Msg("getUserAppPasswords failed")
return nil, err
}
userAppPasswordSlice := make([]*apppb.AppPassword, 0, len(userAppPasswords))
for id, p := range userAppPasswords {
p.Password = id
userAppPasswordSlice = append(userAppPasswordSlice, p)
}
return userAppPasswordSlice, nil
}
// InvalidateAppPassword invalidates a generated password.
func (m *manager) InvalidateAppPassword(ctx context.Context, secretOrId string) error {
log := appctx.GetLogger(ctx)
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "InvalidateAppPassword")
defer span.End()
if err := m.initialize(ctx); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
var userID *userpb.UserId
if user, ok := ctxpkg.ContextGetUser(ctx); ok {
userID = user.GetId()
} else {
return errtypes.BadRequest("no user in context")
}
updater := func(a map[string]*apppb.AppPassword) (map[string]*apppb.AppPassword, error) {
// Allow deleting a token using the ID inside the password property. This is needed because of
// some shortcomings of the CS3 APIs. On the API level tokens don't have IDs
// ListAppPasswords in this backend returns the ID as the password value.
if _, ok := a[secretOrId]; ok {
delete(a, secretOrId)
return a, nil
}
// Check if the supplied parameter matches any of the stored password tokens
for key, pw := range a {
ok, err := argon2id.ComparePasswordAndHash(secretOrId, pw.Password)
switch {
case err != nil:
log.Debug().Err(err).Msg("Error comparing password and hash")
case ok:
delete(a, key)
return a, nil
}
}
return a, errtypes.NotFound("password not found")
}
err := m.updateWithRetry(ctx, m.updateRetryCount, false, userID, updater)
if err != nil {
log.Error().Err(err).Msg("getUserAppPasswords failed")
return errtypes.NotFound("password not found")
}
return nil
}
// GetAppPassword retrieves the password information by the combination of username and password.
func (m *manager) GetAppPassword(ctx context.Context, user *userpb.UserId, secret string) (*apppb.AppPassword, error) {
log := appctx.GetLogger(ctx)
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "GetAppPassword")
defer span.End()
if err := m.initialize(ctx); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
}
errUpdateSkipped := errors.New("update skipped")
var (
matchedPw *apppb.AppPassword
matchedID string
)
updater := func(a map[string]*apppb.AppPassword) (map[string]*apppb.AppPassword, error) {
matchedPw = nil
for id, pw := range a {
ok, err := argon2id.ComparePasswordAndHash(secret, pw.Password)
switch {
case err != nil:
log.Debug().Err(err).Msg("Error comparing password and hash")
case ok:
// password found
if pw.Expiration != nil && pw.Expiration.Seconds != 0 && uint64(time.Now().Unix()) > pw.Expiration.Seconds {
log.Debug().Str("AppPasswordId", id).Msg("password expired")
return nil, errtypes.NotFound("password not found")
}
matchedPw = pw
matchedID = id
// password not expired
// Updating the Utime will cause an Upload for every single GetAppPassword request. We are limiting this to one
// update per 'uTimeUpdateInterval' (default 5 min) otherwise this backend will become unusable.
if time.Since(utils.TSToTime(pw.Utime)) > m.uTimeUpdateInterval {
a[id].Utime = utils.TSNow()
return a, nil
}
return a, errUpdateSkipped
}
}
return nil, errtypes.NotFound("password not found")
}
err := m.updateWithRetry(ctx, m.updateRetryCount, false, user, updater)
switch {
case err == nil:
fallthrough
case errors.Is(err, errUpdateSkipped):
// Don't return the hashed password, put the ID into the password field
matchedPw.Password = matchedID
return matchedPw, nil
}
return nil, errtypes.NotFound("password not found")
}
func (m *manager) initialize(ctx context.Context) error {
_, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "initialize")
logger := appctx.GetLogger(ctx)
defer span.End()
if m.initialized {
span.SetStatus(codes.Ok, "already initialized")
return nil
}
m.Lock()
defer m.Unlock()
if m.initialized { // check if initialization happened while grabbing the lock
span.SetStatus(codes.Ok, "initialized while grabbing lock")
return nil
}
ctx = context.Background()
ctx = appctx.WithLogger(ctx, logger)
err := m.mds.Init(ctx, "jsoncs3-appauth-data")
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
m.initialized = true
return nil
}
func (m *manager) updateWithRetry(ctx context.Context, retries int, createIfNotFound bool, userid *userpb.UserId, updater updaterFunc) error {
log := appctx.GetLogger(ctx)
_, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "initialize")
defer span.End()
retry := true
var (
etag string
userAppPasswords map[string]*apppb.AppPassword
err error
)
// retry for the specified number of times, then error out
for i := 0; i < retries && retry; i++ {
if i > 0 {
// if we're retrying, wait a bit before the next try
jitter := time.Duration(rand.Int63n(int64(100 * time.Millisecond)))
time.Sleep(jitter + 100*time.Millisecond)
}
etag, userAppPasswords, err = m.getUserAppPasswords(ctx, userid)
switch err.(type) {
case nil:
// empty
case errtypes.NotFound:
if createIfNotFound {
userAppPasswords = map[string]*apppb.AppPassword{}
} else {
log.Debug().Err(err).Msg("getUserAppPasswords failed (not found)")
span.RecordError(err)
span.SetStatus(codes.Error, "downloading app tokens failed")
return err
}
case errtypes.TooEarly:
// Ideally this should never happen as we disable asynchronous uploads for the metadata storage.
log.Debug().Err(err).Int("try", i).Msg("getUserAppPasswords failed (too early, retrying)")
retry = true
continue
default:
log.Debug().Err(err).Msg("getUserAppPasswords failed")
span.RecordError(err)
span.SetStatus(codes.Error, "downloading app tokens failed")
return err
}
userAppPasswords, err = updater(userAppPasswords)
if err != nil {
return err
}
err = m.updateUserAppPassword(ctx, userid, userAppPasswords, etag)
switch err.(type) {
case nil:
retry = false
case errtypes.Aborted:
log.Debug().Err(err).Int("attempt", i).Msg("updateUserAppPassword failed (retrying)")
retry = true
default:
log.Debug().Err(err).Int("attempt", i).Msg("updateUserAppPassword failed (not retrying)")
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
}
if retry {
log.Debug().Err(err).Msg("updateUserAppPassword failed")
span.RecordError(err)
span.SetStatus(codes.Error, "updating app token failed")
return err
}
return nil
}
func (m *manager) updateUserAppPassword(ctx context.Context, userid *userpb.UserId, appPasswords map[string]*apppb.AppPassword, ifMatchEtag string) error {
log := appctx.GetLogger(ctx)
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "getUserAppPasswords")
jsonPath := userAppTokenJSONPath(userid)
pwBytes, err := json.Marshal(appPasswords)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
ur := metadata.UploadRequest{
Path: jsonPath,
Content: pwBytes,
IfMatchEtag: ifMatchEtag,
}
// If there is no etag, make sure to only upload if the file wasn't craeted yet
if ifMatchEtag == "" {
ur.IfNoneMatch = []string{"*"}
}
_, err = m.mds.Upload(ctx, ur)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
log.Debug().Err(err).Msg("failed to upload AppPasswword")
return err
}
return nil
}
func (m *manager) getUserAppPasswords(ctx context.Context, userid *userpb.UserId) (string, map[string]*apppb.AppPassword, error) {
log := appctx.GetLogger(ctx)
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "getUserAppPasswords")
jsonPath := userAppTokenJSONPath(userid)
dlreq := metadata.DownloadRequest{
Path: jsonPath,
}
var userAppPasswords = map[string]*apppb.AppPassword{}
dlres, err := m.mds.Download(ctx, dlreq)
switch err.(type) {
case nil:
err = json.Unmarshal(dlres.Content, &userAppPasswords)
if err != nil {
log.Error().Err(err).Msg("unmarshaling app tokens failed")
return "", nil, err
}
case errtypes.NotFound:
return "", nil, errtypes.NotFound("password not found")
default:
span.RecordError(err)
span.SetStatus(codes.Error, "downloading app tokens failed")
return "", nil, err
}
return dlres.Etag, userAppPasswords, nil
}
func userAppTokenJSONPath(userID *userpb.UserId) string {
return userID.GetOpaqueId() + ".json"
}
type randomPassword struct {
Strength int `mapstructure:"token_strength"`
}
func NewRandGenerator(config map[string]any) (*randomPassword, error) {
r := &randomPassword{}
if err := mapstructure.Decode(config, r); err != nil {
err = errors.Wrap(err, "error configuring password generator")
return nil, err
}
if r.Strength <= 0 {
r.Strength = 11
}
return r, nil
}
func (r randomPassword) GeneratePassword() (string, error) {
token, err := password.Generate(r.Strength, r.Strength/2, 0, false, false)
if err != nil {
return "", errors.Wrap(err, "error creating new token")
}
return token, nil
}
type dicewarePassword struct {
NumWords int `mapstructure:"number_of_words"`
}
func NewDicewareGenerator(config map[string]any) (*dicewarePassword, error) {
d := &dicewarePassword{}
if err := mapstructure.Decode(config, d); err != nil {
err = errors.Wrap(err, "error creating a new manager")
return nil, err
}
if d.NumWords <= 0 {
d.NumWords = 6
}
return d, nil
}
func (d dicewarePassword) GeneratePassword() (string, error) {
token, err := diceware.Generate(d.NumWords)
if err != nil {
return "", errors.Wrap(err, "error creating new token")
}
return strings.Join(token, " "), nil
}
@@ -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 application auth manager drivers.
_ "github.com/opencloud-eu/reva/v2/pkg/appauth/manager/json"
_ "github.com/opencloud-eu/reva/v2/pkg/appauth/manager/jsoncs3"
// Add your own here
)
@@ -0,0 +1,34 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package registry
import "github.com/opencloud-eu/reva/v2/pkg/appauth"
// NewFunc is the function that application auth implementations
// should register at init time.
type NewFunc func(map[string]interface{}) (appauth.Manager, error)
// NewFuncs is a map containing all the registered application auth managers.
var NewFuncs = map[string]NewFunc{}
// Register registers a new application auth manager new function.
// Not safe for concurrent use. Safe for use from package init.
func Register(name string, f NewFunc) {
NewFuncs[name] = f
}
+59
View File
@@ -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 appctx
import (
"context"
"github.com/go-chi/chi/v5/middleware"
rtrace "github.com/opencloud-eu/reva/v2/pkg/trace"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/trace"
)
// WithLogger returns a context with an associated logger.
func WithLogger(ctx context.Context, l *zerolog.Logger) context.Context {
return l.WithContext(ctx)
}
// GetLogger returns the logger associated with the given context
// or a disabled logger in case no logger is stored inside the context.
func GetLogger(ctx context.Context) *zerolog.Logger {
logger := zerolog.Ctx(ctx)
reqID := middleware.GetReqID(ctx)
if reqID != "" {
sublogger := logger.With().Str("request-id", reqID).Logger()
logger = &sublogger
}
return logger
}
// WithTracerProvider returns a context with an associated TracerProvider
func WithTracerProvider(ctx context.Context, p trace.TracerProvider) context.Context {
return rtrace.ContextSetTracerProvider(ctx, p)
}
// GetTracerProvider returns the TracerProvider associated with
// the given context. (Or the global default TracerProvider if there
// is no TracerProvider in the context)
func GetTracerProvider(ctx context.Context) trace.TracerProvider {
return rtrace.ContextGetTracerProvider(ctx)
}
+73
View File
@@ -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 appctx
import (
"context"
"reflect"
"unsafe"
)
// PutKeyValuesToCtx puts all the key-value pairs from the provided map to a background context.
func PutKeyValuesToCtx(m map[interface{}]interface{}) context.Context {
ctx := context.Background()
for key, value := range m {
ctx = context.WithValue(ctx, key, value)
}
return ctx
}
// GetKeyValuesFromCtx retrieves all the key-value pairs from the provided context.
func GetKeyValuesFromCtx(ctx context.Context) map[interface{}]interface{} {
m := make(map[interface{}]interface{})
getKeyValue(ctx, m)
return m
}
func getKeyValue(ctx interface{}, m map[interface{}]interface{}) {
// This is a dirty hack to run with go 1.21.x
reflectCtxValues := reflect.ValueOf(ctx)
if reflectCtxValues.Kind() != reflect.Struct {
ctxVals := reflectCtxValues.Elem()
ctxType := reflect.TypeOf(ctx).Elem()
if ctxType.Kind() == reflect.Struct {
for i := 0; i < ctxVals.NumField(); i++ {
currField, currIf := extractField(ctxVals, ctxType, i)
switch currField {
case "Context":
getKeyValue(currIf, m)
case "key":
nextField, nextIf := extractField(ctxVals, ctxType, i+1)
if nextField == "val" {
m[currIf] = nextIf
i++
}
}
}
}
}
}
func extractField(vals reflect.Value, fieldType reflect.Type, pos int) (string, interface{}) {
currVal := vals.Field(pos)
currVal = reflect.NewAt(currVal.Type(), unsafe.Pointer(currVal.UnsafeAddr())).Elem()
currField := fieldType.Field(pos)
return currField.Name, currVal.Interface()
}
+66
View File
@@ -0,0 +1,66 @@
// 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"
"net/http"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
registry "github.com/cs3org/go-cs3apis/cs3/auth/registry/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/plugin"
)
// Manager is the interface to implement to authenticate users
type Manager interface {
plugin.Plugin
Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, map[string]*authpb.Scope, error)
}
// Credentials contains the auth type, client id and secret.
type Credentials struct {
Type string
ClientID string
ClientSecret string
}
// CredentialStrategy obtains Credentials from the request.
type CredentialStrategy interface {
GetCredentials(w http.ResponseWriter, r *http.Request) (*Credentials, error)
AddWWWAuthenticate(w http.ResponseWriter, r *http.Request, realm string)
}
// TokenStrategy obtains a token from the request.
// If token does not exist returns an empty string.
type TokenStrategy interface {
GetToken(r *http.Request) string
}
// TokenWriter stores the token in a http response.
type TokenWriter interface {
WriteToken(token string, w http.ResponseWriter)
}
// Registry is the interface that auth registries implement
// for discovering auth providers
type Registry interface {
ListProviders(ctx context.Context) ([]*registry.ProviderInfo, error)
GetProvider(ctx context.Context, authType string) (*registry.ProviderInfo, error)
}
@@ -0,0 +1,99 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package appauth
import (
"context"
appauthpb "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func init() {
registry.Register("appauth", New)
}
type manager struct {
GatewayAddr string `mapstructure:"gateway_addr"`
}
// New returns a new auth Manager.
func New(m map[string]interface{}) (auth.Manager, error) {
mgr := &manager{}
err := mgr.Configure(m)
if err != nil {
return nil, err
}
return mgr, nil
}
func (m *manager) Configure(ml map[string]interface{}) error {
err := mapstructure.Decode(ml, m)
if err != nil {
return errors.Wrap(err, "error decoding conf")
}
return nil
}
func (m *manager) Authenticate(ctx context.Context, username, password string) (*user.User, map[string]*authpb.Scope, error) {
gtw, err := pool.GetGatewayServiceClient(m.GatewayAddr)
if err != nil {
return nil, nil, err
}
// get user info
userResponse, err := gtw.GetUserByClaim(ctx, &user.GetUserByClaimRequest{
Claim: "username",
Value: username,
})
switch {
case err != nil:
return nil, nil, err
case userResponse.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND:
return nil, nil, errtypes.NotFound(userResponse.Status.Message)
case userResponse.Status.Code != rpcv1beta1.Code_CODE_OK:
return nil, nil, errtypes.InternalError(userResponse.Status.Message)
}
// get the app password associated with the user and password
appAuthResponse, err := gtw.GetAppPassword(ctx, &appauthpb.GetAppPasswordRequest{
User: userResponse.GetUser().Id,
Password: password,
})
switch {
case err != nil:
return nil, nil, err
case appAuthResponse.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND:
return nil, nil, errtypes.NotFound(appAuthResponse.Status.Message)
case appAuthResponse.Status.Code != rpcv1beta1.Code_CODE_OK:
return nil, nil, errtypes.InternalError(appAuthResponse.Status.Message)
}
return userResponse.GetUser(), appAuthResponse.GetAppPassword().TokenScope, nil
}
@@ -0,0 +1,127 @@
// 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 demo
import (
"context"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
)
func init() {
registry.Register("demo", New)
}
type manager struct {
credentials map[string]Credentials
}
// Credentials holds a pair of secret and userid
type Credentials struct {
User *user.User
Secret string
}
// New returns a new auth Manager.
func New(m map[string]interface{}) (auth.Manager, error) {
// m not used
mgr := &manager{}
err := mgr.Configure(m)
return mgr, err
}
func (m *manager) Configure(ml map[string]interface{}) error {
creds := getCredentials()
m.credentials = creds
return nil
}
func (m *manager) Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, map[string]*authpb.Scope, error) {
if c, ok := m.credentials[clientID]; ok {
if c.Secret == clientSecret {
var scopes map[string]*authpb.Scope
var err error
if c.User.Id != nil && (c.User.Id.Type == user.UserType_USER_TYPE_LIGHTWEIGHT || c.User.Id.Type == user.UserType_USER_TYPE_FEDERATED) {
scopes, err = scope.AddLightweightAccountScope(authpb.Role_ROLE_OWNER, nil)
if err != nil {
return nil, nil, err
}
} else {
scopes, err = scope.AddOwnerScope(nil)
if err != nil {
return nil, nil, err
}
}
return c.User, scopes, nil
}
}
return nil, nil, errtypes.InvalidCredentials(clientID)
}
func getCredentials() map[string]Credentials {
return map[string]Credentials{
"einstein": {
Secret: "relativity",
User: &user.User{
Id: &user.UserId{
Idp: "http://localhost:9998",
OpaqueId: "4c510ada-c86b-4815-8820-42cdf82c3d51",
Type: user.UserType_USER_TYPE_PRIMARY,
},
Username: "einstein",
Groups: []string{"sailing-lovers", "violin-haters", "physics-lovers"},
Mail: "einstein@example.org",
DisplayName: "Albert Einstein",
},
},
"marie": {
Secret: "radioactivity",
User: &user.User{
Id: &user.UserId{
Idp: "http://localhost:9998",
OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c",
Type: user.UserType_USER_TYPE_PRIMARY,
},
Username: "marie",
Groups: []string{"radium-lovers", "polonium-lovers", "physics-lovers"},
Mail: "marie@example.org",
DisplayName: "Marie Curie",
},
},
"richard": {
Secret: "superfluidity",
User: &user.User{
Id: &user.UserId{
Idp: "http://localhost:9998",
OpaqueId: "932b4540-8d16-481e-8ef4-588e4b6b151c",
Type: user.UserType_USER_TYPE_PRIMARY,
},
Username: "richard",
Groups: []string{"quantum-lovers", "philosophy-haters", "physics-lovers"},
Mail: "richard@example.org",
DisplayName: "Richard Feynman",
},
},
}
}
@@ -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 impersonator
import (
"context"
"strings"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
)
func init() {
registry.Register("impersonator", New)
}
type mgr struct{}
// New returns an auth manager implementation that allows to authenticate with any credentials.
func New(c map[string]interface{}) (auth.Manager, error) {
return &mgr{}, nil
}
func (m *mgr) Configure(ml map[string]interface{}) error {
return nil
}
func (m *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, map[string]*authpb.Scope, error) {
// allow passing in uid as <opaqueid>@<idp>
at := strings.LastIndex(clientID, "@")
uid := &user.UserId{Type: user.UserType_USER_TYPE_PRIMARY}
if at < 0 {
uid.OpaqueId = clientID
} else {
uid.OpaqueId = clientID[:at]
uid.Idp = clientID[at+1:]
}
scope, err := scope.AddOwnerScope(nil)
if err != nil {
return nil, nil, err
}
return &user.User{
Id: uid,
// not much else to provide
}, scope, nil
}
@@ -0,0 +1,146 @@
// 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 json
import (
"context"
"encoding/json"
"os"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/pkg/errors"
)
func init() {
registry.Register("json", New)
}
// Credentials holds a pair of secret and userid
type Credentials struct {
ID *user.UserId `mapstructure:"id" json:"id"`
Username string `mapstructure:"username" json:"username"`
Mail string `mapstructure:"mail" json:"mail"`
MailVerified bool `mapstructure:"mail_verified" json:"mail_verified"`
DisplayName string `mapstructure:"display_name" json:"display_name"`
Secret string `mapstructure:"secret" json:"secret"`
Groups []string `mapstructure:"groups" json:"groups"`
UIDNumber int64 `mapstructure:"uid_number" json:"uid_number"`
GIDNumber int64 `mapstructure:"gid_number" json:"gid_number"`
Opaque *typespb.Opaque `mapstructure:"opaque" json:"opaque"`
}
type manager struct {
credentials map[string]*Credentials
}
type config struct {
// Users holds a path to a file containing json conforming the Users struct
Users string `mapstructure:"users"`
}
func (c *config) init() {
if c.Users == "" {
c.Users = "/etc/revad/users.json"
}
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
c.init()
return c, nil
}
// New returns a new auth Manager.
func New(m map[string]interface{}) (auth.Manager, error) {
mgr := &manager{}
err := mgr.Configure(m)
if err != nil {
return nil, err
}
return mgr, nil
}
func (m *manager) Configure(ml map[string]interface{}) error {
c, err := parseConfig(ml)
if err != nil {
return err
}
m.credentials = map[string]*Credentials{}
f, err := os.ReadFile(c.Users)
if err != nil {
return err
}
credentials := []*Credentials{}
err = json.Unmarshal(f, &credentials)
if err != nil {
return err
}
for _, c := range credentials {
m.credentials[c.Username] = c
}
return nil
}
func (m *manager) Authenticate(ctx context.Context, username string, secret string) (*user.User, map[string]*authpb.Scope, error) {
if c, ok := m.credentials[username]; ok {
if c.Secret == secret {
var scopes map[string]*authpb.Scope
var err error
if c.ID != nil && (c.ID.Type == user.UserType_USER_TYPE_LIGHTWEIGHT || c.ID.Type == user.UserType_USER_TYPE_FEDERATED) {
scopes, err = scope.AddLightweightAccountScope(authpb.Role_ROLE_OWNER, nil)
if err != nil {
return nil, nil, err
}
} else {
scopes, err = scope.AddOwnerScope(nil)
if err != nil {
return nil, nil, err
}
}
return &user.User{
Id: c.ID,
Username: c.Username,
Mail: c.Mail,
MailVerified: c.MailVerified,
DisplayName: c.DisplayName,
Groups: c.Groups,
UidNumber: c.UIDNumber,
GidNumber: c.GIDNumber,
Opaque: c.Opaque,
// TODO add arbitrary keys as opaque data
}, scopes, nil
}
}
return nil, nil, errtypes.InvalidCredentials(username)
}
@@ -0,0 +1,221 @@
// 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 ldap
import (
"context"
"fmt"
"strconv"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/go-ldap/ldap/v3"
"github.com/google/uuid"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/utils"
ldapIdentity "github.com/opencloud-eu/reva/v2/pkg/utils/ldap"
"github.com/pkg/errors"
)
func init() {
registry.Register("ldap", New)
}
type mgr struct {
c *config
ldapClient ldap.Client
}
type config struct {
utils.LDAPConn `mapstructure:",squash"`
LDAPIdentity ldapIdentity.Identity `mapstructure:",squash"`
Idp string `mapstructure:"idp"`
GatewaySvc string `mapstructure:"gatewaysvc"`
Nobody int64 `mapstructure:"nobody"`
LoginAttributes []string `mapstructure:"login_attributes"`
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{
LDAPIdentity: ldapIdentity.New(),
LoginAttributes: []string{"cn"},
}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
return c, nil
}
// New returns an auth manager implementation that connects to a LDAP server to validate the user.
func New(m map[string]interface{}) (auth.Manager, error) {
manager := &mgr{}
err := manager.Configure(m)
if err != nil {
return nil, err
}
manager.ldapClient, err = utils.GetLDAPClientWithReconnect(&manager.c.LDAPConn)
if err != nil {
return nil, err
}
return manager, nil
}
func (am *mgr) Configure(m map[string]interface{}) error {
c, err := parseConfig(m)
if err != nil {
return err
}
if c.Nobody == 0 {
c.Nobody = 99
}
if err = c.LDAPIdentity.Setup(); err != nil {
return fmt.Errorf("error setting up Identity config: %w", err)
}
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
am.c = c
return nil
}
func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, map[string]*authpb.Scope, error) {
log := appctx.GetLogger(ctx)
filter := am.getLoginFilter(clientID)
userEntry, err := am.c.LDAPIdentity.GetLDAPUserByFilter(ctx, am.ldapClient, filter)
if err != nil {
return nil, nil, err
}
// Bind as the user to verify their password
la, err := utils.GetLDAPClientForAuth(ctx, &am.c.LDAPConn)
if err != nil {
return nil, nil, err
}
defer la.Close()
err = la.Bind(userEntry.DN, clientSecret)
switch {
case err == nil:
break
case ldap.IsErrorWithCode(err, ldap.LDAPResultInvalidCredentials):
return nil, nil, errtypes.InvalidCredentials(clientID)
default:
log.Debug().Err(err).Interface("userdn", userEntry.DN).Msg("bind with user credentials failed")
return nil, nil, err
}
var uid string
if am.c.LDAPIdentity.User.Schema.IDIsOctetString {
rawValue := userEntry.GetEqualFoldRawAttributeValue(am.c.LDAPIdentity.User.Schema.ID)
if value, err := uuid.FromBytes(rawValue); err == nil {
uid = value.String()
}
} else {
uid = userEntry.GetEqualFoldAttributeValue(am.c.LDAPIdentity.User.Schema.ID)
}
userID := &user.UserId{
Idp: am.c.Idp,
OpaqueId: uid,
Type: am.c.LDAPIdentity.GetUserType(userEntry),
}
gwc, err := pool.GetGatewayServiceClient(am.c.GatewaySvc)
if err != nil {
return nil, nil, errors.Wrap(err, "ldap: error getting gateway grpc client")
}
getGroupsResp, err := gwc.GetUserGroups(ctx, &user.GetUserGroupsRequest{
UserId: userID,
})
if err != nil {
log.Warn().Err(err).Msg("error getting user groups")
return nil, nil, errors.Wrap(err, "ldap: error getting user groups")
}
if getGroupsResp.Status.Code != rpc.Code_CODE_OK {
log.Warn().Err(err).Str("msg", getGroupsResp.Status.Message).Msg("grpc getting user groups failed")
return nil, nil, fmt.Errorf("ldap: grpc getting user groups failed: '%s'", getGroupsResp.Status.Message)
}
gidNumber := am.c.Nobody
gidValue := userEntry.GetEqualFoldAttributeValue(am.c.LDAPIdentity.User.Schema.GIDNumber)
if gidValue != "" {
gidNumber, err = strconv.ParseInt(gidValue, 10, 64)
if err != nil {
return nil, nil, err
}
}
uidNumber := am.c.Nobody
uidValue := userEntry.GetEqualFoldAttributeValue(am.c.LDAPIdentity.User.Schema.UIDNumber)
if uidValue != "" {
uidNumber, err = strconv.ParseInt(uidValue, 10, 64)
if err != nil {
return nil, nil, err
}
}
u := &user.User{
Id: userID,
// TODO add more claims from the StandardClaims, eg EmailVerified
Username: userEntry.GetEqualFoldAttributeValue(am.c.LDAPIdentity.User.Schema.Username),
// TODO groups
Groups: getGroupsResp.Groups,
Mail: userEntry.GetEqualFoldAttributeValue(am.c.LDAPIdentity.User.Schema.Mail),
DisplayName: userEntry.GetEqualFoldAttributeValue(am.c.LDAPIdentity.User.Schema.DisplayName),
UidNumber: uidNumber,
GidNumber: gidNumber,
}
var scopes map[string]*authpb.Scope
if userID != nil && userID.Type == user.UserType_USER_TYPE_LIGHTWEIGHT {
scopes, err = scope.AddLightweightAccountScope(authpb.Role_ROLE_OWNER, nil)
if err != nil {
return nil, nil, err
}
} else {
scopes, err = scope.AddOwnerScope(nil)
if err != nil {
return nil, nil, err
}
}
log.Debug().Interface("entry", userEntry).Interface("user", u).Msg("authenticated user")
return u, scopes, nil
}
func (am *mgr) getLoginFilter(login string) string {
var filter string
for _, attr := range am.c.LoginAttributes {
filter = fmt.Sprintf("%s(%s=%s)", filter, attr, ldap.EscapeFilter(login))
}
return fmt.Sprintf("(&%s(objectclass=%s)(|%s))",
am.c.LDAPIdentity.User.Filter,
am.c.LDAPIdentity.User.Objectclass,
filter,
)
}
@@ -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 loader
import (
// Load core authentication managers.
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/appauth"
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/demo"
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/impersonator"
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/json"
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/ldap"
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/machine"
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/ocmshares"
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/oidc"
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/publicshares"
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/serviceaccounts"
// Add your own here
)
@@ -0,0 +1,125 @@
// 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 machine
import (
"context"
"strings"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/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/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
// 'machine' is an authentication method used to impersonate users.
// To impersonate the given user it's only needed an api-key, saved
// in a config file.
// supported claims
var claims = []string{"mail", "uid", "username", "gid", "userid"}
type manager struct {
APIKey string `mapstructure:"api_key"`
GatewayAddr string `mapstructure:"gateway_addr"`
}
func init() {
registry.Register("machine", New)
}
// Configure parses the map conf
func (m *manager) Configure(conf map[string]interface{}) error {
err := mapstructure.Decode(conf, m)
if err != nil {
return errors.Wrap(err, "error decoding conf")
}
return nil
}
// New creates a new manager for the 'machine' authentication
func New(conf map[string]interface{}) (auth.Manager, error) {
m := &manager{}
err := m.Configure(conf)
if err != nil {
return nil, err
}
return m, nil
}
// Authenticate impersonate an user if the provided secret is equal to the api-key
func (m *manager) Authenticate(ctx context.Context, user, secret string) (*userpb.User, map[string]*authpb.Scope, error) {
if m.APIKey != secret {
return nil, nil, errtypes.InvalidCredentials("")
}
gtw, err := pool.GetGatewayServiceClient(m.GatewayAddr)
if err != nil {
return nil, nil, err
}
// username could be either a normal username or a string <claim>:<value>
// in the first case the claim is "username"
claim, value := parseUser(user)
userResponse, err := gtw.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{
Claim: claim,
Value: value,
})
switch {
case err != nil:
return nil, nil, err
case userResponse.Status.Code == rpc.Code_CODE_NOT_FOUND:
return nil, nil, errtypes.NotFound(userResponse.Status.Message)
case userResponse.Status.Code != rpc.Code_CODE_OK:
return nil, nil, errtypes.InternalError(userResponse.Status.Message)
}
scope, err := scope.AddOwnerScope(nil)
if err != nil {
return nil, nil, err
}
return userResponse.GetUser(), scope, nil
}
func contains(lst []string, s string) bool {
for _, e := range lst {
if e == s {
return true
}
}
return false
}
func parseUser(user string) (string, string) {
s := strings.SplitN(user, ":", 2)
if len(s) == 2 && contains(claims, s[0]) {
return s[0], s[1]
}
return "username", user
}
@@ -0,0 +1,196 @@
// 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 (
"context"
provider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocminvite "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/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/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/opencloud-eu/reva/v2/pkg/utils/cfg"
"github.com/pkg/errors"
)
func init() {
registry.Register("ocmshares", New)
}
type manager struct {
c *config
gw gateway.GatewayAPIClient
}
type config struct {
GatewayAddr string `mapstructure:"gatewaysvc"`
}
func (c *config) ApplyDefaults() {
c.GatewayAddr = sharedconf.GetGatewaySVC(c.GatewayAddr)
}
// New creates a new ocmshares authentication manager.
func New(m map[string]interface{}) (auth.Manager, error) {
var mgr manager
if err := mgr.Configure(m); err != nil {
return nil, err
}
gw, err := pool.GetGatewayServiceClient(mgr.c.GatewayAddr)
if err != nil {
return nil, err
}
mgr.gw = gw
return &mgr, nil
}
func (m *manager) Configure(ml map[string]interface{}) error {
var c config
if err := cfg.Decode(ml, &c); err != nil {
return errors.Wrap(err, "ocmshares: error decoding config")
}
m.c = &c
return nil
}
func (m *manager) Authenticate(ctx context.Context, ocmshare, sharedSecret string) (*userpb.User, map[string]*authpb.Scope, error) {
log := appctx.GetLogger(ctx).With().Str("ocmshare", ocmshare).Logger()
// We need to use GetOCMShareByToken, as GetOCMShare would require a user in the context
shareRes, err := m.gw.GetOCMShareByToken(ctx, &ocm.GetOCMShareByTokenRequest{
Token: sharedSecret,
})
switch {
case err != nil:
log.Error().Err(err).Msg("error getting ocm share by token")
return nil, nil, err
case shareRes.Status.Code == rpc.Code_CODE_NOT_FOUND:
log.Debug().Msg("ocm share not found")
return nil, nil, errtypes.NotFound(shareRes.Status.Message)
case shareRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED:
log.Debug().Msg("permission denied")
return nil, nil, errtypes.InvalidCredentials(shareRes.Status.Message)
case shareRes.Status.Code != rpc.Code_CODE_OK:
log.Error().Interface("status", shareRes.Status).Msg("got unexpected error in the grpc call to GetOCMShare")
return nil, nil, errtypes.InternalError(shareRes.Status.Message)
}
// compare ocm share id
if shareRes.GetShare().GetId().GetOpaqueId() != ocmshare {
log.Error().Str("persisted", ocmshare).Str("requested", shareRes.GetShare().GetId().GetOpaqueId()).Msg("mismatching ocm share id for existing secret")
return nil, nil, errtypes.InvalidCredentials("invalid shared secret")
}
// the user authenticated using the ocmshares authentication method
// is the recipient of the share
u := shareRes.Share.Grantee.GetUserId()
d, err := utils.MarshalProtoV1ToJSON(shareRes.GetShare().Creator)
if err != nil {
return nil, nil, err
}
o := &types.Opaque{
Map: map[string]*types.OpaqueEntry{
"user-filter": {
Decoder: "json",
Value: d,
},
},
}
userRes, err := m.gw.GetAcceptedUser(ctx, &ocminvite.GetAcceptedUserRequest{
RemoteUserId: u,
Opaque: o,
})
switch {
case err != nil:
return nil, nil, err
case userRes.Status.Code == rpc.Code_CODE_NOT_FOUND:
return nil, nil, errtypes.NotFound(shareRes.Status.Message)
case userRes.Status.Code != rpc.Code_CODE_OK:
return nil, nil, errtypes.InternalError(userRes.Status.Message)
}
role, roleStr := getRole(shareRes.Share)
scope, err := scope.AddOCMShareScope(shareRes.Share, role, nil)
if err != nil {
return nil, nil, err
}
user := userRes.RemoteUser
user.Opaque = &types.Opaque{
Map: map[string]*types.OpaqueEntry{
"ocm-share-role": {
Decoder: "plain",
Value: []byte(roleStr),
},
},
}
user.Opaque = utils.AppendJSONToOpaque(user.Opaque, "impersonating-user", userRes.RemoteUser)
return user, scope, nil
}
func getRole(s *ocm.Share) (authpb.Role, string) {
// TODO: consider to somehow merge the permissions from all the access methods?
// it's not clear infact which should be the role when webdav is editor role while
// webapp is only view mode for example
// this implementation considers only the simple case in which when a client creates
// a share with multiple access methods, the permissions are matching in all of them.
for _, m := range s.AccessMethods {
switch v := m.Term.(type) {
case *ocm.AccessMethod_WebdavOptions:
p := v.WebdavOptions.Permissions
if p.InitiateFileUpload {
return authpb.Role_ROLE_EDITOR, "editor"
}
if p.InitiateFileDownload {
return authpb.Role_ROLE_VIEWER, "viewer"
}
case *ocm.AccessMethod_WebappOptions:
viewMode := v.WebappOptions.ViewMode
if viewMode == provider.ViewMode_VIEW_MODE_VIEW_ONLY ||
viewMode == provider.ViewMode_VIEW_MODE_READ_ONLY ||
viewMode == provider.ViewMode_VIEW_MODE_PREVIEW {
return authpb.Role_ROLE_VIEWER, "viewer"
}
if viewMode == provider.ViewMode_VIEW_MODE_READ_WRITE {
return authpb.Role_ROLE_EDITOR, "editor"
}
}
}
return authpb.Role_ROLE_INVALID, "invalid"
}
@@ -0,0 +1,363 @@
// 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 oidc verifies an OIDC token against the configured OIDC provider
// and obtains the necessary claims to obtain user information.
package oidc
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"time"
oidc "github.com/coreos/go-oidc/v3/oidc"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/juliangruber/go-intersect"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
"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"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/pkg/errors"
"golang.org/x/oauth2"
)
func init() {
registry.Register("oidc", New)
}
type mgr struct {
provider *oidc.Provider // cached on first request
c *config
oidcUsersMapping map[string]*oidcUserMapping
}
type config struct {
Insecure bool `mapstructure:"insecure" docs:"false;Whether to skip certificate checks when sending requests."`
Issuer string `mapstructure:"issuer" docs:";The issuer of the OIDC token."`
IDClaim string `mapstructure:"id_claim" docs:"sub;The claim containing the ID of the user."`
UIDClaim string `mapstructure:"uid_claim" docs:";The claim containing the UID of the user."`
GIDClaim string `mapstructure:"gid_claim" docs:";The claim containing the GID of the user."`
GatewaySvc string `mapstructure:"gatewaysvc" docs:";The endpoint at which the GRPC gateway is exposed."`
UsersMapping string `mapstructure:"users_mapping" docs:"; The optional OIDC users mapping file path"`
GroupClaim string `mapstructure:"group_claim" docs:"; The group claim to be looked up to map the user (default to 'groups')."`
}
type oidcUserMapping struct {
OIDCIssuer string `mapstructure:"oidc_issuer" json:"oidc_issuer"`
OIDCGroup string `mapstructure:"oidc_group" json:"oidc_group"`
Username string `mapstructure:"username" json:"username"`
}
func (c *config) init() {
if c.IDClaim == "" {
// sub is stable and defined as unique. the user manager needs to take care of the sub to user metadata lookup
c.IDClaim = "sub"
}
if c.GroupClaim == "" {
c.GroupClaim = "groups"
}
if c.UIDClaim == "" {
c.UIDClaim = "uid"
}
if c.GIDClaim == "" {
c.GIDClaim = "gid"
}
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
}
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 an auth manager implementation that verifies the oidc token and obtains the user claims.
func New(m map[string]interface{}) (auth.Manager, error) {
manager := &mgr{}
err := manager.Configure(m)
if err != nil {
return nil, err
}
return manager, nil
}
func (am *mgr) Configure(m map[string]interface{}) error {
c, err := parseConfig(m)
if err != nil {
return err
}
c.init()
am.c = c
am.oidcUsersMapping = map[string]*oidcUserMapping{}
if c.UsersMapping == "" {
// no mapping defined, leave the map empty and move on
return nil
}
f, err := os.ReadFile(c.UsersMapping)
if err != nil {
return fmt.Errorf("oidc: error reading the users mapping file: +%v", err)
}
oidcUsers := []*oidcUserMapping{}
err = json.Unmarshal(f, &oidcUsers)
if err != nil {
return fmt.Errorf("oidc: error unmarshalling the users mapping file: +%v", err)
}
for _, u := range oidcUsers {
if _, found := am.oidcUsersMapping[u.OIDCGroup]; found {
return fmt.Errorf("oidc: mapping error, group \"%s\" is mapped to multiple users", u.OIDCGroup)
}
am.oidcUsersMapping[u.OIDCGroup] = u
}
return nil
}
// The clientID would be empty as we only need to validate the clientSecret variable
// which contains the access token that we can use to contact the UserInfo endpoint
// and get the user claims.
func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, map[string]*authpb.Scope, error) {
ctx = am.getOAuthCtx(ctx)
log := appctx.GetLogger(ctx)
oidcProvider, err := am.getOIDCProvider(ctx)
if err != nil {
return nil, nil, fmt.Errorf("oidc: error creating oidc provider: +%v", err)
}
oauth2Token := &oauth2.Token{
AccessToken: clientSecret,
}
// query the oidc provider for user info
userInfo, err := oidcProvider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token))
if err != nil {
return nil, nil, fmt.Errorf("oidc: error getting userinfo: +%v", err)
}
// claims contains the standard OIDC claims like iss, iat, aud, ... and any other non-standard one.
// TODO(labkode): make claims configuration dynamic from the config file so we can add arbitrary mappings from claims to user struct.
// For now, only the group claim is dynamic.
// TODO(labkode): may do like K8s does it: https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go
var claims map[string]interface{}
if err := userInfo.Claims(&claims); err != nil {
return nil, nil, fmt.Errorf("oidc: error unmarshaling userinfo claims: %v", err)
}
log.Debug().Interface("claims", claims).Interface("userInfo", userInfo).Msg("unmarshalled userinfo")
if claims["iss"] == nil { // This is not set in simplesamlphp
claims["iss"] = am.c.Issuer
}
if claims["email_verified"] == nil { // This is not set in simplesamlphp
claims["email_verified"] = false
}
if claims["preferred_username"] == nil {
claims["preferred_username"] = claims[am.c.IDClaim]
}
if claims["preferred_username"] == nil {
claims["preferred_username"] = claims["email"]
}
if claims["name"] == nil {
claims["name"] = claims[am.c.IDClaim]
}
if claims["name"] == nil {
return nil, nil, fmt.Errorf("no \"name\" attribute found in userinfo: maybe the client did not request the oidc \"profile\"-scope")
}
if claims["email"] == nil {
return nil, nil, fmt.Errorf("no \"email\" attribute found in userinfo: maybe the client did not request the oidc \"email\"-scope")
}
uid, _ := claims[am.c.UIDClaim].(float64)
claims[am.c.UIDClaim] = int64(uid) // in case the uid claim is missing and a mapping is to be performed, resolveUser() will populate it
// Note that if not, will silently carry a user with 0 uid, potentially problematic with storage providers
gid, _ := claims[am.c.GIDClaim].(float64)
claims[am.c.GIDClaim] = int64(gid)
err = am.resolveUser(ctx, claims)
if err != nil {
return nil, nil, errors.Wrapf(err, "oidc: error resolving username for external user '%v'", claims["email"])
}
userID := &user.UserId{
OpaqueId: claims[am.c.IDClaim].(string), // a stable non reassignable id
Idp: claims["iss"].(string), // in the scope of this issuer
Type: getUserType(claims[am.c.IDClaim].(string)),
}
gwc, err := pool.GetGatewayServiceClient(am.c.GatewaySvc)
if err != nil {
return nil, nil, errors.Wrap(err, "oidc: error getting gateway grpc client")
}
getGroupsResp, err := gwc.GetUserGroups(ctx, &user.GetUserGroupsRequest{
UserId: userID,
})
if err != nil {
return nil, nil, errors.Wrapf(err, "oidc: error getting user groups for '%+v'", userID)
}
if getGroupsResp.Status.Code != rpc.Code_CODE_OK {
return nil, nil, status.NewErrorFromCode(getGroupsResp.Status.Code, "oidc")
}
u := &user.User{
Id: userID,
Username: claims["preferred_username"].(string),
Groups: getGroupsResp.Groups,
Mail: claims["email"].(string),
MailVerified: claims["email_verified"].(bool),
DisplayName: claims["name"].(string),
UidNumber: claims[am.c.UIDClaim].(int64),
GidNumber: claims[am.c.GIDClaim].(int64),
}
var scopes map[string]*authpb.Scope
if userID != nil && (userID.Type == user.UserType_USER_TYPE_LIGHTWEIGHT || userID.Type == user.UserType_USER_TYPE_FEDERATED) {
scopes, err = scope.AddLightweightAccountScope(authpb.Role_ROLE_OWNER, nil)
if err != nil {
return nil, nil, err
}
} else {
scopes, err = scope.AddOwnerScope(nil)
if err != nil {
return nil, nil, err
}
}
return u, scopes, nil
}
func (am *mgr) getOAuthCtx(ctx context.Context) context.Context {
// Sometimes for testing we need to skip the TLS check, that's why we need a
// custom HTTP client.
customHTTPClient := rhttp.GetHTTPClient(
rhttp.Context(ctx),
rhttp.Timeout(time.Second*10),
rhttp.Insecure(am.c.Insecure),
// Fixes connection fd leak which might be caused by provider-caching
rhttp.DisableKeepAlive(true),
)
ctx = context.WithValue(ctx, oauth2.HTTPClient, customHTTPClient)
return ctx
}
// getOIDCProvider returns a singleton OIDC provider
func (am *mgr) getOIDCProvider(ctx context.Context) (*oidc.Provider, error) {
ctx = am.getOAuthCtx(ctx)
log := appctx.GetLogger(ctx)
if am.provider != nil {
return am.provider, nil
}
// Initialize a provider by specifying the issuer URL.
// Once initialized this is a singleton that is reused for further requests.
// The provider is responsible to verify the token sent by the client
// against the security keys oftentimes available in the .well-known endpoint.
provider, err := oidc.NewProvider(ctx, am.c.Issuer)
if err != nil {
log.Error().Err(err).Msg("oidc: error creating a new oidc provider")
return nil, fmt.Errorf("oidc: error creating a new oidc provider: %+v", err)
}
am.provider = provider
return am.provider, nil
}
func (am *mgr) resolveUser(ctx context.Context, claims map[string]interface{}) error {
if len(am.oidcUsersMapping) > 0 {
var username string
// map and discover the user's username when a mapping is defined
if claims[am.c.GroupClaim] == nil {
// we are required to perform a user mapping but the group claim is not available
return fmt.Errorf("no \"%s\" claim found in userinfo to map user", am.c.GroupClaim)
}
mappings := make([]string, 0, len(am.oidcUsersMapping))
for _, m := range am.oidcUsersMapping {
if m.OIDCIssuer == claims["iss"] {
mappings = append(mappings, m.OIDCGroup)
}
}
intersection := intersect.Simple(claims[am.c.GroupClaim], mappings)
if len(intersection) > 1 {
// multiple mappings are not implemented as we cannot decide which one to choose
return errtypes.PermissionDenied("more than one user mapping entry exists for the given group claims")
}
if len(intersection) == 0 {
return errtypes.PermissionDenied("no user mapping found for the given group claim(s)")
}
for _, m := range intersection {
username = am.oidcUsersMapping[m.(string)].Username
}
upsc, err := pool.GetUserProviderServiceClient(am.c.GatewaySvc)
if err != nil {
return errors.Wrap(err, "error getting user provider grpc client")
}
getUserByClaimResp, err := upsc.GetUserByClaim(ctx, &user.GetUserByClaimRequest{
Claim: "username",
Value: username,
})
if err != nil {
return errors.Wrapf(err, "error getting user by username '%v'", username)
}
if getUserByClaimResp.Status.Code != rpc.Code_CODE_OK {
return status.NewErrorFromCode(getUserByClaimResp.Status.Code, "oidc")
}
// take the properties of the mapped target user to override the claims
claims["preferred_username"] = username
claims[am.c.IDClaim] = getUserByClaimResp.GetUser().GetId().OpaqueId
claims["iss"] = getUserByClaimResp.GetUser().GetId().Idp
claims[am.c.UIDClaim] = getUserByClaimResp.GetUser().UidNumber
claims[am.c.GIDClaim] = getUserByClaimResp.GetUser().GidNumber
appctx.GetLogger(ctx).Debug().Str("username", username).Interface("claims", claims).Msg("resolveUser: claims overridden from mapped user")
}
return nil
}
func getUserType(upn string) user.UserType {
var t user.UserType
switch {
case strings.HasPrefix(upn, "guest"):
t = user.UserType_USER_TYPE_LIGHTWEIGHT
case strings.Contains(upn, "@"):
t = user.UserType_USER_TYPE_FEDERATED
default:
t = user.UserType_USER_TYPE_PRIMARY
}
return t
}
@@ -0,0 +1,183 @@
// 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 publicshares
import (
"context"
"strings"
"time"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
"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/utils"
"github.com/pkg/errors"
)
func init() {
registry.Register("publicshares", New)
}
type manager struct {
c *config
}
type config struct {
GatewayAddr string `mapstructure:"gateway_addr"`
}
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 auth Manager.
func New(m map[string]interface{}) (auth.Manager, error) {
mgr := &manager{}
err := mgr.Configure(m)
if err != nil {
return nil, err
}
return mgr, nil
}
func (m *manager) Configure(ml map[string]interface{}) error {
conf, err := parseConfig(ml)
if err != nil {
return err
}
m.c = conf
return nil
}
func (m *manager) Authenticate(ctx context.Context, token, secret string) (*user.User, map[string]*authpb.Scope, error) {
gwConn, err := pool.GetGatewayServiceClient(m.c.GatewayAddr)
if err != nil {
return nil, nil, err
}
var auth *link.PublicShareAuthentication
if strings.HasPrefix(secret, "password|") {
secret = strings.TrimPrefix(secret, "password|")
auth = &link.PublicShareAuthentication{
Spec: &link.PublicShareAuthentication_Password{
Password: secret,
},
}
} else if strings.HasPrefix(secret, "signature|") {
secret = strings.TrimPrefix(secret, "signature|")
parts := strings.Split(secret, "|")
sig, expiration := parts[0], parts[1]
exp, _ := time.Parse(time.RFC3339, expiration)
auth = &link.PublicShareAuthentication{
Spec: &link.PublicShareAuthentication_Signature{
Signature: &link.ShareSignature{
Signature: sig,
SignatureExpiration: &types.Timestamp{
Seconds: uint64(exp.UnixNano() / 1000000000),
Nanos: uint32(exp.UnixNano() % 1000000000),
},
},
},
}
}
publicShareResponse, err := gwConn.GetPublicShareByToken(ctx, &link.GetPublicShareByTokenRequest{
Token: token,
Authentication: auth,
Sign: true,
})
switch {
case err != nil:
return nil, nil, err
case publicShareResponse.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND:
return nil, nil, errtypes.NotFound(publicShareResponse.Status.Message)
case publicShareResponse.Status.Code == rpcv1beta1.Code_CODE_PERMISSION_DENIED:
return nil, nil, errtypes.InvalidCredentials(publicShareResponse.Status.Message)
case publicShareResponse.Status.Code != rpcv1beta1.Code_CODE_OK:
return nil, nil, errtypes.InternalError(publicShareResponse.Status.Message)
}
var owner *user.User
// FIXME use new user type SPACE_OWNER
if publicShareResponse.GetShare().GetOwner().GetType() == 8 {
owner = &user.User{Id: publicShareResponse.GetShare().GetOwner(), DisplayName: "Public", Username: "public"}
} else {
getUserResponse, err := gwConn.GetUser(ctx, &user.GetUserRequest{
UserId: publicShareResponse.GetShare().GetCreator(),
})
switch {
case err != nil:
return nil, nil, err
case getUserResponse.GetStatus().GetCode() == rpcv1beta1.Code_CODE_NOT_FOUND:
return nil, nil, errtypes.NotFound(getUserResponse.GetStatus().GetMessage())
case getUserResponse.GetStatus().GetCode() == rpcv1beta1.Code_CODE_PERMISSION_DENIED:
return nil, nil, errtypes.InvalidCredentials(getUserResponse.GetStatus().GetMessage())
case getUserResponse.GetStatus().GetCode() != rpcv1beta1.Code_CODE_OK:
return nil, nil, errtypes.InternalError(getUserResponse.GetStatus().GetMessage())
}
owner = getUserResponse.GetUser()
}
share := publicShareResponse.GetShare()
role := authpb.Role_ROLE_VIEWER
roleStr := "viewer"
if share.Permissions.Permissions.InitiateFileUpload && !share.Permissions.Permissions.InitiateFileDownload {
role = authpb.Role_ROLE_UPLOADER
roleStr = "uploader"
} else if share.Permissions.Permissions.InitiateFileUpload {
role = authpb.Role_ROLE_EDITOR
roleStr = "editor"
}
scope, err := scope.AddPublicShareScope(share, role, nil)
if err != nil {
return nil, nil, err
}
owner.Opaque = &types.Opaque{
Map: map[string]*types.OpaqueEntry{
"public-share-role": {
Decoder: "plain",
Value: []byte(roleStr),
},
},
}
u := &user.User{Id: &user.UserId{OpaqueId: token, Idp: "public", Type: user.UserType_USER_TYPE_GUEST}, DisplayName: "Public", Username: "public"}
owner.Opaque = utils.AppendJSONToOpaque(owner.Opaque, "impersonating-user", u)
return owner, scope, nil
}
// ErrPasswordNotProvided is returned when the public share is password protected, but there was no password on the request
var ErrPasswordNotProvided = errors.New("public share is password protected, but password was not provided")
@@ -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"
)
// NewFunc is the function that auth implementations
// should register to at init time.
type NewFunc func(map[string]interface{}) (auth.Manager, error)
// NewFuncs is a map containing all the registered auth managers.
var NewFuncs = map[string]NewFunc{}
// Register registers a new auth manager new function.
// Not safe for concurrent use. Safe for use from package init.
func Register(name string, f NewFunc) {
NewFuncs[name] = f
}
@@ -0,0 +1,90 @@
package serviceaccounts
import (
"context"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
"github.com/pkg/errors"
)
type conf struct {
ServiceUsers []serviceuser `mapstructure:"service_accounts"`
}
type serviceuser struct {
ID string `mapstructure:"id"`
Secret string `mapstructure:"secret"`
}
type manager struct {
authenticate func(userID, secret string) error
}
func init() {
registry.Register("serviceaccounts", New)
}
// Configure parses the map conf
func (m *manager) Configure(config map[string]interface{}) error {
c := &conf{}
if err := mapstructure.Decode(config, c); err != nil {
return errors.Wrap(err, "error decoding conf")
}
// only inmem authenticator for now
a := &inmemAuthenticator{make(map[string]string)}
for _, s := range c.ServiceUsers {
a.m[s.ID] = s.Secret
}
m.authenticate = a.Authenticate
return nil
}
// New creates a new manager for the 'service' authentication
func New(conf map[string]interface{}) (auth.Manager, error) {
m := &manager{}
err := m.Configure(conf)
if err != nil {
return nil, err
}
return m, nil
}
// Authenticate authenticates the service account
func (m *manager) Authenticate(ctx context.Context, userID string, secret string) (*userpb.User, map[string]*authpb.Scope, error) {
if err := m.authenticate(userID, secret); err != nil {
return nil, nil, err
}
scope, err := scope.AddOwnerScope(nil)
if err != nil {
return nil, nil, err
}
return &userpb.User{
// TODO: more details for service users?
Id: &userpb.UserId{
OpaqueId: userID,
Type: userpb.UserType_USER_TYPE_SERVICE,
Idp: "none",
},
}, scope, nil
}
type inmemAuthenticator struct {
m map[string]string
}
func (a *inmemAuthenticator) Authenticate(userID string, secret string) error {
if secret == "" || a.m[userID] == "" {
return errors.New("unknown user")
}
if a.m[userID] == secret {
return nil
}
return errors.New("secrets do not match")
}
@@ -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 storage broker drivers.
_ "github.com/opencloud-eu/reva/v2/pkg/auth/registry/static"
// Add your own here
)
@@ -0,0 +1,34 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package registry
import "github.com/opencloud-eu/reva/v2/pkg/auth"
// NewFunc is the function that auth registry implementations
// should register at init time.
type NewFunc func(map[string]interface{}) (auth.Registry, error)
// NewFuncs is a map containing all the registered auth registries.
var NewFuncs = map[string]NewFunc{}
// Register registers a new auth registry new function.
// Not safe for concurrent use. Safe for use from package init.
func Register(name string, f NewFunc) {
NewFuncs[name] = f
}
@@ -0,0 +1,91 @@
// 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 static
import (
"context"
registrypb "github.com/cs3org/go-cs3apis/cs3/auth/registry/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/registry/registry"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
)
func init() {
registry.Register("static", New)
}
type config struct {
Rules map[string]string `mapstructure:"rules"`
}
func (c *config) init() {
if len(c.Rules) == 0 {
c.Rules = map[string]string{
"basic": sharedconf.GetGatewaySVC(""),
}
}
}
type reg struct {
rules map[string]string
}
func (r *reg) ListProviders(ctx context.Context) ([]*registrypb.ProviderInfo, error) {
providers := make([]*registrypb.ProviderInfo, 0, len(r.rules))
for k, v := range r.rules {
providers = append(providers, &registrypb.ProviderInfo{
ProviderType: k,
Address: v,
})
}
return providers, nil
}
func (r *reg) GetProvider(ctx context.Context, authType string) (*registrypb.ProviderInfo, error) {
for k, v := range r.rules {
if k == authType {
return &registrypb.ProviderInfo{
ProviderType: k,
Address: v,
}, nil
}
}
return nil, errtypes.NotFound("static: auth type not found: " + authType)
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
return c, nil
}
// New returns an implementation of the auth.Registry interface.
func New(m map[string]interface{}) (auth.Registry, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
return &reg{rules: c.Rules}, nil
}
+118
View File
@@ -0,0 +1,118 @@
// 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"
"net/rpc"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
hcplugin "github.com/hashicorp/go-plugin"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/plugin"
)
func init() {
plugin.Register("authprovider", &ProviderPlugin{})
}
// ProviderPlugin is the implementation of plugin.Plugin so we can serve/consume this.
type ProviderPlugin struct {
Impl Manager
}
// Server returns the RPC Server which serves the methods that the Client calls over net/rpc
func (p *ProviderPlugin) Server(*hcplugin.MuxBroker) (interface{}, error) {
return &RPCServer{Impl: p.Impl}, nil
}
// Client returns interface implementation for the plugin that communicates to the server end of the plugin
func (p *ProviderPlugin) Client(b *hcplugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &RPCClient{Client: c}, nil
}
// RPCClient is an implementation of Manager that talks over RPC.
type RPCClient struct{ Client *rpc.Client }
// ConfigureArg for RPC
type ConfigureArg struct {
Ml map[string]interface{}
}
// ConfigureReply for RPC
type ConfigureReply struct {
Err error
}
// Configure RPCClient configure method
func (m *RPCClient) Configure(ml map[string]interface{}) error {
args := ConfigureArg{Ml: ml}
resp := ConfigureReply{}
err := m.Client.Call("Plugin.Configure", args, &resp)
if err != nil {
return err
}
return resp.Err
}
// AuthenticateArgs for RPC
type AuthenticateArgs struct {
Ctx map[interface{}]interface{}
ClientID string
ClientSecret string
}
// AuthenticateReply for RPC
type AuthenticateReply struct {
User *user.User
Auth map[string]*authpb.Scope
Error error
}
// Authenticate RPCClient Authenticate method
func (m *RPCClient) Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, map[string]*authpb.Scope, error) {
ctxVal := appctx.GetKeyValuesFromCtx(ctx)
args := AuthenticateArgs{Ctx: ctxVal, ClientID: clientID, ClientSecret: clientSecret}
reply := AuthenticateReply{}
err := m.Client.Call("Plugin.Authenticate", args, &reply)
if err != nil {
return nil, nil, err
}
return reply.User, reply.Auth, reply.Error
}
// RPCServer is the server that RPCClient talks to, conforming to the requirements of net/rpc
type RPCServer struct {
// This is the real implementation
Impl Manager
}
// Configure RPCServer Configure method
func (m *RPCServer) Configure(args ConfigureArg, resp *ConfigureReply) error {
resp.Err = m.Impl.Configure(args.Ml)
return nil
}
// Authenticate RPCServer Authenticate method
func (m *RPCServer) Authenticate(args AuthenticateArgs, resp *AuthenticateReply) error {
ctx := appctx.PutKeyValuesToCtx(args.Ctx)
resp.User, resp.Auth, resp.Error = m.Impl.Authenticate(ctx, args.ClientID, args.ClientSecret)
return nil
}
@@ -0,0 +1,91 @@
// 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 scope
import (
"context"
"strings"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/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/utils"
"github.com/rs/zerolog"
)
func lightweightAccountScope(_ context.Context, scope *authpb.Scope, resource interface{}, _ *zerolog.Logger) (bool, error) {
// Lightweight accounts have access to resources shared with them.
// These cannot be resolved from here, but need to be added to the scope from
// where the call to mint tokens is made.
// From here, we only allow ListReceivedShares calls
switch v := resource.(type) {
case *collaboration.ListReceivedSharesRequest:
return true, nil
case string:
return checkLightweightPath(v), nil
}
return false, nil
}
func checkLightweightPath(path string) bool {
paths := []string{
"/ocs/v2.php/apps/files_sharing/api/v1/shares",
"/ocs/v1.php/apps/files_sharing/api/v1/shares",
"/ocs/v2.php/apps/files_sharing//api/v1/shares",
"/ocs/v1.php/apps/files_sharing//api/v1/shares",
"/ocs/v2.php/cloud/capabilities",
"/ocs/v1.php/cloud/capabilities",
"/ocs/v2.php/cloud/user",
"/ocs/v1.php/cloud/user",
"/remote.php/webdav",
"/remote.php/dav/files",
"/app/open",
"/app/new",
"/archiver",
"/dataprovider",
"/data",
}
for _, p := range paths {
if strings.HasPrefix(path, p) {
return true
}
}
return false
}
// AddLightweightAccountScope adds the scope to allow access to lightweight user.
func AddLightweightAccountScope(role authpb.Role, scopes map[string]*authpb.Scope) (map[string]*authpb.Scope, error) {
ref := &provider.Reference{Path: "/"}
val, err := utils.MarshalProtoV1ToJSON(ref)
if err != nil {
return nil, err
}
if scopes == nil {
scopes = make(map[string]*authpb.Scope)
}
scopes["lightweight"] = &authpb.Scope{
Resource: &types.OpaqueEntry{
Decoder: "json",
Value: val,
},
Role: role,
}
return scopes, nil
}
@@ -0,0 +1,201 @@
// 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 scope
import (
"context"
"path/filepath"
"strings"
appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/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"
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/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/rs/zerolog"
)
// FIXME: the namespace here is hardcoded
// find a way to pass it from the config.
const ocmNamespace = "/ocm"
func ocmShareScope(_ context.Context, scope *authpb.Scope, resource interface{}, _ *zerolog.Logger) (bool, error) {
var share ocmv1beta1.Share
if err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &share); err != nil {
return false, err
}
switch v := resource.(type) {
// viewer role
case *registry.ListStorageProvidersRequest:
ref := &provider.Reference{}
if v.Opaque != nil && v.Opaque.Map != nil {
if e, ok := v.Opaque.Map["storage_id"]; ok {
if ref.ResourceId == nil {
ref.ResourceId = &provider.ResourceId{}
}
ref.ResourceId.StorageId = string(e.Value)
}
if e, ok := v.Opaque.Map["space_id"]; ok {
if ref.ResourceId == nil {
ref.ResourceId = &provider.ResourceId{}
}
ref.ResourceId.SpaceId = string(e.Value)
}
if e, ok := v.Opaque.Map["opaque_id"]; ok {
if ref.ResourceId == nil {
ref.ResourceId = &provider.ResourceId{}
}
ref.ResourceId.OpaqueId = string(e.Value)
}
if e, ok := v.Opaque.Map["path"]; ok {
ref.Path = string(e.Value)
}
}
return checkStorageRefForOCMShare(&share, ref, ocmNamespace), nil
case *registry.GetStorageProvidersRequest:
return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.StatRequest:
return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.ListContainerRequest:
return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.InitiateFileDownloadRequest:
return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *appprovider.OpenInAppRequest:
return checkStorageRefForOCMShare(&share, &provider.Reference{ResourceId: v.ResourceInfo.Id}, ocmNamespace), nil
case *gateway.OpenInAppRequest:
return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.GetLockRequest:
return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
// editor role
case *provider.CreateContainerRequest:
return hasRoleEditor(scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.TouchFileRequest:
return hasRoleEditor(scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.DeleteRequest:
return hasRoleEditor(scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.MoveRequest:
return hasRoleEditor(scope) && checkStorageRefForOCMShare(&share, v.GetSource(), ocmNamespace) && checkStorageRefForOCMShare(&share, v.GetDestination(), ocmNamespace), nil
case *provider.InitiateFileUploadRequest:
return hasRoleEditor(scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.SetArbitraryMetadataRequest:
return hasRoleEditor(scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.UnsetArbitraryMetadataRequest:
return hasRoleEditor(scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.SetLockRequest:
return hasRoleEditor(scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.RefreshLockRequest:
return hasRoleEditor(scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
case *provider.UnlockRequest:
return hasRoleEditor(scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil
// App provider requests
case *appregistry.GetDefaultAppProviderForMimeTypeRequest:
return true, nil
case *appregistry.GetAppProvidersRequest:
return true, nil
case *userv1beta1.GetUserByClaimRequest:
return true, nil
case *userv1beta1.GetUserRequest:
return true, nil
case *provider.ListStorageSpacesRequest:
return true, nil
// FIXME why do we need to add them? I think the whole listing of received OCM shares change might be unnecessary ... need to reevaluate that after switching the dav namespace for from /ocm back to /public
case *ocmv1beta1.ListReceivedOCMSharesRequest:
return true, nil
case *ocmv1beta1.ListOCMSharesRequest:
return true, nil
case *collaboration.ListReceivedSharesRequest:
return true, nil
case *ocmv1beta1.GetOCMShareRequest:
return checkOCMShareRef(&share, v.GetRef()), nil
case *ocmv1beta1.GetOCMShareByTokenRequest:
return share.Token == v.GetToken(), nil
case string:
return checkResourcePath(v), nil
}
return false, nil
}
func checkStorageRefForOCMShare(s *ocmv1beta1.Share, r *provider.Reference, ns string) bool {
if r.ResourceId != nil {
return utils.ResourceIDEqual(s.ResourceId, r.GetResourceId()) || strings.HasPrefix(r.ResourceId.OpaqueId, s.GetId().GetOpaqueId())
}
// FIXME: the paths here are hardcoded
if strings.HasPrefix(r.GetPath(), "/public/"+s.GetId().GetOpaqueId()) {
return true
}
return strings.HasPrefix(r.GetPath(), filepath.Join(ns, s.GetId().GetOpaqueId()))
}
func checkOCMShareRef(s *ocmv1beta1.Share, ref *ocmv1beta1.ShareReference) bool {
return ref.GetId().GetOpaqueId() == s.GetId().GetOpaqueId()
}
// AddOCMShareScope adds the scope to allow access to an OCM share and the share resource.
func AddOCMShareScope(share *ocmv1beta1.Share, role authpb.Role, scopes map[string]*authpb.Scope) (map[string]*authpb.Scope, error) {
// Create a new "scope share" to only expose the required fields `ResourceId`, `Id` and `Token` to the scope.
scopeShare := ocmv1beta1.Share{ResourceId: share.ResourceId, Id: share.GetId(), Token: share.Token}
val, err := utils.MarshalProtoV1ToJSON(&scopeShare)
if err != nil {
return nil, err
}
if scopes == nil {
scopes = make(map[string]*authpb.Scope)
}
scopes["ocmshare:"+share.Id.OpaqueId] = &authpb.Scope{
Resource: &types.OpaqueEntry{
Decoder: "json",
Value: val,
},
Role: role,
}
return scopes, nil
}
// GetOCMSharesFromScopes returns all OCM shares in the given scope.
func GetOCMSharesFromScopes(scopes map[string]*authpb.Scope) ([]*ocmv1beta1.Share, error) {
var shares []*ocmv1beta1.Share
for k, s := range scopes {
if strings.HasPrefix(k, "ocmshare:") {
res := s.Resource
if res.Decoder != "json" {
return nil, errtypes.InternalError("resource should be json encoded")
}
var share ocmv1beta1.Share
err := utils.UnmarshalJSONToProtoV1(res.Value, &share)
if err != nil {
return nil, err
}
shares = append(shares, &share)
}
}
return shares, nil
}
@@ -0,0 +1,204 @@
// 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 scope
import (
"context"
"strings"
appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
permissionsv1beta1 "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
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/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/rs/zerolog"
)
// PublicStorageProviderID is the space id used for the public links storage space
const PublicStorageProviderID = "7993447f-687f-490d-875c-ac95e89a62a4"
func publicshareScope(ctx context.Context, scope *authpb.Scope, resource interface{}, logger *zerolog.Logger) (bool, error) {
var share link.PublicShare
err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &share)
if err != nil {
return false, err
}
switch v := resource.(type) {
// Viewer role
case *registry.GetStorageProvidersRequest:
return checkStorageRef(ctx, &share, v.GetRef()), nil
case *registry.ListStorageProvidersRequest:
ref := &provider.Reference{}
if v.Opaque != nil && v.Opaque.Map != nil {
if e, ok := v.Opaque.Map["storage_id"]; ok {
if ref.ResourceId == nil {
ref.ResourceId = &provider.ResourceId{}
}
ref.ResourceId.StorageId = string(e.Value)
}
if e, ok := v.Opaque.Map["space_id"]; ok {
if ref.ResourceId == nil {
ref.ResourceId = &provider.ResourceId{}
}
ref.ResourceId.SpaceId = string(e.Value)
}
if e, ok := v.Opaque.Map["opaque_id"]; ok {
if ref.ResourceId == nil {
ref.ResourceId = &provider.ResourceId{}
}
ref.ResourceId.OpaqueId = string(e.Value)
}
if e, ok := v.Opaque.Map["path"]; ok {
ref.Path = string(e.Value)
}
}
return checkStorageRef(ctx, &share, ref), nil
case *provider.CreateHomeRequest:
return false, nil
case *provider.GetPathRequest:
return checkStorageRef(ctx, &share, &provider.Reference{ResourceId: v.GetResourceId()}), nil
case *provider.StatRequest:
return checkStorageRef(ctx, &share, v.GetRef()), nil
case *provider.GetLockRequest:
return checkStorageRef(ctx, &share, v.GetRef()), nil
case *provider.UnlockRequest:
return checkStorageRef(ctx, &share, v.GetRef()), nil
case *provider.RefreshLockRequest:
return checkStorageRef(ctx, &share, v.GetRef()), nil
case *provider.SetLockRequest:
return checkStorageRef(ctx, &share, v.GetRef()), nil
case *provider.ListContainerRequest:
return checkStorageRef(ctx, &share, v.GetRef()), nil
case *provider.InitiateFileDownloadRequest:
return checkStorageRef(ctx, &share, v.GetRef()), nil
case *appprovider.OpenInAppRequest:
return checkStorageRef(ctx, &share, &provider.Reference{ResourceId: v.ResourceInfo.Id}), nil
case *gateway.OpenInAppRequest:
return checkStorageRef(ctx, &share, v.GetRef()), nil
case *permissionsv1beta1.CheckPermissionRequest:
return true, nil
// Editor role
// need to return appropriate status codes in the ocs/ocdav layers.
case *provider.CreateContainerRequest:
return hasRoleEditor(scope) && checkStorageRef(ctx, &share, v.GetRef()), nil
case *provider.TouchFileRequest:
return hasRoleEditor(scope) && checkStorageRef(ctx, &share, v.GetRef()), nil
case *provider.DeleteRequest:
return hasRoleEditor(scope) && checkStorageRef(ctx, &share, v.GetRef()), nil
case *provider.MoveRequest:
return hasRoleEditor(scope) && checkStorageRef(ctx, &share, v.GetSource()) && checkStorageRef(ctx, &share, v.GetDestination()), nil
case *provider.InitiateFileUploadRequest:
return hasRoleEditor(scope) && checkStorageRef(ctx, &share, v.GetRef()), nil
case *provider.SetArbitraryMetadataRequest:
return hasRoleEditor(scope) && checkStorageRef(ctx, &share, v.GetRef()), nil
case *provider.UnsetArbitraryMetadataRequest:
return hasRoleEditor(scope) && checkStorageRef(ctx, &share, v.GetRef()), nil
// App provider requests
case *appregistry.GetDefaultAppProviderForMimeTypeRequest:
return true, nil
case *appregistry.GetAppProvidersRequest:
return true, nil
case *userv1beta1.GetUserByClaimRequest:
return true, nil
case *userv1beta1.GetUserRequest:
return true, nil
case *provider.ListStorageSpacesRequest:
return true, nil
case *link.GetPublicShareRequest:
return checkPublicShareRef(&share, v.GetRef()), nil
case *link.ListPublicSharesRequest:
// public links must not leak info about other links
return false, nil
case *collaboration.ListReceivedSharesRequest:
// public links must not leak info about collaborative shares
return false, nil
case string:
return checkResourcePath(v), nil
}
msg := "public resource type assertion failed"
logger.Debug().Str("scope", "publicshareScope").Interface("resource", resource).Msg(msg)
return false, errtypes.InternalError(msg)
}
func checkStorageRef(ctx context.Context, s *link.PublicShare, r *provider.Reference) bool {
// r: <resource_id:<storage_id:$storageID space_id:$spaceID opaque_id:$opaqueID> path:$path > >
if utils.ResourceIDEqual(s.ResourceId, r.GetResourceId()) {
return true
}
// r: <path:"/public/$token" >
if strings.HasPrefix(r.GetPath(), "/public/"+s.Token) || strings.HasPrefix(r.GetPath(), "./"+s.Token) {
return true
}
// r: <resource_id:<storage_id: space_id: opaque_id:$token> path:$path>
if id := r.GetResourceId(); id.GetStorageId() == PublicStorageProviderID {
// access to /public
if id.GetOpaqueId() == PublicStorageProviderID {
return true
}
// access relative to /public/$token
if id.GetOpaqueId() == s.Token {
return true
}
}
return false
}
func checkPublicShareRef(s *link.PublicShare, ref *link.PublicShareReference) bool {
// ref: <token:$token >
return ref.GetToken() == s.Token
}
// AddPublicShareScope adds the scope to allow access to a public share and
// the shared resource.
func AddPublicShareScope(share *link.PublicShare, role authpb.Role, scopes map[string]*authpb.Scope) (map[string]*authpb.Scope, error) {
// Create a new "scope share" to only expose the required fields `ResourceId` and `Token` to the scope.
scopeShare := &link.PublicShare{ResourceId: share.ResourceId, Token: share.Token}
val, err := utils.MarshalProtoV1ToJSON(scopeShare)
if err != nil {
return nil, err
}
if scopes == nil {
scopes = make(map[string]*authpb.Scope)
}
scopes["publicshare:"+share.Id.OpaqueId] = &authpb.Scope{
Resource: &types.OpaqueEntry{
Decoder: "json",
Value: val,
},
Role: role,
}
return scopes, nil
}
@@ -0,0 +1,166 @@
// 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 scope
import (
"context"
"fmt"
"strings"
appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
"github.com/rs/zerolog"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
func resourceinfoScope(_ context.Context, scope *authpb.Scope, resource interface{}, logger *zerolog.Logger) (bool, error) {
var r provider.ResourceInfo
err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &r)
if err != nil {
return false, err
}
switch v := resource.(type) {
// Viewer role
case *registry.GetStorageProvidersRequest:
return checkResourceInfo(&r, v.GetRef()), nil
case *registry.ListStorageProvidersRequest:
// the call will only return spaces the current user has access to
ref := &provider.Reference{}
if v.Opaque != nil && v.Opaque.Map != nil {
if e, ok := v.Opaque.Map["storage_id"]; ok {
ref.ResourceId = &provider.ResourceId{
StorageId: string(e.Value),
}
}
if e, ok := v.Opaque.Map["opaque_id"]; ok {
if ref.ResourceId == nil {
ref.ResourceId = &provider.ResourceId{}
}
ref.ResourceId.OpaqueId = string(e.Value)
}
if e, ok := v.Opaque.Map["path"]; ok {
ref.Path = string(e.Value)
}
}
return checkResourceInfo(&r, ref), nil
case *provider.ListStorageSpacesRequest:
// the call will only return spaces the current user has access to
return true, nil
case *provider.StatRequest:
return checkResourceInfo(&r, v.GetRef()), nil
case *provider.ListContainerRequest:
return checkResourceInfo(&r, v.GetRef()), nil
case *provider.InitiateFileDownloadRequest:
return checkResourceInfo(&r, v.GetRef()), nil
case *appprovider.OpenInAppRequest:
return checkResourceInfo(&r, &provider.Reference{ResourceId: v.ResourceInfo.Id}), nil
case *gateway.OpenInAppRequest:
return checkResourceInfo(&r, v.GetRef()), nil
// Editor role
// need to return appropriate status codes in the ocs/ocdav layers.
case *provider.CreateContainerRequest:
return hasRoleEditor(scope) && checkResourceInfo(&r, v.GetRef()), nil
case *provider.TouchFileRequest:
return hasRoleEditor(scope) && checkResourceInfo(&r, v.GetRef()), nil
case *provider.DeleteRequest:
return hasRoleEditor(scope) && checkResourceInfo(&r, v.GetRef()), nil
case *provider.MoveRequest:
return hasRoleEditor(scope) && checkResourceInfo(&r, v.GetSource()) && checkResourceInfo(&r, v.GetDestination()), nil
case *provider.InitiateFileUploadRequest:
return hasRoleEditor(scope) && checkResourceInfo(&r, v.GetRef()), nil
case *provider.SetArbitraryMetadataRequest:
return hasRoleEditor(scope) && checkResourceInfo(&r, v.GetRef()), nil
case *provider.UnsetArbitraryMetadataRequest:
return hasRoleEditor(scope) && checkResourceInfo(&r, v.GetRef()), nil
case string:
return checkResourcePath(v), nil
}
msg := fmt.Sprintf("resource type assertion failed: %+v", resource)
logger.Debug().Str("scope", "resourceinfoScope").Msg(msg)
return false, errtypes.InternalError(msg)
}
func checkResourceInfo(inf *provider.ResourceInfo, ref *provider.Reference) bool {
// ref: <resource_id:<storage_id:$storageID opaque_id:$opaqueID path:$path> >
if ref.ResourceId != nil { // path can be empty or a relative path
if inf.Id.SpaceId == ref.ResourceId.SpaceId && inf.Id.OpaqueId == ref.ResourceId.OpaqueId {
if ref.Path == "" {
// id only reference
return true
}
// check path has same prefix below
} else {
return false
}
}
// ref: <path:$path >
if strings.HasPrefix(ref.GetPath(), inf.Path) {
return true
}
return false
}
func checkResourcePath(path string) bool {
paths := []string{
"/dataprovider",
"/data",
"/app/open",
"/app/new",
"/archiver",
"/ocs/v2.php/cloud/capabilities",
"/ocs/v1.php/cloud/capabilities",
}
for _, p := range paths {
if strings.HasPrefix(path, p) {
return true
}
}
return false
}
// AddResourceInfoScope adds the scope to allow access to a resource info object.
func AddResourceInfoScope(r *provider.ResourceInfo, role authpb.Role, scopes map[string]*authpb.Scope) (map[string]*authpb.Scope, error) {
// Create a new "scope info" to only expose the required fields `Id` and `Path` to the scope.
scopeInfo := &provider.ResourceInfo{Id: r.Id, Path: r.Path}
val, err := utils.MarshalProtoV1ToJSON(scopeInfo)
if err != nil {
return nil, err
}
if scopes == nil {
scopes = make(map[string]*authpb.Scope)
}
scopes["resourceinfo:"+r.Id.String()] = &authpb.Scope{
Resource: &types.OpaqueEntry{
Decoder: "json",
Value: val,
},
Role: role,
}
return scopes, nil
}
+59
View File
@@ -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 scope
import (
"context"
"strings"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/rs/zerolog"
)
// Verifier is the function signature which every scope verifier should implement.
type Verifier func(context.Context, *authpb.Scope, interface{}, *zerolog.Logger) (bool, error)
var supportedScopes = map[string]Verifier{
"user": userScope,
"publicshare": publicshareScope,
"resourceinfo": resourceinfoScope,
"lightweight": lightweightAccountScope,
"ocmshare": ocmShareScope,
}
// VerifyScope is the function to be called when dismantling tokens to check if
// the token has access to a particular resource.
func VerifyScope(ctx context.Context, scopeMap map[string]*authpb.Scope, resource interface{}) (bool, error) {
logger := appctx.GetLogger(ctx)
for k, scope := range scopeMap {
for s, f := range supportedScopes {
if strings.HasPrefix(k, s) {
if valid, err := f(ctx, scope, resource, logger); err == nil && valid {
return true, nil
}
}
}
}
return false, nil
}
func hasRoleEditor(scope *authpb.Scope) bool {
return scope.Role == authpb.Role_ROLE_OWNER || scope.Role == authpb.Role_ROLE_EDITOR || scope.Role == authpb.Role_ROLE_UPLOADER
}
+55
View File
@@ -0,0 +1,55 @@
// 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 scope
import (
"context"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/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/utils"
"github.com/rs/zerolog"
)
func userScope(_ context.Context, scope *authpb.Scope, resource interface{}, _ *zerolog.Logger) (bool, error) {
// Always return true. Registered users can access all paths.
// TODO(ishank011): Add checks for read/write permissions.
return true, nil
}
// AddOwnerScope adds the default owner scope with access to all resources.
func AddOwnerScope(scopes map[string]*authpb.Scope) (map[string]*authpb.Scope, error) {
ref := &provider.Reference{Path: "/"}
val, err := utils.MarshalProtoV1ToJSON(ref)
if err != nil {
return nil, err
}
if scopes == nil {
scopes = make(map[string]*authpb.Scope)
}
scopes["user"] = &authpb.Scope{
Resource: &types.OpaqueEntry{
Decoder: "json",
Value: val,
},
Role: authpb.Role_ROLE_OWNER,
}
return scopes, nil
}
+62
View File
@@ -0,0 +1,62 @@
// 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 scope
import (
"fmt"
"strings"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
// FormatScope create a pretty print of the scope
func FormatScope(scopeType string, scope *authpb.Scope) (string, error) {
// TODO(gmgigi96): check decoder type
switch {
case strings.HasPrefix(scopeType, "user"):
// user scope
var ref provider.Reference
err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &ref)
if err != nil {
return "", err
}
return fmt.Sprintf("%s %s", ref.String(), scope.Role.String()), nil
case strings.HasPrefix(scopeType, "publicshare"):
// public share
var pShare link.PublicShare
err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &pShare)
if err != nil {
return "", err
}
return fmt.Sprintf("share:\"%s\" %s", pShare.Id.OpaqueId, scope.Role.String()), nil
case strings.HasPrefix(scopeType, "resourceinfo"):
var resInfo provider.ResourceInfo
err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &resInfo)
if err != nil {
return "", err
}
return fmt.Sprintf("path:\"%s\" %s", resInfo.Path, scope.Role.String()), nil
default:
return "", errtypes.NotSupported("scope not yet supported")
}
}
+109
View File
@@ -0,0 +1,109 @@
// 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 bytesize provides easy conversions from human readable strings (eg. 10MB) to bytes
package bytesize
import (
"fmt"
"strconv"
"strings"
)
// ByteSize is the size in bytes
type ByteSize uint64
// List of available byte sizes
// NOTE: max is exabyte as we convert to uint64
const (
KB ByteSize = 1000
MB ByteSize = 1000 * KB
GB ByteSize = 1000 * MB
TB ByteSize = 1000 * GB
PB ByteSize = 1000 * TB
EB ByteSize = 1000 * PB
KiB ByteSize = 1024
MiB ByteSize = 1024 * KiB
GiB ByteSize = 1024 * MiB
TiB ByteSize = 1024 * GiB
PiB ByteSize = 1024 * TiB
EiB ByteSize = 1024 * PiB
)
// Parse parses a Bytesize from a string
func Parse(s string) (ByteSize, error) {
sanitized := strings.TrimSpace(s)
if !strings.HasSuffix(sanitized, "B") {
u, err := strconv.Atoi(sanitized)
return ByteSize(u), err
}
var (
value int
unit string
)
template := "%d%s"
_, err := fmt.Sscanf(sanitized, template, &value, &unit)
if err != nil {
return 0, err
}
bytes := ByteSize(value)
switch unit {
case "KB":
bytes *= KB
case "MB":
bytes *= MB
case "GB":
bytes *= GB
case "TB":
bytes *= TB
case "PB":
bytes *= PB
case "EB":
bytes *= EB
case "KiB":
bytes *= KiB
case "MiB":
bytes *= MiB
case "GiB":
bytes *= GiB
case "TiB":
bytes *= TiB
case "PiB":
bytes *= PiB
case "EiB":
bytes *= EiB
default:
return 0, fmt.Errorf("unknown unit '%s'. Use common abbreviations such as KB, MiB, GB", unit)
}
return bytes, nil
}
// Bytes converts the ByteSize to an uint64
func (b ByteSize) Bytes() uint64 {
return uint64(b)
}
// String converts the ByteSize to a string
func (b ByteSize) String() string {
return strconv.FormatUint(uint64(b), 10)
}
+480
View File
@@ -0,0 +1,480 @@
// 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 conversions sits between CS3 type definitions and OCS API Responses
package conversions
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"path"
"path/filepath"
"time"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/mime"
"github.com/opencloud-eu/reva/v2/pkg/publicshare"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/user"
"github.com/opencloud-eu/reva/v2/pkg/utils"
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
ocm "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"
publicsharemgr "github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/registry"
usermgr "github.com/opencloud-eu/reva/v2/pkg/user/manager/registry"
)
const (
// ShareTypeUser refers to user shares
ShareTypeUser ShareType = 0
// ShareTypePublicLink refers to public link shares
ShareTypePublicLink ShareType = 3
// ShareTypeGroup represents a group share
ShareTypeGroup ShareType = 1
// ShareTypeFederatedCloudShare represents a federated share
ShareTypeFederatedCloudShare ShareType = 6
// ShareTypeSpaceMembershipUser represents an action regarding user type space members
ShareTypeSpaceMembershipUser ShareType = 7
// ShareTypeSpaceMembershipGroup represents an action regarding group type space members
ShareTypeSpaceMembershipGroup ShareType = 8
// ShareWithUserTypeUser represents a normal user
ShareWithUserTypeUser ShareWithUserType = 0
// ShareWithUserTypeGuest represents a guest user
ShareWithUserTypeGuest ShareWithUserType = 1
// The datetime format of ISO8601
_iso8601 = "2006-01-02T15:04:05Z0700"
)
// ResourceType indicates the OCS type of the resource
type ResourceType int
func (rt ResourceType) String() (s string) {
switch rt {
case 0:
s = "invalid"
case 1:
s = "file"
case 2:
s = "folder"
case 3:
s = "reference"
default:
s = "invalid"
}
return
}
// ShareType denotes a type of share
type ShareType int
// ShareWithUserType denotes a type of user
type ShareWithUserType int
// ShareData represents https://doc.owncloud.com/server/developer_manual/core/ocs-share-api.html#response-attributes-1
type ShareData struct {
// TODO int?
ID string `json:"id" xml:"id"`
// The shares type
ShareType ShareType `json:"share_type" xml:"share_type"`
// The username of the owner of the share.
UIDOwner string `json:"uid_owner" xml:"uid_owner"`
// The display name of the owner of the share.
DisplaynameOwner string `json:"displayname_owner" xml:"displayname_owner"`
// Additional info to identify the share owner, eg. the email or username
AdditionalInfoOwner string `json:"additional_info_owner" xml:"additional_info_owner"`
// The permission attribute set on the file.
// TODO(jfd) change the default to read only
Permissions Permissions `json:"permissions" xml:"permissions"`
// The UNIX timestamp when the share was created.
STime uint64 `json:"stime" xml:"stime"`
// ?
Parent string `json:"parent" xml:"parent"`
// The UNIX timestamp when the share expires.
Expiration string `json:"expiration" xml:"expiration"`
// The public link to the item being shared.
Token string `json:"token" xml:"token"`
// The unique id of the user that owns the file or folder being shared.
UIDFileOwner string `json:"uid_file_owner" xml:"uid_file_owner"`
// The display name of the user that owns the file or folder being shared.
DisplaynameFileOwner string `json:"displayname_file_owner" xml:"displayname_file_owner"`
// Additional info to identify the file owner, eg. the email or username
AdditionalInfoFileOwner string `json:"additional_info_file_owner" xml:"additional_info_file_owner"`
// share state, 0 = accepted, 1 = pending, 2 = declined
State int `json:"state" xml:"state"`
// The path to the shared file or folder.
Path string `json:"path" xml:"path"`
// The type of the object being shared. This can be one of 'file' or 'folder'.
ItemType string `json:"item_type" xml:"item_type"`
// The RFC2045-compliant mimetype of the file.
MimeType string `json:"mimetype" xml:"mimetype"`
// The space ID of the original file location
SpaceID string `json:"space_id" xml:"space_id"`
// The space alias of the original file location
SpaceAlias string `json:"space_alias" xml:"space_alias"`
StorageID string `json:"storage_id" xml:"storage_id"`
Storage uint64 `json:"storage" xml:"storage"`
// The unique node id of the item being shared.
ItemSource string `json:"item_source" xml:"item_source"`
// The unique node id of the item being shared. For legacy reasons item_source and file_source attributes have the same value.
FileSource string `json:"file_source" xml:"file_source"`
// The unique node id of the parent node of the item being shared.
FileParent string `json:"file_parent" xml:"file_parent"`
// The basename of the shared file.
FileTarget string `json:"file_target" xml:"file_target"`
// The uid of the share recipient. This is either
// - a GID (group id) if it is being shared with a group or
// - a UID (user id) if the share is shared with a user.
// - a password for public links
ShareWith string `json:"share_with,omitempty" xml:"share_with,omitempty"`
// The type of user
// - 0 = normal user
// - 1 = guest account
ShareWithUserType ShareWithUserType `json:"share_with_user_type" xml:"share_with_user_type"`
// The display name of the share recipient
ShareWithDisplayname string `json:"share_with_displayname,omitempty" xml:"share_with_displayname,omitempty"`
// Additional info to identify the share recipient, eg. the email or username
ShareWithAdditionalInfo string `json:"share_with_additional_info" xml:"share_with_additional_info"`
// Whether the recipient was notified, by mail, about the share being shared with them.
MailSend int `json:"mail_send" xml:"mail_send"`
// Name of the public share
Name string `json:"name" xml:"name"`
// URL of the public share
URL string `json:"url,omitempty" xml:"url,omitempty"`
// Attributes associated
Attributes string `json:"attributes,omitempty" xml:"attributes,omitempty"`
// Quicklink indicates if the link is the quicklink
Quicklink bool `json:"quicklink,omitempty" xml:"quicklink,omitempty"`
// PasswordProtected represents a public share is password protected
// PasswordProtected bool `json:"password_protected,omitempty" xml:"password_protected,omitempty"`
Hidden bool `json:"hidden" xml:"hidden"`
}
// ShareeData holds share recipient search results
type ShareeData struct {
Exact *ExactMatchesData `json:"exact" xml:"exact"`
Users []*MatchData `json:"users" xml:"users>element"`
Groups []*MatchData `json:"groups" xml:"groups>element"`
Remotes []*MatchData `json:"remotes" xml:"remotes>element"`
}
// TokenInfo holds token information
type TokenInfo struct {
// for all callers
Token string `json:"token" xml:"token"`
LinkURL string `json:"link_url" xml:"link_url"`
PasswordProtected bool `json:"password_protected" xml:"password_protected"`
Aliaslink bool `json:"alias_link" xml:"alias_link"`
// if not password protected
ID string `json:"id" xml:"id"`
StorageID string `json:"storage_id" xml:"storage_id"`
SpaceID string `json:"space_id" xml:"space_id"`
OpaqueID string `json:"opaque_id" xml:"opaque_id"`
Path string `json:"path" xml:"path"`
// if native access
SpacePath string `json:"space_path" xml:"space_path"`
SpaceAlias string `json:"space_alias" xml:"space_alias"`
SpaceURL string `json:"space_url" xml:"space_url"`
SpaceType string `json:"space_type" xml:"space_type"`
}
// ExactMatchesData hold exact matches
type ExactMatchesData struct {
Users []*MatchData `json:"users" xml:"users>element"`
Groups []*MatchData `json:"groups" xml:"groups>element"`
Remotes []*MatchData `json:"remotes" xml:"remotes>element"`
}
// MatchData describes a single match
type MatchData struct {
Label string `json:"label" xml:"label,omitempty"`
Value *MatchValueData `json:"value" xml:"value"`
}
// MatchValueData holds the type and actual value
type MatchValueData struct {
ShareType int `json:"shareType" xml:"shareType"`
ShareWith string `json:"shareWith" xml:"shareWith"`
ShareWithProvider string `json:"shareWithProvider" xml:"shareWithProvider"`
ShareWithAdditionalInfo string `json:"shareWithAdditionalInfo" xml:"shareWithAdditionalInfo,omitempty"`
UserType int `json:"userType" xml:"userType"`
}
// CS3Share2ShareData converts a cs3api user share into shareData data model
func CS3Share2ShareData(ctx context.Context, share *collaboration.Share) *ShareData {
sd := &ShareData{
// share.permissions are mapped below
// Displaynames are added later
UIDOwner: LocalUserIDToString(share.GetCreator()),
UIDFileOwner: LocalUserIDToString(share.GetOwner()),
}
switch share.Grantee.Type {
case provider.GranteeType_GRANTEE_TYPE_USER:
sd.ShareType = ShareTypeUser
sd.ShareWith = LocalUserIDToString(share.Grantee.GetUserId())
shareType := share.GetGrantee().GetUserId().GetType()
if shareType == userpb.UserType_USER_TYPE_LIGHTWEIGHT || shareType == userpb.UserType_USER_TYPE_GUEST {
sd.ShareWithUserType = ShareWithUserTypeGuest
} else {
sd.ShareWithUserType = ShareWithUserTypeUser
}
case provider.GranteeType_GRANTEE_TYPE_GROUP:
sd.ShareType = ShareTypeGroup
sd.ShareWith = LocalGroupIDToString(share.Grantee.GetGroupId())
}
if share.Id != nil {
sd.ID = share.Id.OpaqueId
}
if share.GetPermissions().GetPermissions() != nil {
sd.Permissions = RoleFromResourcePermissions(share.GetPermissions().GetPermissions(), false).OCSPermissions()
}
if share.Ctime != nil {
sd.STime = share.Ctime.Seconds // TODO CS3 api birth time = btime
}
if share.Expiration != nil {
expiration := time.Unix(int64(share.Expiration.Seconds), int64(share.Expiration.Nanos))
sd.Expiration = expiration.Format(_iso8601)
}
return sd
}
// PublicShare2ShareData converts a cs3api public share into shareData data model
func PublicShare2ShareData(share *link.PublicShare, r *http.Request, publicURL string) *ShareData {
sd := &ShareData{
// share.permissions are mapped below
// Displaynames are added later
ShareType: ShareTypePublicLink,
Token: share.Token,
Name: share.DisplayName,
MailSend: 0,
URL: publicURL + path.Join("/", "s/"+share.Token),
UIDOwner: LocalUserIDToString(share.Creator),
UIDFileOwner: LocalUserIDToString(share.Owner),
Quicklink: share.Quicklink,
}
if share.Id != nil {
sd.ID = share.Id.OpaqueId
}
if s := share.GetPermissions().GetPermissions(); s != nil {
sd.Permissions = RoleFromResourcePermissions(share.GetPermissions().GetPermissions(), true).OCSPermissions()
}
if share.Expiration != nil {
sd.Expiration = timestampToExpiration(share.Expiration)
}
if share.Ctime != nil {
sd.STime = share.Ctime.Seconds // TODO CS3 api birth time = btime
}
// hide password
if share.PasswordProtected {
sd.ShareWith = "***redacted***"
sd.ShareWithDisplayname = "***redacted***"
}
return sd
}
func formatRemoteUser(u *userpb.UserId) string {
return fmt.Sprintf("%s@%s", u.OpaqueId, u.Idp)
}
func webdavInfo(protocols []*ocm.Protocol) (*ocm.WebDAVProtocol, bool) {
for _, p := range protocols {
if opt, ok := p.Term.(*ocm.Protocol_WebdavOptions); ok {
return opt.WebdavOptions, true
}
}
return nil, false
}
// ReceivedOCMShare2ShareData converts a cs3 ocm received share into a share data model.
func ReceivedOCMShare2ShareData(share *ocm.ReceivedShare, path string) (*ShareData, error) {
webdav, ok := webdavInfo(share.Protocols)
if !ok {
return nil, errtypes.InternalError("webdav endpoint not in share")
}
opaqueid := base64.StdEncoding.EncodeToString([]byte("/"))
shareTarget := filepath.Join("/Shares", share.Name)
s := &ShareData{
ID: share.Id.OpaqueId,
UIDOwner: formatRemoteUser(share.Creator),
UIDFileOwner: formatRemoteUser(share.Owner),
ShareWith: share.Grantee.GetUserId().OpaqueId,
Permissions: RoleFromResourcePermissions(webdav.GetPermissions().GetPermissions(), false).OCSPermissions(),
ShareType: ShareTypeFederatedCloudShare,
Path: shareTarget,
FileTarget: shareTarget,
MimeType: mime.Detect(share.ResourceType == provider.ResourceType_RESOURCE_TYPE_CONTAINER, share.Name), //nolint:staticcheck // we will update our ocm implementation later
ItemType: ResourceType(share.ResourceType).String(), //nolint:staticcheck // we will update our ocm implementation later
ItemSource: storagespace.FormatResourceID(&provider.ResourceId{
StorageId: utils.OCMStorageProviderID,
SpaceId: share.Id.OpaqueId,
OpaqueId: opaqueid,
}),
STime: share.Ctime.Seconds,
Name: share.Name,
SpaceID: storagespace.FormatStorageID(utils.OCMStorageProviderID, share.Id.OpaqueId),
}
if share.Expiration != nil {
s.Expiration = timestampToExpiration(share.Expiration)
}
return s, nil
}
func webdavAMInfo(methods []*ocm.AccessMethod) (*ocm.WebDAVAccessMethod, bool) {
for _, a := range methods {
if opt, ok := a.Term.(*ocm.AccessMethod_WebdavOptions); ok {
return opt.WebdavOptions, true
}
}
return nil, false
}
// OCMShare2ShareData converts a cs3 ocm share into a share data model.
func OCMShare2ShareData(share *ocm.Share) (*ShareData, error) {
webdav, ok := webdavAMInfo(share.AccessMethods)
if !ok {
return nil, errtypes.InternalError("webdav endpoint not in share")
}
s := &ShareData{
ID: share.Id.OpaqueId,
UIDOwner: share.Creator.OpaqueId,
UIDFileOwner: share.Owner.OpaqueId,
ShareWith: formatRemoteUser(share.Grantee.GetUserId()),
Permissions: RoleFromResourcePermissions(webdav.GetPermissions(), false).OCSPermissions(),
ShareType: ShareTypeFederatedCloudShare,
STime: share.Ctime.Seconds,
Name: share.Name,
}
if share.Expiration != nil {
s.Expiration = timestampToExpiration(share.Expiration)
}
return s, nil
}
// LocalUserIDToString transforms a cs3api user id into an ocs data model without domain name
// TODO ocs uses user names ... so an additional lookup is needed. see mapUserIds()
func LocalUserIDToString(userID *userpb.UserId) string {
if userID == nil || userID.OpaqueId == "" {
return ""
}
return userID.OpaqueId
}
// LocalGroupIDToString transforms a cs3api group id into an ocs data model without domain name
func LocalGroupIDToString(groupID *grouppb.GroupId) string {
if groupID == nil || groupID.OpaqueId == "" {
return ""
}
return groupID.OpaqueId
}
// GetUserManager returns a connection to a user share manager
func GetUserManager(manager string, m map[string]map[string]interface{}) (user.Manager, error) {
if f, ok := usermgr.NewFuncs[manager]; ok {
return f(m[manager])
}
return nil, fmt.Errorf("driver %s not found for user manager", manager)
}
// GetPublicShareManager returns a connection to a public share manager
func GetPublicShareManager(manager string, m map[string]map[string]interface{}) (publicshare.Manager, error) {
if f, ok := publicsharemgr.NewFuncs[manager]; ok {
return f(m[manager])
}
return nil, fmt.Errorf("driver %s not found for public shares manager", manager)
}
// timestamp is assumed to be UTC ... just human readable ...
// FIXME and ambiguous / error prone because there is no time zone ...
func timestampToExpiration(t *types.Timestamp) string {
return time.Unix(int64(t.Seconds), int64(t.Nanos)).UTC().Format("2006-01-02 15:05:05")
}
// ParseTimestamp tries to parse the ocs expiry into a CS3 Timestamp
func ParseTimestamp(timestampString string) (*types.Timestamp, error) {
parsedTime, err := time.Parse("2006-01-02T15:04:05Z0700", timestampString)
if err != nil {
parsedTime, err = time.Parse("2006-01-02", timestampString)
if err == nil {
// the link needs to be valid for the whole day
parsedTime = parsedTime.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
}
}
if err != nil {
return nil, fmt.Errorf("datetime format invalid: %v, %s", timestampString, err.Error())
}
final := parsedTime.UnixNano()
return &types.Timestamp{
Seconds: uint64(final / 1000000000),
Nanos: uint32(final % 1000000000),
}, nil
}
// UserTypeString returns human readable strings for various user types
func UserTypeString(userType userpb.UserType) string {
switch userType {
case userpb.UserType_USER_TYPE_PRIMARY:
return "primary"
case userpb.UserType_USER_TYPE_SECONDARY:
return "secondary"
case userpb.UserType_USER_TYPE_SERVICE:
return "service"
case userpb.UserType_USER_TYPE_APPLICATION:
return "application"
case userpb.UserType_USER_TYPE_GUEST:
return "guest"
case userpb.UserType_USER_TYPE_FEDERATED:
return "federated"
case userpb.UserType_USER_TYPE_LIGHTWEIGHT:
return "lightweight"
}
return "invalid"
}
@@ -0,0 +1,78 @@
// Copyright 2020 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package conversions
import (
"errors"
"fmt"
"strconv"
)
// Permissions reflects the CRUD permissions used in the OCS sharing API
type Permissions uint
const (
// PermissionInvalid represents an invalid permission
PermissionInvalid Permissions = 0
// PermissionRead grants read permissions on a resource
PermissionRead Permissions = 1 << (iota - 1)
// PermissionWrite grants write permissions on a resource
PermissionWrite
// PermissionCreate grants create permissions on a resource
PermissionCreate
// PermissionDelete grants delete permissions on a resource
PermissionDelete
// PermissionShare grants share permissions on a resource
PermissionShare
// PermissionAll grants all permissions on a resource
PermissionAll Permissions = (1 << (iota - 1)) - 1
// PermissionMaxInput is to be used within value range checks
PermissionMaxInput = PermissionAll
// PermissionMinInput is to be used within value range checks
PermissionMinInput = PermissionRead
// PermissionsNone is to be used to deny access on a resource
PermissionsNone = 64
)
var (
// ErrPermissionNotInRange defines a permission specific error.
ErrPermissionNotInRange = fmt.Errorf("the provided permission is not between %d and %d", PermissionMinInput, PermissionMaxInput)
// ErrZeroPermission defines a permission specific error
ErrZeroPermission = errors.New("permission is zero")
)
// NewPermissions creates a new Permissions instance.
// The value must be in the valid range.
func NewPermissions(val int) (Permissions, error) {
if val == int(PermissionInvalid) {
return PermissionInvalid, ErrZeroPermission
} else if val < int(PermissionInvalid) || int(PermissionMaxInput) < val {
return PermissionInvalid, ErrPermissionNotInRange
}
return Permissions(val), nil
}
// Contain tests if the permissions contain another one.
func (p Permissions) Contain(other Permissions) bool {
return p&other == other
}
func (p Permissions) String() string {
return strconv.FormatUint(uint64(p), 10)
}
+651
View File
@@ -0,0 +1,651 @@
// 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 conversions sits between CS3 type definitions and OCS API Responses
package conversions
import (
"fmt"
"strings"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/grants"
)
// Role is a set of ocs permissions and cs3 resource permissions under a common name.
type Role struct {
Name string
cS3ResourcePermissions *provider.ResourcePermissions
ocsPermissions Permissions
}
const (
// RoleViewer grants non-editor role on a resource.
RoleViewer = "viewer"
// RoleViewerListGrants grants non-editor role on a resource.
RoleViewerListGrants = "viewer-list-grants"
// RoleSpaceViewer grants non-editor role on a space.
RoleSpaceViewer = "spaceviewer"
// RoleEditor grants editor permission on a resource, including folders.
RoleEditor = "editor"
// RoleEditorListGrants grants editor permission on a resource, including folders.
RoleEditorListGrants = "editor-list-grants"
// RoleSpaceEditor grants editor permission on a space.
RoleSpaceEditor = "spaceeditor"
// RoleSpaceEditorWithoutVersions grants editor permission without list/restore versions on a space.
RoleSpaceEditorWithoutVersions = "spaceeditor-without-versions"
// RoleFileEditor grants editor permission on a single file.
RoleFileEditor = "file-editor"
// RoleFileEditorListGrants grants editor permission on a single file.
RoleFileEditorListGrants = "file-editor-list-grants"
// RoleCoowner grants co-owner permissions on a resource.
RoleCoowner = "coowner"
// RoleEditorLite grants permission to upload and download to a resource.
RoleEditorLite = "editor-lite"
// RoleUploader grants uploader permission to upload onto a resource (no download).
RoleUploader = "uploader"
// RoleManager grants manager permissions on a resource. Semantically equivalent to co-owner.
RoleManager = "manager"
// RoleSecureViewer grants secure view permissions on a resource or space.
RoleSecureViewer = "secure-viewer"
// RoleUnknown is used for unknown roles.
RoleUnknown = "unknown"
// RoleLegacy provides backwards compatibility.
RoleLegacy = "legacy"
// RoleDenied grants no permission at all on a resource
RoleDenied = "denied"
)
// CS3ResourcePermissions for the role
func (r *Role) CS3ResourcePermissions() *provider.ResourcePermissions {
return r.cS3ResourcePermissions
}
// OCSPermissions for the role
func (r *Role) OCSPermissions() Permissions {
return r.ocsPermissions
}
// WebDAVPermissions returns the webdav permissions used in propfinds, eg. "WCKDNVR"
/*
from https://github.com/owncloud/core/blob/10715e2b1c85fc3855a38d2b1fe4426b5e3efbad/apps/dav/lib/Files/PublicFiles/SharedNodeTrait.php#L196-L215
$p = '';
if ($node->isDeletable() && $this->checkSharePermissions(Constants::PERMISSION_DELETE)) {
$p .= 'D';
}
if ($node->isUpdateable() && $this->checkSharePermissions(Constants::PERMISSION_UPDATE)) {
$p .= 'NV'; // Renameable, Moveable
}
if ($node->getType() === \OCP\Files\FileInfo::TYPE_FILE) {
if ($node->isUpdateable() && $this->checkSharePermissions(Constants::PERMISSION_UPDATE)) {
$p .= 'W';
}
} else {
if ($node->isCreatable() && $this->checkSharePermissions(Constants::PERMISSION_CREATE)) {
$p .= 'CK';
}
}
*/
// D = delete
// NV = update (renameable moveable)
// W = update (files only)
// CK = create (folders only)
// S = Shared
// R = Shareable
// M = Mounted
// Z = Deniable (NEW)
// P = Purge from trashbin
// X = SecureViewable
func (r *Role) WebDAVPermissions(isDir, isShared, isMountpoint, isPublic bool) string {
var b strings.Builder
if !isPublic && isShared {
fmt.Fprintf(&b, "S")
}
if r.ocsPermissions.Contain(PermissionShare) {
fmt.Fprintf(&b, "R")
}
if !isPublic && isMountpoint {
fmt.Fprintf(&b, "M")
}
if r.ocsPermissions.Contain(PermissionDelete) {
fmt.Fprintf(&b, "D") // TODO oc10 shows received shares as deletable
}
if r.ocsPermissions.Contain(PermissionWrite) {
// Single file public link shares cannot be renamed
if !isPublic || (isPublic && r.cS3ResourcePermissions != nil && r.cS3ResourcePermissions.Move) {
fmt.Fprintf(&b, "NV")
}
if !isDir {
fmt.Fprintf(&b, "W")
}
}
if isDir && r.ocsPermissions.Contain(PermissionCreate) {
fmt.Fprintf(&b, "CK")
}
if r.CS3ResourcePermissions().DenyGrant {
fmt.Fprintf(&b, "Z")
}
if r.CS3ResourcePermissions().PurgeRecycle {
fmt.Fprintf(&b, "P")
}
if r.Name == RoleSecureViewer {
fmt.Fprintf(&b, "X")
}
return b.String()
}
// RoleFromName creates a role from the name
func RoleFromName(name string) *Role {
switch name {
case RoleDenied:
return NewDeniedRole()
case RoleViewer:
return NewViewerRole()
case RoleViewerListGrants:
return NewViewerListGrantsRole()
case RoleSpaceViewer:
return NewSpaceViewerRole()
case RoleEditor:
return NewEditorRole()
case RoleEditorListGrants:
return NewEditorListGrantsRole()
case RoleSpaceEditor:
return NewSpaceEditorRole()
case RoleFileEditor:
return NewFileEditorRole()
case RoleFileEditorListGrants:
return NewFileEditorListGrantsRole()
case RoleUploader:
return NewUploaderRole()
case RoleManager:
return NewManagerRole()
case RoleSecureViewer:
return NewSecureViewerRole()
case RoleCoowner:
return NewCoownerRole()
default:
return NewUnknownRole()
}
}
// NewUnknownRole creates an unknown role. An Unknown role has no permissions over a cs3 resource nor any ocs endpoint.
func NewUnknownRole() *Role {
return &Role{
Name: RoleUnknown,
cS3ResourcePermissions: &provider.ResourcePermissions{},
ocsPermissions: PermissionInvalid,
}
}
// NewDeniedRole creates a fully denied role
func NewDeniedRole() *Role {
return &Role{
Name: RoleDenied,
cS3ResourcePermissions: &provider.ResourcePermissions{},
ocsPermissions: PermissionsNone,
}
}
// NewViewerRole creates a viewer role. `sharing` indicates if sharing permission should be added
func NewViewerRole() *Role {
p := PermissionRead
return &Role{
Name: RoleViewer,
cS3ResourcePermissions: &provider.ResourcePermissions{
GetPath: true,
GetQuota: true,
InitiateFileDownload: true,
ListContainer: true,
ListRecycle: true,
Stat: true,
},
ocsPermissions: p,
}
}
// NewViewerListGrantsRole creates a viewer role. `sharing` indicates if sharing permission should be added
func NewViewerListGrantsRole() *Role {
role := NewViewerRole()
role.cS3ResourcePermissions.ListGrants = true
return role
}
// NewSpaceViewerRole creates a spaceviewer role
func NewSpaceViewerRole() *Role {
return &Role{
Name: RoleSpaceViewer,
cS3ResourcePermissions: &provider.ResourcePermissions{
GetPath: true,
GetQuota: true,
InitiateFileDownload: true,
ListContainer: true,
ListGrants: true,
ListRecycle: true,
Stat: true,
},
ocsPermissions: PermissionRead,
}
}
// NewEditorRole creates an editor role. `sharing` indicates if sharing permission should be added
func NewEditorRole() *Role {
p := PermissionRead | PermissionCreate | PermissionWrite | PermissionDelete
return &Role{
Name: RoleEditor,
cS3ResourcePermissions: &provider.ResourcePermissions{
CreateContainer: true,
Delete: true,
GetPath: true,
GetQuota: true,
InitiateFileDownload: true,
InitiateFileUpload: true,
ListContainer: true,
ListRecycle: true,
Move: true,
RestoreRecycleItem: true,
Stat: true,
},
ocsPermissions: p,
}
}
// NewEditorListGrantsRole creates an editor role. `sharing` indicates if sharing permission should be added
func NewEditorListGrantsRole() *Role {
role := NewEditorRole()
role.cS3ResourcePermissions.ListGrants = true
return role
}
// NewSpaceEditorRole creates an editor role
func NewSpaceEditorRole() *Role {
return &Role{
Name: RoleSpaceEditor,
cS3ResourcePermissions: &provider.ResourcePermissions{
CreateContainer: true,
Delete: true,
GetPath: true,
GetQuota: true,
InitiateFileDownload: true,
InitiateFileUpload: true,
ListContainer: true,
ListFileVersions: true,
ListGrants: true,
ListRecycle: true,
Move: true,
RestoreFileVersion: true,
RestoreRecycleItem: true,
Stat: true,
},
ocsPermissions: PermissionRead | PermissionCreate | PermissionWrite | PermissionDelete,
}
}
// NewSpaceEditorWithoutVersionsRole creates an editor without list/restore versions role
func NewSpaceEditorWithoutVersionsRole() *Role {
return &Role{
Name: RoleSpaceEditorWithoutVersions,
cS3ResourcePermissions: &provider.ResourcePermissions{
CreateContainer: true,
Delete: true,
GetPath: true,
GetQuota: true,
InitiateFileDownload: true,
InitiateFileUpload: true,
ListContainer: true,
ListGrants: true,
ListRecycle: true,
Move: true,
RestoreRecycleItem: true,
Stat: true,
},
ocsPermissions: PermissionRead | PermissionCreate | PermissionWrite | PermissionDelete,
}
}
// NewFileEditorRole creates a file-editor role
func NewFileEditorRole() *Role {
p := PermissionRead | PermissionWrite
return &Role{
Name: RoleEditor,
cS3ResourcePermissions: &provider.ResourcePermissions{
GetPath: true,
GetQuota: true,
InitiateFileDownload: true,
ListContainer: true,
ListRecycle: true,
Stat: true,
InitiateFileUpload: true,
RestoreRecycleItem: true,
},
ocsPermissions: p,
}
}
// NewFileEditorListGrantsRole creates a file-editor role
func NewFileEditorListGrantsRole() *Role {
role := NewFileEditorRole()
role.cS3ResourcePermissions.ListGrants = true
return role
}
// NewCoownerRole creates a coowner role.
func NewCoownerRole() *Role {
return &Role{
Name: RoleCoowner,
cS3ResourcePermissions: &provider.ResourcePermissions{
GetPath: true,
GetQuota: true,
InitiateFileDownload: true,
ListGrants: true,
ListContainer: true,
ListFileVersions: true,
ListRecycle: true,
Stat: true,
InitiateFileUpload: true,
RestoreFileVersion: true,
RestoreRecycleItem: true,
CreateContainer: true,
Delete: true,
Move: true,
PurgeRecycle: true,
AddGrant: true,
UpdateGrant: true,
RemoveGrant: true,
},
ocsPermissions: PermissionAll,
}
}
// NewEditorLiteRole creates an editor-lite role
func NewEditorLiteRole() *Role {
return &Role{
Name: RoleEditorLite,
cS3ResourcePermissions: &provider.ResourcePermissions{
Stat: true,
GetPath: true,
CreateContainer: true,
InitiateFileUpload: true,
InitiateFileDownload: true,
ListContainer: true,
Move: true,
},
ocsPermissions: PermissionCreate,
}
}
// NewUploaderRole creates an uploader role with no download permissions
func NewUploaderRole() *Role {
return &Role{
Name: RoleUploader,
cS3ResourcePermissions: &provider.ResourcePermissions{
Stat: true,
GetPath: true,
CreateContainer: true,
InitiateFileUpload: true,
},
ocsPermissions: PermissionCreate,
}
}
// NewNoneRole creates a role with no permissions
func NewNoneRole() *Role {
return &Role{
Name: "none",
cS3ResourcePermissions: &provider.ResourcePermissions{},
ocsPermissions: PermissionInvalid,
}
}
// NewManagerRole creates an manager role
func NewManagerRole() *Role {
return &Role{
Name: RoleManager,
cS3ResourcePermissions: &provider.ResourcePermissions{
GetPath: true,
GetQuota: true,
InitiateFileDownload: true,
ListGrants: true,
ListContainer: true,
ListFileVersions: true,
ListRecycle: true,
Stat: true,
InitiateFileUpload: true,
RestoreFileVersion: true,
RestoreRecycleItem: true,
Move: true,
CreateContainer: true,
Delete: true,
PurgeRecycle: true,
// these permissions only make sense to enforce them in the root of the storage space.
AddGrant: true, // managers can add users to the space
RemoveGrant: true, // managers can remove users from the space
UpdateGrant: true,
DenyGrant: true, // managers can deny access to sub folders
},
ocsPermissions: PermissionAll,
}
}
// NewSecureViewerRole creates a secure viewer role
func NewSecureViewerRole() *Role {
return &Role{
Name: RoleSecureViewer,
cS3ResourcePermissions: &provider.ResourcePermissions{
GetPath: true,
ListContainer: true,
Stat: true,
},
}
}
// RoleFromOCSPermissions tries to map ocs permissions to a role
// TODO: rethink using this. ocs permissions cannot be assigned 1:1 to roles
func RoleFromOCSPermissions(p Permissions, ri *provider.ResourceInfo) *Role {
switch {
// Invalid
case p == PermissionInvalid:
return NewNoneRole()
// Uploader
case p == PermissionCreate:
return NewUploaderRole()
// Viewer/SpaceViewer
case p == PermissionRead:
if isSpaceRoot(ri) {
return NewSpaceViewerRole()
}
return NewViewerRole()
// Editor/SpaceEditor
case p.Contain(PermissionRead) && p.Contain(PermissionWrite) && p.Contain(PermissionCreate) && p.Contain(PermissionDelete) && !p.Contain(PermissionShare):
if isSpaceRoot(ri) {
return NewSpaceEditorRole()
}
return NewEditorRole()
// Custom
default:
return NewLegacyRoleFromOCSPermissions(p)
}
}
func isSpaceRoot(ri *provider.ResourceInfo) bool {
if ri == nil {
return false
}
if ri.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER {
return false
}
if ri.GetId().GetOpaqueId() != ri.GetSpace().GetRoot().GetOpaqueId() ||
ri.GetId().GetSpaceId() != ri.GetSpace().GetRoot().GetSpaceId() ||
ri.GetId().GetStorageId() != ri.GetSpace().GetRoot().GetStorageId() {
return false
}
return true
}
// NewLegacyRoleFromOCSPermissions tries to map a legacy combination of ocs permissions to cs3 resource permissions as a legacy role
func NewLegacyRoleFromOCSPermissions(p Permissions) *Role {
r := &Role{
Name: RoleLegacy, // TODO custom role?
ocsPermissions: p,
cS3ResourcePermissions: &provider.ResourcePermissions{},
}
if p.Contain(PermissionRead) {
r.cS3ResourcePermissions.ListContainer = true
// r.cS3ResourcePermissions.ListGrants = true
r.cS3ResourcePermissions.ListRecycle = true
r.cS3ResourcePermissions.Stat = true
r.cS3ResourcePermissions.GetPath = true
r.cS3ResourcePermissions.GetQuota = true
r.cS3ResourcePermissions.InitiateFileDownload = true
}
if p.Contain(PermissionWrite) {
r.cS3ResourcePermissions.InitiateFileUpload = true
r.cS3ResourcePermissions.RestoreRecycleItem = true
}
if p.Contain(PermissionCreate) {
r.cS3ResourcePermissions.Stat = true
r.cS3ResourcePermissions.CreateContainer = true
// FIXME permissions mismatch: double check ocs create vs update file
// - if the file exists the ocs api needs to check update permission,
// - if the file does not exist the ocs api needs to check update permission
r.cS3ResourcePermissions.InitiateFileUpload = true
if p.Contain(PermissionWrite) {
r.cS3ResourcePermissions.Move = true // TODO move only when create and write?
}
}
if p.Contain(PermissionDelete) {
r.cS3ResourcePermissions.Delete = true
}
if p.Contain(PermissionShare) {
r.cS3ResourcePermissions.AddGrant = true
// r.cS3ResourcePermissions.RemoveGrant = true // TODO when are you able to unshare / delete
// r.cS3ResourcePermissions.UpdateGrant = true
}
return r
}
// RoleFromResourcePermissions tries to map cs3 resource permissions to a role
// It needs to know whether this is a link or not, because empty permissions on links mean "INTERNAL LINK"
// while empty permissions on other resources mean "DENIAL". Obviously this is not optimal.
func RoleFromResourcePermissions(rp *provider.ResourcePermissions, islink bool) *Role {
r := &Role{
Name: RoleUnknown,
ocsPermissions: PermissionInvalid,
cS3ResourcePermissions: rp,
}
if rp == nil {
return r
}
if grants.PermissionsEqual(rp, &provider.ResourcePermissions{}) {
if !islink {
r.ocsPermissions = PermissionsNone
r.Name = RoleDenied
}
return r
}
if rp.ListContainer &&
rp.ListRecycle &&
rp.Stat &&
rp.GetPath &&
rp.GetQuota &&
rp.InitiateFileDownload {
r.ocsPermissions |= PermissionRead
}
if rp.InitiateFileUpload &&
rp.RestoreRecycleItem {
r.ocsPermissions |= PermissionWrite
}
if rp.Stat &&
rp.CreateContainer &&
rp.InitiateFileUpload {
r.ocsPermissions |= PermissionCreate
}
if rp.Delete {
r.ocsPermissions |= PermissionDelete
}
if rp.AddGrant {
r.ocsPermissions |= PermissionShare
}
if r.ocsPermissions.Contain(PermissionRead) {
if r.ocsPermissions.Contain(PermissionWrite) && r.ocsPermissions.Contain(PermissionCreate) && r.ocsPermissions.Contain(PermissionDelete) && r.ocsPermissions.Contain(PermissionShare) {
r.Name = RoleEditor
if rp.ListGrants {
r.Name = RoleEditorListGrants
}
if rp.RemoveGrant {
r.Name = RoleManager
}
return r // editor or manager
}
if r.ocsPermissions == PermissionRead|PermissionShare {
r.Name = RoleViewer
if rp.ListGrants {
r.Name = RoleViewerListGrants
}
return r
}
} else if rp.Stat && rp.GetPath && rp.ListContainer && !rp.InitiateFileUpload && !rp.Delete && !rp.AddGrant {
r.Name = RoleSecureViewer
return r
}
if r.ocsPermissions == PermissionCreate {
if rp.GetPath && rp.InitiateFileDownload && rp.ListContainer && rp.Move {
r.Name = RoleEditorLite
return r
}
r.Name = RoleUploader
return r
}
r.Name = RoleLegacy
// at this point other ocs permissions may have been mapped.
// TODO what about even more granular cs3 permissions?, eg. only stat
return r
}
// SufficientCS3Permissions returns true if the `existing` permissions contain the `requested` permissions
func SufficientCS3Permissions(existing, requested *provider.ResourcePermissions) bool {
if existing == nil || requested == nil {
return false
}
// empty permissions represent a denial
if grants.PermissionsEqual(requested, &provider.ResourcePermissions{}) {
return existing.DenyGrant
}
numFields := existing.ProtoReflect().Descriptor().Fields().Len()
for i := 0; i < numFields; i++ {
field := existing.ProtoReflect().Descriptor().Fields().Get(i)
existingPermission := existing.ProtoReflect().Get(field).Bool()
requestedPermission := requested.ProtoReflect().Get(field).Bool()
// every requested permission needs to exist for the creator
if requestedPermission && !existingPermission {
return false
}
}
return true
}
+109
View File
@@ -0,0 +1,109 @@
// 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 ctx
import (
"context"
"strings"
ua "github.com/mileusna/useragent"
"google.golang.org/grpc/metadata"
)
// UserAgentHeader is the header used for the user agent
const (
UserAgentHeader = "x-user-agent"
WebUserAgent = "web"
GrpcUserAgent = "grpc"
MobileUserAgent = "mobile"
DesktopUserAgent = "desktop"
)
// ContextGetUserAgent returns the user agent if set in the given context.
// see https://github.com/grpc/grpc-go/issues/1100
func ContextGetUserAgent(ctx context.Context) (*ua.UserAgent, bool) {
if userAgentStr, ok := ContextGetUserAgentString(ctx); ok {
userAgent := ua.Parse(userAgentStr)
return &userAgent, true
}
return nil, false
}
// ContextGetUserAgentString returns the user agent string if set in the given context.
func ContextGetUserAgentString(ctx context.Context) (string, bool) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return "", false
}
userAgentLst, ok := md[UserAgentHeader]
if !ok {
userAgentLst, ok = md["user-agent"]
if !ok {
return "", false
}
}
if len(userAgentLst) == 0 {
return "", false
}
return userAgentLst[0], true
}
// ContextGetUserAgentCategory returns the category of the user agent
// (i.e. if it is a web, mobile, desktop or grpc user agent)
func ContextGetUserAgentCategory(ctx context.Context) (string, bool) {
agent, ok := ContextGetUserAgent(ctx)
if !ok {
return "", false
}
switch {
case isWeb(agent):
return WebUserAgent, true
case isMobile(agent):
return MobileUserAgent, true
case isDesktop(agent):
return DesktopUserAgent, true
case isGRPC(agent):
return GrpcUserAgent, true
default:
return "", false
}
}
func isWeb(ua *ua.UserAgent) bool {
return ua.IsChrome() || ua.IsEdge() || ua.IsFirefox() || ua.IsSafari() ||
ua.IsInternetExplorer() || ua.IsOpera() || ua.IsOperaMini()
}
// isMobile returns true if the useragent is generated by the mobile
func isMobile(ua *ua.UserAgent) bool {
// workaround as the library does not recognise iOS string inside the user agent
isIOS := ua.IsIOS() || strings.Contains(ua.String, "iOS")
return !isWeb(ua) && (ua.IsAndroid() || isIOS)
}
// isDesktop returns true if the useragent is generated by a desktop application
func isDesktop(ua *ua.UserAgent) bool {
return ua.Desktop && !isWeb(ua)
}
// isGRPC returns true if the useragent is generated by a grpc client
func isGRPC(ua *ua.UserAgent) bool {
return strings.HasPrefix(ua.Name, "grpc")
}
+35
View File
@@ -0,0 +1,35 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package ctx
import "context"
// InitiatorHeader is the header key used to pass the initiator id to http services and grpc calls.
var InitiatorHeader = "initiator-id"
// ContextGetInitiator returns the initiator if set in the given context.
func ContextGetInitiator(ctx context.Context) (string, bool) {
i, ok := ctx.Value(initiatorKey).(string)
return i, ok
}
// ContextSetInitiator stores the initiator in the context.
func ContextSetInitiator(ctx context.Context, i string) context.Context {
return context.WithValue(ctx, initiatorKey, i)
}
+34
View File
@@ -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 ctx
import (
"context"
)
// ContextGetLockID returns the lock id if set in the given context.
func ContextGetLockID(ctx context.Context) (string, bool) {
u, ok := ctx.Value(lockIDKey).(string)
return u, ok
}
// ContextSetLockID stores the lock id in the context.
func ContextSetLockID(ctx context.Context, t string) context.Context {
return context.WithValue(ctx, lockIDKey, t)
}
+36
View File
@@ -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 ctx
import (
"context"
auth "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
)
// ContextGetScopes returns the scopes if set in the given context.
func ContextGetScopes(ctx context.Context) (map[string]*auth.Scope, bool) {
s, ok := ctx.Value(scopeKey).(map[string]*auth.Scope)
return s, ok
}
// ContextSetScopes stores the scopes in the context.
func ContextSetScopes(ctx context.Context, s map[string]*auth.Scope) context.Context {
return context.WithValue(ctx, scopeKey, s)
}
+47
View File
@@ -0,0 +1,47 @@
// 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 ctx
import (
"context"
)
// TokenHeader is the header to be used across grpc and http services
// to forward the access token.
const TokenHeader = "x-access-token"
// ContextGetToken returns the token if set in the given context.
func ContextGetToken(ctx context.Context) (string, bool) {
u, ok := ctx.Value(tokenKey).(string)
return u, ok
}
// ContextMustGetToken panics if token is not in context.
func ContextMustGetToken(ctx context.Context) string {
u, ok := ContextGetToken(ctx)
if !ok {
panic("token not found in context")
}
return u
}
// ContextSetToken stores the token in the context.
func ContextSetToken(ctx context.Context, t string) context.Context {
return context.WithValue(ctx, tokenKey, t)
}
+67
View File
@@ -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 ctx
import (
"context"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
)
type key int
const (
userKey key = iota
tokenKey
idKey
lockIDKey
scopeKey
initiatorKey
)
// ContextGetUser returns the user if set in the given context.
func ContextGetUser(ctx context.Context) (*userpb.User, bool) {
u, ok := ctx.Value(userKey).(*userpb.User)
return u, ok
}
// ContextMustGetUser panics if user is not in context.
func ContextMustGetUser(ctx context.Context) *userpb.User {
u, ok := ContextGetUser(ctx)
if !ok {
panic("user not found in context")
}
return u
}
// ContextSetUser stores the user in the context.
func ContextSetUser(ctx context.Context, u *userpb.User) context.Context {
return context.WithValue(ctx, userKey, u)
}
// ContextGetUserID returns the user if set in the given context.
func ContextGetUserID(ctx context.Context) (*userpb.UserId, bool) {
u, ok := ctx.Value(idKey).(*userpb.UserId)
return u, ok
}
// ContextSetUserID stores the userid in the context.
func ContextSetUserID(ctx context.Context, id *userpb.UserId) context.Context {
return context.WithValue(ctx, idKey, id)
}
+38
View File
@@ -0,0 +1,38 @@
// Copyright 2018-2020 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package datatx
import (
"context"
datatx "github.com/cs3org/go-cs3apis/cs3/tx/v1beta1"
)
// Manager the interface any transfer driver should implement
type Manager interface {
// StartTransfer initiates a transfer job and returns a TxInfo object including a unique transfer id, and error if any.
StartTransfer(ctx context.Context, srcRemote string, srcPath string, srcToken string, destRemote string, destPath string, destToken string) (*datatx.TxInfo, error)
// GetTransferStatus returns a TxInfo object including the current status, and error if any.
GetTransferStatus(ctx context.Context, transferID string) (*datatx.TxInfo, error)
// CancelTransfer cancels the transfer and returns a TxInfo object and error if any.
CancelTransfer(ctx context.Context, transferID string) (*datatx.TxInfo, error)
// RetryTransfer retries the transfer and returns a TxInfo object and error if any.
// Note that tokens must still be valid.
RetryTransfer(ctx context.Context, transferID string) (*datatx.TxInfo, error)
}
@@ -0,0 +1,25 @@
// Copyright 2018-2020 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package loader
import (
// Load datatx drivers.
_ "github.com/opencloud-eu/reva/v2/pkg/datatx/manager/rclone"
// Add your own here
)
@@ -0,0 +1,831 @@
// Copyright 2018-2020 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package rclone
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"strconv"
"sync"
"time"
datatx "github.com/cs3org/go-cs3apis/cs3/tx/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/google/uuid"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
txdriver "github.com/opencloud-eu/reva/v2/pkg/datatx"
registry "github.com/opencloud-eu/reva/v2/pkg/datatx/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
"github.com/pkg/errors"
)
func init() {
registry.Register("rclone", New)
}
func (c *config) init(m map[string]interface{}) {
// set sane defaults
if c.File == "" {
c.File = "/var/tmp/reva/datatx-transfers.json"
}
if c.JobStatusCheckInterval == 0 {
c.JobStatusCheckInterval = 2000
}
if c.JobTimeout == 0 {
c.JobTimeout = 50000
}
}
type config struct {
Endpoint string `mapstructure:"endpoint"`
AuthUser string `mapstructure:"auth_user"` // rclone basicauth user
AuthPass string `mapstructure:"auth_pass"` // rclone basicauth pass
File string `mapstructure:"file"`
JobStatusCheckInterval int `mapstructure:"job_status_check_interval"`
JobTimeout int `mapstructure:"job_timeout"`
}
type rclone struct {
config *config
client *http.Client
pDriver *pDriver
}
type rcloneHTTPErrorRes struct {
Error string `json:"error"`
Input map[string]interface{} `json:"input"`
Path string `json:"path"`
Status int `json:"status"`
}
type transferModel struct {
File string
Transfers map[string]*transfer `json:"transfers"`
}
// persistency driver
type pDriver struct {
sync.Mutex // concurrent access to the file
model *transferModel
}
type transfer struct {
TransferID string
JobID int64
TransferStatus datatx.Status
SrcToken string
SrcRemote string
SrcPath string
DestToken string
DestRemote string
DestPath string
Ctime string
}
// txEndStatuses final statuses that cannot be changed anymore
var txEndStatuses = map[string]int32{
"STATUS_INVALID": 0,
"STATUS_DESTINATION_NOT_FOUND": 1,
"STATUS_TRANSFER_COMPLETE": 6,
"STATUS_TRANSFER_FAILED": 7,
"STATUS_TRANSFER_CANCELLED": 8,
"STATUS_TRANSFER_CANCEL_FAILED": 9,
"STATUS_TRANSFER_EXPIRED": 10,
}
// New returns a new rclone driver
func New(m map[string]interface{}) (txdriver.Manager, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init(m)
// TODO insecure should be configurable
client := rhttp.GetHTTPClient(rhttp.Insecure(true))
// The persistency driver
// Load or create 'db'
model, err := loadOrCreate(c.File)
if err != nil {
err = errors.Wrap(err, "error loading the file containing the transfers")
return nil, err
}
pDriver := &pDriver{
model: model,
}
return &rclone{
config: c,
client: client,
pDriver: pDriver,
}, nil
}
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
}
func loadOrCreate(file string) (*transferModel, error) {
_, err := os.Stat(file)
if os.IsNotExist(err) {
if err := os.WriteFile(file, []byte("{}"), 0700); err != nil {
err = errors.Wrap(err, "error creating the transfers storage file: "+file)
return nil, err
}
}
fd, err := os.OpenFile(file, os.O_CREATE, 0644)
if err != nil {
err = errors.Wrap(err, "error opening the transfers storage file: "+file)
return nil, err
}
defer fd.Close()
data, err := io.ReadAll(fd)
if err != nil {
err = errors.Wrap(err, "error reading the data")
return nil, err
}
model := &transferModel{}
if err := json.Unmarshal(data, model); err != nil {
err = errors.Wrap(err, "error decoding transfers data to json")
return nil, err
}
if model.Transfers == nil {
model.Transfers = make(map[string]*transfer)
}
model.File = file
return model, nil
}
// saveTransfer saves the transfer. If an error is specified than that error will be returned, possibly wrapped with additional errors.
func (m *transferModel) saveTransfer(e error) error {
data, err := json.Marshal(m)
if err != nil {
e = errors.Wrap(err, "error encoding transfer data to json")
return e
}
if err := os.WriteFile(m.File, data, 0644); err != nil {
e = errors.Wrap(err, "error writing transfer data to file: "+m.File)
return e
}
return e
}
// StartTransfer initiates a transfer job and returns a TxInfo object that includes a unique transfer id.
func (driver *rclone) StartTransfer(ctx context.Context, srcRemote string, srcPath string, srcToken string, destRemote string, destPath string, destToken string) (*datatx.TxInfo, error) {
return driver.startJob(ctx, "", srcRemote, srcPath, srcToken, destRemote, destPath, destToken)
}
// startJob starts a transfer job. Retries a previous job if transferID is specified.
func (driver *rclone) startJob(ctx context.Context, transferID string, srcRemote string, srcPath string, srcToken string, destRemote string, destPath string, destToken string) (*datatx.TxInfo, error) {
logger := appctx.GetLogger(ctx)
driver.pDriver.Lock()
defer driver.pDriver.Unlock()
var txID string
var cTime *typespb.Timestamp
if transferID == "" {
txID = uuid.New().String()
cTime = &typespb.Timestamp{Seconds: uint64(time.Now().Unix())}
} else { // restart existing transfer if transferID is specified
logger.Debug().Msgf("Restarting transfer (txID: %s)", transferID)
txID = transferID
transfer, err := driver.pDriver.model.getTransfer(txID)
if err != nil {
err = errors.Wrap(err, "rclone: error retrying transfer (transferID: "+txID+")")
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: txID},
Status: datatx.Status_STATUS_INVALID,
Ctime: nil,
}, err
}
seconds, _ := strconv.ParseInt(transfer.Ctime, 10, 64)
cTime = &typespb.Timestamp{Seconds: uint64(seconds)}
_, endStatusFound := txEndStatuses[transfer.TransferStatus.String()]
if !endStatusFound {
err := errors.New("rclone: transfer still running, unable to restart")
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: txID},
Status: transfer.TransferStatus,
Ctime: cTime,
}, err
}
srcToken = transfer.SrcToken
srcRemote = transfer.SrcRemote
srcPath = transfer.SrcPath
destToken = transfer.DestToken
destRemote = transfer.DestRemote
destPath = transfer.DestPath
delete(driver.pDriver.model.Transfers, txID)
}
transferStatus := datatx.Status_STATUS_TRANSFER_NEW
transfer := &transfer{
TransferID: txID,
JobID: int64(-1),
TransferStatus: transferStatus,
SrcToken: srcToken,
SrcRemote: srcRemote,
SrcPath: srcPath,
DestToken: destToken,
DestRemote: destRemote,
DestPath: destPath,
Ctime: fmt.Sprint(cTime.Seconds), // TODO do we need nanos here?
}
driver.pDriver.model.Transfers[txID] = transfer
type rcloneAsyncReqJSON struct {
SrcFs string `json:"srcFs"`
// SrcToken string `json:"srcToken"`
DstFs string `json:"dstFs"`
// DstToken string `json:"destToken"`
Async bool `json:"_async"`
}
srcFs := fmt.Sprintf(":webdav,headers=\"x-access-token,%v\",url=\"%v\":%v", srcToken, srcRemote, srcPath)
dstFs := fmt.Sprintf(":webdav,headers=\"x-access-token,%v\",url=\"%v\":%v", destToken, destRemote, destPath)
rcloneReq := &rcloneAsyncReqJSON{
SrcFs: srcFs,
DstFs: dstFs,
Async: true,
}
data, err := json.Marshal(rcloneReq)
if err != nil {
err = errors.Wrap(err, "rclone: error pulling transfer: error marshalling rclone req data")
transfer.TransferStatus = datatx.Status_STATUS_INVALID
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: txID},
Status: datatx.Status_STATUS_INVALID,
Ctime: cTime,
}, driver.pDriver.model.saveTransfer(err)
}
transferFileMethod := "/sync/copy"
remotePathIsFolder, err := driver.remotePathIsFolder(srcRemote, srcPath, srcToken)
if err != nil {
err = errors.Wrap(err, "rclone: error pulling transfer: error stating src path")
transfer.TransferStatus = datatx.Status_STATUS_INVALID
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: txID},
Status: datatx.Status_STATUS_INVALID,
Ctime: cTime,
}, driver.pDriver.model.saveTransfer(err)
}
if !remotePathIsFolder {
err = errors.Wrap(err, "rclone: error pulling transfer: path is a file, only folder transfer is implemented")
transfer.TransferStatus = datatx.Status_STATUS_INVALID
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: txID},
Status: datatx.Status_STATUS_INVALID,
Ctime: cTime,
}, driver.pDriver.model.saveTransfer(err)
}
u, err := url.Parse(driver.config.Endpoint)
if err != nil {
err = errors.Wrap(err, "rclone: error pulling transfer: error parsing driver endpoint")
transfer.TransferStatus = datatx.Status_STATUS_INVALID
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: txID},
Status: datatx.Status_STATUS_INVALID,
Ctime: cTime,
}, driver.pDriver.model.saveTransfer(err)
}
u.Path = path.Join(u.Path, transferFileMethod)
requestURL := u.String()
req, err := http.NewRequest("POST", requestURL, bytes.NewReader(data))
if err != nil {
err = errors.Wrap(err, "rclone: error pulling transfer: error framing post request")
transfer.TransferStatus = datatx.Status_STATUS_TRANSFER_FAILED
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: txID},
Status: transfer.TransferStatus,
Ctime: cTime,
}, driver.pDriver.model.saveTransfer(err)
}
req.Header.Set("Content-Type", "application/json")
req.SetBasicAuth(driver.config.AuthUser, driver.config.AuthPass)
res, err := driver.client.Do(req)
if err != nil {
err = errors.Wrap(err, "rclone: error pulling transfer: error sending post request")
transfer.TransferStatus = datatx.Status_STATUS_TRANSFER_FAILED
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: txID},
Status: transfer.TransferStatus,
Ctime: cTime,
}, driver.pDriver.model.saveTransfer(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
var errorResData rcloneHTTPErrorRes
if err = json.NewDecoder(res.Body).Decode(&errorResData); err != nil {
err = errors.Wrap(err, "rclone driver: error decoding response data")
transfer.TransferStatus = datatx.Status_STATUS_TRANSFER_FAILED
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: txID},
Status: transfer.TransferStatus,
Ctime: cTime,
}, driver.pDriver.model.saveTransfer(err)
}
e := errors.New("rclone: rclone request responded with error, " + fmt.Sprintf(" status: %v, error: %v", errorResData.Status, errorResData.Error))
transfer.TransferStatus = datatx.Status_STATUS_TRANSFER_FAILED
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: txID},
Status: transfer.TransferStatus,
Ctime: cTime,
}, driver.pDriver.model.saveTransfer(e)
}
type rcloneAsyncResJSON struct {
JobID int64 `json:"jobid"`
}
var resData rcloneAsyncResJSON
if err = json.NewDecoder(res.Body).Decode(&resData); err != nil {
err = errors.Wrap(err, "rclone: error decoding response data")
transfer.TransferStatus = datatx.Status_STATUS_TRANSFER_FAILED
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: txID},
Status: transfer.TransferStatus,
Ctime: cTime,
}, driver.pDriver.model.saveTransfer(err)
}
transfer.JobID = resData.JobID
if err := driver.pDriver.model.saveTransfer(nil); err != nil {
err = errors.Wrap(err, "rclone: error pulling transfer")
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: txID},
Status: datatx.Status_STATUS_INVALID,
Ctime: cTime,
}, err
}
// start separate dedicated process to periodically check the transfer progress
go func() {
// runs for as long as no end state or time out has been reached
startTimeMs := time.Now().Nanosecond() / 1000
timeout := driver.config.JobTimeout
driver.pDriver.Lock()
defer driver.pDriver.Unlock()
for {
transfer, err := driver.pDriver.model.getTransfer(txID)
if err != nil {
transfer.TransferStatus = datatx.Status_STATUS_INVALID
err = driver.pDriver.model.saveTransfer(err)
logger.Error().Err(err).Msgf("rclone driver: unable to retrieve transfer with id: %v", txID)
break
}
// check for end status first
_, endStatusFound := txEndStatuses[transfer.TransferStatus.String()]
if endStatusFound {
logger.Info().Msgf("rclone driver: transfer endstatus reached: %v", transfer.TransferStatus)
break
}
// check for possible timeout and if true were done
currentTimeMs := time.Now().Nanosecond() / 1000
timePastMs := currentTimeMs - startTimeMs
if timePastMs > timeout {
logger.Info().Msgf("rclone driver: transfer timed out: %vms (timeout = %v)", timePastMs, timeout)
// set status to EXPIRED and save
transfer.TransferStatus = datatx.Status_STATUS_TRANSFER_EXPIRED
if err := driver.pDriver.model.saveTransfer(nil); err != nil {
logger.Error().Err(err).Msgf("rclone driver: save transfer failed: %v", err)
}
break
}
jobID := transfer.JobID
type rcloneStatusReqJSON struct {
JobID int64 `json:"jobid"`
}
rcloneStatusReq := &rcloneStatusReqJSON{
JobID: jobID,
}
data, err := json.Marshal(rcloneStatusReq)
if err != nil {
logger.Error().Err(err).Msgf("rclone driver: marshalling request failed: %v", err)
transfer.TransferStatus = datatx.Status_STATUS_INVALID
if err := driver.pDriver.model.saveTransfer(nil); err != nil {
logger.Error().Err(err).Msgf("rclone driver: save transfer failed: %v", err)
}
break
}
transferFileMethod := "/job/status"
u, err := url.Parse(driver.config.Endpoint)
if err != nil {
logger.Error().Err(err).Msgf("rclone driver: could not parse driver endpoint: %v", err)
transfer.TransferStatus = datatx.Status_STATUS_INVALID
if err := driver.pDriver.model.saveTransfer(nil); err != nil {
logger.Error().Err(err).Msgf("rclone driver: save transfer failed: %v", err)
}
break
}
u.Path = path.Join(u.Path, transferFileMethod)
requestURL := u.String()
req, err := http.NewRequest("POST", requestURL, bytes.NewReader(data))
if err != nil {
logger.Error().Err(err).Msgf("rclone driver: error framing post request: %v", err)
transfer.TransferStatus = datatx.Status_STATUS_INVALID
if err := driver.pDriver.model.saveTransfer(nil); err != nil {
logger.Error().Err(err).Msgf("rclone driver: save transfer failed: %v", err)
}
break
}
req.Header.Set("Content-Type", "application/json")
req.SetBasicAuth(driver.config.AuthUser, driver.config.AuthPass)
res, err := driver.client.Do(req)
if err != nil {
logger.Error().Err(err).Msgf("rclone driver: error sending post request: %v", err)
transfer.TransferStatus = datatx.Status_STATUS_INVALID
if err := driver.pDriver.model.saveTransfer(nil); err != nil {
logger.Error().Err(err).Msgf("rclone driver: save transfer failed: %v", err)
}
break
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
var errorResData rcloneHTTPErrorRes
if err = json.NewDecoder(res.Body).Decode(&errorResData); err != nil {
err = errors.Wrap(err, "rclone driver: error decoding response data")
logger.Error().Err(err).Msgf("rclone driver: error reading response body: %v", err)
}
logger.Error().Err(err).Msgf("rclone driver: rclone request responded with error, status: %v, error: %v", errorResData.Status, errorResData.Error)
transfer.TransferStatus = datatx.Status_STATUS_INVALID
if err := driver.pDriver.model.saveTransfer(nil); err != nil {
logger.Error().Err(err).Msgf("rclone driver: save transfer failed: %v", err)
}
break
}
type rcloneStatusResJSON struct {
Finished bool `json:"finished"`
Success bool `json:"success"`
ID int64 `json:"id"`
Error string `json:"error"`
Group string `json:"group"`
StartTime string `json:"startTime"`
EndTime string `json:"endTime"`
Duration float64 `json:"duration"`
// think we don't need this
// "output": {} // output of the job as would have been returned if called synchronously
}
var resData rcloneStatusResJSON
if err = json.NewDecoder(res.Body).Decode(&resData); err != nil {
logger.Error().Err(err).Msgf("rclone driver: error decoding response data: %v", err)
break
}
if resData.Error != "" {
logger.Error().Err(err).Msgf("rclone driver: rclone responded with error: %v", resData.Error)
transfer.TransferStatus = datatx.Status_STATUS_TRANSFER_FAILED
if err := driver.pDriver.model.saveTransfer(nil); err != nil {
logger.Error().Err(err).Msgf("rclone driver: error saving transfer: %v", err)
break
}
break
}
// transfer complete
if resData.Finished && resData.Success {
logger.Info().Msg("rclone driver: transfer job finished")
transfer.TransferStatus = datatx.Status_STATUS_TRANSFER_COMPLETE
if err := driver.pDriver.model.saveTransfer(nil); err != nil {
logger.Error().Err(err).Msgf("rclone driver: error saving transfer: %v", err)
break
}
break
}
// transfer completed unsuccessfully without error
if resData.Finished && !resData.Success {
logger.Info().Msgf("rclone driver: transfer job failed")
transfer.TransferStatus = datatx.Status_STATUS_TRANSFER_FAILED
if err := driver.pDriver.model.saveTransfer(nil); err != nil {
logger.Error().Err(err).Msgf("rclone driver: error saving transfer: %v", err)
break
}
break
}
// transfer not yet finished: continue
if !resData.Finished {
logger.Info().Msgf("rclone driver: transfer job in progress")
transfer.TransferStatus = datatx.Status_STATUS_TRANSFER_IN_PROGRESS
if err := driver.pDriver.model.saveTransfer(nil); err != nil {
logger.Error().Err(err).Msgf("rclone driver: error saving transfer: %v", err)
break
}
}
<-time.After(time.Millisecond * time.Duration(driver.config.JobStatusCheckInterval))
}
}()
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: txID},
Status: transferStatus,
Ctime: cTime,
}, nil
}
// GetTransferStatus returns the status of the transfer with the specified job id
func (driver *rclone) GetTransferStatus(ctx context.Context, transferID string) (*datatx.TxInfo, error) {
transfer, err := driver.pDriver.model.getTransfer(transferID)
if err != nil {
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: transferID},
Status: datatx.Status_STATUS_INVALID,
Ctime: nil,
}, err
}
cTime, _ := strconv.ParseInt(transfer.Ctime, 10, 64)
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: transferID},
Status: transfer.TransferStatus,
Ctime: &typespb.Timestamp{Seconds: uint64(cTime)},
}, nil
}
// CancelTransfer cancels the transfer with the specified transfer id
func (driver *rclone) CancelTransfer(ctx context.Context, transferID string) (*datatx.TxInfo, error) {
transfer, err := driver.pDriver.model.getTransfer(transferID)
if err != nil {
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: transferID},
Status: datatx.Status_STATUS_INVALID,
Ctime: nil,
}, err
}
cTime, _ := strconv.ParseInt(transfer.Ctime, 10, 64)
_, endStatusFound := txEndStatuses[transfer.TransferStatus.String()]
if endStatusFound {
err := errors.New("rclone driver: transfer already in end state")
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: transferID},
Status: datatx.Status_STATUS_INVALID,
Ctime: &typespb.Timestamp{Seconds: uint64(cTime)},
}, err
}
// rcloneStop the rclone job/stop method json request
type rcloneStopRequest struct {
JobID int64 `json:"jobid"`
}
rcloneCancelTransferReq := &rcloneStopRequest{
JobID: transfer.JobID,
}
data, err := json.Marshal(rcloneCancelTransferReq)
if err != nil {
err = errors.Wrap(err, "rclone driver: error marshalling rclone req data")
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: transferID},
Status: datatx.Status_STATUS_INVALID,
Ctime: &typespb.Timestamp{Seconds: uint64(cTime)},
}, err
}
transferFileMethod := "/job/stop"
u, err := url.Parse(driver.config.Endpoint)
if err != nil {
err = errors.Wrap(err, "rclone driver: error parsing driver endpoint")
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: transferID},
Status: datatx.Status_STATUS_INVALID,
Ctime: &typespb.Timestamp{Seconds: uint64(cTime)},
}, err
}
u.Path = path.Join(u.Path, transferFileMethod)
requestURL := u.String()
req, err := http.NewRequest("POST", requestURL, bytes.NewReader(data))
if err != nil {
err = errors.Wrap(err, "rclone driver: error framing post request")
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: transferID},
Status: datatx.Status_STATUS_INVALID,
Ctime: &typespb.Timestamp{Seconds: uint64(cTime)},
}, err
}
req.Header.Set("Content-Type", "application/json")
req.SetBasicAuth(driver.config.AuthUser, driver.config.AuthPass)
res, err := driver.client.Do(req)
if err != nil {
err = errors.Wrap(err, "rclone driver: error sending post request")
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: transferID},
Status: datatx.Status_STATUS_INVALID,
Ctime: &typespb.Timestamp{Seconds: uint64(cTime)},
}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
var errorResData rcloneHTTPErrorRes
if err = json.NewDecoder(res.Body).Decode(&errorResData); err != nil {
err = errors.Wrap(err, "rclone driver: error decoding response data")
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: transferID},
Status: datatx.Status_STATUS_INVALID,
Ctime: &typespb.Timestamp{Seconds: uint64(cTime)},
}, err
}
err = errors.Wrap(errors.Errorf("status: %v, error: %v", errorResData.Status, errorResData.Error), "rclone driver: rclone request responded with error")
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: transferID},
Status: datatx.Status_STATUS_INVALID,
Ctime: &typespb.Timestamp{Seconds: uint64(cTime)},
}, err
}
type rcloneCancelTransferResJSON struct {
Finished bool `json:"finished"`
Success bool `json:"success"`
ID int64 `json:"id"`
Error string `json:"error"`
Group string `json:"group"`
StartTime string `json:"startTime"`
EndTime string `json:"endTime"`
Duration float64 `json:"duration"`
// think we don't need this
// "output": {} // output of the job as would have been returned if called synchronously
}
var resData rcloneCancelTransferResJSON
if err = json.NewDecoder(res.Body).Decode(&resData); err != nil {
err = errors.Wrap(err, "rclone driver: error decoding response data")
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: transferID},
Status: datatx.Status_STATUS_INVALID,
Ctime: &typespb.Timestamp{Seconds: uint64(cTime)},
}, err
}
if resData.Error != "" {
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: transferID},
Status: datatx.Status_STATUS_TRANSFER_CANCEL_FAILED,
Ctime: &typespb.Timestamp{Seconds: uint64(cTime)},
}, errors.New(resData.Error)
}
transfer.TransferStatus = datatx.Status_STATUS_TRANSFER_CANCELLED
if err := driver.pDriver.model.saveTransfer(nil); err != nil {
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: transferID},
Status: datatx.Status_STATUS_INVALID,
Ctime: &typespb.Timestamp{Seconds: uint64(cTime)},
}, err
}
return &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: transferID},
Status: datatx.Status_STATUS_TRANSFER_CANCELLED,
Ctime: &typespb.Timestamp{Seconds: uint64(cTime)},
}, nil
}
// RetryTransfer retries the transfer with the specified transfer ID.
// Note that tokens must still be valid.
func (driver *rclone) RetryTransfer(ctx context.Context, transferID string) (*datatx.TxInfo, error) {
return driver.startJob(ctx, transferID, "", "", "", "", "", "")
}
// getTransfer returns the transfer with the specified transfer ID
func (m *transferModel) getTransfer(transferID string) (*transfer, error) {
transfer, ok := m.Transfers[transferID]
if !ok {
return nil, errors.New("rclone driver: invalid transfer ID")
}
return transfer, nil
}
func (driver *rclone) remotePathIsFolder(remote string, remotePath string, remoteToken string) (bool, error) {
type rcloneListReqJSON struct {
Fs string `json:"fs"`
Remote string `json:"remote"`
}
fs := fmt.Sprintf(":webdav,headers=\"x-access-token,%v\",url=\"%v\":", remoteToken, remote)
rcloneReq := &rcloneListReqJSON{
Fs: fs,
Remote: remotePath,
}
data, err := json.Marshal(rcloneReq)
if err != nil {
return false, errors.Wrap(err, "rclone: error marshalling rclone req data")
}
listMethod := "/operations/list"
u, err := url.Parse(driver.config.Endpoint)
if err != nil {
return false, errors.Wrap(err, "rclone driver: error parsing driver endpoint")
}
u.Path = path.Join(u.Path, listMethod)
requestURL := u.String()
req, err := http.NewRequest("POST", requestURL, bytes.NewReader(data))
if err != nil {
return false, errors.Wrap(err, "rclone driver: error framing post request")
}
req.Header.Set("Content-Type", "application/json")
req.SetBasicAuth(driver.config.AuthUser, driver.config.AuthPass)
res, err := driver.client.Do(req)
if err != nil {
return false, errors.Wrap(err, "rclone driver: error sending post request")
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
var errorResData rcloneHTTPErrorRes
if err = json.NewDecoder(res.Body).Decode(&errorResData); err != nil {
return false, errors.Wrap(err, "rclone driver: error decoding response data")
}
return false, errors.Wrap(errors.Errorf("status: %v, error: %v", errorResData.Status, errorResData.Error), "rclone driver: rclone request responded with error")
}
type item struct {
Path string `json:"Path"`
Name string `json:"Name"`
Size int64 `json:"Size"`
MimeType string `json:"MimeType"`
ModTime string `json:"ModTime"`
IsDir bool `json:"IsDir"`
}
type rcloneListResJSON struct {
List []*item `json:"list"`
}
var resData rcloneListResJSON
if err = json.NewDecoder(res.Body).Decode(&resData); err != nil {
return false, errors.Wrap(err, "rclone driver: error decoding response data")
}
// a file will return one single item, the file, with path being the remote path and IsDir will be false
if len(resData.List) == 1 && resData.List[0].Path == remotePath && !resData.List[0].IsDir {
return false, nil
}
// in all other cases the remote path is a directory
return true, nil
}
@@ -0,0 +1,36 @@
// Copyright 2018-2020 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package registry
import (
"github.com/opencloud-eu/reva/v2/pkg/datatx"
)
// NewFunc is the function that datatx implementations
// should register at init time.
type NewFunc func(map[string]interface{}) (datatx.Manager, error)
// NewFuncs is a map containing all the registered datatx backends.
var NewFuncs = map[string]NewFunc{}
// Register registers a new datatx backend new function.
// Not safe for concurrent use. Safe for use from package init.
func Register(name string, f NewFunc) {
NewFuncs[name] = f
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,150 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package eosclient
import (
"context"
"io"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/acl"
)
// EOSClient is the interface which enables access to EOS instances through various interfaces.
type EOSClient interface {
AddACL(ctx context.Context, auth, rootAuth Authorization, path string, position uint, a *acl.Entry) error
RemoveACL(ctx context.Context, auth, rootAuth Authorization, path string, a *acl.Entry) error
UpdateACL(ctx context.Context, auth, rootAuth Authorization, path string, position uint, a *acl.Entry) error
GetACL(ctx context.Context, auth Authorization, path, aclType, target string) (*acl.Entry, error)
ListACLs(ctx context.Context, auth Authorization, path string) ([]*acl.Entry, error)
GetFileInfoByInode(ctx context.Context, auth Authorization, inode uint64) (*FileInfo, error)
GetFileInfoByFXID(ctx context.Context, auth Authorization, fxid string) (*FileInfo, error)
GetFileInfoByPath(ctx context.Context, auth Authorization, path string) (*FileInfo, error)
SetAttr(ctx context.Context, auth Authorization, attr *Attribute, errorIfExists, recursive bool, path string) error
UnsetAttr(ctx context.Context, auth Authorization, attr *Attribute, recursive bool, path string) error
GetAttr(ctx context.Context, auth Authorization, key, path string) (*Attribute, error)
GetQuota(ctx context.Context, username string, rootAuth Authorization, path string) (*QuotaInfo, error)
SetQuota(ctx context.Context, rooAuth Authorization, info *SetQuotaInfo) error
Touch(ctx context.Context, auth Authorization, path string) error
Chown(ctx context.Context, auth, chownauth Authorization, path string) error
Chmod(ctx context.Context, auth Authorization, mode, path string) error
CreateDir(ctx context.Context, auth Authorization, path string) error
Remove(ctx context.Context, auth Authorization, path string, noRecycle bool) error
Rename(ctx context.Context, auth Authorization, oldPath, newPath string) error
List(ctx context.Context, auth Authorization, path string) ([]*FileInfo, error)
Read(ctx context.Context, auth Authorization, path string) (io.ReadCloser, error)
Write(ctx context.Context, auth Authorization, path string, stream io.ReadCloser) error
WriteFile(ctx context.Context, auth Authorization, path, source string) error
ListDeletedEntries(ctx context.Context, auth Authorization) ([]*DeletedEntry, error)
RestoreDeletedEntry(ctx context.Context, auth Authorization, key string) error
PurgeDeletedEntries(ctx context.Context, auth Authorization) error
ListVersions(ctx context.Context, auth Authorization, p string) ([]*FileInfo, error)
RollbackToVersion(ctx context.Context, auth Authorization, path, version string) error
ReadVersion(ctx context.Context, auth Authorization, p, version string) (io.ReadCloser, error)
GenerateToken(ctx context.Context, auth Authorization, path string, a *acl.Entry) (string, error)
}
// AttrType is the type of extended attribute,
// either system (sys) or user (user).
type AttrType uint32
// Attribute represents an EOS extended attribute.
type Attribute struct {
Type AttrType
Key, Val string
}
// FileInfo represents the metadata information returned by querying the EOS namespace.
type FileInfo struct {
IsDir bool
MTimeNanos uint32
Inode uint64 `json:"inode"`
FID uint64 `json:"fid"`
UID uint64 `json:"uid"`
GID uint64 `json:"gid"`
TreeSize uint64 `json:"tree_size"`
MTimeSec uint64 `json:"mtime_sec"`
Size uint64 `json:"size"`
TreeCount uint64 `json:"tree_count"`
File string `json:"eos_file"`
ETag string `json:"etag"`
Instance string `json:"instance"`
XS *Checksum `json:"xs"`
SysACL *acl.ACLs `json:"sys_acl"`
Attrs map[string]string `json:"attrs"`
}
// DeletedEntry represents an entry from the trashbin.
type DeletedEntry struct {
RestorePath string
RestoreKey string
Size uint64
DeletionMTime uint64
IsDir bool
}
// Checksum represents a cheksum entry for a file returned by EOS.
type Checksum struct {
XSSum string
XSType string
}
// QuotaInfo reports the available bytes and inodes for a particular user.
// eos reports all quota values are unsigned long, see https://github.com/cern-eos/eos/blob/93515df8c0d5a858982853d960bec98f983c1285/mgm/Quota.hh#L135
type QuotaInfo struct {
AvailableBytes, UsedBytes uint64
AvailableInodes, UsedInodes uint64
}
// SetQuotaInfo encapsulates the information needed to
// create a quota space in EOS for a user
type SetQuotaInfo struct {
Username string
UID string
GID string
QuotaNode string
MaxBytes uint64
MaxFiles uint64
}
// Constants for ACL position
const (
EndPosition uint = 0
StartPosition uint = 1
)
// Role holds the attributes required to authenticate to EOS via role-based access.
type Role struct {
UID, GID string
}
// Authorization specifies the mechanisms through which EOS can be accessed.
// One of the data members must be set.
type Authorization struct {
Role Role
Token string
}
// AttrAlreadyExistsError is the error raised when setting
// an already existing attr on a resource
const AttrAlreadyExistsError = errtypes.BadRequest("attr already exists")
// AttrNotExistsError is the error raised when removing
// an attribute that does not exist
const AttrNotExistsError = errtypes.BadRequest("attr not exists")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,604 @@
// @project The CERN Tape Archive (CTA)
// @brief CTA-EOS gRPC API for CASTOR-EOS migration
// @copyright Copyright 2019 CERN
// @license This program is free software: you can redistribute it and/or
// modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be
// useful, but WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more
// details.
//
// You should have received a copy of the GNU General Public
// License along with this program. If not, see
// <http://www.gnu.org/licenses/>.
// NOTE: Compile for Go with:
// protoc ./eos_grpc.proto --go_out=plugins=grpc:.
syntax = "proto3";
package eos.rpc;
option java_multiple_files = true;
option java_package = "io.grpc.eos.rpc";
option java_outer_classname = "EosProto";
option objc_class_prefix = "EOS";
option go_package = "github.com/cern-eos/grpc-proto/protobuf;eos_grpc";
service Eos {
// Replies to a ping
rpc Ping(PingRequest) returns (PingReply) {}
// ---------------------------------------------------------------------
// NAMESPACE
// ---------------------------------------------------------------------
// Replies to MD requests with a stream
rpc MD(MDRequest) returns (stream MDResponse) {}
// Replies to Find requests with a stream
rpc Find(FindRequest) returns (stream MDResponse) {}
// Replies to a NsStat operation
rpc NsStat(NsStatRequest) returns (NsStatResponse) {}
// Replies to an insert
rpc ContainerInsert(ContainerInsertRequest) returns (InsertReply) {}
rpc FileInsert(FileInsertRequest) returns (InsertReply) {}
// Replies to a NsRequest operation
rpc Exec(NSRequest) returns (NSResponse) {}
// ---------------------------------------------------------------------
// OPENSTACK
// ---------------------------------------------------------------------
// Manila Driver
rpc ManilaServerRequest(ManilaRequest) returns (ManilaResponse) {}
}
message PingRequest {
string authkey = 1;
bytes message = 2;
}
message PingReply { bytes message = 1; }
// ---------------------------------------------------------------------
// NAMESPACE
// ---------------------------------------------------------------------
message ContainerInsertRequest {
repeated ContainerMdProto container = 1;
string authkey = 2;
bool inherit_md = 3;
}
message FileInsertRequest {
repeated FileMdProto files = 1;
string authkey = 2;
}
message InsertReply {
repeated string message = 1;
repeated uint32 retc = 2;
}
message Time {
uint64 sec = 1;
uint64 n_sec = 2;
}
message Checksum {
bytes value = 1;
string type = 2;
}
message FileMdProto {
uint64 id = 1;
uint64 cont_id = 2;
uint64 uid = 3;
uint64 gid = 4;
uint64 size = 5;
uint32 layout_id = 6;
uint32 flags = 7;
bytes name = 8;
bytes link_name = 9;
Time ctime = 10; // change time
Time mtime = 11; // modification time
Checksum checksum = 12;
repeated uint32 locations = 13;
repeated uint32 unlink_locations = 14;
map<string, bytes> xattrs = 15;
bytes path = 16;
string etag = 17;
uint64 inode = 18;
}
message ContainerMdProto {
uint64 id = 1;
uint64 parent_id = 2;
uint64 uid = 3;
uint64 gid = 4;
int64 tree_size = 6;
uint32 mode = 5;
uint32 flags = 7;
bytes name = 8;
Time ctime = 9; // change time
Time mtime = 10; // modification time
Time stime = 11; // sync time
map<string, bytes> xattrs = 12;
bytes path = 13;
string etag = 14;
uint64 inode = 15;
}
enum TYPE {
FILE = 0;
CONTAINER = 1;
LISTING = 2;
STAT = 3;
}
enum QUOTATYPE {
USER = 0;
GROUP = 2;
PROJECT = 3;
}
enum QUOTAOP {
GET = 0;
SET = 1;
RM = 2;
RMNODE = 3;
}
enum QUOTAENTRY {
NONE = 0;
VOLUME = 1;
INODE = 2;
}
message QuotaProto {
bytes path = 1; // quota node path
string name = 2; // associated name for the given type
QUOTATYPE type = 3; // user,group,project or all quota
uint64 usedbytes = 4; // bytes used physical
uint64 usedlogicalbytes = 5; // bytes used logical
uint64 usedfiles = 6; // number of files used
uint64 maxbytes = 7; // maximum number of bytes (volume quota)
uint64 maxlogicalbytes =
8; // maximum number of logical bytes (logical volume quota)
uint64 maxfiles = 9; // maximum number of files (inode quota)
float percentageusedbytes =
10; // percentage of volume quota used from 0 to 100
float percentageusedfiles = 11; // percentag of inode quota used from 0 to 100
string statusbytes = 12; // status string for volume quota ok,warning,exceeded
string statusfiles = 13; // status string for inode quota ok,warning,exceeded
}
message RoleId {
uint64 uid = 1;
uint64 gid = 2;
string username = 3;
string groupname = 4;
}
message MDId {
bytes path = 1;
fixed64 id = 2;
fixed64 ino = 3;
TYPE type = 4;
}
message Limit {
bool zero = 1;
uint64 min = 2;
uint64 max = 3;
}
message MDSelection {
bool select = 1;
Limit ctime = 2;
Limit mtime = 3;
Limit stime = 4;
Limit size = 5;
Limit treesize = 6;
Limit children = 7;
Limit locations = 8;
Limit unlinked_locations = 9;
uint64 layoutid = 10;
uint64 flags = 11;
bool symlink = 12;
Checksum checksum = 13;
uint32 owner = 14;
uint32 group = 15;
bool owner_root = 16;
bool group_root = 17;
bytes regexp_filename = 18;
bytes regexp_dirname = 19;
map<string, bytes> xattr = 20;
}
message MDRequest {
TYPE type = 1;
MDId id = 2;
string authkey = 3;
RoleId role = 4;
MDSelection selection = 5;
}
message MDResponse {
TYPE type = 1;
FileMdProto fmd = 2;
ContainerMdProto cmd = 3;
}
message FindRequest {
TYPE type = 1;
MDId id = 2;
RoleId role = 3;
string authkey = 4;
uint64 maxdepth = 5;
MDSelection selection = 6;
}
message ShareAuth {
string prot = 1;
string name = 2;
string host = 3;
}
message ShareProto {
string permission = 1;
uint64 expires = 2;
string owner = 3;
string group = 4;
uint64 generation = 5;
string path = 6;
bool allowtree = 7;
string vtoken = 8;
repeated ShareAuth origins = 9;
}
message ShareToken {
ShareProto token = 1;
bytes signature = 2;
bytes serialized = 3;
int32 seed = 4;
}
message NSRequest {
message MkdirRequest {
MDId id = 1;
bool recursive = 2;
int64 mode = 3;
}
message RmdirRequest { MDId id = 1; }
message TouchRequest { MDId id = 1; }
message UnlinkRequest {
MDId id = 1;
bool norecycle = 3;
}
message RmRequest {
MDId id = 1;
bool recursive = 2;
bool norecycle = 3;
}
message RenameRequest {
MDId id = 1;
bytes target = 2;
}
message SymlinkRequest {
MDId id = 1;
bytes target = 2;
}
message VersionRequest {
enum VERSION_CMD {
CREATE = 0;
PURGE = 1;
LIST = 2;
GRAB = 3;
}
MDId id = 1;
VERSION_CMD cmd = 2;
int32 maxversion = 3;
string grabversion = 4;
}
message RecycleRequest {
string key = 1;
enum RECYCLE_CMD {
RESTORE = 0;
PURGE = 1;
LIST = 2;
}
RECYCLE_CMD cmd = 2;
message RestoreFlags {
bool force = 1;
bool mkpath = 2;
bool versions = 3;
}
message PurgeDate {
int32 year = 1;
int32 month = 2;
int32 day = 3;
}
RestoreFlags restoreflag = 3;
PurgeDate purgedate = 4;
}
message SetXAttrRequest {
MDId id = 1;
map<string, bytes> xattrs = 2;
bool recursive = 3;
repeated string keystodelete = 4;
bool create = 5;
}
message ChownRequest {
MDId id = 1;
RoleId owner = 2;
}
message ChmodRequest {
MDId id = 1;
int64 mode = 2;
}
message AclRequest {
enum ACL_COMMAND {
NONE = 0;
MODIFY = 1;
LIST = 2;
}
enum ACL_TYPE {
USER_ACL = 0;
SYS_ACL = 1;
}
MDId id = 1;
ACL_COMMAND cmd = 2;
bool recursive = 3;
ACL_TYPE type = 4;
string rule = 5;
uint32 position = 6;
}
message TokenRequest { ShareToken token = 1; }
message QuotaRequest {
bytes path = 1;
RoleId id = 2;
QUOTAOP op = 3; // get or set, rm or rmnode
uint64 maxfiles = 4; // maximum number of bytes (volume quota) for setting
uint64 maxbytes = 5; // maximum number of bytes (volume quota) for setting
QUOTAENTRY entry = 6; // select volume or inode entry for deletion
}
message ShareRequest {
message LsShare {
enum OutFormat {
NONE = 0; //
MONITORING = 1; // [-m]
LISTING = 2; // [-l]
JSON = 3; // [grpc]
}
OutFormat outformat = 1; //
string selection = 2; //
}
message OperateShare {
enum Op {
CREATE = 0;
REMOVE = 1;
SHARE = 2;
UNSHARE = 3;
ACCESS = 4;
MODIFY = 5;
}
Op op = 1;
string share = 2;
string acl = 3;
string path = 4;
string user = 5;
string group = 6;
}
oneof subcmd {
LsShare ls = 1;
OperateShare op = 2;
}
}
string authkey = 1;
RoleId role = 2;
// Actual request data object
oneof command {
MkdirRequest mkdir = 21;
RmdirRequest rmdir = 22;
TouchRequest touch = 23;
UnlinkRequest unlink = 24;
RmRequest rm = 25;
RenameRequest rename = 26;
SymlinkRequest symlink = 27;
VersionRequest version = 28;
RecycleRequest recycle = 29;
SetXAttrRequest xattr = 30;
ChownRequest chown = 31;
ChmodRequest chmod = 32;
AclRequest acl = 33;
TokenRequest token = 34;
QuotaRequest quota = 35;
ShareRequest share = 36;
}
}
message NSResponse {
message ErrorResponse {
int64 code = 1;
string msg = 2;
}
message VersionResponse {
message VersionInfo {
MDId id = 1;
Time mtime = 2;
}
int64 code = 1;
string msg = 2;
repeated VersionInfo versions = 3;
}
message RecycleResponse {
int64 code = 1;
string msg = 2;
message RecycleInfo {
enum DELETIONTYPE {
FILE = 0;
TREE = 1;
}
MDId id = 1;
RoleId owner = 2;
Time dtime = 3;
uint64 size = 4;
DELETIONTYPE type = 5;
string key = 6;
}
repeated RecycleInfo recycles = 3;
}
message AclResponse {
int64 code = 1;
string msg = 2;
string rule = 3;
}
message QuotaResponse {
int64 code = 1;
string msg = 2;
repeated QuotaProto quotanode = 3;
}
message ShareInfo {
string name = 1;
string root = 2;
string rule = 3;
uint64 uid = 4;
uint64 nshared = 5;
}
message ShareAccess {
string name = 1;
bool granted = 2;
}
message ShareResponse {
int64 code = 1;
string msg = 2;
repeated ShareInfo shares = 3;
repeated ShareAccess access = 4;
}
ErrorResponse error = 1;
VersionResponse version = 2;
RecycleResponse recycle = 3;
AclResponse acl = 4;
QuotaResponse quota = 5;
ShareResponse share = 6;
}
message NsStatRequest { string authkey = 1; }
message NsStatResponse {
int64 code = 1;
string emsg = 2;
string state = 3;
uint64 nfiles = 4;
uint64 ncontainers = 5;
uint64 boot_time = 6;
uint64 current_fid = 7;
uint64 current_cid = 8;
uint64 mem_virtual = 9;
uint64 mem_resident = 10;
uint64 mem_share = 11;
uint64 mem_growth = 12;
uint64 threads = 13;
uint64 fds = 14;
uint64 uptime = 15;
}
// ---------------------------------------------------------------------
// OPENSTACK
// ---------------------------------------------------------------------
enum MANILA_REQUEST_TYPE {
CREATE_SHARE = 0;
DELETE_SHARE = 1;
EXTEND_SHARE = 2;
SHRINK_SHARE = 3;
MANAGE_EXISTING = 4;
UNMANAGE = 5;
GET_CAPACITIES = 6;
/* EXTRA FUNCTIONS NOT IMPLEMENTED */
/*
CREATE_SNAPSHOT = 7;
DELETE_SNAPSHOT = 8;
CREATE_SHARE_FROM_SNAPSHOT = 9;
ENSURE_SHARE = 10;
ALLOW_ACCESS = 11;
DENY_ACCESS = 12;
GET_SHARE_STATS = 13;
DO_SETUP = 14;
SETUP_SERVER = 15;
TEARDOWN_SERVER = 16;
GET_NETWORK_ALLOCATIONS_NUMBER = 17;
VERIFY_SHARE_SERVER_HANDLING = 18;
CREATE_SHARE_GROUP = 19;
DELETE_SHARE_GROUP = 20;
*/
}
message ManilaRequest {
MANILA_REQUEST_TYPE request_type = 1;
string auth_key = 2;
string protocol = 3;
string share_name = 4;
string description = 5;
string share_id = 6;
string share_group_id = 7;
int32 quota = 8;
string creator = 9;
string egroup = 10;
string admin_egroup = 11;
string share_host = 12;
string share_location = 13;
}
message ManilaResponse {
string msg = 1; // for generic messages
int32 code = 2; // < 1 is an error -- > 1 is OK
int64 total_used = 3;
int64 total_capacity = 4;
int64 new_share_quota = 5;
string new_share_path = 6;
}
@@ -0,0 +1,445 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v3.19.1
// source: Rpc.proto
package eos_grpc
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// EosClient is the client API for Eos service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type EosClient interface {
// Replies to a ping
Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingReply, error)
// Replies to MD requests with a stream
MD(ctx context.Context, in *MDRequest, opts ...grpc.CallOption) (Eos_MDClient, error)
// Replies to Find requests with a stream
Find(ctx context.Context, in *FindRequest, opts ...grpc.CallOption) (Eos_FindClient, error)
// Replies to a NsStat operation
NsStat(ctx context.Context, in *NsStatRequest, opts ...grpc.CallOption) (*NsStatResponse, error)
// Replies to an insert
ContainerInsert(ctx context.Context, in *ContainerInsertRequest, opts ...grpc.CallOption) (*InsertReply, error)
FileInsert(ctx context.Context, in *FileInsertRequest, opts ...grpc.CallOption) (*InsertReply, error)
// Replies to a NsRequest operation
Exec(ctx context.Context, in *NSRequest, opts ...grpc.CallOption) (*NSResponse, error)
// Manila Driver
ManilaServerRequest(ctx context.Context, in *ManilaRequest, opts ...grpc.CallOption) (*ManilaResponse, error)
}
type eosClient struct {
cc grpc.ClientConnInterface
}
func NewEosClient(cc grpc.ClientConnInterface) EosClient {
return &eosClient{cc}
}
func (c *eosClient) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingReply, error) {
out := new(PingReply)
err := c.cc.Invoke(ctx, "/eos.rpc.Eos/Ping", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *eosClient) MD(ctx context.Context, in *MDRequest, opts ...grpc.CallOption) (Eos_MDClient, error) {
stream, err := c.cc.NewStream(ctx, &Eos_ServiceDesc.Streams[0], "/eos.rpc.Eos/MD", opts...)
if err != nil {
return nil, err
}
x := &eosMDClient{stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
type Eos_MDClient interface {
Recv() (*MDResponse, error)
grpc.ClientStream
}
type eosMDClient struct {
grpc.ClientStream
}
func (x *eosMDClient) Recv() (*MDResponse, error) {
m := new(MDResponse)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func (c *eosClient) Find(ctx context.Context, in *FindRequest, opts ...grpc.CallOption) (Eos_FindClient, error) {
stream, err := c.cc.NewStream(ctx, &Eos_ServiceDesc.Streams[1], "/eos.rpc.Eos/Find", opts...)
if err != nil {
return nil, err
}
x := &eosFindClient{stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
type Eos_FindClient interface {
Recv() (*MDResponse, error)
grpc.ClientStream
}
type eosFindClient struct {
grpc.ClientStream
}
func (x *eosFindClient) Recv() (*MDResponse, error) {
m := new(MDResponse)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func (c *eosClient) NsStat(ctx context.Context, in *NsStatRequest, opts ...grpc.CallOption) (*NsStatResponse, error) {
out := new(NsStatResponse)
err := c.cc.Invoke(ctx, "/eos.rpc.Eos/NsStat", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *eosClient) ContainerInsert(ctx context.Context, in *ContainerInsertRequest, opts ...grpc.CallOption) (*InsertReply, error) {
out := new(InsertReply)
err := c.cc.Invoke(ctx, "/eos.rpc.Eos/ContainerInsert", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *eosClient) FileInsert(ctx context.Context, in *FileInsertRequest, opts ...grpc.CallOption) (*InsertReply, error) {
out := new(InsertReply)
err := c.cc.Invoke(ctx, "/eos.rpc.Eos/FileInsert", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *eosClient) Exec(ctx context.Context, in *NSRequest, opts ...grpc.CallOption) (*NSResponse, error) {
out := new(NSResponse)
err := c.cc.Invoke(ctx, "/eos.rpc.Eos/Exec", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *eosClient) ManilaServerRequest(ctx context.Context, in *ManilaRequest, opts ...grpc.CallOption) (*ManilaResponse, error) {
out := new(ManilaResponse)
err := c.cc.Invoke(ctx, "/eos.rpc.Eos/ManilaServerRequest", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// EosServer is the server API for Eos service.
// All implementations must embed UnimplementedEosServer
// for forward compatibility
type EosServer interface {
// Replies to a ping
Ping(context.Context, *PingRequest) (*PingReply, error)
// Replies to MD requests with a stream
MD(*MDRequest, Eos_MDServer) error
// Replies to Find requests with a stream
Find(*FindRequest, Eos_FindServer) error
// Replies to a NsStat operation
NsStat(context.Context, *NsStatRequest) (*NsStatResponse, error)
// Replies to an insert
ContainerInsert(context.Context, *ContainerInsertRequest) (*InsertReply, error)
FileInsert(context.Context, *FileInsertRequest) (*InsertReply, error)
// Replies to a NsRequest operation
Exec(context.Context, *NSRequest) (*NSResponse, error)
// Manila Driver
ManilaServerRequest(context.Context, *ManilaRequest) (*ManilaResponse, error)
mustEmbedUnimplementedEosServer()
}
// UnimplementedEosServer must be embedded to have forward compatible implementations.
type UnimplementedEosServer struct {
}
func (UnimplementedEosServer) Ping(context.Context, *PingRequest) (*PingReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method Ping not implemented")
}
func (UnimplementedEosServer) MD(*MDRequest, Eos_MDServer) error {
return status.Errorf(codes.Unimplemented, "method MD not implemented")
}
func (UnimplementedEosServer) Find(*FindRequest, Eos_FindServer) error {
return status.Errorf(codes.Unimplemented, "method Find not implemented")
}
func (UnimplementedEosServer) NsStat(context.Context, *NsStatRequest) (*NsStatResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method NsStat not implemented")
}
func (UnimplementedEosServer) ContainerInsert(context.Context, *ContainerInsertRequest) (*InsertReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method ContainerInsert not implemented")
}
func (UnimplementedEosServer) FileInsert(context.Context, *FileInsertRequest) (*InsertReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method FileInsert not implemented")
}
func (UnimplementedEosServer) Exec(context.Context, *NSRequest) (*NSResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Exec not implemented")
}
func (UnimplementedEosServer) ManilaServerRequest(context.Context, *ManilaRequest) (*ManilaResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ManilaServerRequest not implemented")
}
func (UnimplementedEosServer) mustEmbedUnimplementedEosServer() {}
// UnsafeEosServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to EosServer will
// result in compilation errors.
type UnsafeEosServer interface {
mustEmbedUnimplementedEosServer()
}
func RegisterEosServer(s grpc.ServiceRegistrar, srv EosServer) {
s.RegisterService(&Eos_ServiceDesc, srv)
}
func _Eos_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PingRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(EosServer).Ping(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/eos.rpc.Eos/Ping",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(EosServer).Ping(ctx, req.(*PingRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Eos_MD_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(MDRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(EosServer).MD(m, &eosMDServer{stream})
}
type Eos_MDServer interface {
Send(*MDResponse) error
grpc.ServerStream
}
type eosMDServer struct {
grpc.ServerStream
}
func (x *eosMDServer) Send(m *MDResponse) error {
return x.ServerStream.SendMsg(m)
}
func _Eos_Find_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(FindRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(EosServer).Find(m, &eosFindServer{stream})
}
type Eos_FindServer interface {
Send(*MDResponse) error
grpc.ServerStream
}
type eosFindServer struct {
grpc.ServerStream
}
func (x *eosFindServer) Send(m *MDResponse) error {
return x.ServerStream.SendMsg(m)
}
func _Eos_NsStat_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(NsStatRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(EosServer).NsStat(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/eos.rpc.Eos/NsStat",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(EosServer).NsStat(ctx, req.(*NsStatRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Eos_ContainerInsert_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ContainerInsertRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(EosServer).ContainerInsert(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/eos.rpc.Eos/ContainerInsert",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(EosServer).ContainerInsert(ctx, req.(*ContainerInsertRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Eos_FileInsert_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(FileInsertRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(EosServer).FileInsert(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/eos.rpc.Eos/FileInsert",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(EosServer).FileInsert(ctx, req.(*FileInsertRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Eos_Exec_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(NSRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(EosServer).Exec(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/eos.rpc.Eos/Exec",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(EosServer).Exec(ctx, req.(*NSRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Eos_ManilaServerRequest_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ManilaRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(EosServer).ManilaServerRequest(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/eos.rpc.Eos/ManilaServerRequest",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(EosServer).ManilaServerRequest(ctx, req.(*ManilaRequest))
}
return interceptor(ctx, in, info, handler)
}
// Eos_ServiceDesc is the grpc.ServiceDesc for Eos service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Eos_ServiceDesc = grpc.ServiceDesc{
ServiceName: "eos.rpc.Eos",
HandlerType: (*EosServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Ping",
Handler: _Eos_Ping_Handler,
},
{
MethodName: "NsStat",
Handler: _Eos_NsStat_Handler,
},
{
MethodName: "ContainerInsert",
Handler: _Eos_ContainerInsert_Handler,
},
{
MethodName: "FileInsert",
Handler: _Eos_FileInsert_Handler,
},
{
MethodName: "Exec",
Handler: _Eos_Exec_Handler,
},
{
MethodName: "ManilaServerRequest",
Handler: _Eos_ManilaServerRequest_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "MD",
Handler: _Eos_MD_Handler,
ServerStreams: true,
},
{
StreamName: "Find",
Handler: _Eos_Find_Handler,
ServerStreams: true,
},
},
Metadata: "Rpc.proto",
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,500 @@
// 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 eosgrpc
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"time"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/eosclient"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/logger"
)
// HTTPOptions to configure the Client.
type HTTPOptions struct {
// HTTP URL of the EOS MGM.
// Default is https://eos-example.org
BaseURL string
// Timeout in seconds for connecting to the service
ConnectTimeout int
// Timeout in seconds for sending a request to the service and getting a response
// Does not include redirections
RWTimeout int
// Timeout in seconds for performing an operation. Includes every redirection, retry, etc
OpTimeout int
// Max idle conns per Transport
MaxIdleConns int
// Max conns per transport per destination host
MaxConnsPerHost int
// Max idle conns per transport per destination host
MaxIdleConnsPerHost int
// TTL for an idle conn per transport
IdleConnTimeout int
// If the URL is https, then we need to configure this client
// with the usual TLS stuff
// Defaults are /etc/grid-security/hostcert.pem and /etc/grid-security/hostkey.pem
ClientCertFile string
ClientKeyFile string
// These will override the defaults, which are common system paths hardcoded
// in the go x509 implementation (why did they do that?!?!?)
// of course /etc/grid-security/certificates is NOT in those defaults!
ClientCADirs string
ClientCAFiles string
}
// Init fills the basic fields
func (opt *HTTPOptions) init() {
if opt.BaseURL == "" {
opt.BaseURL = "https://eos-example.org"
}
if opt.ConnectTimeout == 0 {
opt.ConnectTimeout = 30
}
if opt.RWTimeout == 0 {
opt.RWTimeout = 180
}
if opt.OpTimeout == 0 {
opt.OpTimeout = 360
}
if opt.MaxIdleConns == 0 {
opt.MaxIdleConns = 100
}
if opt.MaxConnsPerHost == 0 {
opt.MaxConnsPerHost = 64
}
if opt.MaxIdleConnsPerHost == 0 {
opt.MaxIdleConnsPerHost = 8
}
if opt.IdleConnTimeout == 0 {
opt.IdleConnTimeout = 30
}
if opt.ClientCertFile == "" {
opt.ClientCertFile = "/etc/grid-security/hostcert.pem"
}
if opt.ClientKeyFile == "" {
opt.ClientKeyFile = "/etc/grid-security/hostkey.pem"
}
if opt.ClientCAFiles != "" {
os.Setenv("SSL_CERT_FILE", opt.ClientCAFiles)
}
if opt.ClientCADirs != "" {
os.Setenv("SSL_CERT_DIR", opt.ClientCADirs)
} else {
os.Setenv("SSL_CERT_DIR", "/etc/grid-security/certificates")
}
}
// EOSHTTPClient performs HTTP-based tasks (e.g. upload, download)
// against a EOS management node (MGM)
// using the EOS XrdHTTP interface.
// In this module we wrap eos-related behaviour, e.g. headers or r/w retries
type EOSHTTPClient struct {
opt *HTTPOptions
cl *http.Client
}
// NewEOSHTTPClient creates a new client with the given options.
func NewEOSHTTPClient(opt *HTTPOptions) (*EOSHTTPClient, error) {
log := logger.New().With().Int("pid", os.Getpid()).Logger()
log.Debug().Str("func", "New").Str("Creating new eoshttp client. opt: ", "'"+fmt.Sprintf("%#v", opt)+"' ").Msg("")
if opt == nil {
log.Debug().Str("opt is nil, error creating http client ", "").Msg("")
return nil, errtypes.InternalError("HTTPOptions is nil")
}
opt.init()
cert, err := tls.LoadX509KeyPair(opt.ClientCertFile, opt.ClientKeyFile)
if err != nil {
return nil, err
}
// TODO: the error reporting of http.transport is insufficient
// we may want to check manually at least the existence of the certfiles
// The point is that also the error reporting of the context that calls this function
// is weak
t := &http.Transport{
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
},
MaxIdleConns: opt.MaxIdleConns,
MaxConnsPerHost: opt.MaxConnsPerHost,
MaxIdleConnsPerHost: opt.MaxIdleConnsPerHost,
IdleConnTimeout: time.Duration(opt.IdleConnTimeout) * time.Second,
DisableCompression: true,
}
cl := &http.Client{
Transport: t,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
return &EOSHTTPClient{
opt: opt,
cl: cl,
}, nil
}
// Format a human readable line that describes a response
func rspdesc(rsp *http.Response) string {
desc := "'" + fmt.Sprintf("%d", rsp.StatusCode) + "'" + ": '" + rsp.Status + "'"
buf := new(bytes.Buffer)
r := "<none>"
n, e := buf.ReadFrom(rsp.Body)
if e != nil {
r = "Error reading body: '" + e.Error() + "'"
} else if n > 0 {
r = buf.String()
}
desc += " - '" + r + "'"
return desc
}
// If the error is not nil, take that
// If there is an error coming from EOS, erturn a descriptive error
func (c *EOSHTTPClient) getRespError(rsp *http.Response, err error) error {
if err != nil {
return err
}
if rsp.StatusCode == 0 {
return nil
}
switch rsp.StatusCode {
case 0, 200, 201:
return nil
case 403:
return errtypes.PermissionDenied(rspdesc(rsp))
case 404:
return errtypes.NotFound(rspdesc(rsp))
}
err2 := errtypes.InternalError("Err from EOS: " + rspdesc(rsp))
return err2
}
// From the basepath and the file path... build an url
func (c *EOSHTTPClient) buildFullURL(urlpath string, auth eosclient.Authorization) (string, error) {
u, err := url.Parse(c.opt.BaseURL)
if err != nil {
return "", err
}
u, err = u.Parse(url.PathEscape(urlpath))
if err != nil {
return "", err
}
// Prohibit malicious users from injecting a false uid/gid into the url
v := u.Query()
if v.Get("eos.ruid") != "" || v.Get("eos.rgid") != "" {
return "", errtypes.PermissionDenied("Illegal malicious url " + urlpath)
}
if len(auth.Role.UID) > 0 {
v.Set("eos.ruid", auth.Role.UID)
}
if len(auth.Role.GID) > 0 {
v.Set("eos.rgid", auth.Role.GID)
}
u.RawQuery = v.Encode()
return u.String(), nil
}
// GETFile does an entire GET to download a full file. Returns a stream to read the content from
func (c *EOSHTTPClient) GETFile(ctx context.Context, remoteuser string, auth eosclient.Authorization, urlpath string, stream io.WriteCloser) (io.ReadCloser, error) {
log := appctx.GetLogger(ctx)
log.Info().Str("func", "GETFile").Str("remoteuser", remoteuser).Str("uid,gid", auth.Role.UID+","+auth.Role.GID).Str("path", urlpath).Msg("")
// Now send the req and see what happens
finalurl, err := c.buildFullURL(urlpath, auth)
if err != nil {
log.Error().Str("func", "GETFile").Str("url", finalurl).Str("err", err.Error()).Msg("can't create request")
return nil, err
}
req, err := http.NewRequestWithContext(ctx, "GET", finalurl, nil)
if err != nil {
log.Error().Str("func", "GETFile").Str("url", finalurl).Str("err", err.Error()).Msg("can't create request")
return nil, err
}
ntries := 0
nredirs := 0
timebegin := time.Now().Unix()
for {
// Check for a max count of redirections or retries
// Check for a global timeout in any case
tdiff := time.Now().Unix() - timebegin
if tdiff > int64(c.opt.OpTimeout) {
log.Error().Str("func", "GETFile").Str("url", finalurl).Int64("timeout", tdiff).Int("ntries", ntries).Msg("")
return nil, errtypes.InternalError("Timeout with url" + finalurl)
}
// Execute the request. I don't like that there is no explicit timeout or buffer control on the input stream
log.Debug().Str("func", "GETFile").Msg("sending req")
resp, err := c.cl.Do(req)
// Let's support redirections... and if we retry we have to retry at the same FST, avoid going back to the MGM
if resp != nil && (resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusTemporaryRedirect) {
// io.Copy(io.Discard, resp.Body)
// resp.Body.Close()
loc, err := resp.Location()
if err != nil {
log.Error().Str("func", "GETFile").Str("url", finalurl).Str("err", err.Error()).Msg("can't get a new location for a redirection")
return nil, err
}
req, err = http.NewRequestWithContext(ctx, "GET", loc.String(), nil)
if err != nil {
log.Error().Str("func", "GETFile").Str("url", loc.String()).Str("err", err.Error()).Msg("can't create redirected request")
return nil, err
}
req.Close = true
log.Debug().Str("func", "GETFile").Str("location", loc.String()).Msg("redirection")
nredirs++
resp = nil
err = nil
continue
}
// And get an error code (if error) that is worth propagating
e := c.getRespError(resp, err)
if e != nil {
if os.IsTimeout(e) {
ntries++
log.Warn().Str("func", "GETFile").Str("url", finalurl).Str("err", e.Error()).Int("try", ntries).Msg("recoverable network timeout")
continue
}
log.Error().Str("func", "GETFile").Str("url", finalurl).Str("err", e.Error()).Msg("")
return nil, e
}
log.Debug().Str("func", "GETFile").Str("url", finalurl).Str("resp:", fmt.Sprintf("%#v", resp)).Msg("")
if resp == nil {
return nil, errtypes.NotFound(fmt.Sprintf("url: %s", finalurl))
}
if stream != nil {
// Streaming versus localfile. If we have bene given a dest stream then copy the body into it
_, err = io.Copy(stream, resp.Body)
return nil, err
}
// If we have not been given a stream to write into then return our stream to read from
return resp.Body, nil
}
}
// PUTFile does an entire PUT to upload a full file, taking the data from a stream
func (c *EOSHTTPClient) PUTFile(ctx context.Context, remoteuser string, auth eosclient.Authorization, urlpath string, stream io.ReadCloser, length int64) error {
log := appctx.GetLogger(ctx)
log.Info().Str("func", "PUTFile").Str("remoteuser", remoteuser).Str("uid,gid", auth.Role.UID+","+auth.Role.GID).Str("path", urlpath).Int64("length", length).Msg("")
// Now send the req and see what happens
finalurl, err := c.buildFullURL(urlpath, auth)
if err != nil {
log.Error().Str("func", "PUTFile").Str("url", finalurl).Str("err", err.Error()).Msg("can't create request")
return err
}
req, err := http.NewRequestWithContext(ctx, "PUT", finalurl, nil)
if err != nil {
log.Error().Str("func", "PUTFile").Str("url", finalurl).Str("err", err.Error()).Msg("can't create request")
return err
}
req.Close = true
ntries := 0
nredirs := 0
timebegin := time.Now().Unix()
for {
// Check for a max count of redirections or retries
// Check for a global timeout in any case
tdiff := time.Now().Unix() - timebegin
if tdiff > int64(c.opt.OpTimeout) {
log.Error().Str("func", "PUTFile").Str("url", finalurl).Int64("timeout", tdiff).Int("ntries", ntries).Msg("")
return errtypes.InternalError("Timeout with url" + finalurl)
}
// Execute the request. I don't like that there is no explicit timeout or buffer control on the input stream
log.Debug().Str("func", "PUTFile").Msg("sending req")
resp, err := c.cl.Do(req)
if resp != nil {
resp.Body.Close()
}
// Let's support redirections... and if we retry we retry at the same FST
if resp != nil && resp.StatusCode == 307 {
// io.Copy(io.Discard, resp.Body)
// resp.Body.Close()
loc, err := resp.Location()
if err != nil {
log.Error().Str("func", "PUTFile").Str("url", finalurl).Str("err", err.Error()).Msg("can't get a new location for a redirection")
return err
}
req, err = http.NewRequestWithContext(ctx, "PUT", loc.String(), stream)
if err != nil {
log.Error().Str("func", "PUTFile").Str("url", loc.String()).Str("err", err.Error()).Msg("can't create redirected request")
return err
}
if length >= 0 {
log.Debug().Str("func", "PUTFile").Int64("Content-Length", length).Msg("setting header")
req.Header.Set("Content-Length", strconv.FormatInt(length, 10))
}
if err != nil {
log.Error().Str("func", "PUTFile").Str("url", loc.String()).Str("err", err.Error()).Msg("can't create redirected request")
return err
}
if length >= 0 {
log.Debug().Str("func", "PUTFile").Int64("Content-Length", length).Msg("setting header")
req.Header.Set("Content-Length", strconv.FormatInt(length, 10))
}
log.Debug().Str("func", "PUTFile").Str("location", loc.String()).Msg("redirection")
nredirs++
resp = nil
err = nil
continue
}
// And get an error code (if error) that is worth propagating
e := c.getRespError(resp, err)
if e != nil {
if os.IsTimeout(e) {
ntries++
log.Warn().Str("func", "PUTFile").Str("url", finalurl).Str("err", e.Error()).Int("try", ntries).Msg("recoverable network timeout")
continue
}
log.Error().Str("func", "PUTFile").Str("url", finalurl).Str("err", e.Error()).Msg("")
return e
}
log.Debug().Str("func", "PUTFile").Str("url", finalurl).Str("resp:", fmt.Sprintf("%#v", resp)).Msg("")
if resp == nil {
return errtypes.NotFound(fmt.Sprintf("url: %s", finalurl))
}
return nil
}
}
// Head performs a HEAD req. Useful to check the server
func (c *EOSHTTPClient) Head(ctx context.Context, remoteuser string, auth eosclient.Authorization, urlpath string) error {
log := appctx.GetLogger(ctx)
log.Info().Str("func", "Head").Str("remoteuser", remoteuser).Str("uid,gid", auth.Role.UID+","+auth.Role.GID).Str("path", urlpath).Msg("")
// Now send the req and see what happens
finalurl, err := c.buildFullURL(urlpath, auth)
if err != nil {
log.Error().Str("func", "Head").Str("url", finalurl).Str("err", err.Error()).Msg("can't create request")
return err
}
req, err := http.NewRequestWithContext(ctx, "HEAD", finalurl, nil)
if err != nil {
log.Error().Str("func", "Head").Str("remoteuser", remoteuser).Str("uid,gid", auth.Role.UID+","+auth.Role.GID).Str("url", finalurl).Str("err", err.Error()).Msg("can't create request")
return err
}
ntries := 0
timebegin := time.Now().Unix()
for {
tdiff := time.Now().Unix() - timebegin
if tdiff > int64(c.opt.OpTimeout) {
log.Error().Str("func", "Head").Str("url", finalurl).Int64("timeout", tdiff).Int("ntries", ntries).Msg("")
return errtypes.InternalError("Timeout with url" + finalurl)
}
// Execute the request. I don't like that there is no explicit timeout or buffer control on the input stream
resp, err := c.cl.Do(req)
if resp != nil {
resp.Body.Close()
}
// And get an error code (if error) that is worth propagating
e := c.getRespError(resp, err)
if e != nil {
if os.IsTimeout(e) {
ntries++
log.Warn().Str("func", "Head").Str("url", finalurl).Str("err", e.Error()).Int("try", ntries).Msg("recoverable network timeout")
continue
}
log.Error().Str("func", "Head").Str("url", finalurl).Str("err", e.Error()).Msg("")
return e
}
log.Debug().Str("func", "Head").Str("url", finalurl).Str("resp:", fmt.Sprintf("%#v", resp)).Msg("")
if resp == nil {
return errtypes.NotFound(fmt.Sprintf("url: %s", finalurl))
}
}
// return nil
}
+61
View File
@@ -0,0 +1,61 @@
// 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 eosclient
import (
"fmt"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
)
const (
// SystemAttr is the system extended attribute.
SystemAttr AttrType = iota
// UserAttr is the user extended attribute.
UserAttr
)
// AttrStringToType converts a string to an AttrType
func AttrStringToType(t string) (AttrType, error) {
switch t {
case "sys":
return SystemAttr, nil
case "user":
return UserAttr, nil
default:
return 0, errtypes.InternalError("attr type not existing")
}
}
// AttrTypeToString converts a type to a string representation.
func AttrTypeToString(at AttrType) string {
switch at {
case SystemAttr:
return "sys"
case UserAttr:
return "user"
default:
return "invalid"
}
}
// GetKey returns the key considering the type of attribute.
func (a *Attribute) GetKey() string {
return fmt.Sprintf("%s.%s", AttrTypeToString(a.Type), a.Key)
}
+428
View File
@@ -0,0 +1,428 @@
// 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 errtypes contains definitions for common errors.
// It would have nice to call this package errors, err or error
// but errors clashes with github.com/pkg/errors, err is used for any error variable
// and error is a reserved word :)
package errtypes
import (
"net/http"
"strings"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
)
// NotFound is the error to use when a something is not found.
type NotFound string
func (e NotFound) Error() string { return "error: not found: " + string(e) }
// IsNotFound implements the IsNotFound interface.
func (e NotFound) IsNotFound() {}
// InternalError is the error to use when we really don't know what happened. Use with care
type InternalError string
func (e InternalError) Error() string { return "internal error: " + string(e) }
// IsInternalError implements the IsInternalError interface.
func (e InternalError) IsInternalError() {}
// PermissionDenied is the error to use when a resource cannot be access because of missing permissions.
type PermissionDenied string
func (e PermissionDenied) Error() string { return "error: permission denied: " + string(e) }
// IsPermissionDenied implements the IsPermissionDenied interface.
func (e PermissionDenied) IsPermissionDenied() {}
// Locked is the error to use when a resource cannot be modified because of a lock.
type Locked string
func (e Locked) Error() string { return "error: locked by " + string(e) }
// LockID returns the lock ID that caused this error
func (e Locked) LockID() string {
return string(e)
}
// IsLocked implements the IsLocked interface.
func (e Locked) IsLocked() {}
// Aborted is the error to use when a client should retry at a higher level
// (e.g., when a client-specified test-and-set fails, indicating the
// client should restart a read-modify-write sequence) request fails
// because a requested etag or lock ID mismatches.
//
// HTTP Mapping: 412 Precondition Failed
type Aborted string
func (e Aborted) Error() string { return "error: aborted: " + string(e) }
// IsAborted implements the IsAborted interface.
func (e Aborted) IsAborted() {}
// PreconditionFailed is the error to use when a client should not retry until
// the system state has been explicitly fixed. E.g., if an "rmdir"
// fails because the directory is non-empty, PreconditionFailed
// should be returned since the client should not retry unless
// the files are deleted from the directory. PreconditionFailed should also be
// returned when an intermediate directory for an MKCOL or PUT is missing.
//
// # FIXME rename to FailedPrecondition to make it less confusable with the http status Precondition Failed
//
// HTTP Mapping: 400 Bad Request, 405 Method Not Allowed, 409 Conflict
type PreconditionFailed string
func (e PreconditionFailed) Error() string { return "error: precondition failed: " + string(e) }
// IsPreconditionFailed implements the IsPreconditionFailed interface.
func (e PreconditionFailed) IsPreconditionFailed() {}
// AlreadyExists is the error to use when a resource something is not found.
type AlreadyExists string
func (e AlreadyExists) Error() string { return "error: already exists: " + string(e) }
// IsAlreadyExists implements the IsAlreadyExists interface.
func (e AlreadyExists) IsAlreadyExists() {}
// UserRequired represents an error when a resource is not found.
type UserRequired string
func (e UserRequired) Error() string { return "error: user required: " + string(e) }
// IsUserRequired implements the IsUserRequired interface.
func (e UserRequired) IsUserRequired() {}
// InvalidCredentials is the error to use when receiving invalid credentials.
type InvalidCredentials string
func (e InvalidCredentials) Error() string { return "error: invalid credentials: " + string(e) }
// IsInvalidCredentials implements the IsInvalidCredentials interface.
func (e InvalidCredentials) IsInvalidCredentials() {}
// NotSupported is the error to use when an action is not supported.
type NotSupported string
func (e NotSupported) Error() string { return "error: not supported: " + string(e) }
// IsNotSupported implements the IsNotSupported interface.
func (e NotSupported) IsNotSupported() {}
// PartialContent is the error to use when the client request has partial data.
type PartialContent string
func (e PartialContent) Error() string { return "error: partial content: " + string(e) }
// IsPartialContent implements the IsPartialContent interface.
func (e PartialContent) IsPartialContent() {}
// BadRequest is the error to use when the server cannot or will not process the request (due to a client error). Reauthenticating won't help.
type BadRequest string
func (e BadRequest) Error() string { return "error: bad request: " + string(e) }
// IsBadRequest implements the IsBadRequest interface.
func (e BadRequest) IsBadRequest() {}
// ChecksumMismatch is the error to use when the transmitted hash does not match the calculated hash.
type ChecksumMismatch string
func (e ChecksumMismatch) Error() string { return "error: checksum mismatch: " + string(e) }
// IsChecksumMismatch implements the IsChecksumMismatch interface.
func (e ChecksumMismatch) IsChecksumMismatch() {}
// StatusChecksumMismatch 419 is an unofficial http status code in an unassigned range that is used for checksum mismatches
// Proposed by https://stackoverflow.com/a/35665694
// Official HTTP status code registry: https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
// Note: TUS uses unassigned 460 Checksum-Mismatch
// RFC proposal for checksum digest uses a `Want-Digest` header: https://tools.ietf.org/html/rfc3230
// oc clienst issue: https://github.com/owncloud/core/issues/22711
const StatusChecksumMismatch = 419
// InsufficientStorage is the error to use when there is insufficient storage.
type InsufficientStorage string
func (e InsufficientStorage) Error() string { return "error: insufficient storage: " + string(e) }
// IsInsufficientStorage implements the IsInsufficientStorage interface.
func (e InsufficientStorage) IsInsufficientStorage() {}
// StatusCode returns StatusInsufficientStorage, this implementation is needed to allow TUS to cast the correct http errors.
func (e InsufficientStorage) StatusCode() int {
return StatusInsufficientStorage
}
// NotModified is the error to use when a resource was not modified, e.g. the requested etag did not change.
type NotModified string
func (e NotModified) Error() string { return "error: not modified: " + string(e) }
// IsNotModified implements the IsNotModified interface.
func (e NotModified) IsNotModified() {}
// StatusCode returns StatusInsufficientStorage, this implementation is needed to allow TUS to cast the correct http errors.
func (e NotModified) StatusCode() int {
return http.StatusNotModified
}
// Body returns the error body. This implementation is needed to allow TUS to cast the correct http errors
func (e InsufficientStorage) Body() []byte {
return []byte(e.Error())
}
// StatusInsufficientStorage 507 is an official HTTP status code to indicate that there is insufficient storage
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/507
const StatusInsufficientStorage = 507
// TooEarly is the error to use when some are not finished job over resource is still in process.
type TooEarly string
func (e TooEarly) Error() string { return "error: too early: " + string(e) }
// IsTooEarly implements the IsTooEarly interface.
func (e TooEarly) IsTooEarly() {}
// Unavailable is the error to use when a backend service (e.g. LDAP, database) is
// temporarily unreachable. Callers should treat this as a transient failure and retry.
type Unavailable string
func (e Unavailable) Error() string { return "error: unavailable: " + string(e) }
// IsUnavailable implements the IsUnavailable interface.
func (e Unavailable) IsUnavailable() {}
// IsNotFound is the interface to implement
// to specify that a resource is not found.
type IsNotFound interface {
IsNotFound()
}
// IsAlreadyExists is the interface to implement
// to specify that a resource already exists.
type IsAlreadyExists interface {
IsAlreadyExists()
}
// IsInternalError is the interface to implement
// to specify that there was some internal error
type IsInternalError interface {
IsInternalError()
}
// IsUserRequired is the interface to implement
// to specify that a user is required.
type IsUserRequired interface {
IsUserRequired()
}
// IsInvalidCredentials is the interface to implement
// to specify that credentials were wrong.
type IsInvalidCredentials interface {
IsInvalidCredentials()
}
// IsNotSupported is the interface to implement
// to specify that an action is not supported.
type IsNotSupported interface {
IsNotSupported()
}
// IsPermissionDenied is the interface to implement
// to specify that an action is denied.
type IsPermissionDenied interface {
IsPermissionDenied()
}
// IsLocked is the interface to implement
// to specify that a resource is locked.
type IsLocked interface {
IsLocked()
}
// IsAborted is the interface to implement
// to specify that a request was aborted.
type IsAborted interface {
IsAborted()
}
// IsPreconditionFailed is the interface to implement
// to specify that a precondition failed.
type IsPreconditionFailed interface {
IsPreconditionFailed()
}
// IsPartialContent is the interface to implement
// to specify that the client request has partial data.
type IsPartialContent interface {
IsPartialContent()
}
// IsBadRequest is the interface to implement
// to specify that the server cannot or will not process the request.
type IsBadRequest interface {
IsBadRequest()
}
// IsChecksumMismatch is the interface to implement
// to specify that a checksum does not match.
type IsChecksumMismatch interface {
IsChecksumMismatch()
}
// IsInsufficientStorage is the interface to implement
// to specify that there is insufficient storage.
type IsInsufficientStorage interface {
IsInsufficientStorage()
}
// IsTooEarly is the interface to implement
// to specify that there is some not finished job over resource is still in process.
type IsTooEarly interface {
IsTooEarly()
}
// IsUnavailable is the interface to implement to specify that a backend service is
// temporarily unavailable and the caller should retry.
type IsUnavailable interface {
IsUnavailable()
}
// NewErrtypeFromStatus maps a rpc status to an errtype
func NewErrtypeFromStatus(status *rpc.Status) error {
switch status.Code {
case rpc.Code_CODE_OK:
return nil
case rpc.Code_CODE_NOT_FOUND:
return NotFound(status.Message)
case rpc.Code_CODE_ALREADY_EXISTS:
return AlreadyExists(status.Message)
// case rpc.Code_CODE_FAILED_PRECONDITION: ?
// return UserRequired(status.Message)
// case rpc.Code_CODE_PERMISSION_DENIED: ?
// IsInvalidCredentials
case rpc.Code_CODE_UNIMPLEMENTED:
return NotSupported(status.Message)
case rpc.Code_CODE_PERMISSION_DENIED:
return PermissionDenied(status.Message)
case rpc.Code_CODE_LOCKED:
// FIXME make something better for that
msg := strings.Split(status.Message, "error: locked by ")
if len(msg) > 1 {
return Locked(msg[len(msg)-1])
}
return Locked(status.Message)
// case rpc.Code_CODE_DATA_LOSS: ?
// IsPartialContent
case rpc.Code_CODE_ABORTED:
return Aborted(status.Message)
case rpc.Code_CODE_FAILED_PRECONDITION:
return PreconditionFailed(status.Message)
case rpc.Code_CODE_INSUFFICIENT_STORAGE:
return InsufficientStorage(status.Message)
case rpc.Code_CODE_INVALID_ARGUMENT, rpc.Code_CODE_OUT_OF_RANGE:
return BadRequest(status.Message)
case rpc.Code_CODE_TOO_EARLY:
return TooEarly(status.Message)
case rpc.Code_CODE_UNAVAILABLE:
return Unavailable(status.Message)
default:
return InternalError(status.Message)
}
}
// NewErrtypeFromHTTPStatusCode maps a http status to an errtype
func NewErrtypeFromHTTPStatusCode(code int, message string) error {
switch code {
case http.StatusOK:
return nil
case http.StatusNotFound:
return NotFound(message)
case http.StatusConflict:
return AlreadyExists(message)
case http.StatusNotImplemented:
return NotSupported(message)
case http.StatusNotModified:
return NotModified(message)
case http.StatusForbidden:
return PermissionDenied(message)
case http.StatusLocked:
return Locked(message)
case http.StatusPreconditionFailed:
return Aborted(message)
case http.StatusMethodNotAllowed:
return PreconditionFailed(message)
case http.StatusInsufficientStorage:
return InsufficientStorage(message)
case http.StatusBadRequest:
return BadRequest(message)
case http.StatusPartialContent:
return PartialContent(message)
case http.StatusTooEarly:
return TooEarly(message)
case http.StatusServiceUnavailable:
return Unavailable(message)
case StatusChecksumMismatch:
return ChecksumMismatch(message)
default:
return InternalError(message)
}
}
// NewHTTPStatusCodeFromErrtype maps an errtype to a http status
func NewHTTPStatusCodeFromErrtype(err error) int {
switch err.(type) {
case NotFound:
return http.StatusNotFound
case AlreadyExists:
return http.StatusConflict
case NotSupported:
return http.StatusNotImplemented
case NotModified:
return http.StatusNotModified
case InvalidCredentials:
return http.StatusUnauthorized
case PermissionDenied:
return http.StatusForbidden
case Locked:
return http.StatusLocked
case Aborted:
return http.StatusPreconditionFailed
case PreconditionFailed:
return http.StatusMethodNotAllowed
case InsufficientStorage:
return http.StatusInsufficientStorage
case BadRequest:
return http.StatusBadRequest
case PartialContent:
return http.StatusPartialContent
case TooEarly:
return http.StatusTooEarly
case Unavailable:
return http.StatusServiceUnavailable
case ChecksumMismatch:
return StatusChecksumMismatch
default:
return http.StatusInternalServerError
}
}
+203
View File
@@ -0,0 +1,203 @@
// 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 events
import (
"context"
"log"
"reflect"
"github.com/google/uuid"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"go-micro.dev/v4/events"
"go.opentelemetry.io/otel/propagation"
)
var (
// MainQueueName is the name of the main queue
// All events will go through here as they are forwarded to the consumer via the
// group name
// TODO: "fan-out" so not all events go through the same queue? requires investigation
MainQueueName = "main-queue"
// MetadatakeyEventType is the key used for the eventtype in the metadata map of the event
MetadatakeyEventType = "eventtype"
// MetadatakeyEventID is the key used for the eventID in the metadata map of the event
MetadatakeyEventID = "eventid"
// MetadatakeyTraceParent is the key used for the traceparent in the metadata map of the event
MetadatakeyTraceParent = "traceparent"
// MetadatakeyInitiatorID is the key used for the initiator id in the metadata map of the event
MetadatakeyInitiatorID = "initiatorid"
)
type (
// Unmarshaller is the interface events need to fulfill
Unmarshaller interface {
Unmarshal([]byte) (interface{}, error)
}
// Publisher is the interface publishers need to fulfill
Publisher interface {
Publish(string, interface{}, ...events.PublishOption) error
}
// Consumer is the interface consumer need to fulfill
Consumer interface {
Consume(string, ...events.ConsumeOption) (<-chan events.Event, error)
}
// Stream is the interface common to Publisher and Consumer
Stream interface {
Publish(string, interface{}, ...events.PublishOption) error
Consume(string, ...events.ConsumeOption) (<-chan events.Event, error)
}
// Event is the envelope for events
Event struct {
Type string
ID string
TraceParent string
InitiatorID string
Event interface{}
}
)
// Consume returns a channel that will get all events that match the given evs
// group defines the service type: One group will get exactly one copy of a event that is emitted
// NOTE: uses reflect on initialization
func Consume(s Consumer, group string, evs ...Unmarshaller) (<-chan Event, error) {
return ConsumeWithOptions(s, ConsumeOptions{Group: group}, evs...)
}
// ConsumeWithOptions returns a channel that will get all events that match the given evs
// opts defines the options for the consumer
func ConsumeWithOptions(s Consumer, opts ConsumeOptions, evs ...Unmarshaller) (<-chan Event, error) {
o := []events.ConsumeOption{
events.WithGroup(opts.Group),
}
if !opts.AutoAck {
o = append(o, events.WithAutoAck(false, opts.AckWait))
}
c, err := s.Consume(MainQueueName, o...)
if err != nil {
return nil, err
}
registeredEvents := map[string]Unmarshaller{}
for _, e := range evs {
typ := reflect.TypeOf(e)
registeredEvents[typ.String()] = e
}
outchan := make(chan Event)
go func() {
for {
e := <-c
et := e.Metadata[MetadatakeyEventType]
ev, ok := registeredEvents[et]
if !ok {
continue
}
event, err := ev.Unmarshal(e.Payload)
if err != nil {
log.Printf("can't unmarshal event %v", err)
continue
}
outchan <- Event{
Type: et,
ID: e.Metadata[MetadatakeyEventID],
TraceParent: e.Metadata[MetadatakeyTraceParent],
InitiatorID: e.Metadata[MetadatakeyInitiatorID],
Event: event,
}
}
}()
return outchan, nil
}
// ConsumeAll allows consuming all events. Note that unmarshalling must be done manually in this case, therefore Event.Event will always be of type []byte
func ConsumeAll(s Consumer, group string) (<-chan Event, error) {
return ConsumeAllWithOptions(s, ConsumeOptions{Group: group})
}
// ConsumeAllWithOptions allows consuming all events. Note that unmarshalling must be done manually in this case, therefore Event.Event will always be of type []byte
func ConsumeAllWithOptions(s Consumer, opts ConsumeOptions) (<-chan Event, error) {
o := []events.ConsumeOption{
events.WithGroup(opts.Group),
}
if !opts.AutoAck {
o = append(o, events.WithAutoAck(false, opts.AckWait))
}
c, err := s.Consume(MainQueueName, o...)
if err != nil {
return nil, err
}
outchan := make(chan Event)
go func() {
for {
e := <-c
outchan <- Event{
Type: e.Metadata[MetadatakeyEventType],
ID: e.Metadata[MetadatakeyEventID],
TraceParent: e.Metadata[MetadatakeyTraceParent],
InitiatorID: e.Metadata[MetadatakeyInitiatorID],
Event: e.Payload,
}
}
}()
return outchan, nil
}
// Publish publishes the ev to the MainQueue from where it is distributed to all subscribers
// NOTE: needs to use reflect on runtime
func Publish(ctx context.Context, s Publisher, ev interface{}) error {
evName := reflect.TypeOf(ev).String()
traceParent := getTraceParentFromCtx(ctx)
iid, _ := ctxpkg.ContextGetInitiator(ctx)
return s.Publish(MainQueueName, ev, events.WithMetadata(map[string]string{
MetadatakeyEventType: evName,
MetadatakeyEventID: uuid.New().String(),
MetadatakeyTraceParent: traceParent,
MetadatakeyInitiatorID: iid,
}))
}
// GetTraceContext extracts the trace context from the event and injects it into the given
// context.
func (e *Event) GetTraceContext(ctx context.Context) context.Context {
return propagation.TraceContext{}.Extract(ctx, propagation.MapCarrier{
"traceparent": e.TraceParent,
})
}
// getTraceParentFromCtx will return a traceparent from the context if it exists.
// it will be a string as specificied here: https://www.w3.org/TR/trace-context/
// If no trace info in the context, the return will be an empty string
func getTraceParentFromCtx(ctx context.Context) string {
mc := propagation.MapCarrier{}
tc := propagation.TraceContext{}
tc.Inject(ctx, &mc)
return mc["traceparent"]
}
+234
View File
@@ -0,0 +1,234 @@
// 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 events
import (
"encoding/json"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
)
// ContainerCreated is emitted when a directory has been created
type ContainerCreated struct {
SpaceOwner *user.UserId
Executant *user.UserId
Ref *provider.Reference
ParentID *provider.ResourceId
Owner *user.UserId
Timestamp *types.Timestamp
ImpersonatingUser *user.User
}
// Unmarshal to fulfill umarshaller interface
func (ContainerCreated) Unmarshal(v []byte) (interface{}, error) {
e := ContainerCreated{}
err := json.Unmarshal(v, &e)
return e, err
}
// FileUploaded is emitted when a file is uploaded
type FileUploaded struct {
SpaceOwner *user.UserId
Executant *user.UserId
Ref *provider.Reference
Owner *user.UserId
Timestamp *types.Timestamp
ImpersonatingUser *user.User
}
// Unmarshal to fulfill umarshaller interface
func (FileUploaded) Unmarshal(v []byte) (interface{}, error) {
e := FileUploaded{}
err := json.Unmarshal(v, &e)
return e, err
}
// FileTouched is emitted when a file is uploaded
type FileTouched struct {
SpaceOwner *user.UserId
Executant *user.UserId
Ref *provider.Reference
ParentID *provider.ResourceId
Timestamp *types.Timestamp
ImpersonatingUser *user.User
}
// Unmarshal to fulfill umarshaller interface
func (FileTouched) Unmarshal(v []byte) (interface{}, error) {
e := FileTouched{}
err := json.Unmarshal(v, &e)
return e, err
}
// FileDownloaded is emitted when a file is downloaded
type FileDownloaded struct {
Executant *user.UserId
Ref *provider.Reference
Owner *user.UserId
Timestamp *types.Timestamp
ImpersonatingUser *user.User
}
// Unmarshal to fulfill umarshaller interface
func (FileDownloaded) Unmarshal(v []byte) (interface{}, error) {
e := FileDownloaded{}
err := json.Unmarshal(v, &e)
return e, err
}
// FileLocked is emitted when a file is locked
type FileLocked struct {
Executant *user.UserId
Ref *provider.Reference
Owner *user.UserId
Timestamp *types.Timestamp
ImpersonatingUser *user.User
}
// Unmarshal to fulfill umarshaller interface
func (FileLocked) Unmarshal(v []byte) (interface{}, error) {
e := FileLocked{}
err := json.Unmarshal(v, &e)
return e, err
}
// FileUnlocked is emitted when a file is unlocked
type FileUnlocked struct {
Executant *user.UserId
Ref *provider.Reference
Owner *user.UserId
Timestamp *types.Timestamp
ImpersonatingUser *user.User
}
// Unmarshal to fulfill umarshaller interface
func (FileUnlocked) Unmarshal(v []byte) (interface{}, error) {
e := FileUnlocked{}
err := json.Unmarshal(v, &e)
return e, err
}
// ItemTrashed is emitted when a file or folder is trashed
type ItemTrashed struct {
SpaceOwner *user.UserId
Executant *user.UserId
ID *provider.ResourceId
Ref *provider.Reference
Owner *user.UserId
Timestamp *types.Timestamp
ImpersonatingUser *user.User
}
// Unmarshal to fulfill umarshaller interface
func (ItemTrashed) Unmarshal(v []byte) (interface{}, error) {
e := ItemTrashed{}
err := json.Unmarshal(v, &e)
return e, err
}
// ItemMoved is emitted when a file or folder is moved
type ItemMoved struct {
SpaceOwner *user.UserId
Executant *user.UserId
Ref *provider.Reference
Owner *user.UserId
OldReference *provider.Reference
Timestamp *types.Timestamp
ImpersonatingUser *user.User
}
// Unmarshal to fulfill umarshaller interface
func (ItemMoved) Unmarshal(v []byte) (interface{}, error) {
e := ItemMoved{}
err := json.Unmarshal(v, &e)
return e, err
}
// TrashbinPurged is emitted when the whole trashbin is purged
type TrashbinPurged struct {
Executant *user.UserId
Ref *provider.Reference
Owner *user.UserId
Timestamp *types.Timestamp
ImpersonatingUser *user.User
}
// Unmarshal to fulfill umarshaller interface
func (TrashbinPurged) Unmarshal(v []byte) (interface{}, error) {
e := TrashbinPurged{}
err := json.Unmarshal(v, &e)
return e, err
}
// ItemPurged is emitted when a file or folder is removed from trashbin
type ItemPurged struct {
Executant *user.UserId
ID *provider.ResourceId
Ref *provider.Reference
Owner *user.UserId
Timestamp *types.Timestamp
ImpersonatingUser *user.User
}
// Unmarshal to fulfill umarshaller interface
func (ItemPurged) Unmarshal(v []byte) (interface{}, error) {
e := ItemPurged{}
err := json.Unmarshal(v, &e)
return e, err
}
// ItemRestored is emitted when a file or folder is restored from trashbin
type ItemRestored struct {
SpaceOwner *user.UserId
Executant *user.UserId
ID *provider.ResourceId
Ref *provider.Reference
Owner *user.UserId
OldReference *provider.Reference
Key string
Timestamp *types.Timestamp
ImpersonatingUser *user.User
}
// Unmarshal to fulfill umarshaller interface
func (ItemRestored) Unmarshal(v []byte) (interface{}, error) {
e := ItemRestored{}
err := json.Unmarshal(v, &e)
return e, err
}
// FileVersionRestored is emitted when a file version is restored
type FileVersionRestored struct {
SpaceOwner *user.UserId
Executant *user.UserId
Ref *provider.Reference
Owner *user.UserId
Key string
Timestamp *types.Timestamp
ImpersonatingUser *user.User
}
// Unmarshal to fulfill umarshaller interface
func (FileVersionRestored) Unmarshal(v []byte) (interface{}, error) {
e := FileVersionRestored{}
err := json.Unmarshal(v, &e)
return e, err
}
+106
View File
@@ -0,0 +1,106 @@
// 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 events
import (
"encoding/json"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
)
// GroupCreated is emitted when a group was created
type GroupCreated struct {
Executant *user.UserId
GroupID string
Timestamp *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (GroupCreated) Unmarshal(v []byte) (interface{}, error) {
e := GroupCreated{}
err := json.Unmarshal(v, &e)
return e, err
}
// GroupDeleted is emitted when a group was deleted
type GroupDeleted struct {
Executant *user.UserId
GroupID string
Timestamp *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (GroupDeleted) Unmarshal(v []byte) (interface{}, error) {
e := GroupDeleted{}
err := json.Unmarshal(v, &e)
return e, err
}
// GroupMemberAdded is emitted when a user was added to a group
type GroupMemberAdded struct {
Executant *user.UserId
GroupID string
UserID string
Timestamp *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (GroupMemberAdded) Unmarshal(v []byte) (interface{}, error) {
e := GroupMemberAdded{}
err := json.Unmarshal(v, &e)
return e, err
}
// GroupMemberRemoved is emitted when a user was removed from a group
type GroupMemberRemoved struct {
Executant *user.UserId
GroupID string
UserID string
Timestamp *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (GroupMemberRemoved) Unmarshal(v []byte) (interface{}, error) {
e := GroupMemberRemoved{}
err := json.Unmarshal(v, &e)
return e, err
}
// GroupFeature represents a group feature
type GroupFeature struct {
Name string
Value string
Timestamp *types.Timestamp
}
// GroupFeatureChanged is emitted when a group feature was changed
type GroupFeatureChanged struct {
Executant *user.UserId
GroupID string
Features []GroupFeature
Timestamp *types.Timestamp
}
// Unmarshal to fulfill unmarshaller interface
func (GroupFeatureChanged) Unmarshal(v []byte) (interface{}, error) {
e := GroupFeatureChanged{}
err := json.Unmarshal(v, &e)
return e, err
}
+44
View File
@@ -0,0 +1,44 @@
// Copyright 2026 OpenCloud GmbH <mail@opencloud.eu>
// SPDX-License-Identifier: Apache-2.0
package events
import (
"encoding/json"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
)
// LabelAdded is emitted when a user adds a label to a resource
type LabelAdded struct {
Ref *provider.Reference
Label string
Executant *user.UserId
UserID *user.UserId
Timestamp *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (LabelAdded) Unmarshal(v []byte) (interface{}, error) {
e := LabelAdded{}
err := json.Unmarshal(v, &e)
return e, err
}
// LabelRemoved is emitted when a user removes a label from a resource
type LabelRemoved struct {
Ref *provider.Reference
Label string
Executant *user.UserId
UserID *user.UserId
Timestamp *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (LabelRemoved) Unmarshal(v []byte) (interface{}, error) {
e := LabelRemoved{}
err := json.Unmarshal(v, &e)
return e, err
}
@@ -0,0 +1,186 @@
// 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.
// Code generated by mockery v2.53.2. DO NOT EDIT.
package mocks
import (
mock "github.com/stretchr/testify/mock"
events "go-micro.dev/v4/events"
)
// Stream is an autogenerated mock type for the Stream type
type Stream struct {
mock.Mock
}
type Stream_Expecter struct {
mock *mock.Mock
}
func (_m *Stream) EXPECT() *Stream_Expecter {
return &Stream_Expecter{mock: &_m.Mock}
}
// Consume provides a mock function with given fields: _a0, _a1
func (_m *Stream) Consume(_a0 string, _a1 ...events.ConsumeOption) (<-chan events.Event, error) {
var tmpRet mock.Arguments
if len(_a1) > 0 {
tmpRet = _m.Called(_a0, _a1)
} else {
tmpRet = _m.Called(_a0)
}
ret := tmpRet
if len(ret) == 0 {
panic("no return value specified for Consume")
}
var r0 <-chan events.Event
var r1 error
if rf, ok := ret.Get(0).(func(string, ...events.ConsumeOption) (<-chan events.Event, error)); ok {
return rf(_a0, _a1...)
}
if rf, ok := ret.Get(0).(func(string, ...events.ConsumeOption) <-chan events.Event); ok {
r0 = rf(_a0, _a1...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(<-chan events.Event)
}
}
if rf, ok := ret.Get(1).(func(string, ...events.ConsumeOption) error); ok {
r1 = rf(_a0, _a1...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Stream_Consume_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Consume'
type Stream_Consume_Call struct {
*mock.Call
}
// Consume is a helper method to define mock.On call
// - _a0 string
// - _a1 ...events.ConsumeOption
func (_e *Stream_Expecter) Consume(_a0 interface{}, _a1 ...interface{}) *Stream_Consume_Call {
return &Stream_Consume_Call{Call: _e.mock.On("Consume",
append([]interface{}{_a0}, _a1...)...)}
}
func (_c *Stream_Consume_Call) Run(run func(_a0 string, _a1 ...events.ConsumeOption)) *Stream_Consume_Call {
_c.Call.Run(func(args mock.Arguments) {
variadicArgs := make([]events.ConsumeOption, len(args)-1)
for i, a := range args[1:] {
if a != nil {
variadicArgs[i] = a.(events.ConsumeOption)
}
}
run(args[0].(string), variadicArgs...)
})
return _c
}
func (_c *Stream_Consume_Call) Return(_a0 <-chan events.Event, _a1 error) *Stream_Consume_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *Stream_Consume_Call) RunAndReturn(run func(string, ...events.ConsumeOption) (<-chan events.Event, error)) *Stream_Consume_Call {
_c.Call.Return(run)
return _c
}
// Publish provides a mock function with given fields: _a0, _a1, _a2
func (_m *Stream) Publish(_a0 string, _a1 interface{}, _a2 ...events.PublishOption) error {
var tmpRet mock.Arguments
if len(_a2) > 0 {
tmpRet = _m.Called(_a0, _a1, _a2)
} else {
tmpRet = _m.Called(_a0, _a1)
}
ret := tmpRet
if len(ret) == 0 {
panic("no return value specified for Publish")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, interface{}, ...events.PublishOption) error); ok {
r0 = rf(_a0, _a1, _a2...)
} else {
r0 = ret.Error(0)
}
return r0
}
// Stream_Publish_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Publish'
type Stream_Publish_Call struct {
*mock.Call
}
// Publish is a helper method to define mock.On call
// - _a0 string
// - _a1 interface{}
// - _a2 ...events.PublishOption
func (_e *Stream_Expecter) Publish(_a0 interface{}, _a1 interface{}, _a2 ...interface{}) *Stream_Publish_Call {
return &Stream_Publish_Call{Call: _e.mock.On("Publish",
append([]interface{}{_a0, _a1}, _a2...)...)}
}
func (_c *Stream_Publish_Call) Run(run func(_a0 string, _a1 interface{}, _a2 ...events.PublishOption)) *Stream_Publish_Call {
_c.Call.Run(func(args mock.Arguments) {
variadicArgs := make([]events.PublishOption, len(args)-2)
for i, a := range args[2:] {
if a != nil {
variadicArgs[i] = a.(events.PublishOption)
}
}
run(args[0].(string), args[1].(interface{}), variadicArgs...)
})
return _c
}
func (_c *Stream_Publish_Call) Return(_a0 error) *Stream_Publish_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Stream_Publish_Call) RunAndReturn(run func(string, interface{}, ...events.PublishOption) error) *Stream_Publish_Call {
_c.Call.Return(run)
return _c
}
// NewStream creates a new instance of Stream. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewStream(t interface {
mock.TestingT
Cleanup(func())
}) *Stream {
mock := &Stream{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
@@ -0,0 +1,17 @@
package events
import (
"encoding/json"
)
// SendEmailsEvent instructs the notification service to send grouped emails
type SendEmailsEvent struct {
Interval string
}
// Unmarshal to fulfill umarshaller interface
func (SendEmailsEvent) Unmarshal(v []byte) (interface{}, error) {
e := SendEmailsEvent{}
err := json.Unmarshal(v, &e)
return e, err
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright 2018-2024 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 events
import (
"encoding/json"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
)
// OCMCoreShareCreated is emitted when an ocm share is received
type OCMCoreShareCreated struct {
ShareID string
Executant *user.UserId
Sharer *user.UserId
GranteeUserID *user.UserId
ItemID string
ResourceName string
Permissions *provider.ResourcePermissions
CTime *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (OCMCoreShareCreated) Unmarshal(v []byte) (interface{}, error) {
e := OCMCoreShareCreated{}
err := json.Unmarshal(v, &e)
return e, err
}
+12
View File
@@ -0,0 +1,12 @@
package events
import (
"time"
)
// ConsumeOptions contains all the options which can be provided when consuming events
type ConsumeOptions struct {
Group string
AutoAck bool
AckWait time.Duration
}
@@ -0,0 +1,226 @@
// 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 events
import (
"encoding/json"
"time"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
)
type (
// Postprocessingstep are the available postprocessingsteps
Postprocessingstep string
// PostprocessingOutcome defines the result of the postprocessing
PostprocessingOutcome string
)
var (
// PPStepAntivirus is the step that scans for viruses
PPStepAntivirus Postprocessingstep = "virusscan"
// PPStepPolicies is the step the step that enforces policies
PPStepPolicies Postprocessingstep = "policies"
// PPStepDelay is the step that processing. Useful for testing or user annoyment
PPStepDelay Postprocessingstep = "delay"
// PPStepFinished is the step that signals that postprocessing is finished, but storage provider hasn't acknowledged it yet
PPStepFinished Postprocessingstep = "finished"
// PPOutcomeDelete means that the file and the upload should be deleted
PPOutcomeDelete PostprocessingOutcome = "delete"
// PPOutcomeAbort means that the upload is cancelled but the bytes are being kept in the upload folder
PPOutcomeAbort PostprocessingOutcome = "abort"
// PPOutcomeContinue means that the upload is moved to its final destination (eventually being marked with pp results)
PPOutcomeContinue PostprocessingOutcome = "continue"
// PPOutcomeRetry means that there was a temporary issue and the postprocessing should be retried at a later point in time
PPOutcomeRetry PostprocessingOutcome = "retry"
)
// BytesReceived is emitted by the server when it received all bytes of an upload
type BytesReceived struct {
UploadID string
SpaceOwner *user.UserId
ExecutingUser *user.User
ResourceID *provider.ResourceId
Filename string
Filesize uint64
URL string
Timestamp *types.Timestamp
ImpersonatingUser *user.User
}
// Unmarshal to fulfill umarshaller interface
func (BytesReceived) Unmarshal(v []byte) (interface{}, error) {
e := BytesReceived{}
err := json.Unmarshal(v, &e)
return e, err
}
// StartPostprocessingStep can be issued by the server to start a postprocessing step
type StartPostprocessingStep struct {
UploadID string
URL string
ExecutingUser *user.User
Filename string
Filesize uint64
Token string // for file retrieval in after upload case
ResourceID *provider.ResourceId // for file retrieval in after upload case
RevaToken string // for file retrieval in after upload case
StepToStart Postprocessingstep
Timestamp *types.Timestamp
ImpersonatingUser *user.User
}
// Unmarshal to fulfill umarshaller interface
func (StartPostprocessingStep) Unmarshal(v []byte) (interface{}, error) {
e := StartPostprocessingStep{}
err := json.Unmarshal(v, &e)
return e, err
}
// PostprocessingStepFinished can be issued by the server when a postprocessing step is finished
type PostprocessingStepFinished struct {
UploadID string
ExecutingUser *user.User
Filename string
FinishedStep Postprocessingstep // name of the step
Result interface{} // result information see VirusscanResult for example
Error error // possible error of the step
Outcome PostprocessingOutcome // some services may cause postprocessing to stop
Timestamp *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (PostprocessingStepFinished) Unmarshal(v []byte) (interface{}, error) {
e := PostprocessingStepFinished{}
err := json.Unmarshal(v, &e)
if err != nil {
return nil, err
}
switch e.FinishedStep {
case PPStepAntivirus:
var res VirusscanResult
b, _ := json.Marshal(e.Result)
err = json.Unmarshal(b, &res)
e.Result = res
case PPStepPolicies:
// nothing to do, but this makes the linter happy
}
return e, err
}
// VirusscanResult is the Result of a PostprocessingStepFinished event from the antivirus
type VirusscanResult struct {
Infected bool
Description string
Scandate time.Time
ResourceID *provider.ResourceId
ErrorMsg string // empty when no error
Timestamp *types.Timestamp
}
// PostprocessingFinished is emitted by *some* service which can decide that
type PostprocessingFinished struct {
UploadID string
Filename string
SpaceOwner *user.UserId
ExecutingUser *user.User
Result map[Postprocessingstep]interface{} // it is a map[step]Event
Outcome PostprocessingOutcome
Timestamp *types.Timestamp
ImpersonatingUser *user.User
}
// Unmarshal to fulfill umarshaller interface
func (PostprocessingFinished) Unmarshal(v []byte) (interface{}, error) {
e := PostprocessingFinished{}
err := json.Unmarshal(v, &e)
return e, err
}
// PostprocessingRetry is emitted by *some* service which can decide that
type PostprocessingRetry struct {
UploadID string
Filename string
ExecutingUser *user.User
Failures int
BackoffDuration time.Duration
}
// Unmarshal to fulfill umarshaller interface
func (PostprocessingRetry) Unmarshal(v []byte) (interface{}, error) {
e := PostprocessingRetry{}
err := json.Unmarshal(v, &e)
return e, err
}
// UploadReady is emitted by the storage provider when postprocessing is finished
type UploadReady struct {
UploadID string
Filename string
SpaceOwner *user.UserId
ExecutingUser *user.User
ImpersonatingUser *user.User
FileRef *provider.Reference
ParentID *provider.ResourceId
Timestamp *types.Timestamp
Failed bool
IsVersion bool
// add reference here? We could use it to inform client pp is finished
}
// Unmarshal to fulfill umarshaller interface
func (UploadReady) Unmarshal(v []byte) (interface{}, error) {
e := UploadReady{}
err := json.Unmarshal(v, &e)
return e, err
}
// ResumePostprocessing can be emitted to repair broken postprocessing
type ResumePostprocessing struct {
UploadID string
Step Postprocessingstep
Timestamp *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (ResumePostprocessing) Unmarshal(v []byte) (interface{}, error) {
e := ResumePostprocessing{}
err := json.Unmarshal(v, &e)
return e, err
}
// RestartPostprocessing will be emitted by postprocessing service if it doesn't know about an upload
type RestartPostprocessing struct {
UploadID string
Timestamp *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (RestartPostprocessing) Unmarshal(v []byte) (interface{}, error) {
e := RestartPostprocessing{}
err := json.Unmarshal(v, &e)
return e, err
}
+27
View File
@@ -0,0 +1,27 @@
package raw
import (
"bytes"
"crypto/x509"
"errors"
"io"
)
// newCertPoolFromPEM reads certificates from io.Reader and returns a x509.CertPool
// containing those certificates.
func newCertPoolFromPEM(crts ...io.Reader) (*x509.CertPool, error) {
certPool := x509.NewCertPool()
var buf bytes.Buffer
for _, c := range crts {
if _, err := io.Copy(&buf, c); err != nil {
return nil, err
}
if !certPool.AppendCertsFromPEM(buf.Bytes()) {
return nil, errors.New("failed to append cert from PEM")
}
buf.Reset()
}
return certPool, nil
}
@@ -0,0 +1,176 @@
// 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.
// Code generated by mockery v2.53.2. DO NOT EDIT.
package mocks
import (
jetstream "github.com/nats-io/nats.go/jetstream"
events "github.com/opencloud-eu/reva/v2/pkg/events"
mock "github.com/stretchr/testify/mock"
raw "github.com/opencloud-eu/reva/v2/pkg/events/raw"
)
// Stream is an autogenerated mock type for the Stream type
type Stream struct {
mock.Mock
}
type Stream_Expecter struct {
mock *mock.Mock
}
func (_m *Stream) EXPECT() *Stream_Expecter {
return &Stream_Expecter{mock: &_m.Mock}
}
// Consume provides a mock function with given fields: group, evs
func (_m *Stream) Consume(group string, evs ...events.Unmarshaller) (<-chan raw.Event, error) {
var tmpRet mock.Arguments
if len(evs) > 0 {
tmpRet = _m.Called(group, evs)
} else {
tmpRet = _m.Called(group)
}
ret := tmpRet
if len(ret) == 0 {
panic("no return value specified for Consume")
}
var r0 <-chan raw.Event
var r1 error
if rf, ok := ret.Get(0).(func(string, ...events.Unmarshaller) (<-chan raw.Event, error)); ok {
return rf(group, evs...)
}
if rf, ok := ret.Get(0).(func(string, ...events.Unmarshaller) <-chan raw.Event); ok {
r0 = rf(group, evs...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(<-chan raw.Event)
}
}
if rf, ok := ret.Get(1).(func(string, ...events.Unmarshaller) error); ok {
r1 = rf(group, evs...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Stream_Consume_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Consume'
type Stream_Consume_Call struct {
*mock.Call
}
// Consume is a helper method to define mock.On call
// - group string
// - evs ...events.Unmarshaller
func (_e *Stream_Expecter) Consume(group interface{}, evs ...interface{}) *Stream_Consume_Call {
return &Stream_Consume_Call{Call: _e.mock.On("Consume",
append([]interface{}{group}, evs...)...)}
}
func (_c *Stream_Consume_Call) Run(run func(group string, evs ...events.Unmarshaller)) *Stream_Consume_Call {
_c.Call.Run(func(args mock.Arguments) {
variadicArgs := make([]events.Unmarshaller, len(args)-1)
for i, a := range args[1:] {
if a != nil {
variadicArgs[i] = a.(events.Unmarshaller)
}
}
run(args[0].(string), variadicArgs...)
})
return _c
}
func (_c *Stream_Consume_Call) Return(_a0 <-chan raw.Event, _a1 error) *Stream_Consume_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *Stream_Consume_Call) RunAndReturn(run func(string, ...events.Unmarshaller) (<-chan raw.Event, error)) *Stream_Consume_Call {
_c.Call.Return(run)
return _c
}
// JetStream provides a mock function with no fields
func (_m *Stream) JetStream() jetstream.Stream {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for JetStream")
}
var r0 jetstream.Stream
if rf, ok := ret.Get(0).(func() jetstream.Stream); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(jetstream.Stream)
}
}
return r0
}
// Stream_JetStream_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'JetStream'
type Stream_JetStream_Call struct {
*mock.Call
}
// JetStream is a helper method to define mock.On call
func (_e *Stream_Expecter) JetStream() *Stream_JetStream_Call {
return &Stream_JetStream_Call{Call: _e.mock.On("JetStream")}
}
func (_c *Stream_JetStream_Call) Run(run func()) *Stream_JetStream_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *Stream_JetStream_Call) Return(_a0 jetstream.Stream) *Stream_JetStream_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Stream_JetStream_Call) RunAndReturn(run func() jetstream.Stream) *Stream_JetStream_Call {
_c.Call.Return(run)
return _c
}
// NewStream creates a new instance of Stream. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewStream(t interface {
mock.TestingT
Cleanup(func())
}) *Stream {
mock := &Stream{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
+225
View File
@@ -0,0 +1,225 @@
package raw
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"os"
"reflect"
"time"
"github.com/cenkalti/backoff"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/pkg/errors"
)
// Config is the configuration needed for a NATS event stream
type Config struct {
Endpoint string `mapstructure:"address"` // Endpoint of the nats server
Cluster string `mapstructure:"clusterID"` // CluserID of the nats cluster
TLSInsecure bool `mapstructure:"tls-insecure"` // Whether to verify TLS certificates
TLSRootCACertificate string `mapstructure:"tls-root-ca-cert"` // The root CA certificate used to validate the TLS certificate
EnableTLS bool `mapstructure:"enable-tls"` // Enable TLS
AuthUsername string `mapstructure:"username"` // Username for authentication
AuthPassword string `mapstructure:"password"` // Password for authentication
MaxAckPending int `mapstructure:"max-ack-pending"` // Maximum number of unacknowledged messages
AckWait time.Duration `mapstructure:"ack-wait"` // Time to wait for an ack
}
type RawEvent struct {
Timestamp time.Time
Metadata map[string]string
ID string
Topic string
Payload []byte
msg jetstream.Msg
}
type Event struct {
events.Event
msg jetstream.Msg
}
func (re *Event) Ack() error {
if re.msg == nil {
return errors.New("cannot ack event without message")
}
return re.msg.Ack()
}
func (re *Event) InProgress() error {
if re.msg == nil {
return errors.New("cannot mark event as in progress without message")
}
return re.msg.InProgress()
}
type Stream interface {
Consume(group string, evs ...events.Unmarshaller) (<-chan Event, error)
JetStream() jetstream.Stream
}
type RawStream struct {
js jetstream.Stream
c Config
}
func JetStream(ctx context.Context, name string, cfg Config) (jetstream.JetStream, error) {
var js jetstream.JetStream
connect := func() error {
var tlsConf *tls.Config
if cfg.EnableTLS {
var rootCAPool *x509.CertPool
if cfg.TLSRootCACertificate != "" {
rootCrtFile, err := os.Open(cfg.TLSRootCACertificate)
if err != nil {
return err
}
rootCAPool, err = newCertPoolFromPEM(rootCrtFile)
if err != nil {
return err
}
cfg.TLSInsecure = false
}
tlsConf = &tls.Config{
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: cfg.TLSInsecure,
RootCAs: rootCAPool,
}
}
nopts := nats.GetDefaultOptions()
nopts.Name = name
if tlsConf != nil {
nopts.Secure = true
nopts.TLSConfig = tlsConf
}
if len(cfg.Endpoint) > 0 {
nopts.Servers = []string{cfg.Endpoint}
}
if cfg.AuthUsername != "" && cfg.AuthPassword != "" {
nopts.User = cfg.AuthUsername
nopts.Password = cfg.AuthPassword
}
conn, err := nopts.Connect()
if err != nil {
return err
}
js, err = jetstream.New(conn)
return err
}
err := backoff.Retry(connect, backoff.NewExponentialBackOff())
if err != nil {
return nil, errors.Wrap(err, "could not connect to nats jetstream")
}
return js, nil
}
func FromConfig(ctx context.Context, name string, cfg Config) (Stream, error) {
jsConn, err := JetStream(ctx, name, cfg)
if err != nil {
return nil, err
}
js, err := jsConn.Stream(ctx, events.MainQueueName)
if err != nil {
return nil, err
}
return &RawStream{
js: js,
c: cfg,
}, nil
}
func (s *RawStream) Consume(group string, evs ...events.Unmarshaller) (<-chan Event, error) {
c, err := s.consumeRaw(group)
if err != nil {
return nil, err
}
registeredEvents := map[string]events.Unmarshaller{}
for _, e := range evs {
typ := reflect.TypeOf(e)
registeredEvents[typ.String()] = e
}
outchan := make(chan Event)
go func() {
for {
e := <-c
eventType := e.Metadata[events.MetadatakeyEventType]
ev, ok := registeredEvents[eventType]
if !ok {
_ = e.msg.Ack() // Discard. We are not interested in this event type
continue
}
event, err := ev.Unmarshal(e.Payload)
if err != nil {
continue
}
outchan <- Event{
Event: events.Event{
Type: eventType,
ID: e.Metadata[events.MetadatakeyEventID],
TraceParent: e.Metadata[events.MetadatakeyTraceParent],
InitiatorID: e.Metadata[events.MetadatakeyInitiatorID],
Event: event,
},
msg: e.msg,
}
}
}()
return outchan, nil
}
func (s *RawStream) consumeRaw(group string) (<-chan RawEvent, error) {
consumer, err := s.js.CreateOrUpdateConsumer(context.Background(), jetstream.ConsumerConfig{
Durable: group,
DeliverPolicy: jetstream.DeliverNewPolicy,
AckPolicy: jetstream.AckExplicitPolicy, // Require manual acknowledgment
MaxAckPending: s.c.MaxAckPending, // Maximum number of unacknowledged messages
AckWait: s.c.AckWait, // Time to wait for an ack
})
if err != nil {
return nil, err
}
channel := make(chan RawEvent)
callback := func(msg jetstream.Msg) {
var rawEvent RawEvent
if err := json.Unmarshal(msg.Data(), &rawEvent); err != nil {
fmt.Printf("error unmarshalling event: %v\n", err)
return
}
rawEvent.msg = msg
channel <- rawEvent
}
_, err = consumer.Consume(callback)
if err != nil {
return nil, err
}
return channel, nil
}
func (s *RawStream) JetStream() jetstream.Stream {
return s.js
}
@@ -0,0 +1,26 @@
package events
import (
"encoding/json"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
)
// ScienceMeshInviteTokenGenerated is emitted when a sciencemesh token is generated
type ScienceMeshInviteTokenGenerated struct {
Sharer *user.UserId
RecipientMail string
Token string
Description string
Expiration uint64
InviteLink string
Timestamp *types.Timestamp
}
// Unmarshal to fulfill unmarshaller interface
func (ScienceMeshInviteTokenGenerated) Unmarshal(v []byte) (interface{}, error) {
e := ScienceMeshInviteTokenGenerated{}
err := json.Unmarshal(v, &e)
return e, err
}
+244
View File
@@ -0,0 +1,244 @@
// 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 events
import (
"encoding/json"
"time"
group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
)
// ShareCreated is emitted when a share is created
type ShareCreated struct {
ShareID *collaboration.ShareId
Executant *user.UserId
Sharer *user.UserId
// split the protobuf Grantee oneof so we can use stdlib encoding/json
GranteeUserID *user.UserId
GranteeGroupID *group.GroupId
Sharee *provider.Grantee
ItemID *provider.ResourceId
ResourceName string
Permissions *collaboration.SharePermissions
CTime *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (ShareCreated) Unmarshal(v []byte) (interface{}, error) {
e := ShareCreated{}
err := json.Unmarshal(v, &e)
return e, err
}
// ShareRemoved is emitted when a share is removed
type ShareRemoved struct {
Executant *user.UserId
// split protobuf Spec
ShareID *collaboration.ShareId
ShareKey *collaboration.ShareKey
// split the protobuf Grantee oneof so we can use stdlib encoding/json
GranteeUserID *user.UserId
GranteeGroupID *group.GroupId
ItemID *provider.ResourceId
ResourceName string
Timestamp time.Time
}
// Unmarshal to fulfill umarshaller interface
func (ShareRemoved) Unmarshal(v []byte) (interface{}, error) {
e := ShareRemoved{}
err := json.Unmarshal(v, &e)
return e, err
}
// ShareUpdated is emitted when a share is updated
type ShareUpdated struct {
Executant *user.UserId
ShareID *collaboration.ShareId
ItemID *provider.ResourceId
ResourceName string
Permissions *collaboration.SharePermissions
GranteeUserID *user.UserId
GranteeGroupID *group.GroupId
Sharer *user.UserId
MTime *types.Timestamp
Updated string // Deprecated
// indicates what was updated
UpdateMask []string
}
// Unmarshal to fulfill umarshaller interface
func (ShareUpdated) Unmarshal(v []byte) (interface{}, error) {
e := ShareUpdated{}
err := json.Unmarshal(v, &e)
return e, err
}
// ShareExpired is emitted when a share expires
type ShareExpired struct {
ShareID *collaboration.ShareId
ShareOwner *user.UserId
ItemID *provider.ResourceId
Path string
ExpiredAt time.Time
// split the protobuf Grantee oneof so we can use stdlib encoding/json
GranteeUserID *user.UserId
GranteeGroupID *group.GroupId
}
// Unmarshal to fulfill umarshaller interface
func (ShareExpired) Unmarshal(v []byte) (interface{}, error) {
e := ShareExpired{}
err := json.Unmarshal(v, &e)
return e, err
}
// ReceivedShareUpdated is emitted when a received share is accepted or declined
type ReceivedShareUpdated struct {
Executant *user.UserId
ShareID *collaboration.ShareId
ItemID *provider.ResourceId
Path string
Permissions *collaboration.SharePermissions
GranteeUserID *user.UserId
GranteeGroupID *group.GroupId
Sharer *user.UserId
MTime *types.Timestamp
State string
}
// Unmarshal to fulfill umarshaller interface
func (ReceivedShareUpdated) Unmarshal(v []byte) (interface{}, error) {
e := ReceivedShareUpdated{}
err := json.Unmarshal(v, &e)
return e, err
}
// LinkCreated is emitted when a public link is created
type LinkCreated struct {
Executant *user.UserId
ShareID *link.PublicShareId
Sharer *user.UserId
ItemID *provider.ResourceId
ResourceName string
Permissions *link.PublicSharePermissions
DisplayName string
Expiration *types.Timestamp
PasswordProtected bool
CTime *types.Timestamp
Token string
}
// Unmarshal to fulfill umarshaller interface
func (LinkCreated) Unmarshal(v []byte) (interface{}, error) {
e := LinkCreated{}
err := json.Unmarshal(v, &e)
return e, err
}
// LinkUpdated is emitted when a public link is updated
type LinkUpdated struct {
Executant *user.UserId
ShareID *link.PublicShareId
Sharer *user.UserId
ItemID *provider.ResourceId
ResourceName string
Permissions *link.PublicSharePermissions
DisplayName string
Expiration *types.Timestamp
PasswordProtected bool
MTime *types.Timestamp
Token string
FieldUpdated string
}
// Unmarshal to fulfill umarshaller interface
func (LinkUpdated) Unmarshal(v []byte) (interface{}, error) {
e := LinkUpdated{}
err := json.Unmarshal(v, &e)
return e, err
}
// LinkAccessed is emitted when a public link is accessed successfully (by token)
type LinkAccessed struct {
Executant *user.UserId
ShareID *link.PublicShareId
Sharer *user.UserId
ItemID *provider.ResourceId
Path string
Permissions *link.PublicSharePermissions
DisplayName string
Expiration *types.Timestamp
PasswordProtected bool
CTime *types.Timestamp
Token string
}
// Unmarshal to fulfill umarshaller interface
func (LinkAccessed) Unmarshal(v []byte) (interface{}, error) {
e := LinkAccessed{}
err := json.Unmarshal(v, &e)
return e, err
}
// LinkAccessFailed is emitted when an access to a public link has resulted in an error (by token)
type LinkAccessFailed struct {
Executant *user.UserId
ShareID *link.PublicShareId
Token string
Status rpc.Code
Message string
Timestamp *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (LinkAccessFailed) Unmarshal(v []byte) (interface{}, error) {
e := LinkAccessFailed{}
err := json.Unmarshal(v, &e)
return e, err
}
// LinkRemoved is emitted when a share is removed
type LinkRemoved struct {
Executant *user.UserId
// split protobuf Ref
ShareID *link.PublicShareId
ShareToken string
Timestamp *types.Timestamp
ItemID *provider.ResourceId
ResourceName string
}
// Unmarshal to fulfill umarshaller interface
func (LinkRemoved) Unmarshal(v []byte) (interface{}, error) {
e := LinkRemoved{}
err := json.Unmarshal(v, &e)
return e, err
}
+192
View File
@@ -0,0 +1,192 @@
// 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 events
import (
"encoding/json"
"time"
group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
)
// SpaceCreated is emitted when a space is created
type SpaceCreated struct {
Executant *user.UserId
ID *provider.StorageSpaceId
Owner *user.UserId
Root *provider.ResourceId
Name string
Type string
Quota *provider.Quota
MTime *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (SpaceCreated) Unmarshal(v []byte) (interface{}, error) {
e := SpaceCreated{}
err := json.Unmarshal(v, &e)
return e, err
}
// SpaceRenamed is emitted when a space is renamed
type SpaceRenamed struct {
Executant *user.UserId
ID *provider.StorageSpaceId
Owner *user.UserId
Name string
Timestamp *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (SpaceRenamed) Unmarshal(v []byte) (interface{}, error) {
e := SpaceRenamed{}
err := json.Unmarshal(v, &e)
return e, err
}
// SpaceDisabled is emitted when a space is disabled
type SpaceDisabled struct {
Executant *user.UserId
ID *provider.StorageSpaceId
Timestamp time.Time
}
// Unmarshal to fulfill umarshaller interface
func (SpaceDisabled) Unmarshal(v []byte) (interface{}, error) {
e := SpaceDisabled{}
err := json.Unmarshal(v, &e)
return e, err
}
// SpaceEnabled is emitted when a space is (re-)enabled
type SpaceEnabled struct {
Executant *user.UserId
ID *provider.StorageSpaceId
Owner *user.UserId
Timestamp *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (SpaceEnabled) Unmarshal(v []byte) (interface{}, error) {
e := SpaceEnabled{}
err := json.Unmarshal(v, &e)
return e, err
}
// SpaceDeleted is emitted when a space is deleted
type SpaceDeleted struct {
Executant *user.UserId
ID *provider.StorageSpaceId
SpaceName string
FinalMembers map[string]provider.ResourcePermissions
Timestamp time.Time
}
// Unmarshal to fulfill umarshaller interface
func (SpaceDeleted) Unmarshal(v []byte) (interface{}, error) {
e := SpaceDeleted{}
err := json.Unmarshal(v, &e)
return e, err
}
// SpaceShared is emitted when a space is shared
type SpaceShared struct {
Executant *user.UserId
GranteeUserID *user.UserId
GranteeGroupID *group.GroupId
Creator *user.UserId
ID *provider.StorageSpaceId
Timestamp time.Time
}
// Unmarshal to fulfill umarshaller interface
func (SpaceShared) Unmarshal(v []byte) (interface{}, error) {
e := SpaceShared{}
err := json.Unmarshal(v, &e)
return e, err
}
// SpaceShareUpdated is emitted when a space share is updated
type SpaceShareUpdated struct {
Executant *user.UserId
GranteeUserID *user.UserId
GranteeGroupID *group.GroupId
ID *provider.StorageSpaceId
Timestamp time.Time
}
// Unmarshal to fulfill umarshaller interface
func (SpaceShareUpdated) Unmarshal(v []byte) (interface{}, error) {
ev := SpaceShareUpdated{}
err := json.Unmarshal(v, &ev)
return ev, err
}
// SpaceUnshared is emitted when a space is unshared
type SpaceUnshared struct {
Executant *user.UserId
GranteeUserID *user.UserId
GranteeGroupID *group.GroupId
ID *provider.StorageSpaceId
Timestamp time.Time
}
// Unmarshal to fulfill umarshaller interface
func (SpaceUnshared) Unmarshal(v []byte) (interface{}, error) {
e := SpaceUnshared{}
err := json.Unmarshal(v, &e)
return e, err
}
// SpaceUpdated is emitted when a space is updated
type SpaceUpdated struct {
Executant *user.UserId
ID *provider.StorageSpaceId
Space *provider.StorageSpace
Timestamp *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (SpaceUpdated) Unmarshal(v []byte) (interface{}, error) {
e := SpaceUpdated{}
err := json.Unmarshal(v, &e)
return e, err
}
// SpaceMembershipExpired is emitted when a space membership expires
type SpaceMembershipExpired struct {
SpaceOwner *user.UserId
SpaceID *provider.StorageSpaceId
SpaceName string
ExpiredAt time.Time
// split the protobuf Grantee oneof so we can use stdlib encoding/json
GranteeUserID *user.UserId
GranteeGroupID *group.GroupId
Timestamp *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (SpaceMembershipExpired) Unmarshal(v []byte) (interface{}, error) {
e := SpaceMembershipExpired{}
err := json.Unmarshal(v, &e)
return e, err
}
+19
View File
@@ -0,0 +1,19 @@
package events
import (
"encoding/json"
)
// SendSSE instructs the sse service to send one or multiple notifications
type SendSSE struct {
UserIDs []string
Type string
Message []byte
}
// Unmarshal to fulfill umarshaller interface
func (SendSSE) Unmarshal(v []byte) (interface{}, error) {
e := SendSSE{}
err := json.Unmarshal(v, &e)
return e, err
}
+141
View File
@@ -0,0 +1,141 @@
package stream
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"errors"
"io"
"os"
"time"
"github.com/cenkalti/backoff"
"github.com/go-micro/plugins/v4/events/natsjs"
"github.com/nats-io/nats.go/jetstream"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/events/raw"
"github.com/opencloud-eu/reva/v2/pkg/logger"
)
// NatsConfig is the configuration needed for a NATS event stream
type NatsConfig struct {
Endpoint string `mapstructure:"address"` // Endpoint of the nats server
Cluster string `mapstructure:"clusterID"` // CluserID of the nats cluster
TLSInsecure bool `mapstructure:"tls-insecure"` // Whether to verify TLS certificates
TLSRootCACertificate string `mapstructure:"tls-root-ca-cert"` // The root CA certificate used to validate the TLS certificate
EnableTLS bool `mapstructure:"enable-tls"` // Enable TLS
AuthUsername string `mapstructure:"username"` // Username for authentication
AuthPassword string `mapstructure:"password"` // Password for authentication
}
// NatsFromConfig returns a nats stream from the given config
func NatsFromConfig(connName string, disableDurability bool, cfg NatsConfig) (events.Stream, error) {
var tlsConf *tls.Config
if cfg.EnableTLS {
var rootCAPool *x509.CertPool
if cfg.TLSRootCACertificate != "" {
rootCrtFile, err := os.Open(cfg.TLSRootCACertificate)
if err != nil {
return nil, err
}
rootCAPool, err = newCertPoolFromPEM(rootCrtFile)
if err != nil {
return nil, err
}
cfg.TLSInsecure = false
}
tlsConf = &tls.Config{
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: cfg.TLSInsecure, //nolint:gosec
RootCAs: rootCAPool,
}
}
opts := []natsjs.Option{
natsjs.TLSConfig(tlsConf),
natsjs.Address(cfg.Endpoint),
natsjs.ClusterID(cfg.Cluster),
natsjs.SynchronousPublish(true),
natsjs.Name(connName),
natsjs.Authenticate(cfg.AuthUsername, cfg.AuthPassword),
}
if disableDurability {
opts = append(opts, natsjs.DisableDurableStreams())
}
s, err := Nats(opts...)
if err != nil {
return nil, err
}
// apply a MaxAge to the main queue to prevent it from filling up
ctx := context.Background()
jsConn, err := raw.JetStream(ctx, connName, raw.Config{
Endpoint: cfg.Endpoint,
Cluster: cfg.Cluster,
TLSInsecure: cfg.TLSInsecure,
TLSRootCACertificate: cfg.TLSRootCACertificate,
EnableTLS: cfg.EnableTLS,
AuthUsername: cfg.AuthUsername,
AuthPassword: cfg.AuthPassword,
})
if err != nil {
return nil, err
}
streamCfg := jetstream.StreamConfig{
Name: "main-queue",
MaxAge: 7 * 24 * time.Hour,
}
_, err = jsConn.CreateStream(ctx, streamCfg)
if err != nil {
// If the stream already exists, update its configuration
if err == jetstream.ErrStreamNameAlreadyInUse {
_, _ = jsConn.UpdateStream(ctx, streamCfg)
}
}
return s, nil
}
// nats returns a nats streaming client
// retries exponentially to connect to a nats server
func Nats(opts ...natsjs.Option) (events.Stream, error) {
b := backoff.NewExponentialBackOff()
var stream events.Stream
o := func() error {
n := b.NextBackOff()
s, err := natsjs.NewStream(opts...)
if err != nil && n > time.Second {
logger.New().Error().Err(err).Msgf("can't connect to nats (jetstream) server, retrying in %s", n)
}
stream = s
return err
}
err := backoff.Retry(o, b)
return stream, err
}
// newCertPoolFromPEM reads certificates from io.Reader and returns a x509.CertPool
// containing those certificates.
func newCertPoolFromPEM(crts ...io.Reader) (*x509.CertPool, error) {
certPool := x509.NewCertPool()
var buf bytes.Buffer
for _, c := range crts {
if _, err := io.Copy(&buf, c); err != nil {
return nil, err
}
if !certPool.AppendCertsFromPEM(buf.Bytes()) {
return nil, errors.New("failed to append cert from PEM")
}
buf.Reset()
}
return certPool, nil
}
@@ -0,0 +1,42 @@
// Package stream provides streaming clients used by `Consume` and `Publish` methods
package stream
import (
"encoding/json"
"reflect"
"go-micro.dev/v4/events"
)
// Chan is a channel based streaming clients
// Useful for tests or in memory applications
type Chan [2]chan interface{}
// Publish implementation
func (ch Chan) Publish(_ string, msg interface{}, _ ...events.PublishOption) error {
go func() {
ch[0] <- msg
}()
return nil
}
// Consume implementation
func (ch Chan) Consume(_ string, _ ...events.ConsumeOption) (<-chan events.Event, error) {
evch := make(chan events.Event)
go func() {
for {
e := <-ch[1]
if e == nil {
// channel closed
return
}
b, _ := json.Marshal(e)
evname := reflect.TypeOf(e).String()
evch <- events.Event{
Payload: b,
Metadata: map[string]string{"eventtype": evname},
}
}
}()
return evch, nil
}
+59
View File
@@ -0,0 +1,59 @@
// 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 events
import (
"encoding/json"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
)
// TagsAdded is emitted when a Tag has been added
type TagsAdded struct {
SpaceOwner *user.UserId
Tags string
Ref *provider.Reference
Executant *user.UserId
Timestamp *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (TagsAdded) Unmarshal(v []byte) (interface{}, error) {
e := TagsAdded{}
err := json.Unmarshal(v, &e)
return e, err
}
// TagsRemoved is emitted when a Tag has been added
type TagsRemoved struct {
SpaceOwner *user.UserId
Tags string
Ref *provider.Reference
Executant *user.UserId
Timestamp *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (TagsRemoved) Unmarshal(v []byte) (interface{}, error) {
e := TagsRemoved{}
err := json.Unmarshal(v, &e)
return e, err
}
+134
View File
@@ -0,0 +1,134 @@
// 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 events
import (
"encoding/json"
"time"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
)
// UserCreated is emitted when a user was created
type UserCreated struct {
Executant *user.UserId
UserID string
Timestamp *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (UserCreated) Unmarshal(v []byte) (interface{}, error) {
e := UserCreated{}
err := json.Unmarshal(v, &e)
return e, err
}
// UserDeleted is emitted when a user was deleted
type UserDeleted struct {
Executant *user.UserId
UserID string
Timestamp *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (UserDeleted) Unmarshal(v []byte) (interface{}, error) {
e := UserDeleted{}
err := json.Unmarshal(v, &e)
return e, err
}
// UserSoftDeleted is emitted when a user was soft-deleted
type UserSoftDeleted struct {
Executant *user.UserId
UserID string
Timestamp *types.Timestamp
RetentionTime time.Duration
Reason string
}
// Unmarshal to fulfill unmarshaller interface
func (UserSoftDeleted) Unmarshal(v []byte) (interface{}, error) {
e := UserSoftDeleted{}
err := json.Unmarshal(v, &e)
return e, err
}
// UserFeature represents a user feature
type UserFeature struct {
Name string
Value string
OldValue *string
}
// UserFeatureChanged is emitted when a user feature was changed
type UserFeatureChanged struct {
Executant *user.UserId
UserID string
Features []UserFeature
Timestamp *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (UserFeatureChanged) Unmarshal(v []byte) (interface{}, error) {
e := UserFeatureChanged{}
err := json.Unmarshal(v, &e)
return e, err
}
// PersonalDataExtracted is emitted when a user data extraction is finished
type PersonalDataExtracted struct {
Executant *user.UserId
Timestamp *types.Timestamp
ErrorMsg string
}
// Unmarshal to fulfill umarshaller interface
func (PersonalDataExtracted) Unmarshal(v []byte) (interface{}, error) {
e := PersonalDataExtracted{}
err := json.Unmarshal(v, &e)
return e, err
}
// BackchannelLogout is emitted when the callback from the identity provider is received
type BackchannelLogout struct {
Executant *user.UserId
SessionId string
Timestamp *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (BackchannelLogout) Unmarshal(v []byte) (interface{}, error) {
e := BackchannelLogout{}
err := json.Unmarshal(v, &e)
return e, err
}
// UserSignedIn is emitted when a user signs in
type UserSignedIn struct {
Executant *user.UserId
Timestamp *types.Timestamp
}
// Unmarshal to fulfill umarshaller interface
func (UserSignedIn) Unmarshal(v []byte) (interface{}, error) {
e := UserSignedIn{}
err := json.Unmarshal(v, &e)
return e, err
}
+35
View File
@@ -0,0 +1,35 @@
// Copyright 2018-2020 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package group
import (
"context"
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
)
// Manager is the interface to implement to manipulate groups.
type Manager interface {
GetGroup(ctx context.Context, gid *grouppb.GroupId, skipFetchingMembers bool) (*grouppb.Group, error)
GetGroupByClaim(ctx context.Context, claim, value string, skipFetchingMembers bool) (*grouppb.Group, error)
FindGroups(ctx context.Context, query string, skipFetchingMembers bool) ([]*grouppb.Group, error)
GetMembers(ctx context.Context, gid *grouppb.GroupId) ([]*userpb.UserId, error)
HasMember(ctx context.Context, gid *grouppb.GroupId, uid *userpb.UserId) (bool, error)
}
@@ -0,0 +1,172 @@
// Copyright 2018-2020 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package json
import (
"context"
"encoding/json"
"os"
"strconv"
"strings"
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/group"
"github.com/opencloud-eu/reva/v2/pkg/group/manager/registry"
"github.com/pkg/errors"
"google.golang.org/protobuf/proto"
)
func init() {
registry.Register("json", New)
}
type manager struct {
groups []*grouppb.Group
}
type config struct {
// Groups holds a path to a file containing json conforming to the Groups struct
Groups string `mapstructure:"groups"`
}
func (c *config) init() {
if c.Groups == "" {
c.Groups = "/etc/revad/groups.json"
}
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
c.init()
return c, nil
}
// New returns a group manager implementation that reads a json file to provide group metadata.
func New(m map[string]interface{}) (group.Manager, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
f, err := os.ReadFile(c.Groups)
if err != nil {
return nil, err
}
groups := []*grouppb.Group{}
err = json.Unmarshal(f, &groups)
if err != nil {
return nil, err
}
return &manager{
groups: groups,
}, nil
}
func (m *manager) GetGroup(ctx context.Context, gid *grouppb.GroupId, skipFetchingMembers bool) (*grouppb.Group, error) {
for _, g := range m.groups {
if (g.Id.GetOpaqueId() == gid.OpaqueId || g.GroupName == gid.OpaqueId) && (gid.Idp == "" || gid.Idp == g.Id.GetIdp()) {
group := proto.Clone(g).(*grouppb.Group)
if skipFetchingMembers {
group.Members = nil
}
return group, nil
}
}
return nil, errtypes.NotFound(gid.OpaqueId)
}
func (m *manager) GetGroupByClaim(ctx context.Context, claim, value string, skipFetchingMembers bool) (*grouppb.Group, error) {
for _, g := range m.groups {
if groupClaim, err := extractClaim(g, claim); err == nil && value == groupClaim {
group := proto.Clone(g).(*grouppb.Group)
if skipFetchingMembers {
group.Members = nil
}
return group, nil
}
}
return nil, errtypes.NotFound(value)
}
func extractClaim(g *grouppb.Group, claim string) (string, error) {
switch claim {
case "group_name":
return g.GroupName, nil
case "gid_number":
return strconv.FormatInt(g.GidNumber, 10), nil
case "display_name":
return g.DisplayName, nil
case "mail":
return g.Mail, nil
}
return "", errors.New("json: invalid field")
}
func (m *manager) FindGroups(ctx context.Context, query string, skipFetchingMembers bool) ([]*grouppb.Group, error) {
groups := []*grouppb.Group{}
for _, g := range m.groups {
if groupContains(g, query) {
group := proto.Clone(g).(*grouppb.Group)
if skipFetchingMembers {
group.Members = nil
}
groups = append(groups, group)
}
}
return groups, nil
}
func groupContains(g *grouppb.Group, query string) bool {
query = strings.ToLower(query)
return strings.Contains(strings.ToLower(g.GroupName), query) || strings.Contains(strings.ToLower(g.DisplayName), query) ||
strings.Contains(strings.ToLower(g.Mail), query) || strings.Contains(strings.ToLower(g.Id.OpaqueId), query)
}
func (m *manager) GetMembers(ctx context.Context, gid *grouppb.GroupId) ([]*userpb.UserId, error) {
for _, g := range m.groups {
if g.Id.GetOpaqueId() == gid.OpaqueId || g.GroupName == gid.OpaqueId {
return g.Members, nil
}
}
return nil, errtypes.NotFound(gid.OpaqueId)
}
func (m *manager) HasMember(ctx context.Context, gid *grouppb.GroupId, uid *userpb.UserId) (bool, error) {
members, err := m.GetMembers(ctx, gid)
if err != nil {
return false, err
}
for _, u := range members {
if u.OpaqueId == uid.OpaqueId && u.Idp == uid.Idp {
return true, nil
}
}
return false, nil
}
@@ -0,0 +1,377 @@
// Copyright 2018-2020 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package ldap
import (
"context"
"fmt"
"strconv"
"strings"
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/go-ldap/ldap/v3"
"github.com/google/uuid"
"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/group"
"github.com/opencloud-eu/reva/v2/pkg/group/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/utils"
ldapIdentity "github.com/opencloud-eu/reva/v2/pkg/utils/ldap"
"go.opentelemetry.io/otel/attribute"
)
func init() {
registry.Register("ldap", New)
}
type manager struct {
c *config
ldapClient ldap.Client
}
type config struct {
utils.LDAPConn `mapstructure:",squash"`
LDAPIdentity ldapIdentity.Identity `mapstructure:",squash"`
Idp string `mapstructure:"idp"`
// Nobody specifies the fallback gid number for groups that don't have a gidNumber set in LDAP
Nobody int64 `mapstructure:"nobody"`
}
const tracerName = "pkg/group/manager/ldap"
func parseConfig(m map[string]interface{}) (*config, error) {
c := config{
LDAPIdentity: ldapIdentity.New(),
}
if err := mapstructure.Decode(m, &c); err != nil {
return nil, fmt.Errorf("error decoding conf: %w", err)
}
return &c, nil
}
// New returns a group manager implementation that connects to a LDAP server to provide group metadata.
func New(m map[string]interface{}) (group.Manager, error) {
if sharedconf.MultiTenantEnabled() {
return nil, errtypes.NotSupported("ldap group manager does not support multi-tenancy")
}
mgr := &manager{}
err := mgr.Configure(m)
if err != nil {
return nil, err
}
mgr.ldapClient, err = utils.GetLDAPClientWithReconnect(&mgr.c.LDAPConn)
if err != nil {
return nil, err
}
return mgr, nil
}
// Configure initializes the configuration of the group manager from the supplied config map
func (m *manager) Configure(ml map[string]interface{}) error {
c, err := parseConfig(ml)
if err != nil {
return err
}
if c.Nobody == 0 {
c.Nobody = 99
}
if err = c.LDAPIdentity.Setup(); err != nil {
return fmt.Errorf("error setting up Identity config: %w", err)
}
m.c = c
return nil
}
// GetGroup implements the group.Manager interface. Looks up a group by Id and return the group
func (m *manager) GetGroup(ctx context.Context, gid *grouppb.GroupId, skipFetchingMembers bool) (*grouppb.Group, error) {
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "GetGroup")
defer span.End()
span.SetAttributes(
attribute.Stringer("group_id", gid),
attribute.Bool("skip_fetching_members", skipFetchingMembers),
)
log := appctx.GetLogger(ctx)
if gid.Idp != "" && gid.Idp != m.c.Idp {
return nil, errtypes.NotFound("idp mismatch")
}
groupEntry, err := m.c.LDAPIdentity.GetLDAPGroupByID(ctx, m.ldapClient, gid.OpaqueId)
if err != nil {
return nil, err
}
log.Debug().Interface("entry", groupEntry).Msg("entries")
g, err := m.ldapEntryToGroup(groupEntry)
if err != nil {
return nil, err
}
if skipFetchingMembers {
return g, nil
}
members, err := m.c.LDAPIdentity.GetLDAPGroupMembers(ctx, m.ldapClient, groupEntry)
if err != nil {
return nil, err
}
memberIDs := make([]*userpb.UserId, 0, len(members))
for _, member := range members {
userid, err := m.ldapEntryToUserID(member)
if err != nil {
log.Warn().Err(err).Interface("member", member).Msg("Failed convert member entry to userid")
continue
}
memberIDs = append(memberIDs, userid)
}
g.Members = memberIDs
return g, nil
}
// GetGroupByClaim implements the group.Manager interface. Looks up a group by
// claim ('group_name', 'group_id', 'display_name') and returns the group.
func (m *manager) GetGroupByClaim(ctx context.Context, claim, value string, skipFetchingMembers bool) (*grouppb.Group, error) {
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "GetGroupByClaim")
defer span.End()
span.SetAttributes(
attribute.String("claim", claim),
attribute.String("value", value),
attribute.Bool("skip_fetching_members", skipFetchingMembers),
)
log := appctx.GetLogger(ctx)
groupEntry, err := m.c.LDAPIdentity.GetLDAPGroupByAttribute(ctx, m.ldapClient, claim, value)
if err != nil {
log.Debug().Err(err).Msg("GetGroupByClaim")
return nil, err
}
log.Debug().Interface("entry", groupEntry).Msg("entries")
g, err := m.ldapEntryToGroup(groupEntry)
if err != nil {
return nil, err
}
if skipFetchingMembers {
return g, nil
}
members, err := m.c.LDAPIdentity.GetLDAPGroupMembers(ctx, m.ldapClient, groupEntry)
if err != nil {
return nil, err
}
memberIDs := make([]*userpb.UserId, 0, len(members))
for _, member := range members {
userid, err := m.ldapEntryToUserID(member)
if err != nil {
log.Warn().Err(err).Interface("member", member).Msg("Failed convert member entry to userid")
continue
}
memberIDs = append(memberIDs, userid)
}
g.Members = memberIDs
return g, nil
}
// FindGroups implements the group.Manager interface. Searches for groups using
// a prefix-substring search on the group attributes ('group_name',
// 'display_name', 'group_id') and returns the groups. FindGroups does NOT expand the
// members of the Groups.
func (m *manager) FindGroups(ctx context.Context, query string, skipFetchingMembers bool) ([]*grouppb.Group, error) {
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "FindGroups")
defer span.End()
span.SetAttributes(
attribute.String("query", query),
attribute.Bool("skip_fetching_members", skipFetchingMembers),
)
entries, err := m.c.LDAPIdentity.GetLDAPGroups(ctx, m.ldapClient, query)
if err != nil {
return nil, err
}
groups := make([]*grouppb.Group, 0, len(entries))
for _, entry := range entries {
g, err := m.ldapEntryToGroup(entry)
if err != nil {
return nil, err
}
groups = append(groups, g)
}
return groups, nil
}
// GetMembers implements the group.Manager interface. It returns all the userids of the members
// of the group identified by the supplied id.
func (m *manager) GetMembers(ctx context.Context, gid *grouppb.GroupId) ([]*userpb.UserId, error) {
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "GetMembers")
defer span.End()
span.SetAttributes(
attribute.Stringer("group_id", gid),
)
log := appctx.GetLogger(ctx)
if gid.Idp != "" && gid.Idp != m.c.Idp {
return nil, errtypes.NotFound("idp mismatch")
}
groupEntry, err := m.c.LDAPIdentity.GetLDAPGroupByID(ctx, m.ldapClient, gid.OpaqueId)
if err != nil {
return nil, err
}
log.Debug().Interface("entry", groupEntry).Msg("entries")
members, err := m.c.LDAPIdentity.GetLDAPGroupMembers(ctx, m.ldapClient, groupEntry)
if err != nil {
return nil, err
}
memberIDs := make([]*userpb.UserId, 0, len(members))
for _, member := range members {
userid, err := m.ldapEntryToUserID(member)
if err != nil {
log.Warn().Err(err).Interface("member", member).Msg("Failed convert member entry to userid")
continue
}
memberIDs = append(memberIDs, userid)
}
return memberIDs, nil
}
// HasMember implements the group.Member interface. Checks whether the supplied userid is a member
// of the supplied groupid.
func (m *manager) HasMember(ctx context.Context, gid *grouppb.GroupId, uid *userpb.UserId) (bool, error) {
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "HasMember")
defer span.End()
span.SetAttributes(
attribute.Stringer("group_id", gid),
attribute.Stringer("user_id", uid),
)
// It might be possible to do a somewhat more clever LDAP search here. (First lookup the user and then
// search for (&(objectclass=<groupoc>)(<groupid>=gid)(member=<username/userdn>)
// The GetMembers call used below can be quiet ineffecient for large groups
members, err := m.GetMembers(ctx, gid)
if err != nil {
return false, err
}
for _, u := range members {
if u.OpaqueId == uid.OpaqueId && u.Idp == uid.Idp {
return true, nil
}
}
return false, nil
}
func (m *manager) ldapEntryToGroup(entry *ldap.Entry) (*grouppb.Group, error) {
id, err := m.ldapEntryToGroupID(entry)
if err != nil {
return nil, err
}
gidNumber := m.c.Nobody
gidValue := entry.GetEqualFoldAttributeValue(m.c.LDAPIdentity.Group.Schema.GIDNumber)
if gidValue != "" {
gidNumber, err = strconv.ParseInt(gidValue, 10, 64)
if err != nil {
return nil, err
}
}
g := &grouppb.Group{
Id: id,
GroupName: entry.GetEqualFoldAttributeValue(m.c.LDAPIdentity.Group.Schema.Groupname),
Mail: entry.GetEqualFoldAttributeValue(m.c.LDAPIdentity.Group.Schema.Mail),
DisplayName: entry.GetEqualFoldAttributeValue(m.c.LDAPIdentity.Group.Schema.DisplayName),
GidNumber: gidNumber,
}
return g, nil
}
func (m *manager) ldapEntryToGroupID(entry *ldap.Entry) (*grouppb.GroupId, error) {
var id string
if m.c.LDAPIdentity.Group.Schema.IDIsOctetString {
attribute := m.c.LDAPIdentity.Group.Schema.ID
rawValue := entry.GetEqualFoldRawAttributeValue(attribute)
if strings.EqualFold(attribute, "objectguid") {
rawValue = ldapIdentity.SwapObjectGUIDBytes(rawValue)
}
if value, err := uuid.FromBytes(rawValue); err == nil {
id = value.String()
} else {
return nil, err
}
} else {
id = entry.GetEqualFoldAttributeValue(m.c.LDAPIdentity.Group.Schema.ID)
}
return &grouppb.GroupId{
Idp: m.c.Idp,
OpaqueId: id,
}, nil
}
func (m *manager) ldapEntryToUserID(entry *ldap.Entry) (*userpb.UserId, error) {
var uid string
if m.c.LDAPIdentity.User.Schema.IDIsOctetString {
attribute := m.c.LDAPIdentity.User.Schema.ID
rawValue := entry.GetEqualFoldRawAttributeValue(attribute)
if strings.EqualFold(attribute, "objectguid") {
rawValue = ldapIdentity.SwapObjectGUIDBytes(rawValue)
}
if value, err := uuid.FromBytes(rawValue); err == nil {
uid = value.String()
} else {
return nil, err
}
} else {
uid = entry.GetEqualFoldAttributeValue(m.c.LDAPIdentity.User.Schema.ID)
}
return &userpb.UserId{
Idp: m.c.Idp,
OpaqueId: uid,
Type: userpb.UserType_USER_TYPE_PRIMARY,
}, nil
}
@@ -0,0 +1,27 @@
// Copyright 2018-2020 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package loader
import (
// Load core group manager drivers.
_ "github.com/opencloud-eu/reva/v2/pkg/group/manager/json"
_ "github.com/opencloud-eu/reva/v2/pkg/group/manager/ldap"
_ "github.com/opencloud-eu/reva/v2/pkg/group/manager/null"
// Add your own here
)
@@ -0,0 +1,62 @@
// Copyright 2018-2020 CERN
// Copyright 2025 OpenCloud GmbH
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package null
import (
"context"
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/group"
"github.com/opencloud-eu/reva/v2/pkg/group/manager/registry"
)
func init() {
registry.Register("null", New)
}
type manager struct {
}
// New returns a group manager implementation that return NOT FOUND or empty result set for every call
func New(m map[string]interface{}) (group.Manager, error) {
return &manager{}, nil
}
func (m *manager) GetGroup(ctx context.Context, gid *grouppb.GroupId, skipFetchingMembers bool) (*grouppb.Group, error) {
return nil, errtypes.NotFound(gid.OpaqueId)
}
func (m *manager) GetGroupByClaim(ctx context.Context, claim, value string, skipFetchingMembers bool) (*grouppb.Group, error) {
return nil, errtypes.NotFound(value)
}
func (m *manager) FindGroups(ctx context.Context, query string, skipFetchingMembers bool) ([]*grouppb.Group, error) {
return []*grouppb.Group{}, nil
}
func (m *manager) GetMembers(ctx context.Context, gid *grouppb.GroupId) ([]*userpb.UserId, error) {
return nil, errtypes.NotFound(gid.OpaqueId)
}
func (m *manager) HasMember(ctx context.Context, gid *grouppb.GroupId, uid *userpb.UserId) (bool, error) {
return false, errtypes.NotFound(gid.OpaqueId)
}
@@ -0,0 +1,34 @@
// Copyright 2018-2020 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package registry
import "github.com/opencloud-eu/reva/v2/pkg/group"
// NewFunc is the function that group managers
// should register at init time.
type NewFunc func(map[string]interface{}) (group.Manager, error)
// NewFuncs is a map containing all the registered group managers.
var NewFuncs = map[string]NewFunc{}
// Register registers a new group manager new function.
// Not safe for concurrent use. Safe for use from package init.
func Register(name string, f NewFunc) {
NewFuncs[name] = f
}
+165
View File
@@ -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 logger
import (
"fmt"
"io"
"os"
"time"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"github.com/rs/zerolog"
)
func init() {
zerolog.CallerSkipFrameCount = 2
zerolog.TimeFieldFormat = time.RFC3339Nano
}
// Mode changes the logging format.
type Mode string
const (
// JSONMode outputs JSON.
JSONMode Mode = "json"
// ConsoleMode outputs human-readable logs.
ConsoleMode Mode = "console"
)
// Option is the option to use to configure the logger.
type Option func(l *zerolog.Logger)
// New creates a new logger.
func New(opts ...Option) *zerolog.Logger {
// create a default logger
zerolog.SetGlobalLevel(zerolog.TraceLevel)
zl := zerolog.New(os.Stderr).With().Timestamp().Caller().Logger()
for _, opt := range opts {
opt(&zl)
}
return &zl
}
// WithLevel is an option to configure the logging level.
func WithLevel(lvl string) Option {
return func(l *zerolog.Logger) {
zlvl := parseLevel(lvl)
*l = l.Level(zlvl)
}
}
// WithWriter is an option to configure the logging output.
func WithWriter(w io.Writer, m Mode) Option {
return func(l *zerolog.Logger) {
if m == ConsoleMode {
*l = l.Output(zerolog.ConsoleWriter{Out: w, TimeFormat: "2006-01-02 15:04:05.999"})
} else {
*l = l.Output(w)
}
}
}
func parseLevel(v string) zerolog.Level {
if v == "" {
return zerolog.InfoLevel
}
lvl, err := zerolog.ParseLevel(v)
if err != nil {
return zerolog.InfoLevel
}
return lvl
}
func InitLoggerOrDie(v interface{}, logLevel string) *zerolog.Logger {
conf := ParseLogConfOrDie(v, logLevel)
log, err := fromConfig(conf)
if err != nil {
fmt.Fprintf(os.Stderr, "error creating logger, exiting ...")
os.Exit(1)
}
return log
}
func ParseLogConfOrDie(v interface{}, logLevel string) *LogConf {
c := &LogConf{}
if err := mapstructure.Decode(v, c); err != nil {
fmt.Fprintf(os.Stderr, "error decoding log config: %s\n", err.Error())
os.Exit(1)
}
// if mode is not set, we use console mode, easier for devs
if c.Mode == "" {
c.Mode = "console"
}
// Give priority to the log level passed through the command line.
if logLevel != "" {
c.Level = logLevel
}
return c
}
type LogConf struct {
Output string `mapstructure:"output"`
Mode string `mapstructure:"mode"`
Level string `mapstructure:"level"`
}
func fromConfig(conf *LogConf) (*zerolog.Logger, error) {
if conf.Level == "" {
conf.Level = zerolog.InfoLevel.String()
}
var opts []Option
opts = append(opts, WithLevel(conf.Level))
w, err := getWriter(conf.Output)
if err != nil {
return nil, err
}
opts = append(opts, WithWriter(w, Mode(conf.Mode)))
l := New(opts...)
sub := l.With().Int("pid", os.Getpid()).Logger()
return &sub, nil
}
func getWriter(out string) (io.Writer, error) {
if out == "stderr" || out == "" {
return os.Stderr, nil
}
if out == "stdout" {
return os.Stdout, nil
}
fd, err := os.OpenFile(out, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
err = errors.Wrap(err, "error creating log file: "+out)
return nil, err
}
return fd, nil
}
@@ -0,0 +1,100 @@
// Copyright 2018-2020 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package accservice
import (
"encoding/json"
"fmt"
"net/url"
"path"
"strings"
"github.com/pkg/errors"
"github.com/opencloud-eu/reva/v2/pkg/mentix/config"
"github.com/opencloud-eu/reva/v2/pkg/mentix/utils/network"
)
// RequestResponse holds the response of an accounts service query.
type RequestResponse struct {
Success bool
Error string
Data interface{}
}
type accountsServiceSettings struct {
URL *url.URL
User string
Password string
}
var settings accountsServiceSettings
// Query performs an account service query.
func Query(endpoint string, params network.URLParams) (*RequestResponse, error) {
fullURL, err := network.GenerateURL(fmt.Sprintf("%v://%v", settings.URL.Scheme, settings.URL.Host), path.Join(settings.URL.Path, endpoint), params)
if err != nil {
return nil, errors.Wrap(err, "error while building the service accounts query URL")
}
data, err := network.ReadEndpoint(fullURL, &network.BasicAuth{User: settings.User, Password: settings.Password}, false)
if err != nil {
return nil, errors.Wrap(err, "unable to query the service accounts endpoint")
}
resp := &RequestResponse{}
if err := json.Unmarshal(data, resp); err != nil {
return nil, errors.Wrap(err, "unable to unmarshal response data")
}
return resp, nil
}
// GetResponseValue gets a value from an account service query using a dotted path notation.
func GetResponseValue(resp *RequestResponse, path string) interface{} {
if data, ok := resp.Data.(map[string]interface{}); ok {
tokens := strings.Split(path, ".")
for i, name := range tokens {
if i == len(tokens)-1 {
if value, ok := data[name]; ok {
return value
}
}
if data, ok = data[name].(map[string]interface{}); !ok {
break
}
}
}
return nil
}
// InitAccountsService initializes the global accounts service.
func InitAccountsService(conf *config.Configuration) error {
URL, err := url.Parse(conf.AccountsService.URL)
if err != nil {
return errors.Wrap(err, "unable to parse the accounts service URL")
}
settings.URL = URL
settings.User = conf.AccountsService.User
settings.Password = conf.AccountsService.Password
return nil
}
@@ -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 config
// Configuration holds the general Mentix configuration.
type Configuration struct {
Prefix string `mapstructure:"prefix"`
Connectors struct {
GOCDB struct {
Address string `mapstructure:"address"`
Scope string `mapstructure:"scope"`
APIKey string `mapstructure:"apikey"`
} `mapstructure:"gocdb"`
} `mapstructure:"connectors"`
UpdateInterval string `mapstructure:"update_interval"`
Services struct {
CriticalTypes []string `mapstructure:"critical_types"`
} `mapstructure:"services"`
Exporters struct {
WebAPI struct {
Endpoint string `mapstructure:"endpoint"`
EnabledConnectors []string `mapstructure:"enabled_connectors"`
IsProtected bool `mapstructure:"is_protected"`
} `mapstructure:"webapi"`
CS3API struct {
Endpoint string `mapstructure:"endpoint"`
EnabledConnectors []string `mapstructure:"enabled_connectors"`
IsProtected bool `mapstructure:"is_protected"`
ElevatedServiceTypes []string `mapstructure:"elevated_service_types"`
} `mapstructure:"cs3api"`
SiteLocations struct {
Endpoint string `mapstructure:"endpoint"`
EnabledConnectors []string `mapstructure:"enabled_connectors"`
IsProtected bool `mapstructure:"is_protected"`
} `mapstructure:"siteloc"`
PrometheusSD struct {
OutputPath string `mapstructure:"output_path"`
EnabledConnectors []string `mapstructure:"enabled_connectors"`
} `mapstructure:"promsd"`
Metrics struct {
EnabledConnectors []string `mapstructure:"enabled_connectors"`
} `mapstructure:"metrics"`
} `mapstructure:"exporters"`
AccountsService struct {
URL string `mapstructure:"url"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
} `mapstructure:"accounts"`
// Internal settings
EnabledConnectors []string `mapstructure:"-"`
EnabledImporters []string `mapstructure:"-"`
EnabledExporters []string `mapstructure:"-"`
}
// Init sets sane defaults.
func (c *Configuration) Init() {
if c.Prefix == "" {
c.Prefix = "mentix"
}
// TODO(daniel): add default that works out of the box
}
+37
View File
@@ -0,0 +1,37 @@
// 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
const (
// ConnectorIDGOCDB is the connector identifier for GOCDB.
ConnectorIDGOCDB = "gocdb"
)
const (
// ExporterIDWebAPI is the identifier for the WebAPI exporter.
ExporterIDWebAPI = "webapi"
// ExporterIDCS3API is the identifier for the CS3API exporter.
ExporterIDCS3API = "cs3api"
// ExporterIDSiteLocations is the identifier for the Site Locations exporter.
ExporterIDSiteLocations = "siteloc"
// ExporterIDPrometheusSD is the identifier for the PrometheusSD exporter.
ExporterIDPrometheusSD = "promsd"
// ExporterIDMetrics is the identifier for the Metrics exporter.
ExporterIDMetrics = "metrics"
)
@@ -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 connectors
import (
"fmt"
"github.com/rs/zerolog"
"github.com/opencloud-eu/reva/v2/pkg/mentix/config"
"github.com/opencloud-eu/reva/v2/pkg/mentix/entity"
"github.com/opencloud-eu/reva/v2/pkg/mentix/meshdata"
)
// Connector is the interface that all connectors must implement.
type Connector interface {
entity.Entity
// RetrieveMeshData fetches new mesh data.
RetrieveMeshData() (*meshdata.MeshData, error)
// UpdateMeshData updates the provided mesh data on the target side. The provided data only contains the data that
// should be updated, not the entire data set.
UpdateMeshData(data *meshdata.MeshData) error
}
// BaseConnector implements basic connector functionality common to all connectors.
type BaseConnector struct {
conf *config.Configuration
log *zerolog.Logger
}
// Activate activates the connector.
func (connector *BaseConnector) Activate(conf *config.Configuration, log *zerolog.Logger) error {
if conf == nil {
return fmt.Errorf("no configuration provided")
}
connector.conf = conf
if log == nil {
return fmt.Errorf("no logger provided")
}
connector.log = log
return nil
}
// UpdateMeshData updates the provided mesh data on the target side. The provided data only contains the data that
// should be updated, not the entire data set.
func (connector *BaseConnector) UpdateMeshData(data *meshdata.MeshData) error {
return fmt.Errorf("the connector doesn't support updating of mesh data")
}
@@ -0,0 +1,68 @@
// 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 connectors
import (
"github.com/rs/zerolog"
"github.com/opencloud-eu/reva/v2/pkg/mentix/config"
"github.com/opencloud-eu/reva/v2/pkg/mentix/entity"
)
var (
registeredConnectors = entity.NewRegistry()
)
// Collection represents a collection of connectors.
type Collection struct {
Connectors []Connector
}
// Entities gets the entities in this collection.
func (collection *Collection) Entities() []entity.Entity {
entities := make([]entity.Entity, 0, len(collection.Connectors))
for _, connector := range collection.Connectors {
entities = append(entities, connector)
}
return entities
}
// ActivateAll activates all entities in the collection.
func (collection *Collection) ActivateAll(conf *config.Configuration, log *zerolog.Logger) error {
return entity.ActivateEntities(collection, conf, log)
}
// AvailableConnectors returns a collection of all connectors that are enabled in the configuration.
func AvailableConnectors(conf *config.Configuration) (*Collection, error) {
entities, err := registeredConnectors.FindEntities(conf.EnabledConnectors, true, true)
if err != nil {
return nil, err
}
connectors := make([]Connector, 0, len(entities))
for _, entry := range entities {
connectors = append(connectors, entry.(Connector))
}
return &Collection{Connectors: connectors}, nil
}
func registerConnector(connector Connector) {
registeredConnectors.Register(connector)
}
@@ -0,0 +1,317 @@
// 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 connectors
import (
"encoding/xml"
"fmt"
"net/url"
"path"
"strings"
"time"
"github.com/opencloud-eu/reva/v2/pkg/mentix/utils"
"github.com/rs/zerolog"
"github.com/opencloud-eu/reva/v2/pkg/mentix/config"
"github.com/opencloud-eu/reva/v2/pkg/mentix/connectors/gocdb"
"github.com/opencloud-eu/reva/v2/pkg/mentix/meshdata"
"github.com/opencloud-eu/reva/v2/pkg/mentix/utils/network"
)
// GOCDBConnector is used to read mesh data from a GOCDB instance.
type GOCDBConnector struct {
BaseConnector
gocdbAddress string
}
// Activate activates the connector.
func (connector *GOCDBConnector) Activate(conf *config.Configuration, log *zerolog.Logger) error {
if err := connector.BaseConnector.Activate(conf, log); err != nil {
return err
}
// Check and store GOCDB specific settings
connector.gocdbAddress = conf.Connectors.GOCDB.Address
if len(connector.gocdbAddress) == 0 {
return fmt.Errorf("no GOCDB address configured")
}
return nil
}
// RetrieveMeshData fetches new mesh data.
func (connector *GOCDBConnector) RetrieveMeshData() (*meshdata.MeshData, error) {
meshData := new(meshdata.MeshData)
// Query all data from GOCDB
if err := connector.queryServiceTypes(meshData); err != nil {
return nil, fmt.Errorf("could not query service types: %v", err)
}
if err := connector.querySites(meshData); err != nil {
return nil, fmt.Errorf("could not query sites: %v", err)
}
for _, site := range meshData.Sites {
// Get services associated with the current site
if err := connector.queryServices(meshData, site); err != nil {
return nil, fmt.Errorf("could not query services of site '%v': %v", site.Name, err)
}
// Get downtimes scheduled for the current site
if err := connector.queryDowntimes(meshData, site); err != nil {
return nil, fmt.Errorf("could not query downtimes of site '%v': %v", site.Name, err)
}
}
meshData.InferMissingData()
return meshData, nil
}
func (connector *GOCDBConnector) query(v interface{}, method string, isPrivate bool, hasScope bool, params network.URLParams) error {
var scope string
if hasScope {
scope = connector.conf.Connectors.GOCDB.Scope
}
// Get the data from GOCDB
data, err := gocdb.QueryGOCDB(connector.gocdbAddress, method, isPrivate, scope, connector.conf.Connectors.GOCDB.APIKey, params)
if err != nil {
return err
}
// Unmarshal it
if err := xml.Unmarshal(data, v); err != nil {
return fmt.Errorf("unable to unmarshal data: %v", err)
}
return nil
}
func (connector *GOCDBConnector) queryServiceTypes(meshData *meshdata.MeshData) error {
var serviceTypes gocdb.ServiceTypes
if err := connector.query(&serviceTypes, "get_service_types", false, false, network.URLParams{}); err != nil {
return err
}
// Copy retrieved data into the mesh data
meshData.ServiceTypes = nil
for _, serviceType := range serviceTypes.Types {
meshData.ServiceTypes = append(meshData.ServiceTypes, &meshdata.ServiceType{
Name: serviceType.Name,
Description: serviceType.Description,
})
}
return nil
}
func (connector *GOCDBConnector) querySites(meshData *meshdata.MeshData) error {
var sites gocdb.Sites
if err := connector.query(&sites, "get_site", false, true, network.URLParams{}); err != nil {
return err
}
// Copy retrieved data into the mesh data
meshData.Sites = nil
for _, site := range sites.Sites {
properties := connector.extensionsToMap(&site.Extensions)
// The site ID can be set through a property; by default, the site short name will be used
siteID := meshdata.GetPropertyValue(properties, meshdata.PropertySiteID, site.ShortName)
// See if an organization has been defined using properties; otherwise, use the official name
organization := meshdata.GetPropertyValue(properties, meshdata.PropertyOrganization, site.OfficialName)
meshsite := &meshdata.Site{
ID: siteID,
Name: site.ShortName,
FullName: site.OfficialName,
Organization: organization,
Domain: site.Domain,
Homepage: site.Homepage,
Email: site.Email,
Description: site.Description,
Country: site.Country,
CountryCode: site.CountryCode,
Longitude: site.Longitude,
Latitude: site.Latitude,
Services: nil,
Properties: properties,
Downtimes: meshdata.Downtimes{},
}
meshData.Sites = append(meshData.Sites, meshsite)
}
return nil
}
func (connector *GOCDBConnector) queryServices(meshData *meshdata.MeshData, site *meshdata.Site) error {
var services gocdb.Services
if err := connector.query(&services, "get_service", false, true, network.URLParams{"sitename": site.Name}); err != nil {
return err
}
getServiceURLString := func(service *gocdb.Service, endpoint *gocdb.ServiceEndpoint, host string) string {
urlstr := "https://" + host // Fall back to the provided hostname
if svcURL, err := connector.getServiceURL(service, endpoint); err == nil {
urlstr = svcURL.String()
}
return urlstr
}
// Copy retrieved data into the mesh data
site.Services = nil
for _, service := range services.Services {
host := service.Host
// If a URL is provided, extract the port from it and append it to the host
if len(service.URL) > 0 {
if hostURL, err := url.Parse(service.URL); err == nil {
if port := hostURL.Port(); len(port) > 0 {
host += ":" + port
}
}
}
// Assemble additional endpoints
var endpoints []*meshdata.ServiceEndpoint
for _, endpoint := range service.Endpoints.Endpoints {
endpoints = append(endpoints, &meshdata.ServiceEndpoint{
Type: connector.findServiceType(meshData, endpoint.Type),
Name: endpoint.Name,
RawURL: endpoint.URL,
URL: getServiceURLString(service, endpoint, host),
IsMonitored: strings.EqualFold(endpoint.IsMonitored, "Y"),
Properties: connector.extensionsToMap(&endpoint.Extensions),
})
}
// Add the service to the site
site.Services = append(site.Services, &meshdata.Service{
ServiceEndpoint: &meshdata.ServiceEndpoint{
Type: connector.findServiceType(meshData, service.Type),
Name: service.Type,
RawURL: service.URL,
URL: getServiceURLString(service, nil, host),
IsMonitored: strings.EqualFold(service.IsMonitored, "Y"),
Properties: connector.extensionsToMap(&service.Extensions),
},
Host: host,
AdditionalEndpoints: endpoints,
})
}
return nil
}
func (connector *GOCDBConnector) queryDowntimes(meshData *meshdata.MeshData, site *meshdata.Site) error {
var downtimes gocdb.Downtimes
if err := connector.query(&downtimes, "get_downtime_nested_services", false, true, network.URLParams{"topentity": site.Name, "ongoing_only": "yes"}); err != nil {
return err
}
// Copy retrieved data into the mesh data
site.Downtimes.Clear()
for _, dt := range downtimes.Downtimes {
if !strings.EqualFold(dt.Severity, "outage") { // Only take real outages into account
continue
}
services := make([]string, 0, len(dt.AffectedServices.Services))
for _, service := range dt.AffectedServices.Services {
// Only add critical services to the list of affected services
if utils.FindInStringArray(service.Type, connector.conf.Services.CriticalTypes, false) != -1 {
services = append(services, service.Type)
}
}
_, _ = site.Downtimes.ScheduleDowntime(time.Unix(dt.StartDate, 0), time.Unix(dt.EndDate, 0), services)
}
return nil
}
func (connector *GOCDBConnector) findServiceType(meshData *meshdata.MeshData, name string) *meshdata.ServiceType {
for _, serviceType := range meshData.ServiceTypes {
if strings.EqualFold(serviceType.Name, name) {
return serviceType
}
}
// If the service type doesn't exist, create a default one
return &meshdata.ServiceType{Name: name, Description: ""}
}
func (connector *GOCDBConnector) extensionsToMap(extensions *gocdb.Extensions) map[string]string {
properties := make(map[string]string)
for _, ext := range extensions.Extensions {
properties[ext.Key] = ext.Value
}
return properties
}
func (connector *GOCDBConnector) getServiceURL(service *gocdb.Service, endpoint *gocdb.ServiceEndpoint) (*url.URL, error) {
urlstr := service.URL
if len(urlstr) == 0 {
// The URL defaults to the hostname using the HTTPS protocol
urlstr = "https://" + service.Host
}
svcURL, err := url.ParseRequestURI(urlstr)
if err != nil {
return nil, fmt.Errorf("unable to parse URL '%v': %v", urlstr, err)
}
// If an endpoint was provided, use its path
if endpoint != nil {
// If the endpoint URL is an absolute one, just use that; otherwise, make an absolute one out of it
if endpointURL, err := url.ParseRequestURI(endpoint.URL); err == nil && len(endpointURL.Scheme) > 0 {
svcURL = endpointURL
} else {
// Replace entire URL path if the relative path starts with a slash; otherwise, just append
if strings.HasPrefix(endpoint.URL, "/") {
svcURL.Path = endpoint.URL
} else {
svcURL.Path = path.Join(svcURL.Path, endpoint.URL)
if strings.HasSuffix(endpoint.URL, "/") { // Restore trailing slash if necessary
svcURL.Path += "/"
}
}
}
}
return svcURL, nil
}
// GetID returns the ID of the connector.
func (connector *GOCDBConnector) GetID() string {
return config.ConnectorIDGOCDB
}
// GetName returns the display name of the connector.
func (connector *GOCDBConnector) GetName() string {
return "GOCDB"
}
func init() {
registerConnector(&GOCDBConnector{})
}
@@ -0,0 +1,61 @@
// 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 gocdb
import (
"fmt"
"github.com/opencloud-eu/reva/v2/pkg/mentix/utils/network"
)
// QueryGOCDB retrieves data from one of GOCDB's endpoints.
func QueryGOCDB(address string, method string, isPrivate bool, scope string, apiKey string, params network.URLParams) ([]byte, error) {
// The method must always be specified
params["method"] = method
// If a scope or an API key were specified, pass them to the endpoint as well
if len(scope) > 0 {
params["scope"] = scope
}
if len(apiKey) > 0 {
params["apikey"] = apiKey
}
// GOCDB's public API is located at <gocdb-host>/gocdbpi/public, the private one at <gocdb-host>/gocdbpi/private
var path string
if isPrivate {
path = "/gocdbpi/private"
} else {
path = "/gocdbpi/public"
}
// Query the data from GOCDB
endpointURL, err := network.GenerateURL(address, path, params)
if err != nil {
return nil, fmt.Errorf("unable to generate the GOCDB URL: %v", err)
}
data, err := network.ReadEndpoint(endpointURL, nil, true)
if err != nil {
return nil, fmt.Errorf("unable to read GOCDB endpoint: %v", err)
}
return data, nil
}
@@ -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 gocdb
// Extension represents Key-Value pairs in GOCDB.
type Extension struct {
Key string `xml:"KEY"`
Value string `xml:"VALUE"`
}
// Extensions is a list of Extension objects.
type Extensions struct {
Extensions []*Extension `xml:"EXTENSION"`
}
// ServiceType represents a service type in GOCDB.
type ServiceType struct {
Name string `xml:"SERVICE_TYPE_NAME"`
Description string `xml:"SERVICE_TYPE_DESC"`
}
// ServiceTypes is a list of ServiceType objects.
type ServiceTypes struct {
Types []*ServiceType `xml:"SERVICE_TYPE"`
}
// Site represents a site in GOCDB.
type Site struct {
ShortName string `xml:"SHORT_NAME"`
OfficialName string `xml:"OFFICIAL_NAME"`
Description string `xml:"SITE_DESCRIPTION"`
Homepage string `xml:"HOME_URL"`
Email string `xml:"CONTACT_EMAIL"`
Domain string `xml:"DOMAIN>DOMAIN_NAME"`
Country string `xml:"COUNTRY"`
CountryCode string `xml:"COUNTRY_CODE"`
Latitude float32 `xml:"LATITUDE"`
Longitude float32 `xml:"LONGITUDE"`
Extensions Extensions `xml:"EXTENSIONS"`
}
// Sites is a list of Site objects.
type Sites struct {
Sites []*Site `xml:"SITE"`
}
// ServiceEndpoint represents an additional service endpoint of a service in GOCDB.
type ServiceEndpoint struct {
Name string `xml:"NAME"`
URL string `xml:"URL"`
Type string `xml:"INTERFACENAME"`
IsMonitored string `xml:"ENDPOINT_MONITORED"`
Extensions Extensions `xml:"EXTENSIONS"`
}
// ServiceEndpoints is a list of ServiceEndpoint objects.
type ServiceEndpoints struct {
Endpoints []*ServiceEndpoint `xml:"ENDPOINT"`
}
// Service represents a service in GOCDB.
type Service struct {
Host string `xml:"HOSTNAME"`
Type string `xml:"SERVICE_TYPE"`
IsMonitored string `xml:"NODE_MONITORED"`
URL string `xml:"URL"`
Endpoints ServiceEndpoints `xml:"ENDPOINTS"`
Extensions Extensions `xml:"EXTENSIONS"`
}
// Services is a list of Service objects.
type Services struct {
Services []*Service `xml:"SERVICE_ENDPOINT"`
}
// DowntimeService represents a service scheduled for downtime.
type DowntimeService struct {
Type string `xml:"SERVICE_TYPE"`
}
// DowntimeServices represents a list of DowntimeService objects.
type DowntimeServices struct {
Services []*DowntimeService `xml:"SERVICE"`
}
// Downtime is a scheduled downtime for a site.
type Downtime struct {
Severity string `xml:"SEVERITY"`
StartDate int64 `xml:"START_DATE"`
EndDate int64 `xml:"END_DATE"`
AffectedServices DowntimeServices `xml:"SERVICES"`
}
// Downtimes represents a list of Downtime objects.
type Downtimes struct {
Downtimes []*Downtime `xml:"DOWNTIME"`
}

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