Initial QSfera import
This commit is contained in:
+89
@@ -0,0 +1,89 @@
|
||||
// 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 cfg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
|
||||
"github.com/go-playground/locales/en"
|
||||
ut "github.com/go-playground/universal-translator"
|
||||
"github.com/go-playground/validator/v10"
|
||||
en_translations "github.com/go-playground/validator/v10/translations/en"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
// Setter is the interface a configuration struct may implement
|
||||
// to set the default options.
|
||||
type Setter interface {
|
||||
// ApplyDefaults applies the default options.
|
||||
ApplyDefaults()
|
||||
}
|
||||
|
||||
var validate = validator.New()
|
||||
var english = en.New()
|
||||
var uni = ut.New(english, english)
|
||||
var trans, _ = uni.GetTranslator("en")
|
||||
var _ = en_translations.RegisterDefaultTranslations(validate, trans)
|
||||
|
||||
func init() {
|
||||
validate.RegisterTagNameFunc(func(field reflect.StructField) string {
|
||||
if k := field.Tag.Get("mapstructure"); k != "" {
|
||||
return k
|
||||
}
|
||||
// if not specified, fall back to field name
|
||||
return field.Name
|
||||
})
|
||||
}
|
||||
|
||||
// Decode decodes the given raw input interface to the target pointer c.
|
||||
// It applies the default configuration if the target struct
|
||||
// implements the Setter interface.
|
||||
// It also perform a validation to all the fields of the configuration.
|
||||
func Decode(input map[string]any, c any) error {
|
||||
config := &mapstructure.DecoderConfig{
|
||||
Metadata: nil,
|
||||
Result: c,
|
||||
}
|
||||
|
||||
decoder, err := mapstructure.NewDecoder(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := decoder.Decode(input); err != nil {
|
||||
return err
|
||||
}
|
||||
if s, ok := c.(Setter); ok {
|
||||
s.ApplyDefaults()
|
||||
}
|
||||
|
||||
return translateError(validate.Struct(c), trans)
|
||||
}
|
||||
|
||||
func translateError(err error, trans ut.Translator) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
errs := err.(validator.ValidationErrors)
|
||||
translated := make([]error, 0, len(errs))
|
||||
for _, err := range errs {
|
||||
translated = append(translated, errors.New(err.Translate(trans)))
|
||||
}
|
||||
return errors.Join(translated...)
|
||||
}
|
||||
+337
@@ -0,0 +1,337 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
invitev1beta1 "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
|
||||
permissions "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
// SpaceRole defines the user role on space
|
||||
type SpaceRole func(*storageprovider.ResourcePermissions) bool
|
||||
|
||||
// Possible roles in spaces
|
||||
var (
|
||||
AllRole SpaceRole = func(perms *storageprovider.ResourcePermissions) bool { return true }
|
||||
ViewerRole SpaceRole = func(perms *storageprovider.ResourcePermissions) bool { return perms.Stat }
|
||||
EditorRole SpaceRole = func(perms *storageprovider.ResourcePermissions) bool { return perms.InitiateFileUpload }
|
||||
ManagerRole SpaceRole = func(perms *storageprovider.ResourcePermissions) bool { return perms.DenyGrant }
|
||||
)
|
||||
|
||||
var _errStatusCodeTmpl = "unexpected status code while %s: %v"
|
||||
|
||||
// Package error checkers
|
||||
var (
|
||||
IsErrNotFound = func(err error) bool { return IsStatusCodeError(err, rpc.Code_CODE_NOT_FOUND) }
|
||||
IsErrPermissionDenied = func(err error) bool { return IsStatusCodeError(err, rpc.Code_CODE_PERMISSION_DENIED) }
|
||||
)
|
||||
|
||||
// GetServiceUserContext returns an authenticated context of the given service user
|
||||
//
|
||||
// Deprecated: Use GetServiceUserContextWithContext()
|
||||
func GetServiceUserContext(serviceUserID string, gwc gateway.GatewayAPIClient, serviceUserSecret string) (context.Context, error) {
|
||||
return GetServiceUserContextWithContext(context.Background(), gwc, serviceUserID, serviceUserSecret)
|
||||
}
|
||||
|
||||
// GetServiceUserContextWithContext returns an authenticated context of the given service user
|
||||
func GetServiceUserContextWithContext(ctx context.Context, gwc gateway.GatewayAPIClient, serviceUserID string, serviceUserSecret string) (context.Context, error) {
|
||||
token, err := GetServiceUserToken(ctx, gwc, serviceUserID, serviceUserSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, token), nil
|
||||
}
|
||||
|
||||
// GetServiceUserToken returns a reva authentication token for the given service user
|
||||
func GetServiceUserToken(ctx context.Context, gwc gateway.GatewayAPIClient, serviceUserID string, serviceUserSecret string) (string, error) {
|
||||
authRes, err := gwc.Authenticate(ctx, &gateway.AuthenticateRequest{
|
||||
Type: "serviceaccounts",
|
||||
ClientId: serviceUserID,
|
||||
ClientSecret: serviceUserSecret,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := checkStatusCode("authenticating service user", authRes.GetStatus().GetMessage(), authRes.GetStatus().GetCode()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return authRes.Token, nil
|
||||
}
|
||||
|
||||
// GetUser gets the specified user
|
||||
func GetUser(ctx context.Context, userID *user.UserId, gwc gateway.GatewayAPIClient) (*user.User, error) {
|
||||
return getUser(ctx, userID, false, gwc)
|
||||
}
|
||||
|
||||
// GetUserNoGroups gets the specified user without expanding groupmemberships
|
||||
func GetUserNoGroups(ctx context.Context, userID *user.UserId, gwc gateway.GatewayAPIClient) (*user.User, error) {
|
||||
return getUser(ctx, userID, true, gwc)
|
||||
}
|
||||
|
||||
// getUser gets the specified user
|
||||
func getUser(ctx context.Context, userID *user.UserId, skipGroups bool, gwc gateway.GatewayAPIClient) (*user.User, error) {
|
||||
getUserResponse, err := gwc.GetUser(ctx, &user.GetUserRequest{
|
||||
UserId: userID,
|
||||
SkipFetchingUserGroups: skipGroups,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := checkStatusCode("getting user", getUserResponse.GetStatus().GetMessage(), getUserResponse.GetStatus().GetCode()); err != nil {
|
||||
return nil, err
|
||||
|
||||
}
|
||||
return getUserResponse.GetUser(), nil
|
||||
}
|
||||
|
||||
// GetUserWithContext gets the specified accepted user
|
||||
func GetAcceptedUserWithContext(ctx context.Context, userID *user.UserId, gwc gateway.GatewayAPIClient) (*user.User, error) {
|
||||
getAcceptedUserResponse, err := gwc.GetAcceptedUser(ctx, &invitev1beta1.GetAcceptedUserRequest{RemoteUserId: userID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := checkStatusCode("getting accepted user", getAcceptedUserResponse.GetStatus().GetMessage(), getAcceptedUserResponse.GetStatus().GetCode()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return getAcceptedUserResponse.GetRemoteUser(), nil
|
||||
}
|
||||
|
||||
// GetSpace returns the given space
|
||||
func GetSpace(ctx context.Context, spaceID string, gwc gateway.GatewayAPIClient) (*storageprovider.StorageSpace, error) {
|
||||
res, err := gwc.ListStorageSpaces(ctx, listStorageSpaceRequest(spaceID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := checkStatusCode("getting space", res.GetStatus().GetMessage(), res.GetStatus().GetCode()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(res.StorageSpaces) == 0 {
|
||||
return nil, statusCodeError{"getting space", "", rpc.Code_CODE_NOT_FOUND}
|
||||
}
|
||||
|
||||
return res.StorageSpaces[0], nil
|
||||
}
|
||||
|
||||
// GetGroupMembers returns all members of the given group
|
||||
func GetGroupMembers(ctx context.Context, groupID string, gwc gateway.GatewayAPIClient) ([]string, error) {
|
||||
r, err := gwc.GetGroup(ctx, &group.GetGroupRequest{GroupId: &group.GroupId{OpaqueId: groupID}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := checkStatusCode("getting group", r.GetStatus().GetMessage(), r.GetStatus().GetCode()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users := make([]string, 0, len(r.GetGroup().GetMembers()))
|
||||
for _, u := range r.GetGroup().GetMembers() {
|
||||
users = append(users, u.GetOpaqueId())
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// ResolveID returns either the given userID or all members of the given groupID (if userID is nil)
|
||||
func ResolveID(ctx context.Context, userid *user.UserId, groupid *group.GroupId, gwc gateway.GatewayAPIClient) ([]string, error) {
|
||||
if userid != nil {
|
||||
return []string{userid.GetOpaqueId()}, nil
|
||||
}
|
||||
|
||||
if ctx == nil {
|
||||
return nil, errors.New("need ctx to resolve group id")
|
||||
}
|
||||
|
||||
return GetGroupMembers(ctx, groupid.GetOpaqueId(), gwc)
|
||||
}
|
||||
|
||||
// GetSpaceMembers returns all members of the given space that have at least the given role. `nil` role will be interpreted as all
|
||||
func GetSpaceMembers(ctx context.Context, spaceID string, gwc gateway.GatewayAPIClient, role SpaceRole) ([]string, error) {
|
||||
if ctx == nil {
|
||||
return nil, errors.New("need authenticated context to find space members")
|
||||
}
|
||||
|
||||
space, err := GetSpace(ctx, spaceID, gwc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var users []string
|
||||
switch space.SpaceType {
|
||||
case "personal":
|
||||
users = append(users, space.GetOwner().GetId().GetOpaqueId())
|
||||
case "project":
|
||||
if users, err = gatherProjectSpaceMembers(ctx, space, gwc, role); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
// TODO: shares? other space types?
|
||||
return nil, fmt.Errorf("unsupported space type: %s", space.SpaceType)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// GetResourceByID is a convenience method to get a resource by its resourceID
|
||||
func GetResourceByID(ctx context.Context, resourceid *storageprovider.ResourceId, gwc gateway.GatewayAPIClient) (*storageprovider.ResourceInfo, error) {
|
||||
return GetResource(ctx, &storageprovider.Reference{ResourceId: resourceid}, gwc)
|
||||
}
|
||||
|
||||
// GetResource returns a resource by reference
|
||||
func GetResource(ctx context.Context, ref *storageprovider.Reference, gwc gateway.GatewayAPIClient) (*storageprovider.ResourceInfo, error) {
|
||||
res, err := gwc.Stat(ctx, &storageprovider.StatRequest{Ref: ref})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := checkStatusCode("getting resource", res.GetStatus().GetMessage(), res.GetStatus().GetCode()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res.GetInfo(), nil
|
||||
}
|
||||
|
||||
// CheckPermission checks if the user role contains the given permission
|
||||
func CheckPermission(ctx context.Context, perm string, gwc gateway.GatewayAPIClient) (bool, error) {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
resp, err := gwc.CheckPermission(ctx, &permissions.CheckPermissionRequest{
|
||||
SubjectRef: &permissions.SubjectReference{
|
||||
Spec: &permissions.SubjectReference_UserId{
|
||||
UserId: user.Id,
|
||||
},
|
||||
},
|
||||
Permission: perm,
|
||||
})
|
||||
return resp.GetStatus().GetCode() == rpc.Code_CODE_OK, err
|
||||
}
|
||||
|
||||
// IsStatusCodeError returns true if `err` was caused because of status code `code`
|
||||
func IsStatusCodeError(err error, code rpc.Code) bool {
|
||||
sce, ok := err.(statusCodeError)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return sce.code == code
|
||||
}
|
||||
|
||||
// StatusCodeErrorToCS3Status translate the `statusCodeError` type to CS3 Status
|
||||
// returns nil if `err` does not match to the `statusCodeError` type
|
||||
func StatusCodeErrorToCS3Status(err error) *rpc.Status {
|
||||
var sce statusCodeError
|
||||
ok := errors.As(err, &sce)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if sce.message == "" {
|
||||
sce.message = sce.reason
|
||||
}
|
||||
return &rpc.Status{Message: sce.message, Code: sce.code}
|
||||
}
|
||||
|
||||
// IsSpaceRoot checks if the given resource info is referring to a space root
|
||||
func IsSpaceRoot(ri *storageprovider.ResourceInfo) bool {
|
||||
f := ri.GetId()
|
||||
s := ri.GetSpace().GetRoot()
|
||||
return f.GetOpaqueId() == s.GetOpaqueId() && f.GetSpaceId() == s.GetSpaceId()
|
||||
}
|
||||
|
||||
func checkStatusCode(reason, message string, code rpc.Code) error {
|
||||
if code == rpc.Code_CODE_OK {
|
||||
return nil
|
||||
}
|
||||
return statusCodeError{reason, message, code}
|
||||
}
|
||||
|
||||
func gatherProjectSpaceMembers(ctx context.Context, space *storageprovider.StorageSpace, gwc gateway.GatewayAPIClient, role SpaceRole) ([]string, error) {
|
||||
var permissionsMap map[string]*storageprovider.ResourcePermissions
|
||||
if err := ReadJSONFromOpaque(space.GetOpaque(), "grants", &permissionsMap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groupsMap := make(map[string]struct{})
|
||||
if opaqueGroups, ok := space.Opaque.Map["groups"]; ok {
|
||||
_ = json.Unmarshal(opaqueGroups.GetValue(), &groupsMap)
|
||||
}
|
||||
|
||||
if role == nil {
|
||||
role = AllRole
|
||||
}
|
||||
|
||||
// we use a map to avoid duplicates
|
||||
usermap := make(map[string]struct{})
|
||||
for id, perm := range permissionsMap {
|
||||
if !role(perm) {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, isGroup := groupsMap[id]; !isGroup {
|
||||
usermap[id] = struct{}{}
|
||||
continue
|
||||
}
|
||||
|
||||
usrs, err := GetGroupMembers(ctx, id, gwc)
|
||||
if err != nil {
|
||||
// TODO: continue?
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, u := range usrs {
|
||||
usermap[u] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
users := make([]string, 0, len(usermap))
|
||||
for id := range usermap {
|
||||
users = append(users, id)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func listStorageSpaceRequest(spaceID string) *storageprovider.ListStorageSpacesRequest {
|
||||
return &storageprovider.ListStorageSpacesRequest{
|
||||
Opaque: AppendPlainToOpaque(nil, "unrestricted", "true"),
|
||||
Filters: []*storageprovider.ListStorageSpacesRequest_Filter{
|
||||
{
|
||||
Type: storageprovider.ListStorageSpacesRequest_Filter_TYPE_ID,
|
||||
Term: &storageprovider.ListStorageSpacesRequest_Filter_Id{
|
||||
Id: &storageprovider.StorageSpaceId{
|
||||
OpaqueId: spaceID,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// statusCodeError is a helper struct to return errors
|
||||
type statusCodeError struct {
|
||||
reason string
|
||||
message string // represents the v1beta11.Status.Message
|
||||
code rpc.Code
|
||||
}
|
||||
|
||||
// Error implements error interface
|
||||
func (sce statusCodeError) Error() string {
|
||||
if sce.reason != "" {
|
||||
return fmt.Sprintf(_errStatusCodeTmpl, sce.reason, sce.code)
|
||||
}
|
||||
return fmt.Sprintf(_errStatusCodeTmpl, sce.message, sce.code)
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"os"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/logger"
|
||||
ldapReconnect "github.com/opencloud-eu/reva/v2/pkg/utils/ldap"
|
||||
"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(ctx context.Context, c *LDAPConn) (ldap.Client, error) {
|
||||
var tlsConf *tls.Config
|
||||
if c.Insecure {
|
||||
appctx.GetLogger(ctx).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
|
||||
}
|
||||
+997
@@ -0,0 +1,997 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
identityUser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/google/uuid"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
|
||||
"github.com/rs/zerolog/log"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// Identity provides methods to query users and groups from an LDAP server
|
||||
type Identity struct {
|
||||
User userConfig `mapstructure:",squash"`
|
||||
Group groupConfig `mapstructure:",squash"`
|
||||
Tenant tenantConfig `mapstructure:",squash"`
|
||||
}
|
||||
|
||||
const tracerName = "pkg/utils/ldap"
|
||||
|
||||
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 tenantConfig struct {
|
||||
BaseDN string `mapstructure:"tenant_base_dn"`
|
||||
Scope string `mapstructure:"tenant_search_scope"`
|
||||
scopeVal int
|
||||
Filter string `mapstructure:"tenant_filter"`
|
||||
Objectclass string `mapstructure:"tenant_objectclass"`
|
||||
Schema tenantSchema `mapstructure:"tenant_schema"`
|
||||
}
|
||||
|
||||
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"`
|
||||
// TenantID is the tenant id of the user, if applicable.
|
||||
TenantID string `mapstructure:"tenantId"`
|
||||
}
|
||||
|
||||
type tenantSchema struct {
|
||||
ID string `mapstructure:"id"`
|
||||
ExternalID string `mapstructure:"externalId"`
|
||||
Name string `mapstructure:"name"`
|
||||
}
|
||||
|
||||
// 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",
|
||||
}
|
||||
|
||||
// Default tenantConfig (works with OpenCloud's education Schema)
|
||||
var tenantDefaults = tenantConfig{
|
||||
Scope: "sub",
|
||||
Objectclass: "openCloudEducationSchool",
|
||||
Schema: tenantSchema{
|
||||
ID: "openCloudUUID",
|
||||
ExternalID: "openCloudEducationExternalId",
|
||||
Name: "ou",
|
||||
},
|
||||
}
|
||||
|
||||
// New initializes the default config
|
||||
func New() Identity {
|
||||
return Identity{
|
||||
User: userDefaults,
|
||||
Group: groupDefaults,
|
||||
Tenant: tenantDefaults,
|
||||
}
|
||||
}
|
||||
|
||||
// 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.Tenant.scopeVal, err = stringToScope(i.Tenant.Scope); err != nil {
|
||||
return fmt.Errorf("error configuring tenant 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)
|
||||
}
|
||||
|
||||
if sharedconf.MultiTenantEnabled() {
|
||||
if i.User.Schema.TenantID == "" {
|
||||
return fmt.Errorf("invalid configuration: a 'tenantId' user schema attribute must be defined for multi-tenant setups")
|
||||
}
|
||||
} else {
|
||||
if i.User.Schema.TenantID != "" {
|
||||
return fmt.Errorf("invalid configuration: superfluous 'tenantId' user schema attribute defined for single-tenant setups")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLDAPUserByID looks up a user by the supplied Id. Returns the corresponding
|
||||
// ldap.Entry
|
||||
func (i *Identity) GetLDAPUserByID(ctx context.Context, lc ldap.Client, id *identityUser.UserId) (*ldap.Entry, error) {
|
||||
var filter string
|
||||
var err error
|
||||
if filter, err = i.getUserFilter(id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return i.GetLDAPUserByFilter(ctx, 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(ctx context.Context, lc ldap.Client, attribute, value, tenantID string) (*ldap.Entry, error) {
|
||||
var filter string
|
||||
var err error
|
||||
if filter, err = i.getUserAttributeFilter(attribute, value, tenantID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return i.GetLDAPUserByFilter(ctx, lc, filter)
|
||||
}
|
||||
|
||||
// GetLDAPUserByFilter looks up a single user by the supplied LDAP filter
|
||||
// returns the corresponding ldap.Entry
|
||||
func (i *Identity) GetLDAPUserByFilter(ctx context.Context, lc ldap.Client, filter string) (*ldap.Entry, error) {
|
||||
log := appctx.GetLogger(ctx)
|
||||
_, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "GetLDAPUserByFilter")
|
||||
defer span.End()
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
i.User.BaseDN, i.User.scopeVal, ldap.NeverDerefAliases, 1, 0, false,
|
||||
filter,
|
||||
i.getUserLDAPAttrTypes(),
|
||||
nil,
|
||||
)
|
||||
|
||||
setLDAPSearchSpanAttributes(span, searchRequest)
|
||||
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")
|
||||
classified := classifySearchError(err, fmt.Sprintf("too many results searching for user '%s'", filter))
|
||||
span.SetAttributes(attribute.String("ldap.error", classified.Error()))
|
||||
span.SetStatus(codes.Error, classified.Error())
|
||||
return nil, classified
|
||||
}
|
||||
if len(res.Entries) == 0 {
|
||||
return nil, errtypes.NotFound(filter)
|
||||
}
|
||||
span.SetStatus(codes.Ok, "")
|
||||
|
||||
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(ctx context.Context, lc ldap.Client, dn string) (*ldap.Entry, error) {
|
||||
log := appctx.GetLogger(ctx)
|
||||
_, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "GetLDAPUserByDN")
|
||||
defer span.End()
|
||||
|
||||
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,
|
||||
i.getUserLDAPAttrTypes(),
|
||||
nil,
|
||||
)
|
||||
setLDAPSearchSpanAttributes(span, searchRequest)
|
||||
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")
|
||||
classified := classifySearchError(err, "")
|
||||
span.SetAttributes(attribute.String("ldap.error", classified.Error()))
|
||||
span.SetStatus(codes.Error, classified.Error())
|
||||
return nil, classified
|
||||
}
|
||||
span.SetStatus(codes.Ok, "")
|
||||
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(ctx context.Context, lc ldap.Client, query, tenantID string) ([]*ldap.Entry, error) {
|
||||
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "GetLDAPUsers")
|
||||
defer span.End()
|
||||
log := appctx.GetLogger(ctx)
|
||||
filter := i.getUserFindFilter(query, tenantID)
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
i.User.BaseDN,
|
||||
i.User.scopeVal, ldap.NeverDerefAliases, 0, 0, false,
|
||||
filter,
|
||||
i.getUserLDAPAttrTypes(),
|
||||
nil,
|
||||
)
|
||||
setLDAPSearchSpanAttributes(span, searchRequest)
|
||||
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")
|
||||
classified := classifySearchError(err, "")
|
||||
span.SetAttributes(attribute.String("ldap.error", classified.Error()))
|
||||
span.SetStatus(codes.Error, classified.Error())
|
||||
return nil, classified
|
||||
}
|
||||
|
||||
span.SetAttributes(attribute.Int("ldap.result_count", len(sr.Entries)))
|
||||
span.SetStatus(codes.Ok, "")
|
||||
|
||||
return sr.Entries, nil
|
||||
}
|
||||
|
||||
// IsLDAPUserInDisabledGroup checkes if the user is in the disabled group.
|
||||
func (i *Identity) IsLDAPUserInDisabledGroup(ctx context.Context, lc ldap.Client, userEntry *ldap.Entry) bool {
|
||||
_, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "IsLDAPUserInDisabledGroup")
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(attribute.String("identity.config.disable_mechanism", i.User.DisableMechanism))
|
||||
span.SetStatus(codes.Ok, "")
|
||||
|
||||
// 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,
|
||||
)
|
||||
setLDAPSearchSpanAttributes(span, searchRequest)
|
||||
|
||||
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: treat search failures (including network
|
||||
// errors) as if the user is in the disabled group.
|
||||
span.SetAttributes(attribute.String("ldap.error", err.Error()))
|
||||
span.SetStatus(codes.Error, "")
|
||||
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(ctx context.Context, lc ldap.Client, userEntry *ldap.Entry) ([]string, error) {
|
||||
var memberValue string
|
||||
|
||||
log := appctx.GetLogger(ctx)
|
||||
|
||||
_, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "GetLDAPUserGroups")
|
||||
defer span.End()
|
||||
|
||||
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")
|
||||
setLDAPSearchSpanAttributes(span, searchRequest)
|
||||
sr, err := lc.Search(searchRequest)
|
||||
if err != nil {
|
||||
log.Debug().Str("backend", "ldap").Err(err).Str("filter", filter).Msg("Error looking up group memberships")
|
||||
var lerr *ldap.Error
|
||||
if errors.As(err, &lerr) && lerr.ResultCode == ldap.LDAPResultNoSuchObject {
|
||||
// Don't error out if the search base doesn't exist. We are probably just
|
||||
// not having any groups in LDAP
|
||||
return []string{}, nil
|
||||
}
|
||||
classified := classifySearchError(err, "")
|
||||
span.SetAttributes(attribute.String("ldap.error", classified.Error()))
|
||||
span.SetStatus(codes.Error, classified.Error())
|
||||
return nil, classified
|
||||
}
|
||||
span.SetStatus(codes.Ok, "")
|
||||
span.SetAttributes(attribute.Int("ldap.result_count", len(sr.Entries)))
|
||||
|
||||
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
|
||||
attribute := i.Group.Schema.ID
|
||||
if i.Group.Schema.IDIsOctetString {
|
||||
rawValue := entry.GetEqualFoldRawAttributeValue(attribute)
|
||||
if strings.EqualFold(attribute, "objectguid") {
|
||||
rawValue = SwapObjectGUIDBytes(rawValue)
|
||||
}
|
||||
value, err := uuid.FromBytes(rawValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
groupID = value.String()
|
||||
} else {
|
||||
groupID = entry.GetEqualFoldAttributeValue(attribute)
|
||||
}
|
||||
|
||||
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(ctx context.Context, 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(ctx, 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(ctx context.Context, 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(ctx, lc, filter)
|
||||
}
|
||||
|
||||
// GetLDAPGroupByFilter looks up a single group by the supplied LDAP filter
|
||||
// returns the corresponding ldap.Entry
|
||||
func (i *Identity) GetLDAPGroupByFilter(ctx context.Context, lc ldap.Client, filter string) (*ldap.Entry, error) {
|
||||
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "GetLDAPGroupByFilter")
|
||||
defer span.End()
|
||||
log := appctx.GetLogger(ctx)
|
||||
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,
|
||||
)
|
||||
setLDAPSearchSpanAttributes(span, searchRequest)
|
||||
|
||||
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")
|
||||
classified := classifySearchError(err, fmt.Sprintf("too many results searching for group '%s'", filter))
|
||||
span.SetAttributes(attribute.String("ldap.error", classified.Error()))
|
||||
span.SetStatus(codes.Error, classified.Error())
|
||||
return nil, classified
|
||||
}
|
||||
if len(res.Entries) == 0 {
|
||||
return nil, errtypes.NotFound(filter)
|
||||
}
|
||||
span.SetStatus(codes.Ok, "")
|
||||
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(ctx context.Context, lc ldap.Client, query string) ([]*ldap.Entry, error) {
|
||||
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "GetLDAPGroups")
|
||||
defer span.End()
|
||||
log := appctx.GetLogger(ctx)
|
||||
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,
|
||||
)
|
||||
setLDAPSearchSpanAttributes(span, searchRequest)
|
||||
sr, err := lc.Search(searchRequest)
|
||||
if err != nil {
|
||||
log.Debug().Str("backend", "ldap").Err(err).Str("query", query).Msg("Error search for groups")
|
||||
classified := classifySearchError(err, "")
|
||||
span.SetAttributes(attribute.String("ldap.error", classified.Error()))
|
||||
span.SetStatus(codes.Error, classified.Error())
|
||||
return nil, classified
|
||||
}
|
||||
span.SetStatus(codes.Ok, "")
|
||||
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(ctx context.Context, lc ldap.Client, group *ldap.Entry) ([]*ldap.Entry, error) {
|
||||
log := appctx.GetLogger(ctx)
|
||||
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(ctx, lc, "username", member, "")
|
||||
} else {
|
||||
e, err = i.GetLDAPUserByDN(ctx, 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 filterEscapeAttribute(attribute string, binary bool, id string) (string, error) {
|
||||
var escaped string
|
||||
if binary {
|
||||
pid, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error parsing id '%s' as UUID: %w", id, err)
|
||||
return "", err
|
||||
}
|
||||
escaped = filterEscapeBinaryUUID(attribute, pid)
|
||||
} else {
|
||||
escaped = ldap.EscapeFilter(id)
|
||||
}
|
||||
return escaped, nil
|
||||
}
|
||||
|
||||
// swapObjectGUIDBytes converts between AD's mixed-endian objectGUID format and standard UUID byte order
|
||||
// AD stores objectGUID with mixed endianness 🤪 - swap first 3 components
|
||||
func SwapObjectGUIDBytes(value []byte) []byte {
|
||||
if len(value) != 16 {
|
||||
return value
|
||||
}
|
||||
return []byte{
|
||||
value[3], value[2], value[1], value[0], // First component (4 bytes) - reverse
|
||||
value[5], value[4], // Second component (2 bytes) - reverse
|
||||
value[7], value[6], // Third component (2 bytes) - reverse
|
||||
value[8], value[9], value[10], value[11], value[12], value[13], value[14], value[15], // Last 8 bytes - keep as-is
|
||||
}
|
||||
}
|
||||
|
||||
func filterEscapeBinaryUUID(attribute string, value uuid.UUID) string {
|
||||
bytes := value[:]
|
||||
|
||||
// AD stores objectGUID with mixed endianness 🤪 - swap first 3 components
|
||||
if strings.EqualFold(attribute, "objectguid") {
|
||||
bytes = SwapObjectGUIDBytes(bytes)
|
||||
}
|
||||
|
||||
var filtered strings.Builder
|
||||
filtered.Grow(len(bytes) * 3) // Pre-allocate: each byte becomes "\xx"
|
||||
for _, b := range bytes {
|
||||
fmt.Fprintf(&filtered, "\\%02x", b)
|
||||
}
|
||||
return filtered.String()
|
||||
}
|
||||
|
||||
func (i *Identity) getUserFilter(uid *identityUser.UserId) (string, error) {
|
||||
var escapedUUID string
|
||||
escapedUUID, err := filterEscapeAttribute(i.User.Schema.ID, i.User.Schema.IDIsOctetString, uid.GetOpaqueId())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error parsing OpaqueID '%s' as UUID: %w", uid, err)
|
||||
}
|
||||
return fmt.Sprintf("(&%s(objectclass=%s)%s(%s=%s))",
|
||||
i.User.Filter,
|
||||
i.User.Objectclass,
|
||||
i.tenantFilter(uid.GetTenantId()),
|
||||
i.User.Schema.ID,
|
||||
escapedUUID,
|
||||
), nil
|
||||
}
|
||||
|
||||
func (i *Identity) getUserAttributeFilter(attribute, value, tenantID 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
|
||||
case "tenantid":
|
||||
attribute = i.User.Schema.TenantID
|
||||
default:
|
||||
return "", errors.New("ldap: invalid field " + attribute)
|
||||
}
|
||||
value, err := filterEscapeAttribute(i.User.Schema.ID, i.User.Schema.IDIsOctetString, value)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error parsing attribute '%s' value '%s' as UUID: %w", attribute, value, err)
|
||||
}
|
||||
return fmt.Sprintf("(&%s(objectclass=%s)(%s=%s)%s%s)",
|
||||
i.User.Filter,
|
||||
i.User.Objectclass,
|
||||
attribute,
|
||||
value,
|
||||
i.tenantFilter(tenantID),
|
||||
i.disabledFilter(),
|
||||
), nil
|
||||
}
|
||||
|
||||
func (i *Identity) tenantFilter(tenantID string) string {
|
||||
if tenantID != "" && i.User.Schema.TenantID != "" {
|
||||
return fmt.Sprintf("(%s=%s)", i.User.Schema.TenantID, ldap.EscapeFilter(tenantID))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
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, tenantID 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)
|
||||
}
|
||||
if i.Group.Schema.IDIsOctetString {
|
||||
// try parsing query as uuid
|
||||
idFilter, err := filterEscapeAttribute(i.User.Schema.ID, i.User.Schema.IDIsOctetString, query)
|
||||
if err == nil {
|
||||
filter = fmt.Sprintf("%s(%s=%s)", filter, i.User.Schema.ID, idFilter)
|
||||
}
|
||||
} else {
|
||||
// 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(|%s))",
|
||||
i.User.Filter,
|
||||
i.User.Objectclass,
|
||||
i.tenantFilter(tenantID),
|
||||
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)
|
||||
}
|
||||
if i.Group.Schema.IDIsOctetString {
|
||||
// try parsing query as uuid
|
||||
idFilter, err := filterEscapeAttribute(i.Group.Schema.ID, i.Group.Schema.IDIsOctetString, query)
|
||||
if err == nil {
|
||||
filter = fmt.Sprintf("%s(%s=%s)", filter, i.Group.Schema.ID, idFilter)
|
||||
}
|
||||
} else {
|
||||
// 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) {
|
||||
escapedUUID, err := filterEscapeAttribute(i.Group.Schema.ID, i.Group.Schema.IDIsOctetString, id)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error parsing attribute '%s' value '%s' as UUID: %w", i.Group.Schema.ID, id, err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
value, err := filterEscapeAttribute(i.Group.Schema.ID, i.Group.Schema.IDIsOctetString, value)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error parsing attribute '%s' value '%s' as UUID: %w", attribute, value, err)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Identity) getUserLDAPAttrTypes() []string {
|
||||
// The are the attributes we request unconditionally when looking up users
|
||||
// as they are needed to populate a user object
|
||||
attrs := []string{
|
||||
i.User.Schema.ID,
|
||||
i.User.Schema.Username,
|
||||
i.User.Schema.Mail,
|
||||
i.User.Schema.DisplayName,
|
||||
}
|
||||
|
||||
// Only add optional attributes if they are configured
|
||||
if i.User.Schema.UIDNumber != "" {
|
||||
attrs = append(attrs, i.User.Schema.UIDNumber)
|
||||
}
|
||||
if i.User.Schema.GIDNumber != "" {
|
||||
attrs = append(attrs, i.User.Schema.GIDNumber)
|
||||
}
|
||||
if i.User.Schema.TenantID != "" {
|
||||
attrs = append(attrs, i.User.Schema.TenantID)
|
||||
}
|
||||
if i.User.EnabledProperty != "" {
|
||||
attrs = append(attrs, i.User.EnabledProperty)
|
||||
}
|
||||
if i.User.UserTypeProperty != "" {
|
||||
attrs = append(attrs, i.User.UserTypeProperty)
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
// GetLDAPTenantByID looks up a tenant by the supplied Id. Returns the corresponding
|
||||
// ldap.Entry
|
||||
func (i *Identity) GetLDAPTenantByID(ctx context.Context, lc ldap.Client, id string) (*ldap.Entry, error) {
|
||||
var filter string
|
||||
var err error
|
||||
if filter, err = i.getTenantFilter(id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return i.GetLDAPTenantByFilter(ctx, lc, filter)
|
||||
}
|
||||
|
||||
// GetLDAPTenantByAttribute looks up a single user by attribute (can be "externalid" or "id")
|
||||
func (i *Identity) GetLDAPTenantByAttribute(ctx context.Context, lc ldap.Client, attribute, value string) (*ldap.Entry, error) {
|
||||
var filter string
|
||||
var err error
|
||||
if filter, err = i.getTenantAttributeFilter(attribute, value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return i.GetLDAPTenantByFilter(ctx, lc, filter)
|
||||
}
|
||||
|
||||
// GetLDAPTenantByFilter looks up a single user by the supplied LDAP filter
|
||||
// returns the corresponding ldap.Entry
|
||||
func (i *Identity) GetLDAPTenantByFilter(ctx context.Context, lc ldap.Client, filter string) (*ldap.Entry, error) {
|
||||
log := appctx.GetLogger(ctx)
|
||||
_, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "GetLDAPTenantByFilter")
|
||||
defer span.End()
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
i.Tenant.BaseDN, i.Tenant.scopeVal, ldap.NeverDerefAliases, 1, 0, false,
|
||||
filter,
|
||||
i.getTenantLDAPAttrTypes(),
|
||||
nil,
|
||||
)
|
||||
|
||||
setLDAPSearchSpanAttributes(span, searchRequest)
|
||||
log.Debug().Str("backend", "ldap").Str("basedn", i.Tenant.BaseDN).Str("filter", filter).Int("scope", i.Tenant.scopeVal).Msg("LDAP Search")
|
||||
res, err := lc.Search(searchRequest)
|
||||
if err != nil {
|
||||
log.Debug().Str("backend", "ldap").Err(err).Str("tenantfilter", filter).Msg("Error looking up tenant by filter")
|
||||
classified := classifySearchError(err, fmt.Sprintf("too many results searching for tenant '%s'", filter))
|
||||
span.SetAttributes(attribute.String("ldap.error", classified.Error()))
|
||||
span.SetStatus(codes.Error, classified.Error())
|
||||
return nil, classified
|
||||
}
|
||||
if len(res.Entries) == 0 {
|
||||
return nil, errtypes.NotFound(filter)
|
||||
}
|
||||
span.SetStatus(codes.Ok, "")
|
||||
|
||||
return res.Entries[0], nil
|
||||
}
|
||||
|
||||
func (i *Identity) getTenantLDAPAttrTypes() []string {
|
||||
// The are the attributes we request unconditionally when looking up users
|
||||
// as they are needed to populate a user object
|
||||
return []string{
|
||||
i.Tenant.Schema.ID,
|
||||
i.Tenant.Schema.ExternalID,
|
||||
i.Tenant.Schema.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Identity) getTenantFilter(id string) (string, error) {
|
||||
var escapedUUID string
|
||||
escapedUUID, err := filterEscapeAttribute(i.Tenant.Schema.ID, false, id)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error parsing id '%s' as UUID: %w", id, err)
|
||||
}
|
||||
return fmt.Sprintf("(&%s(objectclass=%s)(%s=%s))",
|
||||
i.Tenant.Filter,
|
||||
i.Tenant.Objectclass,
|
||||
i.Tenant.Schema.ID,
|
||||
escapedUUID,
|
||||
), nil
|
||||
}
|
||||
|
||||
func (i *Identity) getTenantAttributeFilter(attribute, value string) (string, error) {
|
||||
switch attribute {
|
||||
case "id":
|
||||
attribute = i.Tenant.Schema.ID
|
||||
case "externalid":
|
||||
attribute = i.Tenant.Schema.ExternalID
|
||||
}
|
||||
escapedValue, err := filterEscapeAttribute("", false, value)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error escaping filter value %q: %w", value, err)
|
||||
}
|
||||
return fmt.Sprintf("(&%s(objectclass=%s)(%s=%s))",
|
||||
i.Tenant.Filter,
|
||||
i.Tenant.Objectclass,
|
||||
attribute,
|
||||
escapedValue,
|
||||
), nil
|
||||
}
|
||||
|
||||
// classifySearchError maps a raw error from lc.Search to the appropriate
|
||||
// errtypes value:
|
||||
// - ldap.ErrorNetwork → errtypes.Unavailable (transient; caller should retry)
|
||||
// - ldap.LDAPResultSizeLimitExceeded → errtypes.NotFound(sizeExceededMsg)
|
||||
// - anything else → errtypes.NotFound("") (preserving prior behaviour)
|
||||
//
|
||||
// The sizeExceededMsg is only used for the SizeLimitExceeded case; pass an
|
||||
// empty string if the caller does not need a custom message for that case.
|
||||
func classifySearchError(err error, sizeExceededMsg string) error {
|
||||
if ldap.IsErrorWithCode(err, ldap.ErrorNetwork) {
|
||||
return errtypes.Unavailable("ldap server unreachable: " + err.Error())
|
||||
}
|
||||
if sizeExceededMsg != "" && ldap.IsErrorWithCode(err, ldap.LDAPResultSizeLimitExceeded) {
|
||||
return errtypes.NotFound(sizeExceededMsg)
|
||||
}
|
||||
return errtypes.NotFound("")
|
||||
}
|
||||
|
||||
func setLDAPSearchSpanAttributes(span trace.Span, request *ldap.SearchRequest) {
|
||||
span.SetAttributes(
|
||||
attribute.String("ldap.basedn", request.BaseDN),
|
||||
attribute.String("ldap.filter", request.Filter),
|
||||
attribute.Int("ldap.scope", request.Scope),
|
||||
attribute.Int("ldap.size_limit", request.SizeLimit),
|
||||
)
|
||||
}
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"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
|
||||
}
|
||||
|
||||
// PasswordModify implements the ldap.Client interface
|
||||
func (c *ConnWithReconnect) PasswordModify(m *ldap.PasswordModifyRequest) (*ldap.PasswordModifyResult, error) {
|
||||
var err error
|
||||
var res *ldap.PasswordModifyResult
|
||||
|
||||
retryErr := c.retry(func(c ldap.Client) error {
|
||||
res, err = c.PasswordModify(m)
|
||||
return err
|
||||
})
|
||||
|
||||
return res, retryErr
|
||||
}
|
||||
|
||||
// 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) {
|
||||
var (
|
||||
l *ldap.Conn
|
||||
err error
|
||||
)
|
||||
|
||||
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.Error().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() (err error) {
|
||||
conn, err := c.getConnection()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return conn.Close()
|
||||
}
|
||||
|
||||
func (c *ConnWithReconnect) GetLastError() error {
|
||||
conn, err := c.getConnection()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return conn.GetLastError()
|
||||
}
|
||||
|
||||
// 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"))
|
||||
}
|
||||
|
||||
// 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"))
|
||||
}
|
||||
|
||||
// SearchAsync implements the ldap.Client interface
|
||||
func (c *ConnWithReconnect) SearchAsync(ctx context.Context, searchRequest *ldap.SearchRequest, bufferSize int) ldap.Response {
|
||||
// unimplemented
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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"))
|
||||
}
|
||||
|
||||
// DirSync implements the ldap.Client interface
|
||||
func (c *ConnWithReconnect) DirSync(searchRequest *ldap.SearchRequest, flags, maxAttrCount int64, cookie []byte) (*ldap.SearchResult, error) {
|
||||
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
// DirSyncAsync implements the ldap.Client interface
|
||||
func (c *ConnWithReconnect) DirSyncAsync(ctx context.Context, searchRequest *ldap.SearchRequest, bufferSize int, flags, maxAttrCount int64, cookie []byte) ldap.Response {
|
||||
// unimplemented
|
||||
return nil
|
||||
}
|
||||
|
||||
// Syncrepl implements the ldap.Client interface
|
||||
func (c *ConnWithReconnect) Syncrepl(ctx context.Context, searchRequest *ldap.SearchRequest, bufferSize int, mode ldap.ControlSyncRequestMode, cookie []byte, reloadHint bool) ldap.Response {
|
||||
// unimplemented
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ConnWithReconnect) Extended(_ *ldap.ExtendedRequest) (*ldap.ExtendedResponse, error) {
|
||||
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
// Copyright 2018-2023 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package list
|
||||
|
||||
// Map returns a list constructed by appling a function f
|
||||
// to all items in the list l.
|
||||
func Map[T, V any](l []T, f func(T) V) []V {
|
||||
m := make([]V, 0, len(l))
|
||||
for _, e := range l {
|
||||
m = append(m, f(e))
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Remove removes the element in position i from the list.
|
||||
// It does not preserve the order of the original slice.
|
||||
func Remove[T any](l []T, i int) []T {
|
||||
l[i] = l[len(l)-1]
|
||||
return l[:len(l)-1]
|
||||
}
|
||||
|
||||
// TakeFirst returns the first elemen, if any, that satisfies
|
||||
// the predicate p.
|
||||
func TakeFirst[T any](l []T, p func(T) bool) (T, bool) {
|
||||
for _, e := range l {
|
||||
if p(e) {
|
||||
return e, true
|
||||
}
|
||||
}
|
||||
var z T
|
||||
return z, false
|
||||
}
|
||||
|
||||
// ToMap returns a map from l where the keys are obtainined applying
|
||||
// the func k to the elements of l.
|
||||
func ToMap[K comparable, T any](l []T, k func(T) K) map[K]T {
|
||||
m := make(map[K]T, len(l))
|
||||
for _, e := range l {
|
||||
m[k(e)] = e
|
||||
}
|
||||
return m
|
||||
}
|
||||
+520
@@ -0,0 +1,520 @@
|
||||
// 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user