521 lines
16 KiB
Go
521 lines
16 KiB
Go
// Copyright 2018-2021 CERN
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
// In applying this license, CERN does not waive the privileges and immunities
|
|
// granted to it by virtue of its status as an Intergovernmental Organization
|
|
// or submit itself to any jurisdiction.
|
|
|
|
package utils
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"math/rand"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/user"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
|
|
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
|
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
|
|
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
|
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
|
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
|
"github.com/golang/protobuf/proto"
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
)
|
|
|
|
var (
|
|
matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
|
|
matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
|
|
matchEmail = regexp.MustCompile(`^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$`)
|
|
|
|
// ShareStorageProviderID is the provider id used by the sharestorageprovider
|
|
ShareStorageProviderID = "a0ca6a90-a365-4782-871e-d44447bbc668"
|
|
// ShareStorageSpaceID is the space id used by the sharestorageprovider share jail space
|
|
ShareStorageSpaceID = "a0ca6a90-a365-4782-871e-d44447bbc668"
|
|
|
|
// PublicStorageProviderID is the storage id used by the sharestorageprovider
|
|
PublicStorageProviderID = "7993447f-687f-490d-875c-ac95e89a62a4"
|
|
// PublicStorageSpaceID is the space id used by the sharestorageprovider
|
|
PublicStorageSpaceID = "7993447f-687f-490d-875c-ac95e89a62a4"
|
|
|
|
// OCMStorageProviderID is the storage id used by the ocmreceived storageprovider
|
|
OCMStorageProviderID = "89f37a33-858b-45fa-8890-a1f2b27d90e1"
|
|
// OCMStorageSpaceID is the space id used by the ocmreceived storageprovider
|
|
OCMStorageSpaceID = "89f37a33-858b-45fa-8890-a1f2b27d90e1"
|
|
|
|
// SpaceGrant is used to signal the storageprovider that the grant is on a space
|
|
SpaceGrant struct{}
|
|
)
|
|
|
|
// Skip evaluates whether a source endpoint contains any of the prefixes.
|
|
// i.e: /a/b/c/d/e contains prefix /a/b/c
|
|
func Skip(source string, prefixes []string) bool {
|
|
for i := range prefixes {
|
|
if strings.HasPrefix(source, prefixes[i]) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetClientIP retrieves the client IP from incoming requests
|
|
func GetClientIP(r *http.Request) (string, error) {
|
|
var clientIP string
|
|
forwarded := r.Header.Get("X-FORWARDED-FOR")
|
|
|
|
if forwarded != "" {
|
|
clientIP = forwarded
|
|
} else {
|
|
if ip, _, err := net.SplitHostPort(r.RemoteAddr); err != nil {
|
|
ipObj := net.ParseIP(r.RemoteAddr)
|
|
if ipObj == nil {
|
|
return "", err
|
|
}
|
|
clientIP = ipObj.String()
|
|
} else {
|
|
clientIP = ip
|
|
}
|
|
}
|
|
return clientIP, nil
|
|
}
|
|
|
|
// ToSnakeCase converts a CamelCase string to a snake_case string.
|
|
func ToSnakeCase(str string) string {
|
|
snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}")
|
|
snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
|
|
return strings.ToLower(snake)
|
|
}
|
|
|
|
// ResolvePath converts relative local paths to absolute paths
|
|
func ResolvePath(path string) (string, error) {
|
|
usr, err := user.Current()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
homeDir := usr.HomeDir
|
|
|
|
if path == "~" {
|
|
path = homeDir
|
|
} else if strings.HasPrefix(path, "~/") {
|
|
path = filepath.Join(homeDir, path[2:])
|
|
}
|
|
|
|
return filepath.Abs(path)
|
|
}
|
|
|
|
// RandString is a helper to create tokens.
|
|
func RandString(n int) string {
|
|
rand.Seed(time.Now().UTC().UnixNano())
|
|
var l = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
|
b := make([]rune, n)
|
|
for i := range b {
|
|
b[i] = l[rand.Intn(len(l))]
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
// TSToUnixNano converts a protobuf Timestamp to uint64
|
|
// with nanoseconds resolution.
|
|
func TSToUnixNano(ts *types.Timestamp) uint64 {
|
|
if ts == nil {
|
|
return 0
|
|
}
|
|
return uint64(time.Unix(int64(ts.Seconds), int64(ts.Nanos)).UnixNano())
|
|
}
|
|
|
|
// TSToTime converts a protobuf Timestamp to Go's time.Time.
|
|
func TSToTime(ts *types.Timestamp) time.Time {
|
|
if ts == nil {
|
|
return time.Time{}
|
|
}
|
|
return time.Unix(int64(ts.Seconds), int64(ts.Nanos))
|
|
}
|
|
|
|
// TimeToTS converts Go's time.Time to a protobuf Timestamp.
|
|
func TimeToTS(t time.Time) *types.Timestamp {
|
|
return &types.Timestamp{
|
|
Seconds: uint64(t.Unix()), // implicitly returns UTC
|
|
Nanos: uint32(t.Nanosecond()),
|
|
}
|
|
}
|
|
|
|
// LaterTS returns the timestamp which occurs later.
|
|
func LaterTS(t1 *types.Timestamp, t2 *types.Timestamp) *types.Timestamp {
|
|
if TSToUnixNano(t1) > TSToUnixNano(t2) {
|
|
return t1
|
|
}
|
|
return t2
|
|
}
|
|
|
|
// TSNow returns the current UTC timestamp
|
|
func TSNow() *types.Timestamp {
|
|
t := time.Now().UTC()
|
|
return &types.Timestamp{
|
|
Seconds: uint64(t.Unix()),
|
|
Nanos: uint32(t.Nanosecond()),
|
|
}
|
|
}
|
|
|
|
// MTimeToTS converts a string in the form "<unix>.<nanoseconds>" into a CS3 Timestamp
|
|
func MTimeToTS(v string) (ts types.Timestamp, err error) {
|
|
p := strings.SplitN(v, ".", 2)
|
|
var sec, nsec uint64
|
|
if sec, err = strconv.ParseUint(p[0], 10, 64); err == nil {
|
|
if len(p) > 1 {
|
|
nsec, err = strconv.ParseUint(p[1], 10, 32)
|
|
}
|
|
}
|
|
return types.Timestamp{Seconds: sec, Nanos: uint32(nsec)}, err
|
|
}
|
|
|
|
// MTimeToTime converts a string in the form "<unix>.<nanoseconds>" into a go time.Time
|
|
func MTimeToTime(v string) (t time.Time, err error) {
|
|
p := strings.SplitN(v, ".", 2)
|
|
var sec, nsec int64
|
|
if sec, err = strconv.ParseInt(p[0], 10, 64); err == nil {
|
|
if len(p) > 1 {
|
|
nsec, err = strconv.ParseInt(p[1], 10, 64)
|
|
}
|
|
}
|
|
return time.Unix(sec, nsec), err
|
|
}
|
|
|
|
// TimeToOCMtime converts a Go time.Time to a string in the form "<unix>.<nanoseconds>"
|
|
func TimeToOCMtime(t time.Time) string {
|
|
return strconv.FormatInt(t.Unix(), 10) + "." + strconv.FormatInt(int64(t.Nanosecond()), 10)
|
|
}
|
|
|
|
// ExtractGranteeID returns the ID, user or group, set in the GranteeId object
|
|
func ExtractGranteeID(grantee *provider.Grantee) (*userpb.UserId, *grouppb.GroupId) {
|
|
switch t := grantee.Id.(type) {
|
|
case *provider.Grantee_UserId:
|
|
return t.UserId, nil
|
|
case *provider.Grantee_GroupId:
|
|
return nil, t.GroupId
|
|
default:
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
// UserEqual returns whether two users have the same field values.
|
|
func UserEqual(u, v *userpb.UserId) bool {
|
|
return u != nil && v != nil && u.Idp == v.Idp && u.OpaqueId == v.OpaqueId
|
|
}
|
|
|
|
// UserIDEqual returns whether two users have the same opaqueid values. The idp is ignored
|
|
func UserIDEqual(u, v *userpb.UserId) bool {
|
|
return u != nil && v != nil && u.OpaqueId == v.OpaqueId
|
|
}
|
|
|
|
// GroupEqual returns whether two groups have the same field values.
|
|
func GroupEqual(u, v *grouppb.GroupId) bool {
|
|
return u != nil && v != nil && u.Idp == v.Idp && u.OpaqueId == v.OpaqueId
|
|
}
|
|
|
|
// ResourceIDEqual returns whether two resources have the same field values.
|
|
func ResourceIDEqual(u, v *provider.ResourceId) bool {
|
|
return u != nil && v != nil && u.StorageId == v.StorageId && u.OpaqueId == v.OpaqueId && u.SpaceId == v.SpaceId
|
|
}
|
|
|
|
// ResourceEqual returns whether two resources have the same field values.
|
|
func ResourceEqual(u, v *provider.Reference) bool {
|
|
return u != nil && v != nil && u.Path == v.Path && ((u.ResourceId == nil && v.ResourceId == nil) || (ResourceIDEqual(u.ResourceId, v.ResourceId)))
|
|
}
|
|
|
|
// GranteeEqual returns whether two grantees have the same field values.
|
|
func GranteeEqual(u, v *provider.Grantee) bool {
|
|
if u == nil || v == nil {
|
|
return false
|
|
}
|
|
uu, ug := ExtractGranteeID(u)
|
|
vu, vg := ExtractGranteeID(v)
|
|
return u.Type == v.Type && (UserEqual(uu, vu) || GroupEqual(ug, vg))
|
|
}
|
|
|
|
// IsEmailValid checks whether the provided email has a valid format.
|
|
func IsEmailValid(e string) bool {
|
|
if len(e) < 3 || len(e) > 254 {
|
|
return false
|
|
}
|
|
return matchEmail.MatchString(e)
|
|
}
|
|
|
|
// IsValidWebAddress checks whether the provided address is a valid URL.
|
|
func IsValidWebAddress(address string) bool {
|
|
_, err := url.ParseRequestURI(address)
|
|
return err == nil
|
|
}
|
|
|
|
// IsValidPhoneNumber checks whether the provided phone number has a valid format.
|
|
func IsValidPhoneNumber(number string) bool {
|
|
re := regexp.MustCompile(`^(?:(?:\(?(?:00|\+)([1-4]\d\d|[1-9]\d?)\)?)?[\-\.\ \\\/]?)?((?:\(?\d{1,}\)?[\-\.\ \\\/]?){0,})(?:[\-\.\ \\\/]?(?:#|ext\.?|extension|x)[\-\.\ \\\/]?(\d+))?$`)
|
|
return re.MatchString(number)
|
|
}
|
|
|
|
// IsValidName cheks if the given name doesn't contain any non-alpha, space or dash characters.
|
|
func IsValidName(name string) bool {
|
|
re := regexp.MustCompile(`^[A-Za-z\s\-]*$`)
|
|
return re.MatchString(name)
|
|
}
|
|
|
|
// MarshalProtoV1ToJSON marshals a proto V1 message to a JSON byte array
|
|
// TODO: update this once we start using V2 in CS3APIs
|
|
func MarshalProtoV1ToJSON(m proto.Message) ([]byte, error) {
|
|
mV2 := proto.MessageV2(m)
|
|
return protojson.Marshal(mV2)
|
|
}
|
|
|
|
// UnmarshalJSONToProtoV1 decodes a JSON byte array to a specified proto message type
|
|
// TODO: update this once we start using V2 in CS3APIs
|
|
func UnmarshalJSONToProtoV1(b []byte, m proto.Message) error {
|
|
mV2 := proto.MessageV2(m)
|
|
if err := protojson.Unmarshal(b, mV2); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IsRelativeReference returns true if the given reference qualifies as relative
|
|
// when the resource id is set and the path starts with a .
|
|
//
|
|
// TODO(corby): Currently if the path begins with a dot, the ResourceId is set but has empty storageId and OpaqueId
|
|
// then the reference is still being viewed as relative. We need to check if we want that because in some
|
|
// places we might not want to set both StorageId and OpaqueId so we can't do a hard check if they are set.
|
|
func IsRelativeReference(ref *provider.Reference) bool {
|
|
return ref.ResourceId != nil && strings.HasPrefix(ref.Path, ".")
|
|
}
|
|
|
|
// IsAbsoluteReference returns true if the given reference qualifies as absolute
|
|
// when either only the resource id is set or only the path is set and starts with /
|
|
//
|
|
// TODO(corby): Currently if the path is empty, the ResourceId is set but has empty storageId and OpaqueId
|
|
// then the reference is still being viewed as absolute. We need to check if we want that because in some
|
|
// places we might not want to set both StorageId and OpaqueId so we can't do a hard check if they are set.
|
|
func IsAbsoluteReference(ref *provider.Reference) bool {
|
|
return (ref.ResourceId != nil && ref.Path == "") || (ref.ResourceId == nil) && strings.HasPrefix(ref.Path, "/")
|
|
}
|
|
|
|
// IsAbsolutePathReference returns true if the given reference qualifies as a global path
|
|
// when only the path is set and starts with /
|
|
func IsAbsolutePathReference(ref *provider.Reference) bool {
|
|
return ref.ResourceId == nil && strings.HasPrefix(ref.Path, "/")
|
|
}
|
|
|
|
// MakeRelativePath prefixes the path with a . to use it in a relative reference
|
|
func MakeRelativePath(p string) string {
|
|
p = path.Join("/", p)
|
|
|
|
if p == "/" {
|
|
return "."
|
|
}
|
|
return "." + p
|
|
}
|
|
|
|
// UserTypeMap translates account type string to CS3 UserType
|
|
func UserTypeMap(accountType string) userpb.UserType {
|
|
var t userpb.UserType
|
|
switch accountType {
|
|
case "primary":
|
|
t = userpb.UserType_USER_TYPE_PRIMARY
|
|
case "secondary":
|
|
t = userpb.UserType_USER_TYPE_SECONDARY
|
|
case "service":
|
|
t = userpb.UserType_USER_TYPE_SERVICE
|
|
case "application":
|
|
t = userpb.UserType_USER_TYPE_APPLICATION
|
|
case "guest":
|
|
t = userpb.UserType_USER_TYPE_GUEST
|
|
case "federated":
|
|
t = userpb.UserType_USER_TYPE_FEDERATED
|
|
case "lightweight":
|
|
t = userpb.UserType_USER_TYPE_LIGHTWEIGHT
|
|
// FIXME new user type
|
|
case "spaceowner":
|
|
t = 8
|
|
}
|
|
return t
|
|
}
|
|
|
|
// UserTypeToString translates CS3 UserType to user-readable string
|
|
func UserTypeToString(accountType userpb.UserType) string {
|
|
var t string
|
|
switch accountType {
|
|
case userpb.UserType_USER_TYPE_PRIMARY:
|
|
t = "primary"
|
|
case userpb.UserType_USER_TYPE_SECONDARY:
|
|
t = "secondary"
|
|
case userpb.UserType_USER_TYPE_SERVICE:
|
|
t = "service"
|
|
case userpb.UserType_USER_TYPE_APPLICATION:
|
|
t = "application"
|
|
case userpb.UserType_USER_TYPE_GUEST:
|
|
t = "guest"
|
|
case userpb.UserType_USER_TYPE_FEDERATED:
|
|
t = "federated"
|
|
case userpb.UserType_USER_TYPE_LIGHTWEIGHT:
|
|
t = "lightweight"
|
|
// FIXME new user type
|
|
case 8:
|
|
t = "spaceowner"
|
|
}
|
|
return t
|
|
}
|
|
|
|
// GetViewMode converts a human-readable string to a view mode for opening a resource in an app.
|
|
func GetViewMode(viewMode string) gateway.OpenInAppRequest_ViewMode {
|
|
switch viewMode {
|
|
case "view":
|
|
return gateway.OpenInAppRequest_VIEW_MODE_VIEW_ONLY
|
|
case "read":
|
|
return gateway.OpenInAppRequest_VIEW_MODE_READ_ONLY
|
|
case "write":
|
|
return gateway.OpenInAppRequest_VIEW_MODE_READ_WRITE
|
|
default:
|
|
return gateway.OpenInAppRequest_VIEW_MODE_INVALID
|
|
}
|
|
}
|
|
|
|
// GetAppViewMode converts a human-readable string to an appprovider view mode for opening a resource in an app.
|
|
func GetAppViewMode(viewMode string) appprovider.ViewMode {
|
|
switch viewMode {
|
|
case "view":
|
|
return appprovider.ViewMode_VIEW_MODE_VIEW_ONLY
|
|
case "read":
|
|
return appprovider.ViewMode_VIEW_MODE_READ_ONLY
|
|
case "write":
|
|
return appprovider.ViewMode_VIEW_MODE_READ_WRITE
|
|
case "preview":
|
|
return appprovider.ViewMode_VIEW_MODE_PREVIEW
|
|
default:
|
|
return appprovider.ViewMode_VIEW_MODE_INVALID
|
|
}
|
|
}
|
|
|
|
// AppendPlainToOpaque adds a new key value pair as a plain string on the given opaque and returns it
|
|
func AppendPlainToOpaque(o *types.Opaque, key, value string) *types.Opaque {
|
|
o = ensureOpaque(o)
|
|
|
|
o.Map[key] = &types.OpaqueEntry{
|
|
Decoder: "plain",
|
|
Value: []byte(value),
|
|
}
|
|
return o
|
|
}
|
|
|
|
// AppendJSONToOpaque adds a new key value pair as a json on the given opaque and returns it. Ignores errors
|
|
func AppendJSONToOpaque(o *types.Opaque, key string, value interface{}) *types.Opaque {
|
|
o = ensureOpaque(o)
|
|
|
|
b, _ := json.Marshal(value)
|
|
o.Map[key] = &types.OpaqueEntry{
|
|
Decoder: "json",
|
|
Value: b,
|
|
}
|
|
return o
|
|
}
|
|
|
|
// ReadPlainFromOpaque reads a plain string from the given opaque map
|
|
func ReadPlainFromOpaque(o *types.Opaque, key string) string {
|
|
if o.GetMap() == nil {
|
|
return ""
|
|
}
|
|
if e, ok := o.Map[key]; ok && e.Decoder == "plain" {
|
|
return string(e.Value)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// ReadJSONFromOpaque reads and unmarshals a value from the opaque in the given interface{} (Make sure it's a pointer!)
|
|
func ReadJSONFromOpaque(o *types.Opaque, key string, valptr interface{}) error {
|
|
if o.GetMap() == nil {
|
|
return errors.New("not found")
|
|
}
|
|
|
|
e, ok := o.Map[key]
|
|
if !ok || e.Decoder != "json" {
|
|
return errors.New("not found")
|
|
}
|
|
|
|
return json.Unmarshal(e.Value, valptr)
|
|
}
|
|
|
|
// ExistsInOpaque returns true if the key exists in the opaque (ignoring the value)
|
|
func ExistsInOpaque(o *types.Opaque, key string) bool {
|
|
if o.GetMap() == nil {
|
|
return false
|
|
}
|
|
|
|
_, ok := o.Map[key]
|
|
return ok
|
|
}
|
|
|
|
// SpaceGrantOpaque returns an Opaque with the "spacegrant" key set, which
|
|
// signals to storage and event middleware that a grant targets a space root.
|
|
func SpaceGrantOpaque() *types.Opaque {
|
|
return &types.Opaque{
|
|
Map: map[string]*types.OpaqueEntry{
|
|
"spacegrant": {},
|
|
},
|
|
}
|
|
}
|
|
|
|
// MergeOpaques will merge the opaques. If a key exists in both opaques
|
|
// the values from the first opaque will be taken
|
|
func MergeOpaques(o *types.Opaque, p *types.Opaque) *types.Opaque {
|
|
p = ensureOpaque(p)
|
|
for k, v := range o.GetMap() {
|
|
p.Map[k] = v
|
|
}
|
|
return p
|
|
}
|
|
|
|
// ensures the opaque is initialized
|
|
func ensureOpaque(o *types.Opaque) *types.Opaque {
|
|
if o == nil {
|
|
o = &types.Opaque{}
|
|
}
|
|
if o.Map == nil {
|
|
o.Map = map[string]*types.OpaqueEntry{}
|
|
}
|
|
return o
|
|
}
|
|
|
|
// RemoveItem removes the given item, its children and all empty parent folders
|
|
func RemoveItem(path string) error {
|
|
if err := os.RemoveAll(path); err != nil {
|
|
return err
|
|
}
|
|
|
|
for {
|
|
path = filepath.Dir(path)
|
|
if err := os.Remove(path); err != nil {
|
|
// remove will fail when the dir is not empty.
|
|
// We can exit in that case
|
|
return nil
|
|
}
|
|
|
|
}
|
|
|
|
}
|