338 lines
11 KiB
Go
338 lines
11 KiB
Go
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)
|
|
}
|