From d848fa8f961921591772e7558beae13469ac7774 Mon Sep 17 00:00:00 2001 From: Michael Quigley Date: Tue, 3 Feb 2026 15:54:26 -0500 Subject: [PATCH] 'zrok2 create share'/'zrok2 delete share' with changes in semantics for 'zrok2 share private --share-token' (#1140) --- cmd/zrok2/createShare.go | 65 +++++++++++++ cmd/zrok2/{unshare.go => deleteShare.go} | 16 ++-- cmd/zrok2/sharePrivate.go | 117 ++++++++++++++++------- sdk/golang/sdk/share.go | 23 +++++ 4 files changed, 181 insertions(+), 40 deletions(-) create mode 100644 cmd/zrok2/createShare.go rename cmd/zrok2/{unshare.go => deleteShare.go} (67%) diff --git a/cmd/zrok2/createShare.go b/cmd/zrok2/createShare.go new file mode 100644 index 00000000..bb91c8c2 --- /dev/null +++ b/cmd/zrok2/createShare.go @@ -0,0 +1,65 @@ +package main + +import ( + "github.com/michaelquigley/df/dl" + "github.com/openziti/zrok/v2/environment" + "github.com/openziti/zrok/v2/sdk/golang/sdk" + "github.com/openziti/zrok/v2/tui" + "github.com/spf13/cobra" +) + +func init() { + createCmd.AddCommand(newCreateShareCommand().cmd) +} + +type createShareCommand struct { + cmd *cobra.Command + backendMode string + shareToken string + open bool + accessGrants []string +} + +func newCreateShareCommand() *createShareCommand { + cmd := &cobra.Command{ + Use: "share", + Short: "Create a private share without starting a backend", + Args: cobra.NoArgs, + } + command := &createShareCommand{cmd: cmd} + cmd.Flags().StringVarP(&command.backendMode, "backend-mode", "b", "proxy", "The backend mode {proxy, web, tcpTunnel, udpTunnel, caddy, drive, socks}") + cmd.Flags().StringVarP(&command.shareToken, "share-token", "s", "", "Request a specific share token name") + cmd.Flags().BoolVar(&command.open, "open", false, "Enable open permission mode") + cmd.Flags().StringArrayVar(&command.accessGrants, "access-grant", []string{}, "zrok accounts that are allowed to access this share") + cmd.Run = command.run + return command +} + +func (cmd *createShareCommand) run(_ *cobra.Command, _ []string) { + root, err := environment.LoadRoot() + if err != nil { + tui.Error("error loading environment", err) + } + + if !root.IsEnabled() { + tui.Error("unable to load environment; did you 'zrok2 enable'?", nil) + } + + req := &sdk.ShareRequest{ + BackendMode: sdk.BackendMode(cmd.backendMode), + ShareMode: sdk.PrivateShareMode, + PrivateShareToken: cmd.shareToken, + PermissionMode: sdk.ClosedPermissionMode, + AccessGrants: cmd.accessGrants, + } + if cmd.open { + req.PermissionMode = sdk.OpenPermissionMode + } + + shr, err := sdk.CreateShare(root, req) + if err != nil { + tui.Error("unable to create share", err) + } + + dl.Infof("created share '%v'", shr.Token) +} diff --git a/cmd/zrok2/unshare.go b/cmd/zrok2/deleteShare.go similarity index 67% rename from cmd/zrok2/unshare.go rename to cmd/zrok2/deleteShare.go index a2f157a2..e9a16e9a 100644 --- a/cmd/zrok2/unshare.go +++ b/cmd/zrok2/deleteShare.go @@ -7,27 +7,27 @@ import ( ) func init() { - rootCmd.AddCommand(newUnshareCommand().cmd) + deleteCmd.AddCommand(newDeleteShareCommand().cmd) } -type unshareCommand struct { +type deleteShareCommand struct { cmd *cobra.Command envZId string } -func newUnshareCommand() *unshareCommand { +func newDeleteShareCommand() *deleteShareCommand { cmd := &cobra.Command{ - Use: "unshare", - Short: "Remove a share", + Use: "share ", + Short: "Delete a share", Args: cobra.ExactArgs(1), } - command := &unshareCommand{cmd: cmd} + command := &deleteShareCommand{cmd: cmd} cmd.Flags().StringVar(&command.envZId, "envzid", "", "Override environment ziti identifier") cmd.Run = command.run return command } -func (cmd *unshareCommand) run(_ *cobra.Command, args []string) { +func (cmd *deleteShareCommand) run(_ *cobra.Command, args []string) { env, auth := mustGetEnvironmentAuth() zrok, err := env.Client() if err != nil { @@ -46,5 +46,5 @@ func (cmd *unshareCommand) run(_ *cobra.Command, args []string) { dl.Fatal(err) } - dl.Infof("removed share '%v' from environment '%v'", req.Body.ShareToken, req.Body.EnvZID) + dl.Infof("deleted share '%v' from environment '%v'", req.Body.ShareToken, req.Body.EnvZID) } diff --git a/cmd/zrok2/sharePrivate.go b/cmd/zrok2/sharePrivate.go index b007326d..7c1d2ef1 100644 --- a/cmd/zrok2/sharePrivate.go +++ b/cmd/zrok2/sharePrivate.go @@ -60,7 +60,7 @@ func newSharePrivateCommand() *sharePrivateCommand { headless, _ = root.Headless() } cmd.Flags().StringVarP(&command.backendMode, "backend-mode", "b", "proxy", "The backend mode {proxy, web, tcpTunnel, udpTunnel, caddy, drive, socks}") - cmd.Flags().StringVarP(&command.shareToken, "share-token", "s", "", "Request a specific share token name") + cmd.Flags().StringVarP(&command.shareToken, "share-token", "s", "", "Use an existing share instead of creating new") cmd.Flags().BoolVar(&command.headless, "headless", headless, "Disable TUI and run headless") cmd.Flags().BoolVar(&command.subordinate, "subordinate", false, "Enable agent mode") cmd.MarkFlagsMutuallyExclusive("headless", "subordinate") @@ -74,7 +74,7 @@ func newSharePrivateCommand() *sharePrivateCommand { return command } -func (cmd *sharePrivateCommand) run(_ *cobra.Command, args []string) { +func (cmd *sharePrivateCommand) run(cobraCmd *cobra.Command, args []string) { if cmd.subordinate { logrus.SetFormatter(&logrus.JSONFormatter{TimestampFormat: time.RFC3339Nano}) dlOpts := dl.DefaultOptions().SetTrimPrefix(trimPrefix).SetLevel(slog.LevelInfo) @@ -94,14 +94,43 @@ func (cmd *sharePrivateCommand) run(_ *cobra.Command, args []string) { detectAndRouteToAgent( cmd.subordinate, cmd.forceLocal, cmd.forceAgent, root, - func() { cmd.shareLocal(args, root) }, - func() { cmd.shareAgent(args, root) }, + func() { cmd.shareLocal(cobraCmd, args, root) }, + func() { cmd.shareAgent(cobraCmd, args, root) }, ) } -func (cmd *sharePrivateCommand) shareLocal(args []string, root env_core.Root) { +func (cmd *sharePrivateCommand) shareLocal(cobraCmd *cobra.Command, args []string, root env_core.Root) { + var shr *sdk.Share + var skipDelete bool + var backendMode string + + if cmd.shareToken != "" { + // using existing share - verify it exists and get its backend mode + if cobraCmd.Flags().Changed("backend-mode") { + cmd.error("unable to create share", errors.New("--backend-mode cannot be specified when using --share-token")) + } + + shareDetail, err := sdk.GetShareDetail(root, cmd.shareToken) + if err != nil { + cmd.error("share not found", err) + } + if shareDetail.ShareMode != "private" { + cmd.error("share is not private", errors.New("invalid share mode")) + } + + backendMode = shareDetail.BackendMode + shr = &sdk.Share{ + Token: shareDetail.ShareToken, + FrontendEndpoints: shareDetail.FrontendEndpoints, + } + skipDelete = true + } else { + backendMode = cmd.backendMode + skipDelete = false + } + // validate and process backend mode (nil = allow all modes for private shares) - target, forceHeadless, err := validateBackendMode(cmd.backendMode, args, nil) + target, forceHeadless, err := validateBackendMode(backendMode, args, nil) if err != nil { cmd.error("unable to create share", err) } @@ -116,25 +145,27 @@ func (cmd *sharePrivateCommand) shareLocal(args []string, root env_core.Root) { cmd.error("unable to load ziti identity configuration", err) } - req := &sdk.ShareRequest{ - BackendMode: sdk.BackendMode(cmd.backendMode), - ShareMode: sdk.PrivateShareMode, - PrivateShareToken: cmd.shareToken, - Target: target, - PermissionMode: sdk.ClosedPermissionMode, - AccessGrants: cmd.accessGrants, - } - if cmd.open { - req.PermissionMode = sdk.OpenPermissionMode - } + if shr == nil { + // create ephemeral share (existing behavior) + req := &sdk.ShareRequest{ + BackendMode: sdk.BackendMode(backendMode), + ShareMode: sdk.PrivateShareMode, + Target: target, + PermissionMode: sdk.ClosedPermissionMode, + AccessGrants: cmd.accessGrants, + } + if cmd.open { + req.PermissionMode = sdk.OpenPermissionMode + } - shr, err := sdk.CreateShare(root, req) - if err != nil { - cmd.error("unable to create share", err) + shr, err = sdk.CreateShare(root, req) + if err != nil { + cmd.error("unable to create share", err) + } } shareDescription := fmt.Sprintf("access your share with: %v", tui.Code.Render(fmt.Sprintf("zrok2 access private %v", shr.Token))) - mdl := newShareModel(shr.Token, []string{shareDescription}, sdk.PrivateShareMode, sdk.BackendMode(cmd.backendMode)) + mdl := newShareModel(shr.Token, []string{shareDescription}, sdk.PrivateShareMode, sdk.BackendMode(backendMode)) if !cmd.headless && !cmd.subordinate { proxy.SetCaddyLoggingWriter(mdl) } @@ -143,13 +174,13 @@ func (cmd *sharePrivateCommand) shareLocal(args []string, root env_core.Root) { signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c - cmd.shutdown(root, shr) + cmd.shutdown(root, shr, skipDelete) os.Exit(0) }() requests := make(chan *endpoints.Request, 1024) - switch cmd.backendMode { + switch backendMode { case "proxy": cfg := &proxy.BackendConfig{ IdentityPath: zif, @@ -239,7 +270,7 @@ func (cmd *sharePrivateCommand) shareLocal(args []string, root env_core.Root) { be, err := proxy.NewCaddyfileBackend(cfg) if err != nil { - cmd.shutdown(root, shr) + cmd.shutdown(root, shr, skipDelete) cmd.error("unable to create 'caddy' backend", err) } @@ -353,7 +384,7 @@ func (cmd *sharePrivateCommand) shareLocal(args []string, root env_core.Root) { } close(requests) - cmd.shutdown(root, shr) + cmd.shutdown(root, shr, skipDelete) } } @@ -367,18 +398,40 @@ func (cmd *sharePrivateCommand) error(msg string, err error) { panic(errors.Wrap(err, msg)) } -func (cmd *sharePrivateCommand) shutdown(root env_core.Root, shr *sdk.Share) { +func (cmd *sharePrivateCommand) shutdown(root env_core.Root, shr *sdk.Share, skipDelete bool) { dl.Debugf("shutting down '%v'", shr.Token) - if err := sdk.DeleteShare(root, shr); err != nil { - dl.Errorf("error shutting down '%v': %v", shr.Token, err) + if !skipDelete { + if err := sdk.DeleteShare(root, shr); err != nil { + dl.Errorf("error shutting down '%v': %v", shr.Token, err) + } } dl.Debugf("shutdown complete") } -func (cmd *sharePrivateCommand) shareAgent(args []string, root env_core.Root) { +func (cmd *sharePrivateCommand) shareAgent(cobraCmd *cobra.Command, args []string, root env_core.Root) { var target string + var backendMode string - switch cmd.backendMode { + if cmd.shareToken != "" { + // using existing share - verify it exists and get its backend mode + if cobraCmd.Flags().Changed("backend-mode") { + tui.Error("--backend-mode cannot be specified when using --share-token", nil) + } + + shareDetail, err := sdk.GetShareDetail(root, cmd.shareToken) + if err != nil { + tui.Error("share not found", err) + } + if shareDetail.ShareMode != "private" { + tui.Error("share is not private", nil) + } + + backendMode = shareDetail.BackendMode + } else { + backendMode = cmd.backendMode + } + + switch backendMode { case "proxy": if len(args) != 1 { tui.Error("the 'proxy' backend mode expects a ", nil) @@ -450,7 +503,7 @@ func (cmd *sharePrivateCommand) shareAgent(args []string, root env_core.Root) { target = "socks" default: - tui.Error(fmt.Sprintf("invalid backend mode '%v'", cmd.backendMode), nil) + tui.Error(fmt.Sprintf("invalid backend mode '%v'", backendMode), nil) } client, conn, err := agentClient.NewClient(root) @@ -462,7 +515,7 @@ func (cmd *sharePrivateCommand) shareAgent(args []string, root env_core.Root) { shr, err := client.SharePrivate(context.Background(), &agentGrpc.SharePrivateRequest{ Target: target, PrivateShareToken: cmd.shareToken, - BackendMode: cmd.backendMode, + BackendMode: backendMode, Insecure: cmd.insecure, Closed: !cmd.open, AccessGrants: cmd.accessGrants, diff --git a/sdk/golang/sdk/share.go b/sdk/golang/sdk/share.go index d14854d5..ff21849f 100644 --- a/sdk/golang/sdk/share.go +++ b/sdk/golang/sdk/share.go @@ -5,6 +5,7 @@ import ( httptransport "github.com/go-openapi/runtime/client" "github.com/openziti/zrok/v2/environment/env_core" + "github.com/openziti/zrok/v2/rest_client_zrok/metadata" "github.com/openziti/zrok/v2/rest_client_zrok/share" "github.com/openziti/zrok/v2/rest_model_zrok" "github.com/pkg/errors" @@ -116,3 +117,25 @@ func DeleteShare(root env_core.Root, shr *Share) error { return nil } + +func GetShareDetail(root env_core.Root, shareToken string) (*rest_model_zrok.Share, error) { + if !root.IsEnabled() { + return nil, errors.New("environment is not enabled") + } + + zrok, err := root.Client() + if err != nil { + return nil, errors.Wrap(err, "error getting zrok client") + } + auth := httptransport.APIKeyAuth("X-TOKEN", "header", root.Environment().AccountToken) + + params := metadata.NewGetShareDetailParams() + params.ShareToken = shareToken + + resp, err := zrok.Metadata.GetShareDetail(params, auth) + if err != nil { + return nil, errors.Wrap(err, "error getting share detail") + } + + return resp.Payload, nil +}