diff --git a/CHANGELOG.md b/CHANGELOG.md index 9511c772..3d11b8f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ FEATURE: Added self service password change. There is a new tab in the `zrok` co FEATURE: The web console now supports revoking your current account token and generating a new one (https://github.com/openziti/zrok/issues/191) +CHANGE: When specifying OAuth configuration for public shares from the `zrok share public` or `zrok reserve` public commands, the flags and functionality for restricting the allowed email addresses of the authenticating users has changed. The old flag was `--oauth-email-domains`, which took a string value that needed to be contained in the user's email address. The new flag is `--oauth-email-address-patterns`, which accepts a glob-style filter, using https://github.com/gobwas/glob (https://github.com/openziti/zrok/issues/413) + CHANGE: Creating a reserved share checks for token collision and returns a more appropriate error message (https://github.com/openziti/zrok/issues/531) CHANGE: Update UI to add a 'true' value on `reserved` boolean (https://github.com/openziti/zrok/issues/443) diff --git a/cmd/zrok/reserve.go b/cmd/zrok/reserve.go index 519e3ddf..4fb76a42 100644 --- a/cmd/zrok/reserve.go +++ b/cmd/zrok/reserve.go @@ -18,15 +18,15 @@ func init() { } type reserveCommand struct { - uniqueName string - basicAuth []string - frontendSelection []string - backendMode string - jsonOutput bool - oauthProvider string - oauthEmailDomains []string - oauthCheckInterval time.Duration - cmd *cobra.Command + uniqueName string + basicAuth []string + frontendSelection []string + backendMode string + jsonOutput bool + oauthProvider string + oauthEmailAddressPatterns []string + oauthCheckInterval time.Duration + cmd *cobra.Command } func newReserveCommand() *reserveCommand { @@ -42,7 +42,7 @@ func newReserveCommand() *reserveCommand { cmd.Flags().BoolVarP(&command.jsonOutput, "json-output", "j", false, "Emit JSON describing the created reserved share") cmd.Flags().StringArrayVar(&command.basicAuth, "basic-auth", []string{}, "Basic authentication users (,...)") cmd.Flags().StringVar(&command.oauthProvider, "oauth-provider", "", "Enable OAuth provider [google, github]") - cmd.Flags().StringArrayVar(&command.oauthEmailDomains, "oauth-email-domains", []string{}, "Allow only these email domains to authenticate via OAuth") + 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") @@ -139,7 +139,7 @@ func (cmd *reserveCommand) run(_ *cobra.Command, args []string) { tui.Error("--oauth-provider only supported for public shares", nil) } req.OauthProvider = cmd.oauthProvider - req.OauthEmailDomains = cmd.oauthEmailDomains + req.OauthEmailAddressPatterns = cmd.oauthEmailAddressPatterns req.OauthAuthorizationCheckInterval = cmd.oauthCheckInterval } shr, err := sdk.CreateShare(env, req) diff --git a/cmd/zrok/sharePublic.go b/cmd/zrok/sharePublic.go index 10045510..80e859fb 100644 --- a/cmd/zrok/sharePublic.go +++ b/cmd/zrok/sharePublic.go @@ -3,6 +3,7 @@ package main import ( "fmt" tea "github.com/charmbracelet/bubbletea" + "github.com/gobwas/glob" "github.com/openziti/zrok/endpoints" drive "github.com/openziti/zrok/endpoints/drive" "github.com/openziti/zrok/endpoints/proxy" @@ -24,15 +25,15 @@ func init() { } type sharePublicCommand struct { - basicAuth []string - frontendSelection []string - backendMode string - headless bool - insecure bool - oauthProvider string - oauthEmailDomains []string - oauthCheckInterval time.Duration - cmd *cobra.Command + basicAuth []string + frontendSelection []string + backendMode string + headless bool + insecure bool + oauthProvider string + oauthEmailAddressPatterns []string + oauthCheckInterval time.Duration + cmd *cobra.Command } func newSharePublicCommand() *sharePublicCommand { @@ -49,7 +50,7 @@ func newSharePublicCommand() *sharePublicCommand { cmd.Flags().StringArrayVar(&command.basicAuth, "basic-auth", []string{}, "Basic authentication users (,...)") cmd.Flags().StringVar(&command.oauthProvider, "oauth-provider", "", "Enable OAuth provider [google, github]") - cmd.Flags().StringArrayVar(&command.oauthEmailDomains, "oauth-email-domains", []string{}, "Allow only these email domains to authenticate via OAuth") + cmd.Flags().StringArrayVar(&command.oauthEmailAddressPatterns, "oauth-email-address-patterns", []string{}, "Allow only these email domain globs 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") @@ -114,8 +115,18 @@ func (cmd *sharePublicCommand) run(_ *cobra.Command, args []string) { } if cmd.oauthProvider != "" { req.OauthProvider = cmd.oauthProvider - req.OauthEmailDomains = cmd.oauthEmailDomains + req.OauthEmailAddressPatterns = cmd.oauthEmailAddressPatterns req.OauthAuthorizationCheckInterval = cmd.oauthCheckInterval + + for _, g := range cmd.oauthEmailAddressPatterns { + _, err := glob.Compile(g) + if err != nil { + if !panicInstead { + tui.Error(fmt.Sprintf("unable to create share, invalid oauth email glob (%v)", g), err) + } + panic(err) + } + } } shr, err := sdk.CreateShare(root, req) if err != nil { diff --git a/endpoints/publicProxy/http.go b/endpoints/publicProxy/http.go index c372d380..60fc4438 100644 --- a/endpoints/publicProxy/http.go +++ b/endpoints/publicProxy/http.go @@ -4,6 +4,7 @@ import ( "context" "crypto/md5" "fmt" + "github.com/gobwas/glob" "github.com/golang-jwt/jwt/v5" "github.com/openziti/sdk-golang/ziti" "github.com/openziti/zrok/endpoints" @@ -266,21 +267,33 @@ func authHandler(handler http.Handler, pcfg *Config, key []byte, ctx ziti.Contex return } - if validDomains, found := oauthCfg.(map[string]interface{})["email_domains"]; found { - if castedDomains, ok := validDomains.([]interface{}); !ok { - logrus.Error("invalid email domain format") + if validEmailAddressPatterns, found := oauthCfg.(map[string]interface{})["email_domains"]; found { + if castedPatterns, ok := validEmailAddressPatterns.([]interface{}); !ok { + logrus.Error("invalid email pattern array format") return } else { - if len(castedDomains) > 0 { + if len(castedPatterns) > 0 { found := false - for _, domain := range castedDomains { - if strings.HasSuffix(claims.Email, domain.(string)) { - found = true - break + for _, pattern := range castedPatterns { + if castedPattern, ok := pattern.(string); ok { + match, err := glob.Compile(castedPattern) + if err != nil { + logrus.Errorf("invalid email address pattern glob '%v': %v", pattern.(string), err) + unauthorizedUi.WriteUnauthorized(w) + return + } + if match.Match(claims.Email) { + found = true + break + } + } else { + logrus.Errorf("invalid email address pattern '%v'", pattern) + unauthorizedUi.WriteUnauthorized(w) + return } } if !found { - logrus.Warnf("invalid email domain") + logrus.Warnf("unauthorized email '%v' for '%v'", claims.Email, shrToken) unauthorizedUi.WriteUnauthorized(w) return } diff --git a/go.mod b/go.mod index a594c452..e0729ece 100644 --- a/go.mod +++ b/go.mod @@ -106,6 +106,7 @@ require ( github.com/go-resty/resty/v2 v2.11.0 // indirect github.com/go-sql-driver/mysql v1.7.1 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/gobwas/glob v0.2.3 // indirect github.com/golang/glog v1.1.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect diff --git a/go.sum b/go.sum index 0d3fcc8f..f005d219 100644 --- a/go.sum +++ b/go.sum @@ -299,6 +299,8 @@ github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= diff --git a/sdk/golang/sdk/model.go b/sdk/golang/sdk/model.go index 4289b84e..07a85764 100644 --- a/sdk/golang/sdk/model.go +++ b/sdk/golang/sdk/model.go @@ -29,7 +29,7 @@ type ShareRequest struct { Frontends []string BasicAuth []string OauthProvider string - OauthEmailDomains []string + OauthEmailAddressPatterns []string OauthAuthorizationCheckInterval time.Duration } diff --git a/sdk/golang/sdk/share.go b/sdk/golang/sdk/share.go index 49fc5606..04b25eed 100644 --- a/sdk/golang/sdk/share.go +++ b/sdk/golang/sdk/share.go @@ -84,7 +84,7 @@ func newPublicShare(root env_core.Root, request *ShareRequest) *share.ShareParam BackendMode: string(request.BackendMode), BackendProxyEndpoint: request.Target, AuthScheme: string(None), - OauthEmailDomains: request.OauthEmailDomains, + OauthEmailDomains: request.OauthEmailAddressPatterns, OauthProvider: request.OauthProvider, OauthAuthorizationCheckInterval: request.OauthAuthorizationCheckInterval.String(), } diff --git a/sdk/python/sdk/zrok/zrok/model.py b/sdk/python/sdk/zrok/zrok/model.py index bb6f5d08..8a835623 100644 --- a/sdk/python/sdk/zrok/zrok/model.py +++ b/sdk/python/sdk/zrok/zrok/model.py @@ -22,7 +22,7 @@ class ShareRequest: Frontends: list[str] = field(default_factory=list[str]) BasicAuth: list[str] = field(default_factory=list[str]) OauthProvider: str = "" - OauthEmailDomains: list[str] = field(default_factory=list[str]) + OauthEmailAddressPatterns: list[str] = field(default_factory=list[str]) OauthAuthorizationCheckInterval: str = "" Reserved: bool = False UniqueName: str = "" diff --git a/sdk/python/sdk/zrok/zrok/share.py b/sdk/python/sdk/zrok/zrok/share.py index b2a2056a..3ab2c9f7 100644 --- a/sdk/python/sdk/zrok/zrok/share.py +++ b/sdk/python/sdk/zrok/zrok/share.py @@ -78,7 +78,7 @@ def __newPublicShare(root: Root, request: model.ShareRequest) -> ShareRequest: backend_mode=request.BackendMode, backend_proxy_endpoint=request.Target, auth_scheme=model.AUTH_SCHEME_NONE, - oauth_email_domains=request.OauthEmailDomains, + oauth_email_domains=request.OauthEmailAddressPatterns, oauth_authorization_check_interval=request.OauthAuthorizationCheckInterval ) if request.OauthProvider != "":