diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e2e1d5d9..46701d159 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## v0.4.26 +FEATURE: New _permission modes_ available for shares. _Open permission mode_ retains the behavior of previous zrok releases and is the default setting. _Closed permission mode_ (`--closed`) only allows a share to be accessed (`zrok access`) by users who have been granted access with the `--access-grant` flag. See the documentation at (https://docs.zrok.io/docs/guides/permission-modes/) (https://github.com/openziti/zrok/issues/432) + CHANGE: The target for a `socks` share is automatically set to `socks` to improve web console display. CHANGE: Enhancements to the look and feel of the account actions tab in the web console. Textual improvements. diff --git a/cmd/zrok/main.go b/cmd/zrok/main.go index fd8d52f8b..750c71da2 100644 --- a/cmd/zrok/main.go +++ b/cmd/zrok/main.go @@ -26,6 +26,7 @@ func init() { testCmd.AddCommand(loopCmd) rootCmd.AddCommand(adminCmd) rootCmd.AddCommand(configCmd) + rootCmd.AddCommand(modifyCmd) rootCmd.AddCommand(shareCmd) rootCmd.AddCommand(testCmd) transport.AddAddressParser(tcp.AddressParser{}) @@ -85,6 +86,12 @@ var loopCmd = &cobra.Command{ Short: "Loopback testing utilities", } +var modifyCmd = &cobra.Command{ + Use: "modify", + Aliases: []string{"mod"}, + Short: "Modify resources", +} + var shareCmd = &cobra.Command{ Use: "share", Short: "Create backend access for shares", diff --git a/cmd/zrok/modifyShare.go b/cmd/zrok/modifyShare.go new file mode 100644 index 000000000..cd03f2abe --- /dev/null +++ b/cmd/zrok/modifyShare.go @@ -0,0 +1,75 @@ +package main + +import ( + "fmt" + httptransport "github.com/go-openapi/runtime/client" + "github.com/openziti/zrok/environment" + "github.com/openziti/zrok/rest_client_zrok/share" + "github.com/openziti/zrok/rest_model_zrok" + "github.com/openziti/zrok/tui" + "github.com/spf13/cobra" +) + +func init() { + modifyCmd.AddCommand(newModifyShareCommand().cmd) +} + +type modifyShareCommand struct { + addAccessGrants []string + removeAccessGrants []string + cmd *cobra.Command +} + +func newModifyShareCommand() *modifyShareCommand { + cmd := &cobra.Command{ + Use: "share ", + Args: cobra.ExactArgs(1), + Short: "Modify a share", + } + command := &modifyShareCommand{cmd: cmd} + cmd.Flags().StringArrayVar(&command.addAccessGrants, "add-access-grant", []string{}, "Add an access grant (email address)") + cmd.Flags().StringArrayVar(&command.removeAccessGrants, "remove-access-grant", []string{}, "Remove an access grant (email address)") + cmd.Run = command.run + return command +} + +func (cmd *modifyShareCommand) run(_ *cobra.Command, args []string) { + shrToken := args[0] + + root, err := environment.LoadRoot() + if err != nil { + if !panicInstead { + tui.Error("error loading environment", err) + } + panic(err) + } + + if !root.IsEnabled() { + tui.Error("unable to load environment; did you 'zrok enable'?", nil) + } + + zrok, err := root.Client() + if err != nil { + if !panicInstead { + tui.Error("unable to create zrok client", err) + } + panic(err) + } + auth := httptransport.APIKeyAuth("X-TOKEN", "header", root.Environment().Token) + + if len(cmd.addAccessGrants) > 0 || len(cmd.removeAccessGrants) > 0 { + req := share.NewUpdateShareParams() + req.Body = &rest_model_zrok.UpdateShareRequest{ + ShrToken: shrToken, + AddAccessGrants: cmd.addAccessGrants, + RemoveAccessGrants: cmd.removeAccessGrants, + } + if _, err := zrok.Share.UpdateShare(req, auth); err != nil { + if !panicInstead { + tui.Error("unable to update share", err) + } + panic(err) + } + fmt.Println("updated") + } +} diff --git a/cmd/zrok/reserve.go b/cmd/zrok/reserve.go index 4fb76a42e..53341779a 100644 --- a/cmd/zrok/reserve.go +++ b/cmd/zrok/reserve.go @@ -26,6 +26,8 @@ type reserveCommand struct { oauthProvider string oauthEmailAddressPatterns []string oauthCheckInterval time.Duration + closed bool + accessGrants []string cmd *cobra.Command } @@ -45,6 +47,8 @@ func newReserveCommand() *reserveCommand { cmd.Flags().StringArrayVar(&command.oauthEmailAddressPatterns, "oauth-email-address-patterns", []string{}, "Allow only these email domains to authenticate via OAuth") cmd.Flags().DurationVar(&command.oauthCheckInterval, "oauth-check-interval", 3*time.Hour, "Maximum lifetime for OAuth authentication; reauthenticate after expiry") cmd.MarkFlagsMutuallyExclusive("basic-auth", "oauth-provider") + cmd.Flags().BoolVar(&command.closed, "closed", false, "Enable closed permission mode (see --access-grant)") + cmd.Flags().StringArrayVar(&command.accessGrants, "access-grant", []string{}, "zrok accounts that are allowed to access this share (see --closed)") cmd.Run = command.run return command @@ -142,6 +146,10 @@ func (cmd *reserveCommand) run(_ *cobra.Command, args []string) { req.OauthEmailAddressPatterns = cmd.oauthEmailAddressPatterns req.OauthAuthorizationCheckInterval = cmd.oauthCheckInterval } + if cmd.closed { + req.PermissionMode = sdk.ClosedPermissionMode + req.AccessGrants = cmd.accessGrants + } shr, err := sdk.CreateShare(env, req) if err != nil { tui.Error("unable to create share", err) diff --git a/cmd/zrok/sharePrivate.go b/cmd/zrok/sharePrivate.go index 09d6baee3..cdc4c7fcd 100644 --- a/cmd/zrok/sharePrivate.go +++ b/cmd/zrok/sharePrivate.go @@ -25,11 +25,13 @@ func init() { } type sharePrivateCommand struct { - basicAuth []string - backendMode string - headless bool - insecure bool - cmd *cobra.Command + basicAuth []string + backendMode string + headless bool + insecure bool + closed bool + accessGrants []string + cmd *cobra.Command } func newSharePrivateCommand() *sharePrivateCommand { @@ -43,6 +45,8 @@ func newSharePrivateCommand() *sharePrivateCommand { cmd.Flags().StringVarP(&command.backendMode, "backend-mode", "b", "proxy", "The backend mode {proxy, web, tcpTunnel, udpTunnel, caddy, drive, socks}") cmd.Flags().BoolVar(&command.headless, "headless", false, "Disable TUI and run headless") cmd.Flags().BoolVar(&command.insecure, "insecure", false, "Enable insecure TLS certificate validation for ") + cmd.Flags().BoolVar(&command.closed, "closed", false, "Enable closed permission mode (see --access-grant)") + cmd.Flags().StringArrayVar(&command.accessGrants, "access-grant", []string{}, "zrok accounts that are allowed to access this share (see --closed)") cmd.Run = command.run return command } @@ -131,6 +135,10 @@ func (cmd *sharePrivateCommand) run(_ *cobra.Command, args []string) { BasicAuth: cmd.basicAuth, Target: target, } + if cmd.closed { + req.PermissionMode = sdk.ClosedPermissionMode + req.AccessGrants = cmd.accessGrants + } shr, err := sdk.CreateShare(root, req) if err != nil { if !panicInstead { diff --git a/cmd/zrok/sharePublic.go b/cmd/zrok/sharePublic.go index 80e859fbd..931365a5e 100644 --- a/cmd/zrok/sharePublic.go +++ b/cmd/zrok/sharePublic.go @@ -33,6 +33,8 @@ type sharePublicCommand struct { oauthProvider string oauthEmailAddressPatterns []string oauthCheckInterval time.Duration + closed bool + accessGrants []string cmd *cobra.Command } @@ -47,6 +49,8 @@ func newSharePublicCommand() *sharePublicCommand { cmd.Flags().StringVarP(&command.backendMode, "backend-mode", "b", "proxy", "The backend mode {proxy, web, caddy, drive}") cmd.Flags().BoolVar(&command.headless, "headless", false, "Disable TUI and run headless") cmd.Flags().BoolVar(&command.insecure, "insecure", false, "Enable insecure TLS certificate validation for ") + cmd.Flags().BoolVar(&command.closed, "closed", false, "Enable closed permission mode (see --access-grant)") + cmd.Flags().StringArrayVar(&command.accessGrants, "access-grant", []string{}, "zrok accounts that are allowed to access this share (see --closed)") cmd.Flags().StringArrayVar(&command.basicAuth, "basic-auth", []string{}, "Basic authentication users (,...)") cmd.Flags().StringVar(&command.oauthProvider, "oauth-provider", "", "Enable OAuth provider [google, github]") @@ -113,6 +117,10 @@ func (cmd *sharePublicCommand) run(_ *cobra.Command, args []string) { BasicAuth: cmd.basicAuth, Target: target, } + if cmd.closed { + req.PermissionMode = sdk.ClosedPermissionMode + req.AccessGrants = cmd.accessGrants + } if cmd.oauthProvider != "" { req.OauthProvider = cmd.oauthProvider req.OauthEmailAddressPatterns = cmd.oauthEmailAddressPatterns diff --git a/controller/access.go b/controller/access.go index 3d84323a6..5b9d8dceb 100644 --- a/controller/access.go +++ b/controller/access.go @@ -49,7 +49,7 @@ func (h *accessHandler) Handle(params share.AccessParams, principal *rest_model_ shrToken := params.Body.ShrToken shr, err := str.FindShareWithToken(shrToken, trx) if err != nil { - logrus.Errorf("error finding share") + logrus.Errorf("error finding share with token '%v': %v", shrToken, err) return share.NewAccessNotFound() } if shr == nil { @@ -57,6 +57,19 @@ func (h *accessHandler) Handle(params share.AccessParams, principal *rest_model_ return share.NewAccessNotFound() } + if shr.PermissionMode == store.ClosedPermissionMode { + shrEnv, err := str.GetEnvironment(shr.EnvironmentId, trx) + if err != nil { + logrus.Errorf("error getting environment for share '%v': %v", shrToken, err) + return share.NewAccessInternalServerError() + } + + if err := h.checkAccessGrants(shr, *shrEnv.AccountId, principal, trx); err != nil { + logrus.Errorf("closed permission mode for '%v' fails for '%v': %v", shr.Token, principal.Email, err) + return share.NewAccessUnauthorized() + } + } + if err := h.checkLimits(shr, trx); err != nil { logrus.Errorf("cannot access limited share for '%v': %v", principal.Email, err) return share.NewAccessNotFound() @@ -111,3 +124,20 @@ func (h *accessHandler) checkLimits(shr *store.Share, trx *sqlx.Tx) error { } return nil } + +func (h *accessHandler) checkAccessGrants(shr *store.Share, ownerAccountId int, principal *rest_model_zrok.Principal, trx *sqlx.Tx) error { + if int(principal.ID) == ownerAccountId { + logrus.Infof("accessing own share '%v' for '%v'", shr.Token, principal.Email) + return nil + } + count, err := str.CheckAccessGrantForShareAndAccount(shr.Id, int(principal.ID), trx) + if err != nil { + logrus.Infof("error checking access grants for '%v': %v", shr.Token, err) + return err + } + if count > 0 { + logrus.Infof("found '%d' grants for '%v'", count, principal.Email) + return nil + } + return errors.Errorf("access denied for '%v' accessing '%v'", principal.Email, shr.Token) +} diff --git a/controller/share.go b/controller/share.go index f2b73758d..8752a0860 100644 --- a/controller/share.go +++ b/controller/share.go @@ -54,6 +54,19 @@ func (h *shareHandler) Handle(params share.ShareParams, principal *rest_model_zr return share.NewShareUnauthorized() } + var accessGrantAcctIds []int + if store.PermissionMode(params.Body.PermissionMode) == store.ClosedPermissionMode { + for _, email := range params.Body.AccessGrants { + acct, err := str.FindAccountWithEmail(email, trx) + if err != nil { + logrus.Errorf("unable to find account '%v' for share request from '%v'", email, principal.Email) + return share.NewShareNotFound() + } + logrus.Debugf("found id '%d' for '%v'", acct.Id, acct.Email) + accessGrantAcctIds = append(accessGrantAcctIds, acct.Id) + } + } + edge, err := zrokEdgeSdk.Client(cfg.Ziti) if err != nil { logrus.Error(err) @@ -134,6 +147,10 @@ func (h *shareHandler) Handle(params share.ShareParams, principal *rest_model_zr BackendMode: params.Body.BackendMode, BackendProxyEndpoint: ¶ms.Body.BackendProxyEndpoint, Reserved: reserved, + PermissionMode: store.OpenPermissionMode, + } + if params.Body.PermissionMode != "" { + sshr.PermissionMode = store.PermissionMode(params.Body.PermissionMode) } if len(params.Body.FrontendSelection) > 0 { sshr.FrontendSelection = ¶ms.Body.FrontendSelection[0] @@ -150,6 +167,16 @@ func (h *shareHandler) Handle(params share.ShareParams, principal *rest_model_zr return share.NewShareInternalServerError() } + if sshr.PermissionMode == store.ClosedPermissionMode { + for _, acctId := range accessGrantAcctIds { + _, err := str.CreateAccessGrant(sid, acctId, trx) + if err != nil { + logrus.Errorf("error creating access grant for '%v': %v", principal.Email, err) + return share.NewShareInternalServerError() + } + } + } + if err := trx.Commit(); err != nil { logrus.Errorf("error committing share record: %v", err) return share.NewShareInternalServerError() diff --git a/controller/store/accessGrant.go b/controller/store/accessGrant.go new file mode 100644 index 000000000..89b327a14 --- /dev/null +++ b/controller/store/accessGrant.go @@ -0,0 +1,57 @@ +package store + +import ( + "github.com/jmoiron/sqlx" + "github.com/pkg/errors" +) + +type AccessGrant struct { + Model + ShareId int + AccountId int +} + +func (str *Store) CreateAccessGrant(shareId, accountId int, tx *sqlx.Tx) (int, error) { + stmt, err := tx.Prepare("insert into access_grants (share_id, account_id) values ($1, $2) returning id") + if err != nil { + return 0, errors.Wrap(err, "error preparing access_grant insert statement") + } + var id int + if err := stmt.QueryRow(shareId, accountId).Scan(&id); err != nil { + return 0, errors.Wrap(err, "error executing access_grant insert statement") + } + return id, nil +} + +func (str *Store) CheckAccessGrantForShareAndAccount(shrId, acctId int, tx *sqlx.Tx) (int, error) { + count := 0 + err := tx.QueryRowx("select count(0) from access_grants where share_id = $1 and account_id = $2 and not deleted", shrId, acctId).Scan(&count) + if err != nil { + return 0, errors.Wrap(err, "error selecting access_grants by share_id and account_id") + } + return count, nil +} + +func (str *Store) DeleteAccessGrantsForShare(shrId int, tx *sqlx.Tx) error { + stmt, err := tx.Prepare("update access_grants set updated_at = current_timestamp, deleted = true where share_id = $1") + if err != nil { + return errors.Wrap(err, "error preparing access_grants delete for shares statement") + } + _, err = stmt.Exec(shrId) + if err != nil { + return errors.Wrap(err, "error executing access_grants delete for shares statement") + } + return nil +} + +func (str *Store) DeleteAccessGrantsForShareAndAccount(shrId, acctId int, tx *sqlx.Tx) error { + stmt, err := tx.Prepare("update access_grants set updated_at = current_timestamp, deleted = true where share_id = $1 and account_id = $2") + if err != nil { + return errors.Wrap(err, "error preparing access_grants delete for share and account statement") + } + _, err = stmt.Exec(shrId, acctId) + if err != nil { + return errors.Wrap(err, "error executing access_grants delete for share and account statement") + } + return nil +} diff --git a/controller/store/model.go b/controller/store/model.go index c5dae5eb9..dd9bd8b52 100644 --- a/controller/store/model.go +++ b/controller/store/model.go @@ -7,3 +7,10 @@ const ( WarningAction LimitJournalAction = "warning" ClearAction LimitJournalAction = "clear" ) + +type PermissionMode string + +const ( + OpenPermissionMode PermissionMode = "open" + ClosedPermissionMode PermissionMode = "closed" +) diff --git a/controller/store/share.go b/controller/store/share.go index 365e5e031..7005e6037 100644 --- a/controller/store/share.go +++ b/controller/store/share.go @@ -16,16 +16,17 @@ type Share struct { FrontendEndpoint *string BackendProxyEndpoint *string Reserved bool + PermissionMode PermissionMode Deleted bool } func (str *Store) CreateShare(envId int, shr *Share, tx *sqlx.Tx) (int, error) { - stmt, err := tx.Prepare("insert into shares (environment_id, z_id, token, share_mode, backend_mode, frontend_selection, frontend_endpoint, backend_proxy_endpoint, reserved) values ($1, $2, $3, $4, $5, $6, $7, $8, $9) returning id") + stmt, err := tx.Prepare("insert into shares (environment_id, z_id, token, share_mode, backend_mode, frontend_selection, frontend_endpoint, backend_proxy_endpoint, reserved, permission_mode) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) returning id") if err != nil { return 0, errors.Wrap(err, "error preparing shares insert statement") } var id int - if err := stmt.QueryRow(envId, shr.ZId, shr.Token, shr.ShareMode, shr.BackendMode, shr.FrontendSelection, shr.FrontendEndpoint, shr.BackendProxyEndpoint, shr.Reserved).Scan(&id); err != nil { + if err := stmt.QueryRow(envId, shr.ZId, shr.Token, shr.ShareMode, shr.BackendMode, shr.FrontendSelection, shr.FrontendEndpoint, shr.BackendProxyEndpoint, shr.Reserved, shr.PermissionMode).Scan(&id); err != nil { return 0, errors.Wrap(err, "error executing shares insert statement") } return id, nil @@ -96,12 +97,12 @@ func (str *Store) FindSharesForEnvironment(envId int, tx *sqlx.Tx) ([]*Share, er } func (str *Store) UpdateShare(shr *Share, tx *sqlx.Tx) error { - sql := "update shares set z_id = $1, token = $2, share_mode = $3, backend_mode = $4, frontend_selection = $5, frontend_endpoint = $6, backend_proxy_endpoint = $7, reserved = $8, updated_at = current_timestamp where id = $9" + sql := "update shares set z_id = $1, token = $2, share_mode = $3, backend_mode = $4, frontend_selection = $5, frontend_endpoint = $6, backend_proxy_endpoint = $7, reserved = $8, permission_mode = $9, updated_at = current_timestamp where id = $10" stmt, err := tx.Prepare(sql) if err != nil { return errors.Wrap(err, "error preparing shares update statement") } - _, err = stmt.Exec(shr.ZId, shr.Token, shr.ShareMode, shr.BackendMode, shr.FrontendSelection, shr.FrontendEndpoint, shr.BackendProxyEndpoint, shr.Reserved, shr.Id) + _, err = stmt.Exec(shr.ZId, shr.Token, shr.ShareMode, shr.BackendMode, shr.FrontendSelection, shr.FrontendEndpoint, shr.BackendProxyEndpoint, shr.Reserved, shr.PermissionMode, shr.Id) if err != nil { return errors.Wrap(err, "error executing shares update statement") } diff --git a/controller/store/sql/postgresql/019_v0_4_26_permission_model.sql b/controller/store/sql/postgresql/019_v0_4_26_permission_model.sql new file mode 100644 index 000000000..af8dcac60 --- /dev/null +++ b/controller/store/sql/postgresql/019_v0_4_26_permission_model.sql @@ -0,0 +1,14 @@ +-- +migrate Up + +create type permission_mode_type as enum('open', 'closed'); + +alter table shares add column permission_mode permission_mode_type not null default('open'); + +create table access_grants ( + id serial primary key, + share_id integer references shares(id), + account_id integer references accounts(id), + created_at timestamptz not null default(current_timestamp), + updated_at timestamptz not null default(current_timestamp), + deleted boolean not null default(false) +); \ No newline at end of file diff --git a/controller/store/sql/sqlite3/019_v0_4_26_permission_model.sql b/controller/store/sql/sqlite3/019_v0_4_26_permission_model.sql new file mode 100644 index 000000000..eade2df2a --- /dev/null +++ b/controller/store/sql/sqlite3/019_v0_4_26_permission_model.sql @@ -0,0 +1,12 @@ +-- +migrate Up + +alter table shares add column permission_mode string not null default('open'); + +create table access_grants ( + id integer primary key, + share_id integer references shares(id), + account_id integer references accounts(id), + created_at datetime not null default(strftime('%Y-%m-%d %H:%M:%f', 'now')), + updated_at datetime not null default(strftime('%Y-%m-%d %H:%M:%f', 'now')), + deleted boolean not null default(false) +); \ No newline at end of file diff --git a/controller/unshare.go b/controller/unshare.go index eab9a3462..cbadcfcca 100644 --- a/controller/unshare.go +++ b/controller/unshare.go @@ -79,8 +79,12 @@ func (h *unshareHandler) Handle(params share.UnshareParams, principal *rest_mode h.deallocateResources(senv, shrToken, shrZId, edge) logrus.Debugf("deallocated share '%v'", shrToken) + if err := str.DeleteAccessGrantsForShare(sshr.Id, tx); err != nil { + logrus.Errorf("error deleting access grants for share '%v': %v", shrToken, err) + return share.NewUnshareInternalServerError() + } if err := str.DeleteShare(sshr.Id, tx); err != nil { - logrus.Errorf("error deactivating share '%v': %v", shrZId, err) + logrus.Errorf("error deleting share '%v': %v", shrToken, err) return share.NewUnshareInternalServerError() } if err := tx.Commit(); err != nil { diff --git a/controller/updateShare.go b/controller/updateShare.go index 41a477307..0eccffedc 100644 --- a/controller/updateShare.go +++ b/controller/updateShare.go @@ -48,15 +48,49 @@ func (h *updateShareHandler) Handle(params share.UpdateShareParams, principal *r return share.NewUpdateShareNotFound() } - sshr.BackendProxyEndpoint = &backendProxyEndpoint - if err := str.UpdateShare(sshr, tx); err != nil { - logrus.Errorf("error updating share '%v': %v", shrToken, err) - return share.NewUpdateShareInternalServerError() + doCommit := false + if backendProxyEndpoint != "" { + sshr.BackendProxyEndpoint = &backendProxyEndpoint + if err := str.UpdateShare(sshr, tx); err != nil { + logrus.Errorf("error updating share '%v': %v", shrToken, err) + return share.NewUpdateShareInternalServerError() + } + doCommit = true } - if err := tx.Commit(); err != nil { - logrus.Errorf("error committing transaction for share '%v' update: %v", shrToken, err) - return share.NewUpdateShareInternalServerError() + for _, addr := range params.Body.AddAccessGrants { + acct, err := str.FindAccountWithEmail(addr, tx) + if err != nil { + logrus.Errorf("error looking up account by email '%v' for user '%v': %v", addr, principal.Email, err) + return share.NewUpdateShareBadRequest() + } + if _, err := str.CreateAccessGrant(sshr.Id, acct.Id, tx); err != nil { + logrus.Errorf("error adding access grant '%v' for share '%v': %v", acct.Email, shrToken, err) + return share.NewUpdateShareInternalServerError() + } + logrus.Infof("added access grant '%v' to share '%v'", acct.Email, shrToken) + doCommit = true + } + + for _, addr := range params.Body.RemoveAccessGrants { + acct, err := str.FindAccountWithEmail(addr, tx) + if err != nil { + logrus.Errorf("error looking up account by email '%v' for user '%v': %v", addr, principal.Email, err) + return share.NewUpdateShareBadRequest() + } + if err := str.DeleteAccessGrantsForShareAndAccount(sshr.Id, acct.Id, tx); err != nil { + logrus.Errorf("error removing access grant '%v' for share '%v': %v", acct.Email, shrToken, err) + return share.NewUpdateShareInternalServerError() + } + logrus.Infof("removed access grant '%v' from share '%v'", acct.Email, shrToken) + doCommit = true + } + + if doCommit { + if err := tx.Commit(); err != nil { + logrus.Errorf("error committing transaction for share '%v' update: %v", shrToken, err) + return share.NewUpdateShareInternalServerError() + } } return share.NewUpdateShareOK() diff --git a/docs/guides/permission-modes.md b/docs/guides/permission-modes.md new file mode 100644 index 000000000..8cab041bd --- /dev/null +++ b/docs/guides/permission-modes.md @@ -0,0 +1,77 @@ +--- +sidebar_position: 22 +sidebar_label: Permission Modes +--- + +# Permission Modes + +Shares created in zrok `v0.4.26` and newer now include a choice of _permission mode_. + +Shares created with zrok `v0.4.25` and older were created using what is now called the _open permission mode_. Whether _public_ or _private_, these shares can be accessed by any user of the zrok service instance, as long as they know the _share token_ of the share. Effectively shares with the _open permission mode_ are accessible by any user of the zrok service instance. + +zrok now supports a _closed permission mode_, which allows for more fine-grained control over which zrok users are allowed to privately access your shares using `zrok access private`. + +zrok defaults to continuing to create shares with the _open permission mode_. This will likely change in a future release. We're leaving the default behavior in place to allow users a period of time to get comfortable with the new permission modes. + +## Creating a Share with Closed Permission Mode + +Adding the `--closed` flag to the `zrok share` or `zrok reserve` commands will create shares using the _closed permission mode_: + +``` +$ zrok share private --headless --closed -b web . +[ 0.066] INFO main.(*sharePrivateCommand).run: allow other to access your share with the following command: +zrok access private 0vzwzodf0c7g +``` + +By default any environment owned by the account that created the share is _allowed_ to access the new share. But a user trying to access the share from an environment owned by a different account will enounter the following error message: + +``` +$ zrok access private 0vzwzodf0c7g +[ERROR]: unable to access ([POST /access][401] accessUnauthorized) +``` + +The `zrok share` and `zrok reserve` commands now include an `--access-grant` flag, which allows you to specify additional zrok accounts that are allowed to access your shares: + +``` +$ zrok share private --headless --closed --access-grant anotheruser@test.com -b web . +[ 0.062] INFO main.(*sharePrivateCommand).run: allow other to access your share with the following command: +zrok access private y6h4at5xvn6o +``` + +And now `anotheruser@test.com` will be allowed to access the share: + +``` +$ zrok access private --headless y6h4at5xvn6o +[ 0.049] INFO main.(*accessPrivateCommand).run: allocated frontend 'VyvrJihAOEHD' +[ 0.051] INFO main.(*accessPrivateCommand).run: access the zrok share at the following endpoint: http://127.0.0.1:9191 +``` + +## Adding and Removing Access Grants for Existing Shares + +If you've created a share (either reserved or ephemeral) and you forgot to include an access grant, or want to remove an access grant that was mistakenly added, you can use the `zrok modify share` command to make the adjustments: + +Create a share: + +``` +$ zrok share private --headless --closed -b web . +[ 0.064] INFO main.(*sharePrivateCommand).run: allow other to access your share with the following command: +zrok access private s4czjylwk7wa +``` + +In another shell in the same environment you can execute: + +``` +$ zrok modify share s4czjylwk7wa --add-access-grant anotheruser@test.com +updated +``` + +And to remove the grant: + +``` +$ zrok modify share s4czjylwk7wa --remove-access-grant anotheruser@test.com +updated +``` + +## Limitations + +As of `v0.4.26` there is currently no way to _list_ the current access grants. This will be addressed shortly in a subsequent update. \ No newline at end of file diff --git a/rest_client_zrok/share/update_share_responses.go b/rest_client_zrok/share/update_share_responses.go index 96726ef60..5c25b46c8 100644 --- a/rest_client_zrok/share/update_share_responses.go +++ b/rest_client_zrok/share/update_share_responses.go @@ -26,6 +26,12 @@ func (o *UpdateShareReader) ReadResponse(response runtime.ClientResponse, consum return nil, err } return result, nil + case 400: + result := NewUpdateShareBadRequest() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result case 401: result := NewUpdateShareUnauthorized() if err := result.readResponse(response, consumer, o.formats); err != nil { @@ -105,6 +111,62 @@ func (o *UpdateShareOK) readResponse(response runtime.ClientResponse, consumer r return nil } +// NewUpdateShareBadRequest creates a UpdateShareBadRequest with default headers values +func NewUpdateShareBadRequest() *UpdateShareBadRequest { + return &UpdateShareBadRequest{} +} + +/* +UpdateShareBadRequest describes a response with status code 400, with default header values. + +bad request +*/ +type UpdateShareBadRequest struct { +} + +// IsSuccess returns true when this update share bad request response has a 2xx status code +func (o *UpdateShareBadRequest) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this update share bad request response has a 3xx status code +func (o *UpdateShareBadRequest) IsRedirect() bool { + return false +} + +// IsClientError returns true when this update share bad request response has a 4xx status code +func (o *UpdateShareBadRequest) IsClientError() bool { + return true +} + +// IsServerError returns true when this update share bad request response has a 5xx status code +func (o *UpdateShareBadRequest) IsServerError() bool { + return false +} + +// IsCode returns true when this update share bad request response a status code equal to that given +func (o *UpdateShareBadRequest) IsCode(code int) bool { + return code == 400 +} + +// Code gets the status code for the update share bad request response +func (o *UpdateShareBadRequest) Code() int { + return 400 +} + +func (o *UpdateShareBadRequest) Error() string { + return fmt.Sprintf("[PATCH /share][%d] updateShareBadRequest ", 400) +} + +func (o *UpdateShareBadRequest) String() string { + return fmt.Sprintf("[PATCH /share][%d] updateShareBadRequest ", 400) +} + +func (o *UpdateShareBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + return nil +} + // NewUpdateShareUnauthorized creates a UpdateShareUnauthorized with default headers values func NewUpdateShareUnauthorized() *UpdateShareUnauthorized { return &UpdateShareUnauthorized{} diff --git a/rest_model_zrok/share_request.go b/rest_model_zrok/share_request.go index 6125bd95b..68211c5d0 100644 --- a/rest_model_zrok/share_request.go +++ b/rest_model_zrok/share_request.go @@ -21,6 +21,9 @@ import ( // swagger:model shareRequest type ShareRequest struct { + // access grants + AccessGrants []string `json:"accessGrants"` + // auth scheme AuthScheme string `json:"authScheme,omitempty"` @@ -50,6 +53,10 @@ type ShareRequest struct { // Enum: [github google] OauthProvider string `json:"oauthProvider,omitempty"` + // permission mode + // Enum: [open closed] + PermissionMode string `json:"permissionMode,omitempty"` + // reserved Reserved bool `json:"reserved,omitempty"` @@ -77,6 +84,10 @@ func (m *ShareRequest) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validatePermissionMode(formats); err != nil { + res = append(res, err) + } + if err := m.validateShareMode(formats); err != nil { res = append(res, err) } @@ -212,6 +223,48 @@ func (m *ShareRequest) validateOauthProvider(formats strfmt.Registry) error { return nil } +var shareRequestTypePermissionModePropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["open","closed"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + shareRequestTypePermissionModePropEnum = append(shareRequestTypePermissionModePropEnum, v) + } +} + +const ( + + // ShareRequestPermissionModeOpen captures enum value "open" + ShareRequestPermissionModeOpen string = "open" + + // ShareRequestPermissionModeClosed captures enum value "closed" + ShareRequestPermissionModeClosed string = "closed" +) + +// prop value enum +func (m *ShareRequest) validatePermissionModeEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, shareRequestTypePermissionModePropEnum, true); err != nil { + return err + } + return nil +} + +func (m *ShareRequest) validatePermissionMode(formats strfmt.Registry) error { + if swag.IsZero(m.PermissionMode) { // not required + return nil + } + + // value enum + if err := m.validatePermissionModeEnum("permissionMode", "body", m.PermissionMode); err != nil { + return err + } + + return nil +} + var shareRequestTypeShareModePropEnum []interface{} func init() { diff --git a/rest_model_zrok/update_share_request.go b/rest_model_zrok/update_share_request.go index a6b58f758..28a660456 100644 --- a/rest_model_zrok/update_share_request.go +++ b/rest_model_zrok/update_share_request.go @@ -17,9 +17,15 @@ import ( // swagger:model updateShareRequest type UpdateShareRequest struct { + // add access grants + AddAccessGrants []string `json:"addAccessGrants"` + // backend proxy endpoint BackendProxyEndpoint string `json:"backendProxyEndpoint,omitempty"` + // remove access grants + RemoveAccessGrants []string `json:"removeAccessGrants"` + // shr token ShrToken string `json:"shrToken,omitempty"` } diff --git a/rest_server_zrok/embedded_spec.go b/rest_server_zrok/embedded_spec.go index 9d4f6cc4b..227e98203 100644 --- a/rest_server_zrok/embedded_spec.go +++ b/rest_server_zrok/embedded_spec.go @@ -988,6 +988,9 @@ func init() { "200": { "description": "share updated" }, + "400": { + "description": "bad request" + }, "401": { "description": "unauthorized" }, @@ -1562,6 +1565,12 @@ func init() { "shareRequest": { "type": "object", "properties": { + "accessGrants": { + "type": "array", + "items": { + "type": "string" + } + }, "authScheme": { "type": "string" }, @@ -1611,6 +1620,13 @@ func init() { "google" ] }, + "permissionMode": { + "type": "string", + "enum": [ + "open", + "closed" + ] + }, "reserved": { "type": "boolean" }, @@ -1708,9 +1724,21 @@ func init() { "updateShareRequest": { "type": "object", "properties": { + "addAccessGrants": { + "type": "array", + "items": { + "type": "string" + } + }, "backendProxyEndpoint": { "type": "string" }, + "removeAccessGrants": { + "type": "array", + "items": { + "type": "string" + } + }, "shrToken": { "type": "string" } @@ -2715,6 +2743,9 @@ func init() { "200": { "description": "share updated" }, + "400": { + "description": "bad request" + }, "401": { "description": "unauthorized" }, @@ -3289,6 +3320,12 @@ func init() { "shareRequest": { "type": "object", "properties": { + "accessGrants": { + "type": "array", + "items": { + "type": "string" + } + }, "authScheme": { "type": "string" }, @@ -3338,6 +3375,13 @@ func init() { "google" ] }, + "permissionMode": { + "type": "string", + "enum": [ + "open", + "closed" + ] + }, "reserved": { "type": "boolean" }, @@ -3435,9 +3479,21 @@ func init() { "updateShareRequest": { "type": "object", "properties": { + "addAccessGrants": { + "type": "array", + "items": { + "type": "string" + } + }, "backendProxyEndpoint": { "type": "string" }, + "removeAccessGrants": { + "type": "array", + "items": { + "type": "string" + } + }, "shrToken": { "type": "string" } diff --git a/rest_server_zrok/operations/share/update_share_responses.go b/rest_server_zrok/operations/share/update_share_responses.go index 615773819..a651975b5 100644 --- a/rest_server_zrok/operations/share/update_share_responses.go +++ b/rest_server_zrok/operations/share/update_share_responses.go @@ -36,6 +36,31 @@ func (o *UpdateShareOK) WriteResponse(rw http.ResponseWriter, producer runtime.P rw.WriteHeader(200) } +// UpdateShareBadRequestCode is the HTTP code returned for type UpdateShareBadRequest +const UpdateShareBadRequestCode int = 400 + +/* +UpdateShareBadRequest bad request + +swagger:response updateShareBadRequest +*/ +type UpdateShareBadRequest struct { +} + +// NewUpdateShareBadRequest creates UpdateShareBadRequest with default headers values +func NewUpdateShareBadRequest() *UpdateShareBadRequest { + + return &UpdateShareBadRequest{} +} + +// WriteResponse to the client +func (o *UpdateShareBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses + + rw.WriteHeader(400) +} + // UpdateShareUnauthorizedCode is the HTTP code returned for type UpdateShareUnauthorized const UpdateShareUnauthorizedCode int = 401 diff --git a/sdk/golang/sdk/model.go b/sdk/golang/sdk/model.go index 07a857647..818b93cb9 100644 --- a/sdk/golang/sdk/model.go +++ b/sdk/golang/sdk/model.go @@ -20,6 +20,13 @@ const ( PublicShareMode ShareMode = "public" ) +type PermissionMode string + +const ( + OpenPermissionMode PermissionMode = "open" + ClosedPermissionMode PermissionMode = "closed" +) + type ShareRequest struct { Reserved bool UniqueName string @@ -31,6 +38,8 @@ type ShareRequest struct { OauthProvider string OauthEmailAddressPatterns []string OauthAuthorizationCheckInterval time.Duration + PermissionMode PermissionMode + AccessGrants []string } type Share struct { diff --git a/sdk/golang/sdk/share.go b/sdk/golang/sdk/share.go index 04b25eedb..386cb6016 100644 --- a/sdk/golang/sdk/share.go +++ b/sdk/golang/sdk/share.go @@ -71,6 +71,8 @@ func newPrivateShare(root env_core.Root, request *ShareRequest) *share.SharePara BackendMode: string(request.BackendMode), BackendProxyEndpoint: request.Target, AuthScheme: string(None), + PermissionMode: string(request.PermissionMode), + AccessGrants: request.AccessGrants, } return req } @@ -87,6 +89,8 @@ func newPublicShare(root env_core.Root, request *ShareRequest) *share.ShareParam OauthEmailDomains: request.OauthEmailAddressPatterns, OauthProvider: request.OauthProvider, OauthAuthorizationCheckInterval: request.OauthAuthorizationCheckInterval.String(), + PermissionMode: string(request.PermissionMode), + AccessGrants: request.AccessGrants, } return req } diff --git a/sdk/python/sdk/zrok/zrok_api/models/share_request.py b/sdk/python/sdk/zrok/zrok_api/models/share_request.py index 331fa41a0..abfb9eeef 100644 --- a/sdk/python/sdk/zrok/zrok_api/models/share_request.py +++ b/sdk/python/sdk/zrok/zrok_api/models/share_request.py @@ -39,6 +39,8 @@ class ShareRequest(object): 'oauth_email_domains': 'list[str]', 'oauth_authorization_check_interval': 'str', 'reserved': 'bool', + 'permission_mode': 'str', + 'access_grants': 'list[str]', 'unique_name': 'str' } @@ -54,10 +56,12 @@ class ShareRequest(object): 'oauth_email_domains': 'oauthEmailDomains', 'oauth_authorization_check_interval': 'oauthAuthorizationCheckInterval', 'reserved': 'reserved', + 'permission_mode': 'permissionMode', + 'access_grants': 'accessGrants', 'unique_name': 'uniqueName' } - def __init__(self, env_zid=None, share_mode=None, frontend_selection=None, backend_mode=None, backend_proxy_endpoint=None, auth_scheme=None, auth_users=None, oauth_provider=None, oauth_email_domains=None, oauth_authorization_check_interval=None, reserved=None, unique_name=None): # noqa: E501 + def __init__(self, env_zid=None, share_mode=None, frontend_selection=None, backend_mode=None, backend_proxy_endpoint=None, auth_scheme=None, auth_users=None, oauth_provider=None, oauth_email_domains=None, oauth_authorization_check_interval=None, reserved=None, permission_mode=None, access_grants=None, unique_name=None): # noqa: E501 """ShareRequest - a model defined in Swagger""" # noqa: E501 self._env_zid = None self._share_mode = None @@ -70,6 +74,8 @@ def __init__(self, env_zid=None, share_mode=None, frontend_selection=None, backe self._oauth_email_domains = None self._oauth_authorization_check_interval = None self._reserved = None + self._permission_mode = None + self._access_grants = None self._unique_name = None self.discriminator = None if env_zid is not None: @@ -94,6 +100,10 @@ def __init__(self, env_zid=None, share_mode=None, frontend_selection=None, backe self.oauth_authorization_check_interval = oauth_authorization_check_interval if reserved is not None: self.reserved = reserved + if permission_mode is not None: + self.permission_mode = permission_mode + if access_grants is not None: + self.access_grants = access_grants if unique_name is not None: self.unique_name = unique_name @@ -346,6 +356,54 @@ def reserved(self, reserved): self._reserved = reserved + @property + def permission_mode(self): + """Gets the permission_mode of this ShareRequest. # noqa: E501 + + + :return: The permission_mode of this ShareRequest. # noqa: E501 + :rtype: str + """ + return self._permission_mode + + @permission_mode.setter + def permission_mode(self, permission_mode): + """Sets the permission_mode of this ShareRequest. + + + :param permission_mode: The permission_mode of this ShareRequest. # noqa: E501 + :type: str + """ + allowed_values = ["open", "closed"] # noqa: E501 + if permission_mode not in allowed_values: + raise ValueError( + "Invalid value for `permission_mode` ({0}), must be one of {1}" # noqa: E501 + .format(permission_mode, allowed_values) + ) + + self._permission_mode = permission_mode + + @property + def access_grants(self): + """Gets the access_grants of this ShareRequest. # noqa: E501 + + + :return: The access_grants of this ShareRequest. # noqa: E501 + :rtype: list[str] + """ + return self._access_grants + + @access_grants.setter + def access_grants(self, access_grants): + """Sets the access_grants of this ShareRequest. + + + :param access_grants: The access_grants of this ShareRequest. # noqa: E501 + :type: list[str] + """ + + self._access_grants = access_grants + @property def unique_name(self): """Gets the unique_name of this ShareRequest. # noqa: E501 diff --git a/sdk/python/sdk/zrok/zrok_api/models/update_share_request.py b/sdk/python/sdk/zrok/zrok_api/models/update_share_request.py index 069f143b4..3548c1017 100644 --- a/sdk/python/sdk/zrok/zrok_api/models/update_share_request.py +++ b/sdk/python/sdk/zrok/zrok_api/models/update_share_request.py @@ -29,23 +29,33 @@ class UpdateShareRequest(object): """ swagger_types = { 'shr_token': 'str', - 'backend_proxy_endpoint': 'str' + 'backend_proxy_endpoint': 'str', + 'add_access_grants': 'list[str]', + 'remove_access_grants': 'list[str]' } attribute_map = { 'shr_token': 'shrToken', - 'backend_proxy_endpoint': 'backendProxyEndpoint' + 'backend_proxy_endpoint': 'backendProxyEndpoint', + 'add_access_grants': 'addAccessGrants', + 'remove_access_grants': 'removeAccessGrants' } - def __init__(self, shr_token=None, backend_proxy_endpoint=None): # noqa: E501 + def __init__(self, shr_token=None, backend_proxy_endpoint=None, add_access_grants=None, remove_access_grants=None): # noqa: E501 """UpdateShareRequest - a model defined in Swagger""" # noqa: E501 self._shr_token = None self._backend_proxy_endpoint = None + self._add_access_grants = None + self._remove_access_grants = None self.discriminator = None if shr_token is not None: self.shr_token = shr_token if backend_proxy_endpoint is not None: self.backend_proxy_endpoint = backend_proxy_endpoint + if add_access_grants is not None: + self.add_access_grants = add_access_grants + if remove_access_grants is not None: + self.remove_access_grants = remove_access_grants @property def shr_token(self): @@ -89,6 +99,48 @@ def backend_proxy_endpoint(self, backend_proxy_endpoint): self._backend_proxy_endpoint = backend_proxy_endpoint + @property + def add_access_grants(self): + """Gets the add_access_grants of this UpdateShareRequest. # noqa: E501 + + + :return: The add_access_grants of this UpdateShareRequest. # noqa: E501 + :rtype: list[str] + """ + return self._add_access_grants + + @add_access_grants.setter + def add_access_grants(self, add_access_grants): + """Sets the add_access_grants of this UpdateShareRequest. + + + :param add_access_grants: The add_access_grants of this UpdateShareRequest. # noqa: E501 + :type: list[str] + """ + + self._add_access_grants = add_access_grants + + @property + def remove_access_grants(self): + """Gets the remove_access_grants of this UpdateShareRequest. # noqa: E501 + + + :return: The remove_access_grants of this UpdateShareRequest. # noqa: E501 + :rtype: list[str] + """ + return self._remove_access_grants + + @remove_access_grants.setter + def remove_access_grants(self, remove_access_grants): + """Sets the remove_access_grants of this UpdateShareRequest. + + + :param remove_access_grants: The remove_access_grants of this UpdateShareRequest. # noqa: E501 + :type: list[str] + """ + + self._remove_access_grants = remove_access_grants + def to_dict(self): """Returns the model properties as a dict""" result = {} diff --git a/specs/zrok.yml b/specs/zrok.yml index 5371feb01..155f3730d 100644 --- a/specs/zrok.yml +++ b/specs/zrok.yml @@ -651,6 +651,8 @@ paths: responses: 200: description: share updated + 400: + description: bad request 401: description: unauthorized 404: @@ -1057,6 +1059,13 @@ definitions: type: string reserved: type: boolean + permissionMode: + type: string + enum: ["open", "closed"] + accessGrants: + type: array + items: + type: string uniqueName: type: string @@ -1120,6 +1129,14 @@ definitions: type: string backendProxyEndpoint: type: string + addAccessGrants: + type: array + items: + type: string + removeAccessGrants: + type: array + items: + type: string verifyRequest: type: object diff --git a/ui/src/api/types.js b/ui/src/api/types.js index bcadc5f9e..153ff7237 100644 --- a/ui/src/api/types.js +++ b/ui/src/api/types.js @@ -267,6 +267,8 @@ * @property {string[]} oauthEmailDomains * @property {string} oauthAuthorizationCheckInterval * @property {boolean} reserved + * @property {string} permissionMode + * @property {string[]} accessGrants * @property {string} uniqueName */ @@ -319,6 +321,8 @@ * * @property {string} shrToken * @property {string} backendProxyEndpoint + * @property {string[]} addAccessGrants + * @property {string[]} removeAccessGrants */ /**