satellite/console/wasm: expose method to add caveats in the browser

This PR does the following three things:
    1. Defines a high-level interface for this wasm package
        - All return value from this package will be wrapped with an
          result object that contains a value field and an error field
    2. Exposes two new functions to allow users to add permissions for a
       given API key
        - newPermission()
        - setAPIKeyPermission()
    3. Adds API documentation for the newly added API functions

Change-Id: Id995189702b369bba18fa344bef4ddfb0f3f1f44
This commit is contained in:
Yingrong Zhao 2020-11-05 21:00:55 -05:00 committed by Yingrong Zhao
parent 5f840aae6e
commit 2ce3170bb4
2 changed files with 323 additions and 11 deletions

View File

@ -33,3 +33,149 @@ The HTML file should include the following:
Additionally, the HTTP `Content-Security-Policy (CSP) script-src` directive will need to be modified to allow wasm code to be executed.
See: [WebAssembly Content Security Policy docs](https://github.com/WebAssembly/content-security-policy/blob/master/proposals/CSP.md)
### Usage
#### function newPermission
```js
function newPermission()
```
- **Returns:**
```js
{
value: {
AllowDownload: false,
AllowUpload: false,
AllowDelete: false,
AllowList: false,
NotBefore: "0001-01-01T00:00:00Z",
NotAfter: "0001-01-01T00:00:00Z",
},
error: ""
}
```
- **Usage:**
newPermission creates a new Permission object with all available permission settings set to default value.
- **Example:**
```js
var permission = newPermission().value;
permission.AllowedDownload = true;
```
### function setAPIKeyPermission
```js
function setAPIKeyPermission(apiKey, buckets, permission)
```
- **Arguments**
Accepts three arguments: `apiKey`, `buckets` and `permission`
- **apiKey**
- **Type:** `String`
- **Details:**
This parameter is required
- **buckets**
- **Type:** `Array`
- **Details:**
An array of bucket names that restrict the api key to only contain enough information to allow access to just those buckets.
If no bucket names are provided, meaning an empty array, then all buckets are allowed.
This parameter is required.
- **permission**
- **Type:** `Object`
- **Details:**
An object that defines what actions can be used for a given api key.
It should be constructed by calling `newPermission`
See also: https://github.com/storj/uplink/blob/b8e0f0a90665143a8ce975d92530737130874f5a/access.go#L46
This parameter is required.
- **Returns**
```js
{
value: "restricted-api-key",
error: ""
}
```
- if an error message is returned, `value` will be set to an empty string.
- **Usage**
Creates a new api key with specific permissions.
- **Example**
```js
var apiKey = "super-secret-key";
var buckets = ["test-bucket"];
var permission = newPermission().value
permission.allowUpload = true
var restrictedAPIKey = setAPIKeyPermission(apiKey, buckets, permission)
```
### function generateAccessGrant
```js
function generateAccessGrant(satelliteNodeURL, apiKey, encryptionPassphrase, projectID)
```
- **Arguments**
Accepts four arguments: `satelliteNodeURL`, `apiKey`, `encryptionPassphrase` and `projectID`
- **satelliteNodeURL**
- **Type:** `String`
- **Details:**
A string that contains satellite node id and satellite address.
Example: 12tDhBcuMevundiuZPQJd613iW5vCdFtkRDBjBEfjdVtv1hbfCL@127.0.0.1:10000
This parameter is required
- **apiKey**
- **Type:** `String`
- **Details:**
This parameter is required
- **encryptionPassphrase**
- **Type:** `String`
- **Details:**
A string that's used for encryption.
This parameter is required.
- **projectID**
- **Type:** `String`
- **Details:**
A project-based salt for determinitic key derivation.
Currently it's referring to a project ID. However, it might change in the future to have more randomness.
This parameter is required.
- **Returns**
```js
{
value: "access-grant",
error: ""
}
```
- if an error message is returned, `value` will be set to an empty string.
- **Usage**
Creates a new api key with specific permissions.
- **Example**
```js
var satelliteNodeURL = "12tDhBcuMevundiuZPQJd613iW5vCdFtkRDBjBEfjdVtv1hbfCL@127.0.0.1:10000"
var apiKey = "super-secret-key";
var passphrase = "123";
var projectID = "project-id"
var result = generateAccessGrant(satelliteNodeURL, apiKey, passphrase, projectID);
if (result.err != "") {
// something went wrong
}
var access = result.value
```

View File

@ -5,48 +5,68 @@
package main
import (
"fmt"
"crypto/sha256"
"encoding/json"
"syscall/js"
"time"
"github.com/zeebo/errs"
"storj.io/common/encryption"
"storj.io/common/macaroon"
"storj.io/common/storj"
"storj.io/common/uuid"
"storj.io/uplink"
"storj.io/uplink/private/access2"
)
func main() {
js.Global().Set("generateAccessGrant", generateAccessGrant())
js.Global().Set("setAPIKeyPermission", setAPIKeyPermission())
js.Global().Set("newPermission", newPermission())
<-make(chan bool)
}
func generateAccessGrant() js.Func {
return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return js.FuncOf(responseHandler(func(this js.Value, args []js.Value) (interface{}, error) {
if len(args) < 4 {
return fmt.Sprintf("Error not enough arguments. Need 4, but only %d supplied. The order of arguments are: satellite Node URL, API key, encryption passphrase, and project salt.", len(args))
return nil, errs.New("not enough arguments. Need 4, but only %d supplied. The order of arguments are: satellite Node URL, API key, encryption passphrase, and project ID.", len(args))
}
satelliteNodeURL := args[0].String()
apiKey := args[1].String()
encryptionPassphrase := args[2].String()
projectSalt := args[3].String()
return genAccessGrant(satelliteNodeURL,
access, err := genAccessGrant(satelliteNodeURL,
apiKey,
encryptionPassphrase,
projectSalt,
)
})
if err != nil {
return nil, err
}
return access, nil
}))
}
func genAccessGrant(satelliteNodeURL, apiKey, encryptionPassphrase, projectSalt string) string {
func genAccessGrant(satelliteNodeURL, apiKey, encryptionPassphrase, projectID string) (string, error) {
parsedAPIKey, err := macaroon.ParseAPIKey(apiKey)
if err != nil {
return err.Error()
return "", err
}
id, err := uuid.FromString(projectID)
if err != nil {
return "", err
}
const concurrency = 8
key, err := encryption.DeriveRootKey([]byte(encryptionPassphrase), []byte(projectSalt), "", concurrency)
salt := sha256.Sum256(id[:])
key, err := encryption.DeriveRootKey([]byte(encryptionPassphrase), salt[:], "", concurrency)
if err != nil {
return err.Error()
return "", err
}
encAccess := access2.NewEncryptionAccessWithDefaultKey(key)
@ -58,7 +78,153 @@ func genAccessGrant(satelliteNodeURL, apiKey, encryptionPassphrase, projectSalt
}
accessString, err := a.Serialize()
if err != nil {
return err.Error()
return "", err
}
return accessString, nil
}
// setAPIKeyPermission creates a new api key with specific permissions.
func setAPIKeyPermission() js.Func {
return js.FuncOf(responseHandler(func(this js.Value, args []js.Value) (interface{}, error) {
if len(args) < 3 {
return nil, errs.New("not enough arguments. Need 3, but only %d supplied. The order of arguments are: API key, bucket names, and permission object.", len(args))
}
apiKey := args[0].String()
// convert array of bucket names to go []string type
buckets := args[1]
if ok := buckets.InstanceOf(js.Global().Get("Array")); !ok {
return nil, errs.New("invalid data type. Expect Array, Got %s", buckets.Type().String())
}
bucketNames, err := parseArrayOfStrings(buckets)
if err != nil {
return nil, err
}
// convert js permission to go permission type
permissionJS := args[2]
if permissionJS.Type() != js.TypeObject {
return nil, errs.New("invalid argument type. Expect %s, Got %s", js.TypeObject.String(), permissionJS.Type().String())
}
permission, err := parsePermission(permissionJS)
if err != nil {
return nil, err
}
restrictedKey, err := setPermission(apiKey, bucketNames, permission)
if err != nil {
return nil, err
}
return restrictedKey.Serialize(), nil
}))
}
// newPermission creates a new permission object.
func newPermission() js.Func {
return js.FuncOf(responseHandler(func(this js.Value, args []js.Value) (interface{}, error) {
p, err := json.Marshal(uplink.Permission{})
if err != nil {
return nil, err
}
var jsObj map[string]interface{}
err = json.Unmarshal(p, &jsObj)
if err != nil {
return nil, err
}
return jsObj, nil
}))
}
func setPermission(key string, buckets []string, permission uplink.Permission) (*macaroon.APIKey, error) {
if permission == (uplink.Permission{}) {
return nil, errs.New("permission is empty")
}
var notBefore, notAfter *time.Time
if !permission.NotBefore.IsZero() {
notBefore = &permission.NotBefore
}
if !permission.NotAfter.IsZero() {
notAfter = &permission.NotAfter
}
if notBefore != nil && notAfter != nil && notAfter.Before(*notBefore) {
return nil, errs.New("invalid time range")
}
caveat := macaroon.Caveat{
DisallowReads: !permission.AllowDownload,
DisallowWrites: !permission.AllowUpload,
DisallowLists: !permission.AllowList,
DisallowDeletes: !permission.AllowDelete,
NotBefore: notBefore,
NotAfter: notAfter,
}
for _, b := range buckets {
caveat.AllowedPaths = append(caveat.AllowedPaths, &macaroon.Caveat_Path{
Bucket: []byte(b),
})
}
apiKey, err := macaroon.ParseAPIKey(key)
if err != nil {
return nil, err
}
restrictedKey, err := apiKey.Restrict(caveat)
if err != nil {
return nil, err
}
return restrictedKey, nil
}
func parsePermission(arg js.Value) (uplink.Permission, error) {
var permission uplink.Permission
// convert javascript object to a json string
jsJSON := js.Global().Get("JSON")
p := jsJSON.Call("stringify", arg)
err := json.Unmarshal([]byte(p.String()), &permission)
if err != nil {
return permission, err
}
return permission, nil
}
func parseArrayOfStrings(arg js.Value) ([]string, error) {
data := make([]string, arg.Length())
for i := 0; i < arg.Length(); i++ {
data[i] = arg.Index(i).String()
}
return data, nil
}
type result struct {
value interface{}
err error
}
func (r result) ToJS() map[string]interface{} {
var errMsg string
if r.err != nil {
errMsg = r.err.Error()
}
return map[string]interface{}{
"value": js.ValueOf(r.value),
"error": errMsg,
}
}
func responseHandler(fn func(this js.Value, args []js.Value) (value interface{}, err error)) func(js.Value, []js.Value) interface{} {
return func(this js.Value, args []js.Value) interface{} {
value, err := fn(this, args)
return result{value, err}.ToJS()
}
return accessString
}