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:
parent
5f840aae6e
commit
2ce3170bb4
@ -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
|
||||
```
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user