github.com/cs3org/reva/v2@v2.27.7/pkg/share/manager/json/json.go (about)

     1  // Copyright 2018-2021 CERN
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  // In applying this license, CERN does not waive the privileges and immunities
    16  // granted to it by virtue of its status as an Intergovernmental Organization
    17  // or submit itself to any jurisdiction.
    18  
    19  package json
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"io"
    25  	"io/fs"
    26  	"os"
    27  	"strings"
    28  	"sync"
    29  	"time"
    30  
    31  	userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
    32  	rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
    33  	collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
    34  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    35  	typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
    36  	"github.com/cs3org/reva/v2/pkg/appctx"
    37  	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
    38  	"github.com/cs3org/reva/v2/pkg/errtypes"
    39  	"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
    40  	"github.com/cs3org/reva/v2/pkg/share"
    41  	"github.com/golang/protobuf/proto" // nolint:staticcheck // we need the legacy package to convert V1 to V2 messages
    42  	"github.com/google/uuid"
    43  	"github.com/mitchellh/mapstructure"
    44  	"github.com/pkg/errors"
    45  	"google.golang.org/genproto/protobuf/field_mask"
    46  	"google.golang.org/protobuf/encoding/prototext"
    47  
    48  	"github.com/cs3org/reva/v2/pkg/share/manager/registry"
    49  	"github.com/cs3org/reva/v2/pkg/utils"
    50  )
    51  
    52  func init() {
    53  	registry.Register("json", New)
    54  }
    55  
    56  // New returns a new mgr.
    57  func New(m map[string]interface{}) (share.Manager, error) {
    58  	c, err := parseConfig(m)
    59  	if err != nil {
    60  		err = errors.Wrap(err, "error creating a new manager")
    61  		return nil, err
    62  	}
    63  
    64  	if c.GatewayAddr == "" {
    65  		return nil, errors.New("share manager config is missing gateway address")
    66  	}
    67  
    68  	c.init()
    69  
    70  	// load or create file
    71  	model, err := loadOrCreate(c.File)
    72  	if err != nil {
    73  		err = errors.Wrap(err, "error loading the file containing the shares")
    74  		return nil, err
    75  	}
    76  
    77  	return &mgr{
    78  		c:     c,
    79  		model: model,
    80  	}, nil
    81  }
    82  
    83  func loadOrCreate(file string) (*shareModel, error) {
    84  	if info, err := os.Stat(file); errors.Is(err, fs.ErrNotExist) || info.Size() == 0 {
    85  		if err := os.WriteFile(file, []byte("{}"), 0700); err != nil {
    86  			err = errors.Wrap(err, "error opening/creating the file: "+file)
    87  			return nil, err
    88  		}
    89  	}
    90  
    91  	fd, err := os.OpenFile(file, os.O_CREATE, 0644)
    92  	if err != nil {
    93  		err = errors.Wrap(err, "error opening/creating the file: "+file)
    94  		return nil, err
    95  	}
    96  	defer fd.Close()
    97  
    98  	data, err := io.ReadAll(fd)
    99  	if err != nil {
   100  		err = errors.Wrap(err, "error reading the data")
   101  		return nil, err
   102  	}
   103  
   104  	j := &jsonEncoding{}
   105  	if err := json.Unmarshal(data, j); err != nil {
   106  		err = errors.Wrap(err, "error decoding data from json")
   107  		return nil, err
   108  	}
   109  
   110  	m := &shareModel{State: j.State, MountPoint: j.MountPoint}
   111  	for _, s := range j.Shares {
   112  		var decShare collaboration.Share
   113  		if err = utils.UnmarshalJSONToProtoV1([]byte(s), &decShare); err != nil {
   114  			return nil, errors.Wrap(err, "error decoding share from json")
   115  		}
   116  		m.Shares = append(m.Shares, &decShare)
   117  	}
   118  
   119  	if m.State == nil {
   120  		m.State = map[string]map[string]collaboration.ShareState{}
   121  	}
   122  	if m.MountPoint == nil {
   123  		m.MountPoint = map[string]map[string]*provider.Reference{}
   124  	}
   125  
   126  	m.file = file
   127  	return m, nil
   128  }
   129  
   130  type shareModel struct {
   131  	file       string
   132  	State      map[string]map[string]collaboration.ShareState `json:"state"`       // map[username]map[share_id]ShareState
   133  	MountPoint map[string]map[string]*provider.Reference      `json:"mount_point"` // map[username]map[share_id]MountPoint
   134  	Shares     []*collaboration.Share                         `json:"shares"`
   135  }
   136  
   137  type jsonEncoding struct {
   138  	State      map[string]map[string]collaboration.ShareState `json:"state"`       // map[username]map[share_id]ShareState
   139  	MountPoint map[string]map[string]*provider.Reference      `json:"mount_point"` // map[username]map[share_id]MountPoint
   140  	Shares     []string                                       `json:"shares"`
   141  }
   142  
   143  func (m *shareModel) Save() error {
   144  	j := &jsonEncoding{State: m.State, MountPoint: m.MountPoint}
   145  	for _, s := range m.Shares {
   146  		encShare, err := utils.MarshalProtoV1ToJSON(s)
   147  		if err != nil {
   148  			return errors.Wrap(err, "error encoding to json")
   149  		}
   150  		j.Shares = append(j.Shares, string(encShare))
   151  	}
   152  
   153  	data, err := json.Marshal(j)
   154  	if err != nil {
   155  		err = errors.Wrap(err, "error encoding to json")
   156  		return err
   157  	}
   158  
   159  	if err := os.WriteFile(m.file, data, 0644); err != nil {
   160  		err = errors.Wrap(err, "error writing to file: "+m.file)
   161  		return err
   162  	}
   163  
   164  	return nil
   165  }
   166  
   167  type mgr struct {
   168  	c          *config
   169  	sync.Mutex // concurrent access to the file
   170  	model      *shareModel
   171  }
   172  
   173  type config struct {
   174  	File        string `mapstructure:"file"`
   175  	GatewayAddr string `mapstructure:"gateway_addr"`
   176  }
   177  
   178  func (c *config) init() {
   179  	if c.File == "" {
   180  		c.File = "/var/tmp/reva/shares.json"
   181  	}
   182  }
   183  
   184  func parseConfig(m map[string]interface{}) (*config, error) {
   185  	c := &config{}
   186  	if err := mapstructure.Decode(m, c); err != nil {
   187  		return nil, err
   188  	}
   189  	return c, nil
   190  }
   191  
   192  // Dump exports shares and received shares to channels (e.g. during migration)
   193  func (m *mgr) Dump(ctx context.Context, shareChan chan<- *collaboration.Share, receivedShareChan chan<- share.ReceivedShareWithUser) error {
   194  	log := appctx.GetLogger(ctx)
   195  	for _, s := range m.model.Shares {
   196  		shareChan <- s
   197  	}
   198  
   199  	for userIDString, states := range m.model.State {
   200  		userMountPoints := m.model.MountPoint[userIDString]
   201  		id := &userv1beta1.UserId{}
   202  		mV2 := proto.MessageV2(id)
   203  		if err := prototext.Unmarshal([]byte(userIDString), mV2); err != nil {
   204  			log.Error().Err(err).Msg("error unmarshalling the user id")
   205  			continue
   206  		}
   207  
   208  		for shareIDString, state := range states {
   209  			sid := &collaboration.ShareId{}
   210  			mV2 := proto.MessageV2(sid)
   211  			if err := prototext.Unmarshal([]byte(shareIDString), mV2); err != nil {
   212  				log.Error().Err(err).Msg("error unmarshalling the user id")
   213  				continue
   214  			}
   215  
   216  			var s *collaboration.Share
   217  			for _, is := range m.model.Shares {
   218  				if is.Id.OpaqueId == sid.OpaqueId {
   219  					s = is
   220  					break
   221  				}
   222  			}
   223  			if s == nil {
   224  				log.Warn().Str("share id", sid.OpaqueId).Msg("Share not found")
   225  				continue
   226  			}
   227  
   228  			var mp *provider.Reference
   229  			if userMountPoints != nil {
   230  				mp = userMountPoints[shareIDString]
   231  			}
   232  
   233  			receivedShareChan <- share.ReceivedShareWithUser{
   234  				UserID: id,
   235  				ReceivedShare: &collaboration.ReceivedShare{
   236  					Share:      s,
   237  					State:      state,
   238  					MountPoint: mp,
   239  				},
   240  			}
   241  		}
   242  	}
   243  
   244  	return nil
   245  }
   246  
   247  func (m *mgr) Share(ctx context.Context, md *provider.ResourceInfo, g *collaboration.ShareGrant) (*collaboration.Share, error) {
   248  	id := uuid.NewString()
   249  	user := ctxpkg.ContextMustGetUser(ctx)
   250  	now := time.Now().UnixNano()
   251  	ts := &typespb.Timestamp{
   252  		Seconds: uint64(now / int64(time.Second)),
   253  		Nanos:   uint32(now % int64(time.Second)),
   254  	}
   255  
   256  	// do not allow share to myself or the owner if share is for a user
   257  	// TODO(labkode): should not this be caught already at the gw level?
   258  	if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER &&
   259  		(utils.UserEqual(g.Grantee.GetUserId(), user.Id) || utils.UserEqual(g.Grantee.GetUserId(), md.Owner)) {
   260  		return nil, errtypes.BadRequest("json: owner/creator and grantee are the same")
   261  	}
   262  
   263  	// check if share already exists.
   264  	key := &collaboration.ShareKey{
   265  		Owner:      md.Owner,
   266  		ResourceId: md.Id,
   267  		Grantee:    g.Grantee,
   268  	}
   269  
   270  	m.Lock()
   271  	defer m.Unlock()
   272  	_, _, err := m.getByKey(key)
   273  	if err == nil {
   274  		// share already exists
   275  		return nil, errtypes.AlreadyExists(key.String())
   276  	}
   277  
   278  	s := &collaboration.Share{
   279  		Id: &collaboration.ShareId{
   280  			OpaqueId: id,
   281  		},
   282  		ResourceId:  md.Id,
   283  		Permissions: g.Permissions,
   284  		Grantee:     g.Grantee,
   285  		Owner:       md.Owner,
   286  		Creator:     user.Id,
   287  		Ctime:       ts,
   288  		Mtime:       ts,
   289  	}
   290  
   291  	m.model.Shares = append(m.model.Shares, s)
   292  	if err := m.model.Save(); err != nil {
   293  		err = errors.Wrap(err, "error saving model")
   294  		return nil, err
   295  	}
   296  
   297  	return s, nil
   298  }
   299  
   300  // getByID must be called in a lock-controlled block.
   301  func (m *mgr) getByID(id *collaboration.ShareId) (int, *collaboration.Share, error) {
   302  	for i, s := range m.model.Shares {
   303  		if s.GetId().OpaqueId == id.OpaqueId {
   304  			return i, s, nil
   305  		}
   306  	}
   307  	return -1, nil, errtypes.NotFound(id.String())
   308  }
   309  
   310  // getByKey must be called in a lock-controlled block.
   311  func (m *mgr) getByKey(key *collaboration.ShareKey) (int, *collaboration.Share, error) {
   312  	for i, s := range m.model.Shares {
   313  		if (utils.UserEqual(key.Owner, s.Owner) || utils.UserEqual(key.Owner, s.Creator)) &&
   314  			utils.ResourceIDEqual(key.ResourceId, s.ResourceId) && utils.GranteeEqual(key.Grantee, s.Grantee) {
   315  			return i, s, nil
   316  		}
   317  	}
   318  	return -1, nil, errtypes.NotFound(key.String())
   319  }
   320  
   321  // get must be called in a lock-controlled block.
   322  func (m *mgr) get(ref *collaboration.ShareReference) (idx int, s *collaboration.Share, err error) {
   323  	switch {
   324  	case ref.GetId() != nil:
   325  		idx, s, err = m.getByID(ref.GetId())
   326  	case ref.GetKey() != nil:
   327  		idx, s, err = m.getByKey(ref.GetKey())
   328  	default:
   329  		err = errtypes.NotFound(ref.String())
   330  	}
   331  	return
   332  }
   333  
   334  func (m *mgr) GetShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.Share, error) {
   335  	m.Lock()
   336  	defer m.Unlock()
   337  	_, s, err := m.get(ref)
   338  	if err != nil {
   339  		return nil, err
   340  	}
   341  	// check if we are the owner or the grantee
   342  	user := ctxpkg.ContextMustGetUser(ctx)
   343  	if share.IsCreatedByUser(s, user) || share.IsGrantedToUser(s, user) {
   344  		return s, nil
   345  	}
   346  	// we return not found to not disclose information
   347  	return nil, errtypes.NotFound(ref.String())
   348  }
   349  
   350  func (m *mgr) Unshare(ctx context.Context, ref *collaboration.ShareReference) error {
   351  	m.Lock()
   352  	defer m.Unlock()
   353  	user := ctxpkg.ContextMustGetUser(ctx)
   354  
   355  	idx, s, err := m.get(ref)
   356  	if err != nil {
   357  		return err
   358  	}
   359  	if !share.IsCreatedByUser(s, user) {
   360  		return errtypes.NotFound(ref.String())
   361  	}
   362  
   363  	last := len(m.model.Shares) - 1
   364  	m.model.Shares[idx] = m.model.Shares[last]
   365  	// explicitly nil the reference to prevent memory leaks
   366  	// https://github.com/golang/go/wiki/SliceTricks#delete-without-preserving-order
   367  	m.model.Shares[last] = nil
   368  	m.model.Shares = m.model.Shares[:last]
   369  	if err := m.model.Save(); err != nil {
   370  		err = errors.Wrap(err, "error saving model")
   371  		return err
   372  	}
   373  	return nil
   374  }
   375  
   376  func (m *mgr) UpdateShare(ctx context.Context, ref *collaboration.ShareReference, p *collaboration.SharePermissions, updated *collaboration.Share, fieldMask *field_mask.FieldMask) (*collaboration.Share, error) {
   377  	m.Lock()
   378  	defer m.Unlock()
   379  
   380  	var (
   381  		idx      int
   382  		toUpdate *collaboration.Share
   383  	)
   384  
   385  	if ref != nil {
   386  		var err error
   387  		idx, toUpdate, err = m.get(ref)
   388  		if err != nil {
   389  			return nil, err
   390  		}
   391  	} else if updated != nil {
   392  		var err error
   393  		idx, toUpdate, err = m.getByID(updated.Id)
   394  		if err != nil {
   395  			return nil, err
   396  		}
   397  	}
   398  
   399  	if fieldMask != nil {
   400  		for i := range fieldMask.Paths {
   401  			switch fieldMask.Paths[i] {
   402  			case "permissions":
   403  				m.model.Shares[idx].Permissions = updated.Permissions
   404  			case "expiration":
   405  				m.model.Shares[idx].Expiration = updated.Expiration
   406  			case "hidden":
   407  				continue
   408  			default:
   409  				return nil, errtypes.NotSupported("updating " + fieldMask.Paths[i] + " is not supported")
   410  			}
   411  		}
   412  	}
   413  
   414  	user := ctxpkg.ContextMustGetUser(ctx)
   415  	if !share.IsCreatedByUser(toUpdate, user) {
   416  		return nil, errtypes.NotFound(ref.String())
   417  	}
   418  
   419  	now := time.Now().UnixNano()
   420  	if p != nil {
   421  		m.model.Shares[idx].Permissions = p
   422  	}
   423  	m.model.Shares[idx].Mtime = &typespb.Timestamp{
   424  		Seconds: uint64(now / int64(time.Second)),
   425  		Nanos:   uint32(now % int64(time.Second)),
   426  	}
   427  
   428  	if err := m.model.Save(); err != nil {
   429  		err = errors.Wrap(err, "error saving model")
   430  		return nil, err
   431  	}
   432  	return m.model.Shares[idx], nil
   433  }
   434  
   435  func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.Share, error) {
   436  	m.Lock()
   437  	defer m.Unlock()
   438  	log := appctx.GetLogger(ctx)
   439  	user := ctxpkg.ContextMustGetUser(ctx)
   440  
   441  	client, err := pool.GetGatewayServiceClient(m.c.GatewayAddr)
   442  	if err != nil {
   443  		return nil, errors.Wrap(err, "failed to list shares")
   444  	}
   445  	cache := make(map[string]struct{})
   446  	var ss []*collaboration.Share
   447  	for _, s := range m.model.Shares {
   448  		if share.MatchesFilters(s, filters) {
   449  			// Only add the share if the share was created by the user or if
   450  			// the user has ListGrants permissions on the shared resource.
   451  			// The ListGrants check is necessary when a space member wants
   452  			// to list shares in a space.
   453  			// We are using a cache here so that we don't have to stat a
   454  			// resource multiple times.
   455  			key := strings.Join([]string{s.ResourceId.StorageId, s.ResourceId.OpaqueId}, "!")
   456  			if _, hit := cache[key]; !hit && !share.IsCreatedByUser(s, user) {
   457  				sRes, err := client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: s.ResourceId}})
   458  				if err != nil || sRes.Status.Code != rpcv1beta1.Code_CODE_OK {
   459  					log.Error().
   460  						Err(err).
   461  						Interface("status", sRes.Status).
   462  						Interface("resource_id", s.ResourceId).
   463  						Msg("ListShares: could not stat resource")
   464  					continue
   465  				}
   466  				if !sRes.Info.PermissionSet.ListGrants {
   467  					continue
   468  				}
   469  				cache[key] = struct{}{}
   470  			}
   471  			ss = append(ss, s)
   472  		}
   473  	}
   474  	return ss, nil
   475  }
   476  
   477  // we list the shares that are targeted to the user in context or to the user groups.
   478  func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter, forUser *userv1beta1.UserId) ([]*collaboration.ReceivedShare, error) {
   479  	m.Lock()
   480  	defer m.Unlock()
   481  
   482  	user := ctxpkg.ContextMustGetUser(ctx)
   483  	if user.GetId().GetType() == userv1beta1.UserType_USER_TYPE_SERVICE {
   484  		gwc, err := pool.GetGatewayServiceClient(m.c.GatewayAddr)
   485  		if err != nil {
   486  			return nil, errors.Wrap(err, "failed to list shares")
   487  		}
   488  		u, err := utils.GetUser(forUser, gwc)
   489  		if err != nil {
   490  			return nil, errtypes.BadRequest("user not found")
   491  		}
   492  		user = u
   493  	}
   494  	mem := make(map[string]int)
   495  	var rss []*collaboration.ReceivedShare
   496  	for _, s := range m.model.Shares {
   497  		if !share.IsCreatedByUser(s, user) &&
   498  			share.IsGrantedToUser(s, user) &&
   499  			share.MatchesFilters(s, filters) {
   500  
   501  			rs := m.convert(user.Id, s)
   502  			idx, seen := mem[s.ResourceId.OpaqueId]
   503  			if !seen {
   504  				rss = append(rss, rs)
   505  				mem[s.ResourceId.OpaqueId] = len(rss) - 1
   506  				continue
   507  			}
   508  
   509  			// When we arrive here there was already a share for this resource.
   510  			// if there is a mix-up of shares of type group and shares of type user we need to deduplicate them, since it points
   511  			// to the same resource. Leave the more explicit and hide the less explicit. In this case we hide the group shares
   512  			// and return the user share to the user.
   513  			other := rss[idx]
   514  			if other.Share.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_GROUP && s.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER {
   515  				if other.State == rs.State {
   516  					rss[idx] = rs
   517  				} else {
   518  					rss = append(rss, rs)
   519  				}
   520  			}
   521  		}
   522  	}
   523  
   524  	return rss, nil
   525  }
   526  
   527  // convert must be called in a lock-controlled block.
   528  func (m *mgr) convert(currentUser *userv1beta1.UserId, s *collaboration.Share) *collaboration.ReceivedShare {
   529  	rs := &collaboration.ReceivedShare{
   530  		Share: s,
   531  		State: collaboration.ShareState_SHARE_STATE_PENDING,
   532  	}
   533  	if v, ok := m.model.State[currentUser.String()]; ok {
   534  		if state, ok := v[s.Id.String()]; ok {
   535  			rs.State = state
   536  		}
   537  	}
   538  	if v, ok := m.model.MountPoint[currentUser.String()]; ok {
   539  		if mp, ok := v[s.Id.String()]; ok {
   540  			rs.MountPoint = mp
   541  		}
   542  	}
   543  	return rs
   544  }
   545  
   546  func (m *mgr) GetReceivedShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.ReceivedShare, error) {
   547  	return m.getReceived(ctx, ref)
   548  }
   549  
   550  func (m *mgr) getReceived(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.ReceivedShare, error) {
   551  	m.Lock()
   552  	defer m.Unlock()
   553  	_, s, err := m.get(ref)
   554  	if err != nil {
   555  		return nil, err
   556  	}
   557  	user := ctxpkg.ContextMustGetUser(ctx)
   558  	if user.GetId().GetType() != userv1beta1.UserType_USER_TYPE_SERVICE && !share.IsGrantedToUser(s, user) {
   559  		return nil, errtypes.NotFound(ref.String())
   560  	}
   561  	return m.convert(user.Id, s), nil
   562  }
   563  
   564  func (m *mgr) UpdateReceivedShare(ctx context.Context, receivedShare *collaboration.ReceivedShare, fieldMask *field_mask.FieldMask, forUser *userv1beta1.UserId) (*collaboration.ReceivedShare, error) {
   565  	rs, err := m.getReceived(ctx, &collaboration.ShareReference{Spec: &collaboration.ShareReference_Id{Id: receivedShare.Share.Id}})
   566  	if err != nil {
   567  		return nil, err
   568  	}
   569  
   570  	m.Lock()
   571  	defer m.Unlock()
   572  
   573  	for i := range fieldMask.Paths {
   574  		switch fieldMask.Paths[i] {
   575  		case "state":
   576  			rs.State = receivedShare.State
   577  		case "mount_point":
   578  			rs.MountPoint = receivedShare.MountPoint
   579  		default:
   580  			return nil, errtypes.NotSupported("updating " + fieldMask.Paths[i] + " is not supported")
   581  		}
   582  	}
   583  
   584  	u := ctxpkg.ContextMustGetUser(ctx)
   585  	uid := u.GetId().String()
   586  	if u.GetId().GetType() == userv1beta1.UserType_USER_TYPE_SERVICE {
   587  		uid = forUser.String()
   588  	}
   589  	// Persist state
   590  	if v, ok := m.model.State[uid]; ok {
   591  		v[rs.Share.Id.String()] = rs.State
   592  		m.model.State[uid] = v
   593  	} else {
   594  		a := map[string]collaboration.ShareState{
   595  			rs.Share.Id.String(): rs.State,
   596  		}
   597  		m.model.State[uid] = a
   598  	}
   599  
   600  	// Persist mount point
   601  	if v, ok := m.model.MountPoint[uid]; ok {
   602  		v[rs.Share.Id.String()] = rs.MountPoint
   603  		m.model.MountPoint[uid] = v
   604  	} else {
   605  		a := map[string]*provider.Reference{
   606  			rs.Share.Id.String(): rs.MountPoint,
   607  		}
   608  		m.model.MountPoint[uid] = a
   609  	}
   610  
   611  	if err := m.model.Save(); err != nil {
   612  		err = errors.Wrap(err, "error saving model")
   613  		return nil, err
   614  	}
   615  
   616  	return rs, nil
   617  }