Multitenancy in a single bucket and bucket/prefix usaged

Forgive me if this has been asked already, or if there is a really simple solution, but I have been following along the documentation and trying to write a simple program that allows multiple users to use a single bucket depending on their prefix.

This is the code thus far, name auth.go:

package main

import (
   "crypto/sha256"
   "log"
   "time"
   "fmt"
   "context"
   "storj.io/uplink"
)

func hashUser(userId string, userPass string) []byte{
   str := userId + userPass
   data := []byte(str)
   hash := sha256.Sum256(data)
   return hash[:]
}

func createUserToken(ctx context.Context, accessGrant, appBucket, userId, userPass string) (string, error) {

   appAccess, err := uplink.ParseAccess(accessGrant)
   if err != nil{
      return "", err
   }

   //create a user access grant for accessing their files, limited for the next 8 hours.
   now := time.Now()
   permission := uplink.FullPermission()

   //2 minutes leeway to avoid time sync issues with the satellite
   permission.NotBefore = now.Add(-2 * time.Minute)
   //permission.NotAfter = now.Add(8 * time.Hour)
   userPrefix := uplink.SharePrefix{
      Bucket: appBucket,
      Prefix: userId + "/",
   }

   userAccess, err := appAccess.Share(permission, userPrefix)
   if err != nil {
      return "", err
   }



   userSalt := hashUser(userId, userPass)

   saltedUserKey, err := uplink.DeriveEncryptionKey(userPass, userSalt)
   if err != nil {
      return "", err
   }

   err = userAccess.OverrideEncryptionKey(appBucket, userId+"/", saltedUserKey)
   if err != nil {
      return "", err
   }

   //serialize the user access grant
   serializedAccess, err := userAccess.Serialize()
   if err != nil {
      return "", err
   }


   //Open up the Project we will be working with.
   project, err := uplink.OpenProject(ctx, userAccess)
   if err != nil {
      return "", err
   }
   defer project.Close()

   //Ensure the desired Bucket within the Project is created.
   _, err = project.EnsureBucket(ctx, appBucket)
   if err != nil {
      return "", err
   }

   return serializedAccess, err
}

func main() {

   userId := "evan"
   userPass := "pass"

   accGrant := [ACCESS GRANT]
   bucket := "storproj"

   serializedUserAccess, err := createUserToken(context.Background(), accGrant, bucket, userId, userPass)
   if err != nil {
      log.Fatalln("error: ", err)
   }

   fmt.Println(serializedUserAccess)
}

This ends up returning the error:

2021/07/24 00:25:56 error:  uplink: permission denied (uplink: permission denied (bucket: metainfo error: Unauthorized API credentials))
exit status 1

This initially lead me to believe that the access token that i am generating is invalid in some way.

However, I disproved this by removing these two lines from my original program:

   //Open up the Project we will be working with.
   project, err := uplink.OpenProject(ctx, userAccess)
   if err != nil {
      return "", err
   }
   defer project.Close()

   //Ensure the desired Bucket within the Project is created.
   _, err = project.EnsureBucket(ctx, appBucket)
   if err != nil {
      return "", err
   }

Leaving me with:

package main

import (
   "crypto/sha256"
   "log"
   "time"
   "fmt"
   "context"
   "storj.io/uplink"
)

func hashUser(userId string, userPass string) []byte{
   str := userId + userPass
   data := []byte(str)
   hash := sha256.Sum256(data)
   return hash[:]
}

func createUserToken(ctx context.Context, accessGrant, appBucket, userId, userPass string) (string, error) {

   appAccess, err := uplink.ParseAccess(accessGrant)
   if err != nil{
      return "", err
   }

   //create a user access grant for accessing their files, limited for the next 8 hours.
   now := time.Now()
   permission := uplink.FullPermission()

   //2 minutes leeway to avoid time sync issues with the satellite
   permission.NotBefore = now.Add(-2 * time.Minute)
   //permission.NotAfter = now.Add(8 * time.Hour)
   userPrefix := uplink.SharePrefix{
      Bucket: appBucket,
      Prefix: userId + "/",
   }

   userAccess, err := appAccess.Share(permission, userPrefix)
   if err != nil {
      return "", err
   }



   userSalt := hashUser(userId, userPass)

   saltedUserKey, err := uplink.DeriveEncryptionKey(userPass, userSalt)
   if err != nil {
      return "", err
   }

   err = userAccess.OverrideEncryptionKey(appBucket, userId+"/", saltedUserKey)
   if err != nil {
      return "", err
   }

   //serialize the user access grant
   serializedAccess, err := userAccess.Serialize()
   if err != nil {
      return "", err
   }

   return serializedAccess, err
}

func main() {

   userId := "evan"
   userPass := "pass"

   accGrant := [ACCESS GRANT]
   bucket := "storproj"

   serializedUserAccess, err := createUserToken(context.Background(), accGrant, bucket, userId, userPass)
   if err != nil {
      log.Fatalln("error: ", err)
   }

   fmt.Println(serializedUserAccess)
}

And running the command:

uplink --access $(go run auth.go) cp img.jpg sj://storproj/evan/img.jpg

This successfully creates and uploads img.jpg

Also, if I change the userId to “evan2” instead of “evan” and run the command:

uplink --access $(go run auth.go) ls sj://storproj/evan

I receive:

Error: uplink: permission denied ("evan")

As expected.

So, the only conclusion that I can draw is that the bucket name I am passing to

   //Ensure the desired Bucket within the Project is created.
   _, err = project.EnsureBucket(ctx, appBucket)
   if err != nil {
      return "", err
   }

is incorrect as it is simply the original buckets name.
Now, this makes sense because the user doesn’t have access to the entire bucket, but if I add

appBucket = appBucket+"/"+userId

before calling EnsureBucket, then I still receive the original error.

So, I guess my ultimate question is, where am I going wrong? I feel its dependent on how I’m supposed to combine the bucket name and its prefix, but I couldn’t find anything in the docs or past forum posts on how to go about doing that. Also, for all I know, there is something big that I am missing entirely.

I am not sure but maybe you must call OverrideEncryptionKey before Share.

Can you let us know if that works?

It doesn’t look like I can. Share returns userAccess which is what I call OverrideEncryptionKey on.

I looked a bit more carefull and the problem is obvious: you can’t call EnsureBucket when you have only given permission on a prefix within that bucket.

I hope you are able to get your code working.

1 Like