switch to go vendoring

This commit is contained in:
Michael Barz
2023-04-19 20:24:34 +02:00
parent 632fa05ef9
commit afc6ed1e41
8527 changed files with 3004916 additions and 2 deletions
+54
View File
@@ -0,0 +1,54 @@
package utils
import (
"context"
"fmt"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"google.golang.org/grpc/metadata"
)
// Impersonate returns an authenticated reva context and the user it represents
func Impersonate(userID *user.UserId, gwc gateway.GatewayAPIClient, machineAuthAPIKey string) (context.Context, *user.User, error) {
usr, err := GetUser(userID, gwc, machineAuthAPIKey)
if err != nil {
return nil, nil, err
}
ctx, err := ImpersonateUser(usr, gwc, machineAuthAPIKey)
return ctx, usr, err
}
// GetUser gets the specified user
func GetUser(userID *user.UserId, gwc gateway.GatewayAPIClient, machineAuthAPIKey string) (*user.User, error) {
getUserResponse, err := gwc.GetUser(context.Background(), &user.GetUserRequest{UserId: userID})
if err != nil {
return nil, err
}
if getUserResponse.Status.Code != rpc.Code_CODE_OK {
return nil, fmt.Errorf("error getting user: %s", getUserResponse.Status.Message)
}
return getUserResponse.GetUser(), nil
}
// ImpersonateUser impersonates the given user
func ImpersonateUser(usr *user.User, gwc gateway.GatewayAPIClient, machineAuthAPIKey string) (context.Context, error) {
ctx := revactx.ContextSetUser(context.Background(), usr)
authRes, err := gwc.Authenticate(ctx, &gateway.AuthenticateRequest{
Type: "machine",
ClientId: "userid:" + usr.GetId().GetOpaqueId(),
ClientSecret: machineAuthAPIKey,
})
if err != nil {
return nil, err
}
if authRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
return nil, fmt.Errorf("error impersonating user: %s", authRes.Status.Message)
}
return metadata.AppendToOutgoingContext(ctx, revactx.TokenHeader, authRes.Token), nil
}
+106
View File
@@ -0,0 +1,106 @@
// Copyright 2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package utils
import (
"crypto/tls"
"crypto/x509"
"os"
"github.com/cs3org/reva/v2/pkg/logger"
ldapReconnect "github.com/cs3org/reva/v2/pkg/utils/ldap"
"github.com/go-ldap/ldap/v3"
"github.com/pkg/errors"
)
// LDAPConn holds the basic parameter for setting up an
// LDAP connection.
type LDAPConn struct {
URI string `mapstructure:"uri"`
Insecure bool `mapstructure:"insecure"`
CACert string `mapstructure:"cacert"`
BindDN string `mapstructure:"bind_username"`
BindPassword string `mapstructure:"bind_password"`
}
// GetLDAPClientWithReconnect initializes a long-lived LDAP connection that
// automatically reconnects on connection errors. It allows to set TLS options
// e.g. to add trusted Certificates or disable Certificate verification
func GetLDAPClientWithReconnect(c *LDAPConn) (ldap.Client, error) {
var tlsConf *tls.Config
if c.Insecure {
logger.New().Warn().Msg("SSL Certificate verification is disabled. This is strongly discouraged for production environments.")
tlsConf = &tls.Config{
//nolint:gosec // We need the ability to run with "insecure" (dev/testing)
InsecureSkipVerify: true,
}
}
if !c.Insecure && c.CACert != "" {
if pemBytes, err := os.ReadFile(c.CACert); err == nil {
rpool, _ := x509.SystemCertPool()
rpool.AppendCertsFromPEM(pemBytes)
tlsConf = &tls.Config{
RootCAs: rpool,
}
} else {
return nil, errors.Wrapf(err, "Error reading LDAP CA Cert '%s.'", c.CACert)
}
}
conn := ldapReconnect.NewLDAPWithReconnect(
ldapReconnect.Config{
URI: c.URI,
BindDN: c.BindDN,
BindPassword: c.BindPassword,
TLSConfig: tlsConf,
},
)
return conn, nil
}
// GetLDAPClientForAuth initializes an LDAP connection. The connection is not authenticated
// when returned. The main purpose for GetLDAPClientForAuth is to get and LDAP connection that
// can be used to issue a single bind request to authenticate a user.
func GetLDAPClientForAuth(c *LDAPConn) (ldap.Client, error) {
var tlsConf *tls.Config
if c.Insecure {
logger.New().Warn().Msg("SSL Certificate verification is disabled. Is is strongly discouraged for production environments.")
tlsConf = &tls.Config{
//nolint:gosec // We need the ability to run with "insecure" (dev/testing)
InsecureSkipVerify: true,
}
}
if !c.Insecure && c.CACert != "" {
if pemBytes, err := os.ReadFile(c.CACert); err == nil {
rpool, _ := x509.SystemCertPool()
rpool.AppendCertsFromPEM(pemBytes)
tlsConf = &tls.Config{
RootCAs: rpool,
}
} else {
return nil, errors.Wrapf(err, "Error reading LDAP CA Cert '%s.'", c.CACert)
}
}
l, err := ldap.DialURL(c.URI, ldap.DialWithTLSConfig(tlsConf))
if err != nil {
return nil, err
}
return l, nil
}
+719
View File
@@ -0,0 +1,719 @@
// Copyright 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 ldap
import (
"fmt"
"strings"
identityUser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/go-ldap/ldap/v3"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/rs/zerolog"
)
// Identity provides methods to query users and groups from an LDAP server
type Identity struct {
User userConfig `mapstructure:",squash"`
Group groupConfig `mapstructure:",squash"`
}
type userConfig struct {
BaseDN string `mapstructure:"user_base_dn"`
Scope string `mapstructure:"user_search_scope"`
scopeVal int
Filter string `mapstructure:"user_filter"`
Objectclass string `mapstructure:"user_objectclass"`
DisableMechanism string `mapstructure:"user_disable_mechanism"`
EnabledProperty string `mapstructure:"user_enabled_property"`
UserTypeProperty string `mapstructure:"user_type_property"`
Schema userSchema `mapstructure:"user_schema"`
SubstringFilterType string `mapstructure:"user_substring_filter_type"`
substringFilterVal int
}
type groupConfig struct {
BaseDN string `mapstructure:"group_base_dn"`
Scope string `mapstructure:"group_search_scope"`
scopeVal int
Filter string `mapstructure:"group_filter"`
Objectclass string `mapstructure:"group_objectclass"`
Schema groupSchema `mapstructure:"group_schema"`
SubstringFilterType string `mapstructure:"group_substring_filter_type"`
substringFilterVal int
// LocalDisabledDN contains the full DN of a group that contains disabled users.
LocalDisabledDN string `mapstructure:"group_local_disabled_dn"`
}
type groupSchema struct {
// GID is an immutable group id, see https://docs.microsoft.com/en-us/azure/active-directory/hybrid/plan-connect-design-concepts
ID string `mapstructure:"id"`
IDIsOctetString bool `mapstructure:"idIsOctetString"`
// CN is the group name, typically `cn`, `gid` or `samaccountname`
Groupname string `mapstructure:"groupName"`
// Mail is the email address of a group
Mail string `mapstructure:"mail"`
// Displayname is the Human readable name, e.g. `Database Admins`
DisplayName string `mapstructure:"displayName"`
// GIDNumber is a numeric id that maps to a filesystem gid, eg. 654321
GIDNumber string `mapstructure:"gidNumber"`
Member string `mapstructure:"member"`
}
type userSchema struct {
// UID is an immutable user id, see https://docs.microsoft.com/en-us/azure/active-directory/hybrid/plan-connect-design-concepts
ID string `mapstructure:"id"`
// UIDIsOctetString set this to true i the values of the UID attribute are returned as OCTET STRING values (binary byte sequences)
// by the Directory Service. This is e.g. the case for the 'objectGUID' and 'ms-DS-ConsistencyGuid' Attributes in AD
IDIsOctetString bool `mapstructure:"idIsOctetString"`
// Name is the username, typically `cn`, `uid` or `samaccountname`
Username string `mapstructure:"userName"`
// Mail is the email address of a user
Mail string `mapstructure:"mail"`
// Displayname is the Human readable name, e.g. `Albert Einstein`
DisplayName string `mapstructure:"displayName"`
// UIDNumber is a numeric id that maps to a filesystem uid, eg. 123546
UIDNumber string `mapstructure:"uidNumber"`
// GIDNumber is a numeric id that maps to a filesystem gid, eg. 654321
GIDNumber string `mapstructure:"gidNumber"`
}
// Default userConfig (somewhat inspired by Active Directory)
var userDefaults = userConfig{
Scope: "sub",
Objectclass: "posixAccount",
Schema: userSchema{
ID: "ms-DS-ConsistencyGuid",
IDIsOctetString: false,
Username: "cn",
Mail: "mail",
DisplayName: "displayName",
UIDNumber: "uidNumber",
GIDNumber: "gidNumber",
},
SubstringFilterType: "initial",
}
// Default groupConfig (Active Directory)
var groupDefaults = groupConfig{
Scope: "sub",
Objectclass: "posixGroup",
Schema: groupSchema{
ID: "objectGUID",
IDIsOctetString: false,
Groupname: "cn",
Mail: "mail",
DisplayName: "cn",
GIDNumber: "gidNumber",
Member: "memberUid",
},
SubstringFilterType: "initial",
}
// New initializes the default config
func New() Identity {
return Identity{
User: userDefaults,
Group: groupDefaults,
}
}
// Setup initialzes some properties that can't be initialized from the
// mapstructure based config. Currently it just converts the LDAP search scope
// strings from the config to the integer constants expected by the ldap API
func (i *Identity) Setup() error {
var err error
if i.User.scopeVal, err = stringToScope(i.User.Scope); err != nil {
return fmt.Errorf("error configuring user scope: %w", err)
}
if i.Group.scopeVal, err = stringToScope(i.Group.Scope); err != nil {
return fmt.Errorf("error configuring group scope: %w", err)
}
if i.User.substringFilterVal, err = stringToFilterType(i.User.SubstringFilterType); err != nil {
return fmt.Errorf("error configuring user substring filter type: %w", err)
}
if i.Group.substringFilterVal, err = stringToFilterType(i.Group.SubstringFilterType); err != nil {
return fmt.Errorf("error configuring group substring filter type: %w", err)
}
switch i.User.DisableMechanism {
case "group":
if i.Group.LocalDisabledDN == "" {
return fmt.Errorf("error configuring disable mechanism, disabled group DN not set")
}
case "attribute":
if i.User.EnabledProperty == "" {
return fmt.Errorf("error configuring disable mechanism, enabled property not set")
}
case "", "none":
default:
return fmt.Errorf("invalid disable mechanism setting: %s", i.User.DisableMechanism)
}
return nil
}
// GetLDAPUserByID looks up a user by the supplied Id. Returns the corresponding
// ldap.Entry
func (i *Identity) GetLDAPUserByID(log *zerolog.Logger, lc ldap.Client, id string) (*ldap.Entry, error) {
var filter string
var err error
if filter, err = i.getUserFilter(id); err != nil {
return nil, err
}
return i.GetLDAPUserByFilter(log, lc, filter)
}
// GetLDAPUserByAttribute looks up a single user by attribute (can be "mail",
// "uid", "gid", "username" or "userid"). Returns the corresponding ldap.Entry
func (i *Identity) GetLDAPUserByAttribute(log *zerolog.Logger, lc ldap.Client, attribute, value string) (*ldap.Entry, error) {
var filter string
var err error
if filter, err = i.getUserAttributeFilter(attribute, value); err != nil {
return nil, err
}
return i.GetLDAPUserByFilter(log, lc, filter)
}
// GetLDAPUserByFilter looks up a single user by the supplied LDAP filter
// returns the corresponding ldap.Entry
func (i *Identity) GetLDAPUserByFilter(log *zerolog.Logger, lc ldap.Client, filter string) (*ldap.Entry, error) {
searchRequest := ldap.NewSearchRequest(
i.User.BaseDN, i.User.scopeVal, ldap.NeverDerefAliases, 1, 0, false,
filter,
[]string{
i.User.Schema.DisplayName,
i.User.Schema.ID,
i.User.Schema.Mail,
i.User.Schema.Username,
i.User.Schema.UIDNumber,
i.User.Schema.GIDNumber,
i.User.EnabledProperty,
i.User.UserTypeProperty,
},
nil,
)
log.Debug().Str("backend", "ldap").Str("basedn", i.User.BaseDN).Str("filter", filter).Int("scope", i.User.scopeVal).Msg("LDAP Search")
res, err := lc.Search(searchRequest)
if err != nil {
log.Debug().Str("backend", "ldap").Err(err).Str("userfilter", filter).Msg("Error looking up user by filter")
var errmsg string
if lerr, ok := err.(*ldap.Error); ok {
if lerr.ResultCode == ldap.LDAPResultSizeLimitExceeded {
errmsg = fmt.Sprintf("too many results searching for user '%s'", filter)
}
}
return nil, errtypes.NotFound(errmsg)
}
if len(res.Entries) == 0 {
return nil, errtypes.NotFound(filter)
}
return res.Entries[0], nil
}
// GetLDAPUserByDN looks up a single user by the supplied LDAP DN
// returns the corresponding ldap.Entry
func (i *Identity) GetLDAPUserByDN(log *zerolog.Logger, lc ldap.Client, dn string) (*ldap.Entry, error) {
filter := fmt.Sprintf("(objectclass=%s)", i.User.Objectclass)
if i.User.Filter != "" {
filter = fmt.Sprintf("(&%s%s)", i.User.Filter, filter)
}
searchRequest := ldap.NewSearchRequest(
dn, i.User.scopeVal, ldap.NeverDerefAliases, 1, 0, false,
filter,
[]string{
i.User.Schema.DisplayName,
i.User.Schema.ID,
i.User.Schema.Mail,
i.User.Schema.Username,
i.User.Schema.UIDNumber,
i.User.Schema.GIDNumber,
i.User.EnabledProperty,
},
nil,
)
log.Debug().Str("backend", "ldap").Str("basedn", dn).Str("filter", filter).Int("scope", i.User.scopeVal).Msg("LDAP Search")
res, err := lc.Search(searchRequest)
if err != nil {
log.Debug().Str("backend", "ldap").Err(err).Str("dn", dn).Msg("Error looking up user by DN")
return nil, errtypes.NotFound(dn)
}
if len(res.Entries) == 0 {
return nil, errtypes.NotFound(dn)
}
return res.Entries[0], nil
}
// GetLDAPUsers searches for users using a prefix-substring match on the user
// attributes. Returns a slice of matching ldap.Entries
func (i *Identity) GetLDAPUsers(log *zerolog.Logger, lc ldap.Client, query string) ([]*ldap.Entry, error) {
filter := i.getUserFindFilter(query)
searchRequest := ldap.NewSearchRequest(
i.User.BaseDN,
i.User.scopeVal, ldap.NeverDerefAliases, 0, 0, false,
filter,
[]string{
i.User.Schema.ID,
i.User.Schema.Username,
i.User.Schema.Mail,
i.User.Schema.DisplayName,
i.User.Schema.UIDNumber,
i.User.Schema.GIDNumber,
i.User.EnabledProperty,
i.User.UserTypeProperty,
},
nil,
)
log.Debug().Str("backend", "ldap").Str("basedn", i.User.BaseDN).Str("filter", filter).Int("scope", i.User.scopeVal).Msg("LDAP Search")
sr, err := lc.Search(searchRequest)
if err != nil {
log.Debug().Str("backend", "ldap").Err(err).Str("filter", filter).Msg("Error searching users")
return nil, errtypes.NotFound(query)
}
return sr.Entries, nil
}
// IsLDAPUserInDisabledGroup checkes if the user is in the disabled group.
func (i *Identity) IsLDAPUserInDisabledGroup(log *zerolog.Logger, lc ldap.Client, userEntry *ldap.Entry) bool {
// Check if we need to do this here because the configuration is local to Identity.
if i.User.DisableMechanism != "group" {
return false
}
filter := fmt.Sprintf("(&(objectClass=groupOfNames)(%s=%s))", i.Group.Schema.Member, userEntry.DN)
searchRequest := ldap.NewSearchRequest(
i.Group.LocalDisabledDN,
i.Group.scopeVal,
ldap.NeverDerefAliases, 0, 0, false,
filter,
[]string{i.Group.Schema.ID},
nil,
)
log.Debug().Str("backend", "ldap").Str("basedn", i.Group.LocalDisabledDN).Str("filter", filter).Int("scope", i.Group.scopeVal).Msg("LDAP Search")
sr, err := lc.Search(searchRequest)
if err != nil {
log.Error().Str("backend", "ldap").Err(err).Str("filter", filter).Msg("Error looking up error group")
// Err on the side of caution.
return true
}
return len(sr.Entries) > 0
}
// GetLDAPUserGroups looks up the group member ship of the supplied LDAP user entry.
// Returns a slice of strings with groupids
func (i *Identity) GetLDAPUserGroups(log *zerolog.Logger, lc ldap.Client, userEntry *ldap.Entry) ([]string, error) {
var memberValue string
if strings.ToLower(i.Group.Objectclass) == "posixgroup" {
// posixGroup usually means that the member attribute just contains the username
memberValue = userEntry.GetEqualFoldAttributeValue(i.User.Schema.Username)
} else {
// In all other case we assume the member Attribute to contain full LDAP DNs
memberValue = userEntry.DN
}
filter := i.getGroupMemberFilter(memberValue)
searchRequest := ldap.NewSearchRequest(
i.Group.BaseDN, i.Group.scopeVal,
ldap.NeverDerefAliases, 0, 0, false,
filter,
[]string{i.Group.Schema.ID},
nil,
)
log.Debug().Str("backend", "ldap").Str("basedn", i.Group.BaseDN).Str("filter", filter).Int("scope", i.Group.scopeVal).Msg("LDAP Search")
sr, err := lc.Search(searchRequest)
if err != nil {
log.Debug().Str("backend", "ldap").Err(err).Str("filter", filter).Msg("Error looking up group memberships")
return []string{}, err
}
groups := make([]string, 0, len(sr.Entries))
for _, entry := range sr.Entries {
// FIXME this makes the users groups use the cn, not an immutable id
// FIXME 1. use the memberof or members attribute of a user to get the groups
// FIXME 2. ook up the id for each group
var groupID string
if i.Group.Schema.IDIsOctetString {
raw := entry.GetEqualFoldRawAttributeValue(i.Group.Schema.ID)
value, err := uuid.FromBytes(raw)
if err != nil {
return nil, err
}
groupID = value.String()
} else {
groupID = entry.GetEqualFoldAttributeValue(i.Group.Schema.ID)
}
groups = append(groups, groupID)
}
return groups, nil
}
// GetLDAPGroupByID looks up a group by the supplied Id. Returns the corresponding
// ldap.Entry
func (i *Identity) GetLDAPGroupByID(log *zerolog.Logger, lc ldap.Client, id string) (*ldap.Entry, error) {
var filter string
var err error
if filter, err = i.getGroupFilter(id); err != nil {
return nil, err
}
return i.GetLDAPGroupByFilter(log, lc, filter)
}
// GetLDAPGroupByAttribute looks up a single group by attribute (can be "mail", "gid_number",
// "display_name", "group_name", "group_id"). Returns the corresponding ldap.Entry
func (i *Identity) GetLDAPGroupByAttribute(log *zerolog.Logger, lc ldap.Client, attribute, value string) (*ldap.Entry, error) {
var filter string
var err error
if filter, err = i.getGroupAttributeFilter(attribute, value); err != nil {
return nil, err
}
return i.GetLDAPGroupByFilter(log, lc, filter)
}
// GetLDAPGroupByFilter looks up a single group by the supplied LDAP filter
// returns the corresponding ldap.Entry
func (i *Identity) GetLDAPGroupByFilter(log *zerolog.Logger, lc ldap.Client, filter string) (*ldap.Entry, error) {
searchRequest := ldap.NewSearchRequest(
i.Group.BaseDN, i.Group.scopeVal, ldap.NeverDerefAliases, 1, 0, false,
filter,
[]string{
i.Group.Schema.DisplayName,
i.Group.Schema.ID,
i.Group.Schema.Mail,
i.Group.Schema.Groupname,
i.Group.Schema.Member,
i.Group.Schema.GIDNumber,
},
nil,
)
log.Debug().Str("backend", "ldap").Str("basedn", i.Group.BaseDN).Str("filter", filter).Int("scope", i.Group.scopeVal).Msg("LDAP Search")
res, err := lc.Search(searchRequest)
if err != nil {
log.Debug().Str("backend", "ldap").Err(err).Str("filter", filter).Msg("Error looking up group by filter")
var errmsg string
if lerr, ok := err.(*ldap.Error); ok {
if lerr.ResultCode == ldap.LDAPResultSizeLimitExceeded {
errmsg = fmt.Sprintf("too many results searching for group '%s'", filter)
}
}
return nil, errtypes.NotFound(errmsg)
}
if len(res.Entries) == 0 {
return nil, errtypes.NotFound(filter)
}
return res.Entries[0], nil
}
// GetLDAPGroups searches for groups using a prefix-substring match on the group
// attributes. Returns a slice of matching ldap.Entries
func (i *Identity) GetLDAPGroups(log *zerolog.Logger, lc ldap.Client, query string) ([]*ldap.Entry, error) {
searchRequest := ldap.NewSearchRequest(
i.Group.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
i.getGroupFindFilter(query),
[]string{
i.Group.Schema.DisplayName,
i.Group.Schema.ID,
i.Group.Schema.Mail,
i.Group.Schema.Groupname,
i.Group.Schema.GIDNumber,
},
nil,
)
sr, err := lc.Search(searchRequest)
if err != nil {
log.Debug().Str("backend", "ldap").Err(err).Str("query", query).Msg("Error search for groups")
return nil, errtypes.NotFound(query)
}
return sr.Entries, nil
}
// GetLDAPGroupMembers looks up all members of the supplied LDAP group entry and returns the
// corresponding LDAP user entries
func (i *Identity) GetLDAPGroupMembers(log *zerolog.Logger, lc ldap.Client, group *ldap.Entry) ([]*ldap.Entry, error) {
members := group.GetEqualFoldAttributeValues(i.Group.Schema.Member)
log.Debug().Str("dn", group.DN).Interface("member", members).Msg("Get Group members")
memberEntries := make([]*ldap.Entry, 0, len(members))
for _, member := range members {
var e *ldap.Entry
var err error
if strings.ToLower(i.Group.Objectclass) == "posixgroup" {
e, err = i.GetLDAPUserByAttribute(log, lc, "username", member)
} else {
e, err = i.GetLDAPUserByDN(log, lc, member)
}
if err != nil {
log.Warn().Err(err).Interface("member", member).Msg("Failed read user entry for member")
continue
}
memberEntries = append(memberEntries, e)
}
return memberEntries, nil
}
func filterEscapeBinaryUUID(value uuid.UUID) string {
filtered := ""
for _, b := range value {
filtered = fmt.Sprintf("%s\\%02x", filtered, b)
}
return filtered
}
func (i *Identity) getUserFilter(uid string) (string, error) {
var escapedUUID string
if i.User.Schema.IDIsOctetString {
id, err := uuid.Parse(uid)
if err != nil {
err := errors.Wrap(err, fmt.Sprintf("error parsing OpaqueID '%s' as UUID", uid))
return "", err
}
escapedUUID = filterEscapeBinaryUUID(id)
} else {
escapedUUID = ldap.EscapeFilter(uid)
}
return fmt.Sprintf("(&%s(objectclass=%s)(%s=%s))",
i.User.Filter,
i.User.Objectclass,
i.User.Schema.ID,
escapedUUID,
), nil
}
func (i *Identity) getUserAttributeFilter(attribute, value string) (string, error) {
switch attribute {
case "mail":
attribute = i.User.Schema.Mail
case "uid":
attribute = i.User.Schema.UIDNumber
case "gid":
attribute = i.User.Schema.GIDNumber
case "username":
attribute = i.User.Schema.Username
case "userid":
attribute = i.User.Schema.ID
default:
return "", errors.New("ldap: invalid field " + attribute)
}
if attribute == "userid" && i.User.Schema.IDIsOctetString {
id, err := uuid.Parse(value)
if err != nil {
err := errors.Wrap(err, fmt.Sprintf("error parsing OpaqueID '%s' as UUID", value))
return "", err
}
value = filterEscapeBinaryUUID(id)
} else {
value = ldap.EscapeFilter(value)
}
return fmt.Sprintf("(&%s(objectclass=%s)(%s=%s)%s)",
i.User.Filter,
i.User.Objectclass,
attribute,
value,
i.disabledFilter(),
), nil
}
func (i *Identity) disabledFilter() string {
if i.User.DisableMechanism == "attribute" {
return fmt.Sprintf("(!(%s=FALSE))", i.User.EnabledProperty)
}
return ""
}
// getUserFindFilter construct a LDAP filter to perform a prefix-substring
// search for users.
func (i *Identity) getUserFindFilter(query string) string {
searchAttrs := []string{
i.User.Schema.Mail,
i.User.Schema.DisplayName,
i.User.Schema.Username,
}
var filter, squery string
switch i.User.substringFilterVal {
case ldap.FilterSubstringsInitial:
squery = fmt.Sprintf("%s*", ldap.EscapeFilter(query))
case ldap.FilterSubstringsAny:
squery = fmt.Sprintf("*%s*", ldap.EscapeFilter(query))
case ldap.FilterSubstringsFinal:
squery = fmt.Sprintf("*%s", ldap.EscapeFilter(query))
}
for _, attr := range searchAttrs {
filter = fmt.Sprintf("%s(%s=%s)", filter, attr, squery)
}
// substring search for UUID is not possible
filter = fmt.Sprintf("%s(%s=%s)", filter, i.User.Schema.ID, ldap.EscapeFilter(query))
return fmt.Sprintf("(&%s(objectclass=%s)(|%s))",
i.User.Filter,
i.User.Objectclass,
filter,
)
}
// getGroupFindFilter construct a LDAP filter to perform a prefix-substring
// search for groups.
func (i *Identity) getGroupFindFilter(query string) string {
searchAttrs := []string{
i.Group.Schema.Mail,
i.Group.Schema.DisplayName,
i.Group.Schema.Groupname,
}
var filter, squery string
switch i.Group.substringFilterVal {
case ldap.FilterSubstringsInitial:
squery = fmt.Sprintf("%s*", ldap.EscapeFilter(query))
case ldap.FilterSubstringsAny:
squery = fmt.Sprintf("*%s*", ldap.EscapeFilter(query))
case ldap.FilterSubstringsFinal:
squery = fmt.Sprintf("*%s", ldap.EscapeFilter(query))
}
for _, attr := range searchAttrs {
filter = fmt.Sprintf("%s(%s=%s)", filter, attr, squery)
}
// substring search for UUID is not possible
filter = fmt.Sprintf("%s(%s=%s)", filter, i.Group.Schema.ID, ldap.EscapeFilter(query))
return fmt.Sprintf("(&%s(objectclass=%s)(|%s))",
i.Group.Filter,
i.Group.Objectclass,
filter,
)
}
func stringToScope(scope string) (int, error) {
var s int
switch scope {
case "sub":
s = ldap.ScopeWholeSubtree
case "one":
s = ldap.ScopeSingleLevel
case "base":
s = ldap.ScopeBaseObject
default:
return 0, fmt.Errorf("invalid Scope '%s'", scope)
}
return s, nil
}
func stringToFilterType(t string) (int, error) {
var s int
switch t {
case "initial":
s = ldap.FilterSubstringsInitial
case "any":
s = ldap.FilterSubstringsAny
case "final":
s = ldap.FilterSubstringsFinal
default:
return 0, fmt.Errorf("invalid filter type '%s'", t)
}
return s, nil
}
func (i *Identity) getGroupMemberFilter(memberName string) string {
return fmt.Sprintf("(&%s(objectclass=%s)(%s=%s))",
i.Group.Filter,
i.Group.Objectclass,
i.Group.Schema.Member,
ldap.EscapeFilter(memberName),
)
}
func (i *Identity) getGroupFilter(id string) (string, error) {
var escapedUUID string
if i.Group.Schema.IDIsOctetString {
id, err := uuid.Parse(id)
if err != nil {
err := errors.Wrap(err, fmt.Sprintf("error parsing OpaqueID '%s' as UUID", id))
return "", err
}
escapedUUID = filterEscapeBinaryUUID(id)
} else {
escapedUUID = ldap.EscapeFilter(id)
}
return fmt.Sprintf("(&%s(objectclass=%s)(%s=%s))",
i.Group.Filter,
i.Group.Objectclass,
i.Group.Schema.ID,
escapedUUID,
), nil
}
func (i *Identity) getGroupAttributeFilter(attribute, value string) (string, error) {
switch attribute {
case "mail":
attribute = i.Group.Schema.Mail
case "gid_number":
attribute = i.Group.Schema.GIDNumber
case "display_name":
attribute = i.Group.Schema.DisplayName
case "group_name":
attribute = i.Group.Schema.Groupname
case "group_id":
attribute = i.Group.Schema.ID
default:
return "", errors.New("ldap: invalid field " + attribute)
}
if attribute == "group_id" && i.Group.Schema.IDIsOctetString {
id, err := uuid.Parse(value)
if err != nil {
err := errors.Wrap(err, fmt.Sprintf("error parsing OpaqueID '%s' as UUID", value))
return "", err
}
value = filterEscapeBinaryUUID(id)
} else {
value = ldap.EscapeFilter(value)
}
return fmt.Sprintf("(&%s(objectclass=%s)(%s=%s))",
i.Group.Filter,
i.Group.Objectclass,
attribute,
value,
), nil
}
// GetUserType is used to get the proper UserType from ldap entry string
func (i *Identity) GetUserType(userEntry *ldap.Entry) identityUser.UserType {
userTypeString := userEntry.GetEqualFoldAttributeValue(i.User.UserTypeProperty)
switch strings.ToLower(userTypeString) {
case "member":
return identityUser.UserType_USER_TYPE_PRIMARY
case "guest":
return identityUser.UserType_USER_TYPE_GUEST
default:
return identityUser.UserType_USER_TYPE_PRIMARY
}
}
+306
View File
@@ -0,0 +1,306 @@
// Copyright 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 ldap
// LDAP automatic reconnection mechanism, inspired by:
// https://gist.github.com/emsearcy/cba3295d1a06d4c432ab4f6173b65e4f#file-ldap_snippet-go
import (
"crypto/tls"
"errors"
"fmt"
"time"
"github.com/go-ldap/ldap/v3"
"github.com/rs/zerolog"
)
var (
defaultRetries = 1
errMaxRetries = errors.New("max retries")
)
type ldapConnection struct {
Conn *ldap.Conn
Error error
}
// ConnWithReconnect maintains an LDAP Connection that automatically reconnects after network errors
type ConnWithReconnect struct {
conn chan ldapConnection
reset chan *ldap.Conn
retries int
logger *zerolog.Logger
}
// Config holds the basic configuration of the LDAP Connection
type Config struct {
URI string
BindDN string
BindPassword string
TLSConfig *tls.Config
}
// NewLDAPWithReconnect Returns a new ConnWithReconnect initialized from config
func NewLDAPWithReconnect(config Config) *ConnWithReconnect {
conn := ConnWithReconnect{
conn: make(chan ldapConnection),
reset: make(chan *ldap.Conn),
retries: defaultRetries,
}
logger := zerolog.Nop()
conn.logger = &logger
go conn.ldapAutoConnect(config)
return &conn
}
// SetLogger sets the logger for the current instance
func (c *ConnWithReconnect) SetLogger(logger *zerolog.Logger) {
c.logger = logger
}
func (c *ConnWithReconnect) retry(fn func(c ldap.Client) error) error {
conn, err := c.getConnection()
if err != nil {
return err
}
for try := 0; try <= c.retries; try++ {
if try > 0 {
c.logger.Debug().Msgf("retrying attempt %d", try)
conn, err = c.reconnect(conn)
if err != nil {
// reconnection failed stop this attempt
return err
}
}
if err = fn(conn); err == nil {
// function succeed no need to retry
return nil
}
if !ldap.IsErrorWithCode(err, ldap.ErrorNetwork) {
// non network error, stop retrying
return err
}
}
return ldap.NewError(ldap.ErrorNetwork, errMaxRetries)
}
// Search implements the ldap.Client interface
func (c *ConnWithReconnect) Search(sr *ldap.SearchRequest) (*ldap.SearchResult, error) {
var err error
var res *ldap.SearchResult
retryErr := c.retry(func(c ldap.Client) error {
res, err = c.Search(sr)
return err
})
return res, retryErr
}
// Add implements the ldap.Client interface
func (c *ConnWithReconnect) Add(a *ldap.AddRequest) error {
err := c.retry(func(c ldap.Client) error {
return c.Add(a)
})
return err
}
// Del implements the ldap.Client interface
func (c *ConnWithReconnect) Del(d *ldap.DelRequest) error {
err := c.retry(func(c ldap.Client) error {
return c.Del(d)
})
return err
}
// Modify implements the ldap.Client interface
func (c *ConnWithReconnect) Modify(m *ldap.ModifyRequest) error {
err := c.retry(func(c ldap.Client) error {
return c.Modify(m)
})
return err
}
// ModifyDN implements the ldap.Client interface
func (c *ConnWithReconnect) ModifyDN(m *ldap.ModifyDNRequest) error {
err := c.retry(func(c ldap.Client) error {
return c.ModifyDN(m)
})
return err
}
func (c *ConnWithReconnect) getConnection() (*ldap.Conn, error) {
conn := <-c.conn
if conn.Conn != nil && !ldap.IsErrorWithCode(conn.Error, ldap.ErrorNetwork) {
c.logger.Debug().Msg("using existing Connection")
return conn.Conn, conn.Error
}
return c.reconnect(conn.Conn)
}
func (c *ConnWithReconnect) ldapAutoConnect(config Config) {
l, err := c.ldapConnect(config)
if err != nil {
c.logger.Debug().Err(err).Msg("autoconnect could not get ldap Connection")
}
for {
select {
case resConn := <-c.reset:
// Only close the connection and reconnect if the current
// connection, matches the one we got via the reset channel.
// If they differ we already reconnected
switch {
case l == nil:
c.logger.Debug().Msg("reconnecting to LDAP")
l, err = c.ldapConnect(config)
case l != resConn:
c.logger.Debug().Msg("already reconnected")
continue
default:
c.logger.Debug().Msg("closing and reconnecting to LDAP")
l.Close()
l, err = c.ldapConnect(config)
}
case c.conn <- ldapConnection{l, err}:
}
}
}
func (c *ConnWithReconnect) ldapConnect(config Config) (*ldap.Conn, error) {
c.logger.Debug().Msgf("Connecting to %s", config.URI)
var err error
var l *ldap.Conn
if config.TLSConfig != nil {
l, err = ldap.DialURL(config.URI, ldap.DialWithTLSConfig(config.TLSConfig))
} else {
l, err = ldap.DialURL(config.URI)
}
if err != nil {
c.logger.Debug().Err(err).Msg("could not get ldap Connection")
return nil, err
}
c.logger.Debug().Msg("LDAP Connected")
if config.BindDN != "" {
c.logger.Debug().Msgf("Binding as %s", config.BindDN)
err = l.Bind(config.BindDN, config.BindPassword)
if err != nil {
c.logger.Debug().Err(err).Msg("Bind failed")
l.Close()
return nil, err
}
}
return l, err
}
func (c *ConnWithReconnect) reconnect(resetConn *ldap.Conn) (*ldap.Conn, error) {
c.logger.Debug().Msg("LDAP connection reset")
c.reset <- resetConn
c.logger.Debug().Msg("Waiting for new connection")
result := <-c.conn
return result.Conn, result.Error
}
// Remaining methods to fulfill ldap.Client interface
// Start implements the ldap.Client interface
func (c *ConnWithReconnect) Start() {}
// StartTLS implements the ldap.Client interface
func (c *ConnWithReconnect) StartTLS(*tls.Config) error {
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
// Close implements the ldap.Client interface
func (c *ConnWithReconnect) Close() {}
// IsClosing implements the ldap.Client interface
func (c *ConnWithReconnect) IsClosing() bool {
return false
}
// SetTimeout implements the ldap.Client interface
func (c *ConnWithReconnect) SetTimeout(time.Duration) {}
// Bind implements the ldap.Client interface
func (c *ConnWithReconnect) Bind(username, password string) error {
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
// UnauthenticatedBind implements the ldap.Client interface
func (c *ConnWithReconnect) UnauthenticatedBind(username string) error {
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
// SimpleBind implements the ldap.Client interface
func (c *ConnWithReconnect) SimpleBind(*ldap.SimpleBindRequest) (*ldap.SimpleBindResult, error) {
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
// ExternalBind implements the ldap.Client interface
func (c *ConnWithReconnect) ExternalBind() error {
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
// ModifyWithResult implements the ldap.Client interface
func (c *ConnWithReconnect) ModifyWithResult(m *ldap.ModifyRequest) (*ldap.ModifyResult, error) {
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
// Compare implements the ldap.Client interface
func (c *ConnWithReconnect) Compare(dn, attribute, value string) (bool, error) {
return false, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
// PasswordModify implements the ldap.Client interface
func (c *ConnWithReconnect) PasswordModify(*ldap.PasswordModifyRequest) (*ldap.PasswordModifyResult, error) {
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
// SearchWithPaging implements the ldap.Client interface
func (c *ConnWithReconnect) SearchWithPaging(searchRequest *ldap.SearchRequest, pagingSize uint32) (*ldap.SearchResult, error) {
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
// NTLMUnauthenticatedBind implements the ldap.Client interface
func (c *ConnWithReconnect) NTLMUnauthenticatedBind(domain, username string) error {
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
// TLSConnectionState implements the ldap.Client interface
func (c *ConnWithReconnect) TLSConnectionState() (tls.ConnectionState, bool) {
return tls.ConnectionState{}, false
}
// Unbind implements the ldap.Client interface
func (c *ConnWithReconnect) Unbind() error {
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
+473
View File
@@ -0,0 +1,473 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not 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"
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/cs3org/reva/v2/pkg/registry"
"github.com/cs3org/reva/v2/pkg/registry/memory"
"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}$`)
// GlobalRegistry configures a service registry globally accessible. It defaults to a memory registry. The usage of
// globals is not encouraged, and this is a workaround until the PR is out of a draft state.
GlobalRegistry registry.Registry = memory.New(map[string]interface{}{})
// 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"
// 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 {
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
}
// 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
}
}
// 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
}
// 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
}
}
}