cmd/uplink: adds register, url, and dns flags to uplink share

and replaces access grant with access

uplink share <path> --> creates access grant

uplink share --register <path> --> registers access grant

uplink share --url <path> --> creates URL, implies register and public

uplink share --dns <hostname> <path> --> creates dns info, implies register and public

Change-Id: I7930c4973a602d3d721ec6f77170f90957dad8c0
This commit is contained in:
Jennifer Johnson 2020-12-08 21:47:10 -05:00
parent 9fe477899b
commit adb2c83e09
2 changed files with 228 additions and 124 deletions

View File

@ -153,71 +153,74 @@ func registerAccess(cmd *cobra.Command, args []string) (err error) {
if len(args) == 0 { if len(args) == 0 {
return errs.New("no access specified") return errs.New("no access specified")
} }
_, err = register(args[0], registerCfg.AuthService, registerCfg.Public)
return err
}
if registerCfg.AuthService == "" { func register(accessRaw, authService string, public bool) (accessKey string, err error) {
return errs.New("no auth service address provided") if authService == "" {
return "", errs.New("no auth service address provided")
} }
accessRaw := args[0]
// try assuming that accessRaw is a named access // try assuming that accessRaw is a named access
access, err := registerCfg.GetNamedAccess(accessRaw) access, err := registerCfg.GetNamedAccess(accessRaw)
if err == nil && access != nil { if err == nil && access != nil {
accessRaw, err = access.Serialize() accessRaw, err = access.Serialize()
if err != nil { if err != nil {
return errs.New("error serializing named access '%s': %w", accessRaw, err) return "", errs.New("error serializing named access '%s': %w", accessRaw, err)
} }
} }
postData, err := json.Marshal(map[string]interface{}{ postData, err := json.Marshal(map[string]interface{}{
"access_grant": accessRaw, "access_grant": accessRaw,
"public": registerCfg.Public, "public": public,
}) })
if err != nil { if err != nil {
return errs.Wrap(err) return accessKey, errs.Wrap(err)
} }
resp, err := http.Post(fmt.Sprintf("%s/v1/access", registerCfg.AuthService), "application/json", bytes.NewReader(postData)) resp, err := http.Post(fmt.Sprintf("%s/v1/access", authService), "application/json", bytes.NewReader(postData))
if err != nil { if err != nil {
return err return "", err
} }
defer func() { err = errs.Combine(err, resp.Body.Close()) }() defer func() { err = errs.Combine(err, resp.Body.Close()) }()
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return err return "", err
} }
respBody := make(map[string]string) respBody := make(map[string]string)
if err := json.Unmarshal(body, &respBody); err != nil { if err := json.Unmarshal(body, &respBody); err != nil {
return errs.New("unexpected response from auth service: %s", string(body)) return "", errs.New("unexpected response from auth service: %s", string(body))
} }
accessKey, ok := respBody["access_key_id"] accessKey, ok := respBody["access_key_id"]
if !ok { if !ok {
return errs.New("access_key_id missing in response") return "", errs.New("access_key_id missing in response")
} }
secretKey, ok := respBody["secret_key"] secretKey, ok := respBody["secret_key"]
if !ok { if !ok {
return errs.New("secret_key missing in response") return "", errs.New("secret_key missing in response")
} }
fmt.Println("=========== CREDENTIALS =========================================================")
fmt.Println("========== CREDENTIALS ===================================================================")
fmt.Println("Access Key ID: ", accessKey) fmt.Println("Access Key ID: ", accessKey)
fmt.Println("Secret Key: ", secretKey) fmt.Println("Secret Key : ", secretKey)
fmt.Println("Endpoint: ", respBody["endpoint"]) fmt.Println("Endpoint : ", respBody["endpoint"])
// update AWS credential file if requested // update AWS credential file if requested
if registerCfg.AWSProfile != "" { if registerCfg.AWSProfile != "" {
credentialsPath, err := getAwsCredentialsPath() credentialsPath, err := getAwsCredentialsPath()
if err != nil { if err != nil {
return err return "", err
} }
err = writeAWSCredentials(credentialsPath, registerCfg.AWSProfile, accessKey, secretKey) err = writeAWSCredentials(credentialsPath, registerCfg.AWSProfile, accessKey, secretKey)
if err != nil { if err != nil {
return err return "", err
} }
} }
return nil return accessKey, nil
} }
// getAwsCredentialsPath returns the expected AWS credentials path. // getAwsCredentialsPath returns the expected AWS credentials path.

View File

@ -7,8 +7,10 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/url" "net/url"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
"text/tabwriter"
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -31,7 +33,13 @@ var shareCfg struct {
NotAfter string `help:"disallow access after this time (e.g. '+2h', '2020-01-02T15:01:01-01:00')" basic-help:"true"` NotAfter string `help:"disallow access after this time (e.g. '+2h', '2020-01-02T15:01:01-01:00')" basic-help:"true"`
AllowedPathPrefix []string `help:"whitelist of path prefixes to require, overrides the [allowed-path-prefix] arguments"` AllowedPathPrefix []string `help:"whitelist of path prefixes to require, overrides the [allowed-path-prefix] arguments"`
ExportTo string `default:"" help:"path to export the shared access to" basic-help:"true"` ExportTo string `default:"" help:"path to export the shared access to" basic-help:"true"`
BaseURL string `default:"https://link.tardigradeshare.io" help:"the base url for link sharing"` BaseURL string `default:"https://link.tardigradeshare.io" help:"the base url for link sharing" basic-help:"true"`
Register bool `default:"false" help:"if true, creates and registers access grant" basic-help:"true"`
URL bool `default:"false" help:"if true, returns a url for the shared path. implies --register and --public" basic-help:"true"`
DNS string `default:"" help:"specify your custom hostname. if set, returns dns settings for web hosting. implies --register and --public" basic-help:"true"`
AuthService string `default:"https://auth.tardigradeshare.io" help:"url for shared auth service" basic-help:"true"`
Public bool `default:"false" help:"if true, the access will be public. --dns and --url override this" basic-help:"true"`
// Share requires information about the current access // Share requires information about the current access
AccessConfig AccessConfig
@ -46,11 +54,195 @@ func init() {
Short: "Shares restricted access to objects.", Short: "Shares restricted access to objects.",
RunE: shareMain, RunE: shareMain,
} }
RootCmd.AddCommand(shareCmd) RootCmd.AddCommand(shareCmd)
process.Bind(shareCmd, &shareCfg, defaults, cfgstruct.ConfDir(getConfDir())) process.Bind(shareCmd, &shareCfg, defaults, cfgstruct.ConfDir(getConfDir()))
} }
func shareMain(cmd *cobra.Command, args []string) (err error) {
newAccessData, sharePrefixes, permission, err := createAccessGrant(args)
if err != nil {
return err
}
var accessKey string
if shareCfg.Register || shareCfg.URL || shareCfg.DNS != "" {
isPublic := (shareCfg.Public || shareCfg.URL || shareCfg.DNS != "")
accessKey, err = register(newAccessData, shareCfg.AuthService, isPublic)
if err != nil {
return err
}
fmt.Println("Public Access: ", isPublic)
if len(shareCfg.AllowedPathPrefix) == 1 && !permission.AllowUpload && !permission.AllowDelete {
if shareCfg.URL {
if err = createURL(accessKey, sharePrefixes); err != nil {
return err
}
}
if shareCfg.DNS != "" {
if err = createDNS(accessKey); err != nil {
return err
}
}
}
}
if shareCfg.ExportTo != "" {
// convert to an absolute path, mostly for output purposes.
exportTo, err := filepath.Abs(shareCfg.ExportTo)
if err != nil {
return Error.Wrap(err)
}
if err := ioutil.WriteFile(exportTo, []byte(newAccessData+"\n"), 0600); err != nil {
return Error.Wrap(err)
}
fmt.Println("Exported to:", exportTo)
}
return nil
}
// Creates access grant for allowed path prefixes.
func createAccessGrant(args []string) (newAccessData string, sharePrefixes []sharePrefixExtension, permission uplink.Permission, err error) {
now := time.Now()
notBefore, err := parseHumanDate(shareCfg.NotBefore, now)
if err != nil {
return newAccessData, sharePrefixes, permission, err
}
notAfter, err := parseHumanDate(shareCfg.NotAfter, now)
if err != nil {
return newAccessData, sharePrefixes, permission, err
}
if len(shareCfg.AllowedPathPrefix) == 0 {
// if the --allowed-path-prefix flag is not set,
// use any arguments as allowed path prefixes
for _, arg := range args {
shareCfg.AllowedPathPrefix = append(shareCfg.AllowedPathPrefix, strings.Split(arg, ",")...)
}
}
var uplinkSharePrefixes []uplink.SharePrefix
for _, path := range shareCfg.AllowedPathPrefix {
p, err := fpath.New(path)
if err != nil {
return newAccessData, sharePrefixes, permission, err
}
if p.IsLocal() {
return newAccessData, sharePrefixes, permission, errs.New("required path must be remote: %q", path)
}
uplinkSharePrefix := uplink.SharePrefix{
Bucket: p.Bucket(),
Prefix: p.Path(),
}
sharePrefixes = append(sharePrefixes, sharePrefixExtension{
uplinkSharePrefix: uplinkSharePrefix,
hasTrailingSlash: strings.HasSuffix(path, "/"),
})
uplinkSharePrefixes = append(uplinkSharePrefixes, uplinkSharePrefix)
}
access, err := shareCfg.GetAccess()
if err != nil {
return newAccessData, sharePrefixes, permission, err
}
permission = uplink.Permission{}
permission.AllowDelete = !shareCfg.DisallowDeletes && !shareCfg.Readonly
permission.AllowList = !shareCfg.DisallowLists && !shareCfg.Writeonly
permission.AllowDownload = !shareCfg.DisallowReads && !shareCfg.Writeonly
permission.AllowUpload = !shareCfg.DisallowWrites && !shareCfg.Readonly
permission.NotBefore = notBefore
permission.NotAfter = notAfter
newAccess, err := access.Share(permission, uplinkSharePrefixes...)
if err != nil {
return newAccessData, sharePrefixes, permission, err
}
newAccessData, err = newAccess.Serialize()
if err != nil {
return newAccessData, sharePrefixes, permission, err
}
satelliteAddr, _, _, err := parseAccess(newAccessData)
if err != nil {
return newAccessData, sharePrefixes, permission, err
}
fmt.Println("Sharing access to satellite", satelliteAddr)
fmt.Println("=========== ACCESS RESTRICTIONS ==========================================================")
fmt.Println("Download :", formatPermission(permission.AllowDownload))
fmt.Println("Upload :", formatPermission(permission.AllowUpload))
fmt.Println("Lists :", formatPermission(permission.AllowList))
fmt.Println("Deletes :", formatPermission(permission.AllowDelete))
fmt.Println("NotBefore :", formatTimeRestriction(permission.NotBefore))
fmt.Println("NotAfter :", formatTimeRestriction(permission.NotAfter))
fmt.Println("Paths :", formatPaths(sharePrefixes))
fmt.Println("=========== SERIALIZED ACCESS WITH THE ABOVE RESTRICTIONS TO SHARE WITH OTHERS ===========")
fmt.Println("Access :", newAccessData)
return newAccessData, sharePrefixes, permission, nil
}
// Creates linksharing url for allowed path prefixes.
func createURL(newAccessData string, sharePrefixes []sharePrefixExtension) (err error) {
p, err := fpath.New(shareCfg.AllowedPathPrefix[0])
if err != nil {
return err
}
fmt.Println("=========== BROWSER URL ==================================================================")
fmt.Println("REMINDER : Object key must end in '/' when trying to share recursively")
var printFormat string
if p.Path() == "" || !sharePrefixes[0].hasTrailingSlash { // Check if the path is empty (aka sharing the entire bucket) or the path is not a directory or an object that ends in "/".
printFormat = "URL : %s/%s/%s/%s\n"
} else {
printFormat = "URL : %s/%s/%s/%s/\n"
}
fmt.Printf(printFormat, shareCfg.BaseURL, url.PathEscape(newAccessData), p.Bucket(), p.Path())
return nil
}
// Creates dns record info for allowed path prefixes.
func createDNS(accessKey string) (err error) {
p, err := fpath.New(shareCfg.AllowedPathPrefix[0])
if err != nil {
return err
}
CNAME, err := url.Parse(shareCfg.BaseURL)
if err != nil {
return err
}
minWidth := len(shareCfg.DNS) + 5 // add 5 spaces to account for "txt-"
w := new(tabwriter.Writer)
w.Init(os.Stdout, minWidth, minWidth, 0, '\t', 0)
defer func() {
err = errs.Combine(err, w.Flush())
}()
var printStorjRoot string
if p.Path() == "" {
printStorjRoot = fmt.Sprintf("txt-%s\tIN\tTXT \tstorj-root:%s", shareCfg.DNS, p.Bucket())
} else {
printStorjRoot = fmt.Sprintf("txt-%s\tIN\tTXT \tstorj-root:%s/%s", shareCfg.DNS, p.Bucket(), p.Path())
}
fmt.Println("=========== DNS INFO =====================================================================")
fmt.Println("Remember to update the $ORIGIN with your domain name. You may also change the $TTL.")
fmt.Fprintln(w, "$ORIGIN example.com.")
fmt.Fprintln(w, "$TTL 3600")
fmt.Fprintf(w, "%s \tIN\tCNAME\t%s.\n", shareCfg.DNS, CNAME.Host)
fmt.Fprintln(w, printStorjRoot)
fmt.Fprintf(w, "txt-%s\tIN\tTXT \tstorj-access:%s\n", shareCfg.DNS, accessKey)
return nil
}
func parseHumanDate(date string, now time.Time) (time.Time, error) { func parseHumanDate(date string, now time.Time) (time.Time, error) {
switch { switch {
case date == "": case date == "":
@ -71,105 +263,10 @@ func parseHumanDate(date string, now time.Time) (time.Time, error) {
} }
} }
// shareMain is the function executed when shareCmd is called. // sharePrefixExtension is a temporary struct type. We might want to add hasTrailingSlash bool to `uplink.SharePrefix` directly.
func shareMain(cmd *cobra.Command, args []string) (err error) { type sharePrefixExtension struct {
now := time.Now() uplinkSharePrefix uplink.SharePrefix
notBefore, err := parseHumanDate(shareCfg.NotBefore, now) hasTrailingSlash bool
if err != nil {
return err
}
notAfter, err := parseHumanDate(shareCfg.NotAfter, now)
if err != nil {
return err
}
if len(shareCfg.AllowedPathPrefix) == 0 {
// if the --allowed-path-prefix flag is not set,
// use any arguments as allowed path prefixes
for _, arg := range args {
shareCfg.AllowedPathPrefix = append(shareCfg.AllowedPathPrefix, strings.Split(arg, ",")...)
}
}
var sharePrefixes []uplink.SharePrefix
for _, path := range shareCfg.AllowedPathPrefix {
p, err := fpath.New(path)
if err != nil {
return err
}
if p.IsLocal() {
return errs.New("required path must be remote: %q", path)
}
sharePrefixes = append(sharePrefixes, uplink.SharePrefix{
Bucket: p.Bucket(),
Prefix: p.Path(),
})
}
access, err := shareCfg.GetAccess()
if err != nil {
return err
}
permission := uplink.Permission{}
permission.AllowDelete = !shareCfg.DisallowDeletes && !shareCfg.Readonly
permission.AllowList = !shareCfg.DisallowLists && !shareCfg.Writeonly
permission.AllowDownload = !shareCfg.DisallowReads && !shareCfg.Writeonly
permission.AllowUpload = !shareCfg.DisallowWrites && !shareCfg.Readonly
permission.NotBefore = notBefore
permission.NotAfter = notAfter
newAccess, err := access.Share(permission, sharePrefixes...)
if err != nil {
return err
}
newAccessData, err := newAccess.Serialize()
if err != nil {
return err
}
satelliteAddr, _, _, err := parseAccess(newAccessData)
if err != nil {
return err
}
fmt.Println("Sharing access to satellite", satelliteAddr)
fmt.Println("=========== ACCESS RESTRICTIONS ==========================================================")
fmt.Println("Download :", formatPermission(permission.AllowDownload))
fmt.Println("Upload :", formatPermission(permission.AllowUpload))
fmt.Println("Lists :", formatPermission(permission.AllowList))
fmt.Println("Deletes :", formatPermission(permission.AllowDelete))
fmt.Println("NotBefore :", formatTimeRestriction(permission.NotBefore))
fmt.Println("NotAfter :", formatTimeRestriction(permission.NotAfter))
fmt.Println("Paths :", formatPaths(sharePrefixes))
fmt.Println("=========== SERIALIZED ACCESS WITH THE ABOVE RESTRICTIONS TO SHARE WITH OTHERS ===========")
fmt.Println("Access :", newAccessData)
if len(shareCfg.AllowedPathPrefix) == 1 && !permission.AllowUpload && !permission.AllowDelete {
fmt.Println("=========== BROWSER URL ==================================================================")
p, err := fpath.New(shareCfg.AllowedPathPrefix[0])
if err != nil {
return err
}
fmt.Println("URL :", fmt.Sprintf("%s/%s/%s/%s", shareCfg.BaseURL,
url.PathEscape(newAccessData),
url.PathEscape(p.Bucket()),
url.PathEscape(p.Path())))
}
if shareCfg.ExportTo != "" {
// convert to an absolute path, mostly for output purposes.
exportTo, err := filepath.Abs(shareCfg.ExportTo)
if err != nil {
return Error.Wrap(err)
}
if err := ioutil.WriteFile(exportTo, []byte(newAccessData+"\n"), 0600); err != nil {
return Error.Wrap(err)
}
fmt.Println("Exported to:", exportTo)
}
return nil
} }
func formatPermission(allowed bool) string { func formatPermission(allowed bool) string {
@ -186,19 +283,23 @@ func formatTimeRestriction(t time.Time) string {
return formatTime(t) return formatTime(t)
} }
func formatPaths(sharePrefixes []uplink.SharePrefix) string { func formatPaths(sharePrefixes []sharePrefixExtension) string {
if len(sharePrefixes) == 0 { if len(sharePrefixes) == 0 {
return "WARNING! The entire project is shared!" return "WARNING! The entire project is shared!"
} }
var paths []string var paths []string
for _, prefix := range sharePrefixes { for _, prefix := range sharePrefixes {
path := "sj://" + prefix.Bucket path := "sj://" + prefix.uplinkSharePrefix.Bucket
if len(prefix.Prefix) == 0 { if len(prefix.uplinkSharePrefix.Prefix) == 0 {
path += " (entire bucket)" path += "/ (entire bucket)"
} else { } else {
path += "/" + prefix.Prefix path += "/" + prefix.uplinkSharePrefix.Prefix
if prefix.hasTrailingSlash {
path += "/"
}
} }
paths = append(paths, path) paths = append(paths, path)
} }