Skip to content

Commit

Permalink
Merge pull request #573 from openziti/permission_model
Browse files Browse the repository at this point in the history
Permission Model: Phase 1 (#432)
  • Loading branch information
michaelquigley authored Mar 8, 2024
2 parents 698ba19 + 01a18e3 commit f1c9f11
Show file tree
Hide file tree
Showing 27 changed files with 739 additions and 22 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions cmd/zrok/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{})
Expand Down Expand Up @@ -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",
Expand Down
75 changes: 75 additions & 0 deletions cmd/zrok/modifyShare.go
Original file line number Diff line number Diff line change
@@ -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 <shareToken>",
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")
}
}
8 changes: 8 additions & 0 deletions cmd/zrok/reserve.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type reserveCommand struct {
oauthProvider string
oauthEmailAddressPatterns []string
oauthCheckInterval time.Duration
closed bool
accessGrants []string
cmd *cobra.Command
}

Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
18 changes: 13 additions & 5 deletions cmd/zrok/sharePrivate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 <target>")
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
}
Expand Down Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions cmd/zrok/sharePublic.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ type sharePublicCommand struct {
oauthProvider string
oauthEmailAddressPatterns []string
oauthCheckInterval time.Duration
closed bool
accessGrants []string
cmd *cobra.Command
}

Expand All @@ -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 <target>")
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 (<username:password>,...)")
cmd.Flags().StringVar(&command.oauthProvider, "oauth-provider", "", "Enable OAuth provider [google, github]")
Expand Down Expand Up @@ -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
Expand Down
32 changes: 31 additions & 1 deletion controller/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,27 @@ 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 {
logrus.Errorf("unable to find share '%v' for user '%v'", shrToken, principal.Email)
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()
Expand Down Expand Up @@ -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)
}
27 changes: 27 additions & 0 deletions controller/share.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -134,6 +147,10 @@ func (h *shareHandler) Handle(params share.ShareParams, principal *rest_model_zr
BackendMode: params.Body.BackendMode,
BackendProxyEndpoint: &params.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 = &params.Body.FrontendSelection[0]
Expand All @@ -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()
Expand Down
57 changes: 57 additions & 0 deletions controller/store/accessGrant.go
Original file line number Diff line number Diff line change
@@ -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
}
7 changes: 7 additions & 0 deletions controller/store/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,10 @@ const (
WarningAction LimitJournalAction = "warning"
ClearAction LimitJournalAction = "clear"
)

type PermissionMode string

const (
OpenPermissionMode PermissionMode = "open"
ClosedPermissionMode PermissionMode = "closed"
)
Loading

0 comments on commit f1c9f11

Please sign in to comment.