diff --git a/cmd/uplink/cmd/access.go b/cmd/uplink/cmd/access.go index 9622324c8..e860852ef 100644 --- a/cmd/uplink/cmd/access.go +++ b/cmd/uplink/cmd/access.go @@ -153,71 +153,74 @@ func registerAccess(cmd *cobra.Command, args []string) (err error) { if len(args) == 0 { return errs.New("no access specified") } + _, err = register(args[0], registerCfg.AuthService, registerCfg.Public) + return err +} - if registerCfg.AuthService == "" { - return errs.New("no auth service address provided") +func register(accessRaw, authService string, public bool) (accessKey string, err error) { + if authService == "" { + return "", errs.New("no auth service address provided") } - accessRaw := args[0] - // try assuming that accessRaw is a named access access, err := registerCfg.GetNamedAccess(accessRaw) if err == nil && access != nil { accessRaw, err = access.Serialize() 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{}{ "access_grant": accessRaw, - "public": registerCfg.Public, + "public": public, }) 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 { - return err + return "", err } defer func() { err = errs.Combine(err, resp.Body.Close()) }() body, err := ioutil.ReadAll(resp.Body) if err != nil { - return err + return "", err } respBody := make(map[string]string) 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"] 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"] 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("Secret Key: ", secretKey) - fmt.Println("Endpoint: ", respBody["endpoint"]) + fmt.Println("Secret Key : ", secretKey) + fmt.Println("Endpoint : ", respBody["endpoint"]) // update AWS credential file if requested if registerCfg.AWSProfile != "" { credentialsPath, err := getAwsCredentialsPath() if err != nil { - return err + return "", err } err = writeAWSCredentials(credentialsPath, registerCfg.AWSProfile, accessKey, secretKey) if err != nil { - return err + return "", err } } - return nil + return accessKey, nil } // getAwsCredentialsPath returns the expected AWS credentials path. diff --git a/cmd/uplink/cmd/share.go b/cmd/uplink/cmd/share.go index 11e057d08..ddb27322d 100644 --- a/cmd/uplink/cmd/share.go +++ b/cmd/uplink/cmd/share.go @@ -7,8 +7,10 @@ import ( "fmt" "io/ioutil" "net/url" + "os" "path/filepath" "strings" + "text/tabwriter" "time" "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"` 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"` - 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 AccessConfig @@ -46,11 +54,195 @@ func init() { Short: "Shares restricted access to objects.", RunE: shareMain, } + RootCmd.AddCommand(shareCmd) 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) { switch { 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. -func shareMain(cmd *cobra.Command, args []string) (err error) { - now := time.Now() - notBefore, err := parseHumanDate(shareCfg.NotBefore, now) - 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 +// sharePrefixExtension is a temporary struct type. We might want to add hasTrailingSlash bool to `uplink.SharePrefix` directly. +type sharePrefixExtension struct { + uplinkSharePrefix uplink.SharePrefix + hasTrailingSlash bool } func formatPermission(allowed bool) string { @@ -186,19 +283,23 @@ func formatTimeRestriction(t time.Time) string { return formatTime(t) } -func formatPaths(sharePrefixes []uplink.SharePrefix) string { +func formatPaths(sharePrefixes []sharePrefixExtension) string { if len(sharePrefixes) == 0 { return "WARNING! The entire project is shared!" } var paths []string for _, prefix := range sharePrefixes { - path := "sj://" + prefix.Bucket - if len(prefix.Prefix) == 0 { - path += " (entire bucket)" + path := "sj://" + prefix.uplinkSharePrefix.Bucket + if len(prefix.uplinkSharePrefix.Prefix) == 0 { + path += "/ (entire bucket)" } else { - path += "/" + prefix.Prefix + path += "/" + prefix.uplinkSharePrefix.Prefix + if prefix.hasTrailingSlash { + path += "/" + } } + paths = append(paths, path) }