// Copyright (C) 2021 Storj Labs, Inc. // See LICENSE for copying information. package main import ( "context" "fmt" "net/url" "os" "path/filepath" "strconv" "strings" "time" "github.com/zeebo/clingy" "github.com/zeebo/errs" "storj.io/storj/cmd/uplink/ulext" "storj.io/uplink" "storj.io/uplink/edge" ) type cmdShare struct { ex ulext.External ap accessPermissions access string exportTo string baseURL string register bool url bool dns string tls bool authService string caCert string public bool } func newCmdShare(ex ulext.External) *cmdShare { return &cmdShare{ex: ex} } func (c *cmdShare) Setup(params clingy.Parameters) { c.access = params.Flag("access", "Access name or value to share", "").(string) params.Break() c.exportTo = params.Flag("export-to", "Path to export the shared access to", "").(string) c.baseURL = params.Flag("base-url", "The base url for link sharing", "https://link.storjshare.io").(string) c.register = params.Flag("register", "If true, creates and registers access grant", false, clingy.Transform(strconv.ParseBool), clingy.Boolean, ).(bool) c.url = params.Flag("url", "If true, returns a url for the shared path. implies --register and --public", false, clingy.Transform(strconv.ParseBool), clingy.Boolean, ).(bool) c.dns = params.Flag("dns", "Specify your custom domain. if set, returns DNS settings for web hosting. implies --register and --public", "").(string) c.tls = params.Flag("tls", "Return an additional TXT record to secure your domain (Pro Accounts only.) implies --dns and --public", false, clingy.Transform(strconv.ParseBool), clingy.Boolean, ).(bool) c.authService = params.Flag("auth-service", "URL for shared auth service", "https://auth.storjshare.io").(string) c.public = params.Flag("public", "If true, the access will be public. --dns and --url override this", false, clingy.Transform(strconv.ParseBool), clingy.Boolean, ).(bool) params.Break() c.ap.Setup(params, false) } func (c *cmdShare) Execute(ctx context.Context) error { if len(c.ap.prefixes) == 0 { return errs.New("You must specify at least one prefix to share. Use the access restrict command to restrict with no prefixes.") } access, err := c.ex.OpenAccess(c.access) if err != nil { return err } access, err = c.ap.Apply(access) if err != nil { return err } if c.tls && c.dns == "" { return errs.New("You must specify your custom domain with --dns") } c.public = c.public || c.url || c.dns != "" || c.tls if c.public { c.register = true if c.ap.notAfter == nil { fmt.Fprintf(clingy.Stdout(ctx), "It's not recommended to create a shared Access without an expiration date.\n") fmt.Fprintf(clingy.Stdout(ctx), "If you wish to do so anyway, please run this command with --not-after=none.\n") return nil } } newAccessData, err := access.Serialize() if err != nil { return err } fmt.Fprintf(clingy.Stdout(ctx), "Sharing access to satellite %s\n", access.SatelliteAddress()) fmt.Fprintf(clingy.Stdout(ctx), "=========== ACCESS RESTRICTIONS ==========================================================\n") fmt.Fprintf(clingy.Stdout(ctx), "Download : %s\n", formatPermission(c.ap.AllowDownload())) fmt.Fprintf(clingy.Stdout(ctx), "Upload : %s\n", formatPermission(c.ap.AllowUpload())) fmt.Fprintf(clingy.Stdout(ctx), "Lists : %s\n", formatPermission(c.ap.AllowList())) fmt.Fprintf(clingy.Stdout(ctx), "Deletes : %s\n", formatPermission(c.ap.AllowDelete())) fmt.Fprintf(clingy.Stdout(ctx), "NotBefore : %s\n", formatTimeRestriction(c.ap.NotBefore())) fmt.Fprintf(clingy.Stdout(ctx), "NotAfter : %s\n", formatTimeRestriction(c.ap.NotAfter())) fmt.Fprintf(clingy.Stdout(ctx), "Paths : %s\n", formatPaths(c.ap.prefixes)) fmt.Fprintf(clingy.Stdout(ctx), "=========== SERIALIZED ACCESS WITH THE ABOVE RESTRICTIONS TO SHARE WITH OTHERS ===========\n") fmt.Fprintf(clingy.Stdout(ctx), "Access : %s\n", newAccessData) if c.register { credentials, err := RegisterAccess(ctx, access, c.authService, c.public, c.caCert) if err != nil { return err } err = DisplayGatewayCredentials(ctx, *credentials, "", "") if err != nil { return err } _, err = fmt.Fprintln(clingy.Stdout(ctx), "Public Access:", c.public) if err != nil { return err } if c.url { if c.ap.AllowUpload() || c.ap.AllowDelete() { return errs.New("will only generate linksharing URL with readonly restrictions") } err = createURL(ctx, credentials.AccessKeyID, c.ap.prefixes, c.baseURL) if err != nil { return err } } if c.dns != "" { if c.ap.AllowUpload() || c.ap.AllowDelete() { return errs.New("will only generate DNS entries with readonly restrictions") } err = createDNS(ctx, credentials.AccessKeyID, c.ap.prefixes, c.baseURL, c.dns, c.tls) if err != nil { return err } } } if c.exportTo != "" { // convert to an absolute path, mostly for output purposes. exportTo, err := filepath.Abs(c.exportTo) if err != nil { return err } // TODO: this should use the ulfs package so that tests can run without actually // writing files out. if err := os.WriteFile(exportTo, []byte(newAccessData+"\n"), 0600); err != nil { return err } fmt.Fprintln(clingy.Stdout(ctx), "Exported to:", exportTo) } return nil } func formatPermission(allowed bool) string { if allowed { return "Allowed" } return "Disallowed" } func formatTimeRestriction(t time.Time) string { if t.IsZero() { return "No restriction" } return formatTime(true, t) } func formatPaths(sharePrefixes []uplink.SharePrefix) 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)" } else { path += "/" + prefix.Prefix } paths = append(paths, path) } return strings.Join(paths, "\n ") } // RegisterAccess registers an access grant with a Gateway Authorization Service. func RegisterAccess(ctx context.Context, access *uplink.Access, authService string, public bool, certificateFile string) (credentials *edge.Credentials, err error) { if authService == "" { return nil, errs.New("no auth service address provided") } var edgeConfig edge.Config if strings.HasPrefix(authService, "insecure://") { authService = strings.TrimPrefix(authService, "insecure://") edgeConfig.InsecureUnencryptedConnection = true } // preserve compatibility with previous https service authService = strings.TrimPrefix(authService, "https://") authService = strings.TrimSuffix(authService, "/") if !strings.Contains(authService, ":") { authService += ":7777" } var certificatePEM []byte if certificateFile != "" { certificatePEM, err = os.ReadFile(certificateFile) if err != nil { return nil, errs.New("can't read certificate file: %w", err) } } edgeConfig.AuthServiceAddress = authService edgeConfig.CertificatePEM = certificatePEM return edgeConfig.RegisterAccess(ctx, access, &edge.RegisterAccessOptions{Public: public}) } // Creates linksharing url for allowed path prefixes. func createURL(ctx context.Context, accessKeyID string, prefixes []uplink.SharePrefix, baseURL string) (err error) { if len(prefixes) == 0 { return errs.New("need at least a bucket to create a working linkshare URL") } bucket := prefixes[0].Bucket key := prefixes[0].Prefix url, err := edge.JoinShareURL(baseURL, accessKeyID, bucket, key, nil) if err != nil { return err } fmt.Fprintf(clingy.Stdout(ctx), "=========== BROWSER URL ==================================================================\n") if key != "" && key[len(key)-1:] != "/" { fmt.Fprintf(clingy.Stdout(ctx), "REMINDER : Object key must end in '/' when trying to share a prefix\n") } fmt.Fprintf(clingy.Stdout(ctx), "URL : %s\n", url) return nil } // Creates dns record info for allowed path prefixes. func createDNS(ctx context.Context, accessKey string, prefixes []uplink.SharePrefix, baseURL, dns string, tls bool) (err error) { if len(prefixes) == 0 { return errs.New("need at least a bucket to create DNS records") } bucket := prefixes[0].Bucket key := prefixes[0].Prefix CNAME, err := url.Parse(baseURL) if err != nil { return err } var printStorjRoot string if key == "" { printStorjRoot = fmt.Sprintf("txt-%s\tIN\tTXT \tstorj-root:%s", dns, bucket) } else { printStorjRoot = fmt.Sprintf("txt-%s\tIN\tTXT \tstorj-root:%s/%s", dns, bucket, key) } fmt.Fprintf(clingy.Stdout(ctx), "=========== DNS INFO =====================================================================\n") fmt.Fprintf(clingy.Stdout(ctx), "Remember to update the $ORIGIN with your domain name. You may also change the $TTL.\n") fmt.Fprintf(clingy.Stdout(ctx), "$ORIGIN example.com.\n") fmt.Fprintf(clingy.Stdout(ctx), "$TTL 3600\n") fmt.Fprintf(clingy.Stdout(ctx), "%s \tIN\tCNAME\t%s.\n", dns, CNAME.Host) fmt.Fprintln(clingy.Stdout(ctx), printStorjRoot) fmt.Fprintf(clingy.Stdout(ctx), "txt-%s\tIN\tTXT \tstorj-access:%s\n", dns, accessKey) if tls { fmt.Fprintf(clingy.Stdout(ctx), "txt-%s\tIN\tTXT \tstorj-tls:true\n", dns) } return nil } // DisplayGatewayCredentials formats and writes credentials to stdout. func DisplayGatewayCredentials(ctx context.Context, credentials edge.Credentials, format string, awsProfile string) (err error) { switch format { case "env": // export / set compatible format // note that AWS_ENDPOINT configuration is not natively utilized by the AWS CLI _, err = fmt.Fprintf(clingy.Stdout(ctx), "AWS_ACCESS_KEY_ID=%s\n"+ "AWS_SECRET_ACCESS_KEY=%s\n"+ "AWS_ENDPOINT=%s\n", credentials.AccessKeyID, credentials.SecretKey, credentials.Endpoint) if err != nil { return err } case "aws": // aws configuration commands profile := "" if awsProfile != "" { profile = " --profile " + awsProfile _, err = fmt.Fprintf(clingy.Stdout(ctx), "aws configure %s\n", profile) if err != nil { return err } } // note that the endpoint_url configuration is not natively utilized by the AWS CLI _, err = fmt.Fprintf(clingy.Stdout(ctx), "aws configure %s set aws_access_key_id %s\n"+ "aws configure %s set aws_secret_access_key %s\n"+ "aws configure %s set s3.endpoint_url %s\n", profile, credentials.AccessKeyID, profile, credentials.SecretKey, profile, credentials.Endpoint) if err != nil { return err } default: // plain text _, err = fmt.Fprintf(clingy.Stdout(ctx), "========== GATEWAY CREDENTIALS ===========================================================\n"+ "Access Key ID: %s\n"+ "Secret Key : %s\n"+ "Endpoint : %s\n", credentials.AccessKeyID, credentials.SecretKey, credentials.Endpoint) if err != nil { return err } } return nil }