Initial QSfera import
This commit is contained in:
+45
@@ -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)
|
||||
}
|
||||
+76
@@ -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
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package loader
|
||||
|
||||
import (
|
||||
// Load core 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
|
||||
)
|
||||
Generated
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package registry
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/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
|
||||
}
|
||||
+492
@@ -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"
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package loader
|
||||
|
||||
import (
|
||||
// Load core 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
|
||||
)
|
||||
+40
@@ -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
|
||||
}
|
||||
+265
@@ -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, ®istrypb.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 := ®istrypb.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)
|
||||
}
|
||||
+50
@@ -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, "␞")
|
||||
}
|
||||
Generated
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package registry
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/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
|
||||
}
|
||||
+377
@@ -0,0 +1,377 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package 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, ®istrypb.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
@@ -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)
|
||||
}
|
||||
+255
@@ -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
|
||||
}
|
||||
Generated
Vendored
+557
@@ -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
|
||||
}
|
||||
Generated
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package loader
|
||||
|
||||
import (
|
||||
// Load core 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
|
||||
)
|
||||
Generated
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package registry
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
Generated
Vendored
+99
@@ -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
|
||||
}
|
||||
+127
@@ -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",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+67
@@ -0,0 +1,67 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package 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
|
||||
}
|
||||
+146
@@ -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)
|
||||
}
|
||||
+221
@@ -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,
|
||||
)
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package 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
|
||||
)
|
||||
Generated
Vendored
+125
@@ -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
|
||||
}
|
||||
Generated
Vendored
+196
@@ -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"
|
||||
}
|
||||
+363
@@ -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
|
||||
}
|
||||
Generated
Vendored
+183
@@ -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")
|
||||
Generated
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/auth"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
Generated
Vendored
+90
@@ -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")
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package loader
|
||||
|
||||
import (
|
||||
// Load core storage broker drivers.
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/auth/registry/static"
|
||||
// Add your own here
|
||||
)
|
||||
Generated
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package registry
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/auth"
|
||||
|
||||
// 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
|
||||
}
|
||||
+91
@@ -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, ®istrypb.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 ®istrypb.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 ®{rules: c.Rules}, nil
|
||||
}
|
||||
+118
@@ -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
|
||||
}
|
||||
+91
@@ -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
|
||||
}
|
||||
+201
@@ -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
|
||||
}
|
||||
+204
@@ -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
|
||||
}
|
||||
+166
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 share’s 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"
|
||||
}
|
||||
+78
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
Generated
Vendored
+25
@@ -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
|
||||
)
|
||||
Generated
Vendored
+831
@@ -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
|
||||
}
|
||||
Generated
Vendored
+36
@@ -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
|
||||
}
|
||||
Generated
Vendored
+1266
File diff suppressed because it is too large
Load Diff
+150
@@ -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")
|
||||
Generated
Vendored
+6849
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+604
@@ -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;
|
||||
}
|
||||
Generated
Vendored
+445
@@ -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",
|
||||
}
|
||||
+1656
File diff suppressed because it is too large
Load Diff
+500
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
+186
@@ -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
|
||||
}
|
||||
+17
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
+226
@@ -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
@@ -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
|
||||
}
|
||||
+176
@@ -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
@@ -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
|
||||
}
|
||||
+26
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
+42
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
+172
@@ -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
|
||||
}
|
||||
+377
@@ -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
|
||||
}
|
||||
+27
@@ -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
|
||||
)
|
||||
+62
@@ -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)
|
||||
}
|
||||
Generated
Vendored
+34
@@ -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
@@ -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
|
||||
}
|
||||
Generated
Vendored
+100
@@ -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
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package 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
@@ -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"
|
||||
)
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package 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")
|
||||
}
|
||||
Generated
Vendored
+68
@@ -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)
|
||||
}
|
||||
+317
@@ -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{})
|
||||
}
|
||||
Generated
Vendored
+61
@@ -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
|
||||
}
|
||||
Generated
Vendored
+114
@@ -0,0 +1,114 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package 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
Reference in New Issue
Block a user