Merge branch 'main' into snyk-upgrade-d8c6f5b4c55065b4ec64d776fcbd3a56

This commit is contained in:
Michael Quigley 2024-06-21 16:01:17 -04:00
commit d2e8da8b2c
No known key found for this signature in database
GPG Key ID: 9B60314A9DD20A62
46 changed files with 821 additions and 358 deletions

View File

@ -1,5 +1,23 @@
# CHANGELOG # CHANGELOG
## v0.4.33
CHANGE: Updated react-bootstrap to version 2.10.2.
CHANGE: Updated @mui/material to version 5.15.18.
## v0.4.32
FEATURE: New permission mode support for public frontends. Open permission mode frontends are available to all users in the service instance. Closed permission mode frontends reference the new `frontend_grants` table that can be used to control which accounts are allowed to create shares using that frontend. `zrok admin create frontend` now supports `--closed` flag to create closed permission mode frontends (https://github.com/openziti/zrok/issues/539)
FEATURE: New config `defaultFrontend` that specifies the default frontend to be used for an environment. Provides the default `--frontend` for `zrok share public` and `zrok reserve public` (https://github.com/openziti/zrok/issues/663)
FEATURE: Resource count limits now include `share_frontends` to limit the number of frontends that are allowed to make connections to a share (https://github.com/openziti/zrok/issues/650)
CHANGE: The frontend selection flag used by `zrok share public` and `zrok reserve public` has been changed from `--frontends` to `--frontend`
FIX: use controller config spec v4 in the Docker instance
## v0.4.31 ## v0.4.31
FEATURE: New "limits classes" limits implementation (https://github.com/openziti/zrok/issues/606). This new feature allows for extensive limits customization on a per-user basis, with fallback to the global defaults in the controller configuration. FEATURE: New "limits classes" limits implementation (https://github.com/openziti/zrok/issues/606). This new feature allows for extensive limits customization on a per-user basis, with fallback to the global defaults in the controller configuration.

View File

@ -4,6 +4,7 @@ import (
"github.com/openziti/zrok/environment" "github.com/openziti/zrok/environment"
"github.com/openziti/zrok/rest_client_zrok/admin" "github.com/openziti/zrok/rest_client_zrok/admin"
"github.com/openziti/zrok/rest_model_zrok" "github.com/openziti/zrok/rest_model_zrok"
"github.com/openziti/zrok/sdk/golang/sdk"
"github.com/openziti/zrok/tui" "github.com/openziti/zrok/tui"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -16,6 +17,7 @@ func init() {
type adminCreateFrontendCommand struct { type adminCreateFrontendCommand struct {
cmd *cobra.Command cmd *cobra.Command
closed bool
} }
func newAdminCreateFrontendCommand() *adminCreateFrontendCommand { func newAdminCreateFrontendCommand() *adminCreateFrontendCommand {
@ -25,6 +27,7 @@ func newAdminCreateFrontendCommand() *adminCreateFrontendCommand {
Args: cobra.ExactArgs(3), Args: cobra.ExactArgs(3),
} }
command := &adminCreateFrontendCommand{cmd: cmd} command := &adminCreateFrontendCommand{cmd: cmd}
cmd.Flags().BoolVar(&command.closed, "closed", false, "Enabled closed permission mode")
cmd.Run = command.run cmd.Run = command.run
return command return command
} }
@ -44,11 +47,16 @@ func (cmd *adminCreateFrontendCommand) run(_ *cobra.Command, args []string) {
panic(err) panic(err)
} }
permissionMode := sdk.OpenPermissionMode
if cmd.closed {
permissionMode = sdk.ClosedPermissionMode
}
req := admin.NewCreateFrontendParams() req := admin.NewCreateFrontendParams()
req.Body = &rest_model_zrok.CreateFrontendRequest{ req.Body = &rest_model_zrok.CreateFrontendRequest{
ZID: zId, ZID: zId,
PublicName: publicName, PublicName: publicName,
URLTemplate: urlTemplate, URLTemplate: urlTemplate,
PermissionMode: string(permissionMode),
} }
resp, err := zrok.Admin.CreateFrontend(req, mustGetAdminAuth()) resp, err := zrok.Admin.CreateFrontend(req, mustGetAdminAuth())

View File

@ -40,6 +40,12 @@ func (cmd *configGetCommand) run(_ *cobra.Command, args []string) {
} else { } else {
fmt.Println("apiEndpoint = <unset>") fmt.Println("apiEndpoint = <unset>")
} }
case "defaultFrontend":
if env.Config() != nil && env.Config().DefaultFrontend != "" {
fmt.Printf("defaultFrontend = %v\n", env.Config().DefaultFrontend)
} else {
fmt.Println("defaultFrontend = <unset>")
}
default: default:
fmt.Printf("unknown config name '%v'\n", configName) fmt.Printf("unknown config name '%v'\n", configName)
} }

View File

@ -63,6 +63,20 @@ func (cmd *configSetCommand) run(_ *cobra.Command, args []string) {
fmt.Printf("\n[%v]: because you have a %v-d environment, you won't see your config change until you run %v first!\n\n", tui.WarningLabel, tui.Code.Render("zrok enable"), tui.Code.Render("zrok disable")) fmt.Printf("\n[%v]: because you have a %v-d environment, you won't see your config change until you run %v first!\n\n", tui.WarningLabel, tui.Code.Render("zrok enable"), tui.Code.Render("zrok disable"))
} }
case "defaultFrontend":
if env.Config() == nil {
if err := env.SetConfig(&env_core.Config{DefaultFrontend: value}); err != nil {
tui.Error("unable to save config", err)
}
} else {
cfg := env.Config()
cfg.DefaultFrontend = value
if err := env.SetConfig(cfg); err != nil {
tui.Error("unable to save config", err)
}
}
fmt.Println("zrok configuration updated")
default: default:
fmt.Printf("unknown config name '%v'\n", configName) fmt.Printf("unknown config name '%v'\n", configName)
os.Exit(1) os.Exit(1)

View File

@ -3,7 +3,6 @@ package main
import ( import (
"fmt" "fmt"
"github.com/openziti/zrok/environment" "github.com/openziti/zrok/environment"
"github.com/openziti/zrok/environment/env_core"
"github.com/openziti/zrok/tui" "github.com/openziti/zrok/tui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"os" "os"
@ -36,18 +35,25 @@ func (cmd *configUnsetCommand) run(_ *cobra.Command, args []string) {
panic(err) panic(err)
} }
if env.Config() != nil {
cfg := env.Config()
switch configName { switch configName {
case "apiEndpoint": case "apiEndpoint":
if err := env.SetConfig(&env_core.Config{}); err != nil { cfg.ApiEndpoint = ""
tui.Error("unable to save config", err)
}
fmt.Println("zrok configuration updated")
if env.IsEnabled() { if env.IsEnabled() {
fmt.Printf("\n[%v]: because you have a %v-d environment, you won't see your config change until you run %v first!\n\n", tui.WarningLabel, tui.Code.Render("zrok enable"), tui.Code.Render("zrok disable")) fmt.Printf("\n[%v]: because you have a %v-d environment, you won't see your config change until you run %v first!\n\n", tui.WarningLabel, tui.Code.Render("zrok enable"), tui.Code.Render("zrok disable"))
} }
case "defaultFrontend":
cfg.DefaultFrontend = ""
default: default:
fmt.Printf("unknown config name '%v'\n", configName) fmt.Printf("unknown config name '%v'\n", configName)
os.Exit(1) os.Exit(1)
} }
if err := env.SetConfig(cfg); err != nil {
tui.Error("unable to save config", err)
}
fmt.Println("zrok configuration updated")
}
} }

View File

@ -40,8 +40,13 @@ func newReserveCommand() *reserveCommand {
Args: cobra.RangeArgs(1, 2), Args: cobra.RangeArgs(1, 2),
} }
command := &reserveCommand{cmd: cmd} command := &reserveCommand{cmd: cmd}
defaultFrontends := []string{"public"}
if root, err := environment.LoadRoot(); err == nil {
defaultFrontend, _ := root.DefaultFrontend()
defaultFrontends = []string{defaultFrontend}
}
cmd.Flags().StringVarP(&command.uniqueName, "unique-name", "n", "", "A unique name for the reserved share (defaults to generated identifier)") cmd.Flags().StringVarP(&command.uniqueName, "unique-name", "n", "", "A unique name for the reserved share (defaults to generated identifier)")
cmd.Flags().StringArrayVar(&command.frontendSelection, "frontends", []string{"public"}, "Selected frontends to use for the share") cmd.Flags().StringArrayVar(&command.frontendSelection, "frontend", defaultFrontends, "Selected frontends to use for the share")
cmd.Flags().StringVarP(&command.backendMode, "backend-mode", "b", "proxy", "The backend mode (public|private: proxy, web, caddy, drive) (private: tcpTunnel, udpTunnel, socks, vpn)") cmd.Flags().StringVarP(&command.backendMode, "backend-mode", "b", "proxy", "The backend mode (public|private: proxy, web, caddy, drive) (private: tcpTunnel, udpTunnel, socks, vpn)")
cmd.Flags().BoolVarP(&command.jsonOutput, "json-output", "j", false, "Emit JSON describing the created reserved share") 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 (<username:password>,...)") cmd.Flags().StringArrayVar(&command.basicAuth, "basic-auth", []string{}, "Basic authentication users (<username:password>,...)")

View File

@ -45,7 +45,12 @@ func newSharePublicCommand() *sharePublicCommand {
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
} }
command := &sharePublicCommand{cmd: cmd} command := &sharePublicCommand{cmd: cmd}
cmd.Flags().StringArrayVar(&command.frontendSelection, "frontends", []string{"public"}, "Selected frontends to use for the share") defaultFrontends := []string{"public"}
if root, err := environment.LoadRoot(); err == nil {
defaultFrontend, _ := root.DefaultFrontend()
defaultFrontends = []string{defaultFrontend}
}
cmd.Flags().StringArrayVar(&command.frontendSelection, "frontend", defaultFrontends, "Selected frontends to use for the share")
cmd.Flags().StringVarP(&command.backendMode, "backend-mode", "b", "proxy", "The backend mode {proxy, web, caddy, drive}") 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.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.insecure, "insecure", false, "Enable insecure TLS certificate validation for <target>")

View File

@ -48,8 +48,10 @@ func (cmd *statusCommand) run(_ *cobra.Command, _ []string) {
t.SetOutputMirror(os.Stdout) t.SetOutputMirror(os.Stdout)
t.SetStyle(table.StyleColoredDark) t.SetStyle(table.StyleColoredDark)
t.AppendHeader(table.Row{"Config", "Value", "Source"}) t.AppendHeader(table.Row{"Config", "Value", "Source"})
apiEndpoint, from := env.ApiEndpoint() apiEndpoint, apiEndpointFrom := env.ApiEndpoint()
t.AppendRow(table.Row{"apiEndpoint", apiEndpoint, from}) t.AppendRow(table.Row{"apiEndpoint", apiEndpoint, apiEndpointFrom})
defaultFrontend, defaultFrontendFrom := env.DefaultFrontend()
t.AppendRow(table.Row{"defaultFrontend", defaultFrontend, defaultFrontendFrom})
t.Render() t.Render()
_, _ = fmt.Fprintf(os.Stderr, "\n") _, _ = fmt.Fprintf(os.Stderr, "\n")

View File

@ -2,7 +2,6 @@ package controller
import ( import (
"errors" "errors"
"github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware"
"github.com/lib/pq" "github.com/lib/pq"
"github.com/mattn/go-sqlite3" "github.com/mattn/go-sqlite3"
@ -62,6 +61,7 @@ func (h *createFrontendHandler) Handle(params admin.CreateFrontendParams, princi
PublicName: &params.Body.PublicName, PublicName: &params.Body.PublicName,
UrlTemplate: &params.Body.URLTemplate, UrlTemplate: &params.Body.URLTemplate,
Reserved: true, Reserved: true,
PermissionMode: store.PermissionMode(params.Body.PermissionMode),
} }
if _, err := str.CreateGlobalFrontend(fe, tx); err != nil { if _, err := str.CreateGlobalFrontend(fe, tx); err != nil {
perr := &pq.Error{} perr := &pq.Error{}

View File

@ -163,32 +163,49 @@ func (a *Agent) CanAccessShare(shrId int, trx *sqlx.Tx) (bool, error) {
return false, err return false, err
} }
if env.AccountId != nil { if env.AccountId != nil {
if err := a.str.LimitCheckLock(*env.AccountId, trx); err != nil {
return false, err
}
ul, err := a.getUserLimits(*env.AccountId, trx) ul, err := a.getUserLimits(*env.AccountId, trx)
if err != nil { if err != nil {
return false, err return false, err
} }
if ul.resource.IsGlobal() { if scopedBwc, found := ul.scopes[sdk.BackendMode(shr.BackendMode)]; found {
if empty, err := a.str.IsBandwidthLimitJournalEmptyForGlobal(*env.AccountId, trx); err == nil && !empty { latestScopedJe, err := a.isBandwidthClassLimitedForAccount(*env.AccountId, scopedBwc, trx)
lj, err := a.str.FindLatestBandwidthLimitJournalForGlobal(*env.AccountId, trx)
if err != nil { if err != nil {
return false, err return false, err
} }
if lj.Action == store.LimitLimitAction { if latestScopedJe != nil {
return false, nil return false, nil
} }
}
} else { } else {
if empty, err := a.str.IsBandwidthLimitJournalEmptyForLimitClass(*env.AccountId, ul.resource.GetLimitClassId(), trx); err == nil && !empty { for _, bwc := range ul.bandwidth {
lj, err := a.str.FindLatestBandwidthLimitJournalForLimitClass(*env.AccountId, ul.resource.GetLimitClassId(), trx) latestJe, err := a.isBandwidthClassLimitedForAccount(*env.AccountId, bwc, trx)
if err != nil { if err != nil {
return false, err return false, err
} }
if lj.Action == store.LimitLimitAction { if latestJe != nil {
return false, nil return false, nil
} }
} }
} }
rc := ul.resource
if scopeRc, found := ul.scopes[sdk.BackendMode(shr.BackendMode)]; found {
rc = scopeRc
}
if rc.GetShareFrontends() > store.Unlimited {
fes, err := a.str.FindFrontendsForPrivateShare(shr.Id, trx)
if err != nil {
return false, err
}
if len(fes)+1 > rc.GetShareFrontends() {
logrus.Infof("account '#%d' over frontends per share limit '%d'", *env.AccountId, rc.GetReservedShares())
return false, nil
}
}
} else { } else {
return false, nil return false, nil
} }

View File

@ -10,6 +10,7 @@ type Config struct {
Shares int Shares int
ReservedShares int ReservedShares int
UniqueNames int UniqueNames int
ShareFrontends int
Bandwidth *BandwidthPerPeriod Bandwidth *BandwidthPerPeriod
Cycle time.Duration Cycle time.Duration
Enforcing bool Enforcing bool
@ -49,6 +50,7 @@ func DefaultConfig() *Config {
Shares: store.Unlimited, Shares: store.Unlimited,
ReservedShares: store.Unlimited, ReservedShares: store.Unlimited,
UniqueNames: store.Unlimited, UniqueNames: store.Unlimited,
ShareFrontends: store.Unlimited,
Bandwidth: DefaultBandwidthPerPeriod(), Bandwidth: DefaultBandwidthPerPeriod(),
Enforcing: false, Enforcing: false,
Cycle: 15 * time.Minute, Cycle: 15 * time.Minute,

View File

@ -37,6 +37,10 @@ func (rcc *configResourceCountClass) GetUniqueNames() int {
return rcc.cfg.UniqueNames return rcc.cfg.UniqueNames
} }
func (rcc *configResourceCountClass) String() string { func (rcc *configResourceCountClass) GetShareFrontends() int {
return fmt.Sprintf("Config<environments: %d, shares: %d, reservedShares: %d, uniqueNames: %d>", rcc.cfg.Environments, rcc.cfg.Shares, rcc.cfg.ReservedShares, rcc.cfg.UniqueNames) return rcc.cfg.ShareFrontends
}
func (rcc *configResourceCountClass) String() string {
return fmt.Sprintf("Config<environments: %d, shares: %d, reservedShares: %d, uniqueNames: %d, share_frontends: %d>", rcc.cfg.Environments, rcc.cfg.Shares, rcc.cfg.ReservedShares, rcc.cfg.UniqueNames, rcc.cfg.ShareFrontends)
} }

View File

@ -42,7 +42,6 @@ func (ul *userLimits) ignoreBackends(bwc store.BandwidthClass) map[sdk.BackendMo
} }
return ignoreBackends return ignoreBackends
} }
return nil
} }
func (a *Agent) getUserLimits(acctId int, trx *sqlx.Tx) (*userLimits, error) { func (a *Agent) getUserLimits(acctId int, trx *sqlx.Tx) (*userLimits, error) {
@ -85,7 +84,7 @@ func (a *Agent) isResourceCountClass(alc *store.LimitClass) bool {
if alc.BackendMode != nil { if alc.BackendMode != nil {
return false return false
} }
if alc.Environments == store.Unlimited && alc.Shares == store.Unlimited && alc.ReservedShares == store.Unlimited && alc.UniqueNames == store.Unlimited { if alc.Environments == store.Unlimited && alc.Shares == store.Unlimited && alc.ReservedShares == store.Unlimited && alc.UniqueNames == store.Unlimited && alc.ShareFrontends == store.Unlimited {
return false return false
} }
return true return true
@ -95,7 +94,7 @@ func (a *Agent) isUnscopedBandwidthClass(alc *store.LimitClass) bool {
if alc.BackendMode != nil { if alc.BackendMode != nil {
return false return false
} }
if alc.Environments > store.Unlimited || alc.Shares > store.Unlimited || alc.ReservedShares > store.Unlimited || alc.UniqueNames > store.Unlimited { if alc.Environments > store.Unlimited || alc.Shares > store.Unlimited || alc.ReservedShares > store.Unlimited || alc.UniqueNames > store.Unlimited || alc.ShareFrontends > store.Unlimited {
return false return false
} }
if alc.PeriodMinutes < 1 { if alc.PeriodMinutes < 1 {

View File

@ -116,6 +116,17 @@ func (h *shareHandler) Handle(params share.ShareParams, principal *rest_model_zr
logrus.Error(err) logrus.Error(err)
return share.NewShareNotFound() return share.NewShareNotFound()
} }
if sfe.PermissionMode == store.ClosedPermissionMode {
granted, err := str.IsFrontendGrantedToAccount(int(principal.ID), sfe.Id, trx)
if err != nil {
logrus.Error(err)
return share.NewShareInternalServerError()
}
if !granted {
logrus.Errorf("'%v' is not granted access to frontend '%v'", principal.Email, frontendSelection)
return share.NewShareNotFound()
}
}
if sfe != nil && sfe.UrlTemplate != nil { if sfe != nil && sfe.UrlTemplate != nil {
frontendZIds = append(frontendZIds, sfe.ZId) frontendZIds = append(frontendZIds, sfe.ZId)
frontendTemplates = append(frontendTemplates, *sfe.UrlTemplate) frontendTemplates = append(frontendTemplates, *sfe.UrlTemplate)

View File

@ -14,28 +14,28 @@ type Frontend struct {
PublicName *string PublicName *string
UrlTemplate *string UrlTemplate *string
Reserved bool Reserved bool
Deleted bool PermissionMode PermissionMode
} }
func (str *Store) CreateFrontend(envId int, f *Frontend, tx *sqlx.Tx) (int, error) { func (str *Store) CreateFrontend(envId int, f *Frontend, tx *sqlx.Tx) (int, error) {
stmt, err := tx.Prepare("insert into frontends (environment_id, private_share_id, token, z_id, public_name, url_template, reserved) values ($1, $2, $3, $4, $5, $6, $7) returning id") stmt, err := tx.Prepare("insert into frontends (environment_id, private_share_id, token, z_id, public_name, url_template, reserved, permission_mode) values ($1, $2, $3, $4, $5, $6, $7, $8) returning id")
if err != nil { if err != nil {
return 0, errors.Wrap(err, "error preparing frontends insert statement") return 0, errors.Wrap(err, "error preparing frontends insert statement")
} }
var id int var id int
if err := stmt.QueryRow(envId, f.PrivateShareId, f.Token, f.ZId, f.PublicName, f.UrlTemplate, f.Reserved).Scan(&id); err != nil { if err := stmt.QueryRow(envId, f.PrivateShareId, f.Token, f.ZId, f.PublicName, f.UrlTemplate, f.Reserved, f.PermissionMode).Scan(&id); err != nil {
return 0, errors.Wrap(err, "error executing frontends insert statement") return 0, errors.Wrap(err, "error executing frontends insert statement")
} }
return id, nil return id, nil
} }
func (str *Store) CreateGlobalFrontend(f *Frontend, tx *sqlx.Tx) (int, error) { func (str *Store) CreateGlobalFrontend(f *Frontend, tx *sqlx.Tx) (int, error) {
stmt, err := tx.Prepare("insert into frontends (token, z_id, public_name, url_template, reserved) values ($1, $2, $3, $4, $5) returning id") stmt, err := tx.Prepare("insert into frontends (token, z_id, public_name, url_template, reserved, permission_mode) values ($1, $2, $3, $4, $5, $6) returning id")
if err != nil { if err != nil {
return 0, errors.Wrap(err, "error preparing global frontends insert statement") return 0, errors.Wrap(err, "error preparing global frontends insert statement")
} }
var id int var id int
if err := stmt.QueryRow(f.Token, f.ZId, f.PublicName, f.UrlTemplate, f.Reserved).Scan(&id); err != nil { if err := stmt.QueryRow(f.Token, f.ZId, f.PublicName, f.UrlTemplate, f.Reserved, f.PermissionMode).Scan(&id); err != nil {
return 0, errors.Wrap(err, "error executing global frontends insert statement") return 0, errors.Wrap(err, "error executing global frontends insert statement")
} }
return id, nil return id, nil
@ -122,12 +122,12 @@ func (str *Store) FindFrontendsForPrivateShare(shrId int, tx *sqlx.Tx) ([]*Front
} }
func (str *Store) UpdateFrontend(fe *Frontend, tx *sqlx.Tx) error { func (str *Store) UpdateFrontend(fe *Frontend, tx *sqlx.Tx) error {
sql := "update frontends set environment_id = $1, private_share_id = $2, token = $3, z_id = $4, public_name = $5, url_template = $6, reserved = $7, updated_at = current_timestamp where id = $8" sql := "update frontends set environment_id = $1, private_share_id = $2, token = $3, z_id = $4, public_name = $5, url_template = $6, reserved = $7, permission_mode = $8, updated_at = current_timestamp where id = $9"
stmt, err := tx.Prepare(sql) stmt, err := tx.Prepare(sql)
if err != nil { if err != nil {
return errors.Wrap(err, "error preparing frontends update statement") return errors.Wrap(err, "error preparing frontends update statement")
} }
_, err = stmt.Exec(fe.EnvironmentId, fe.PrivateShareId, fe.Token, fe.ZId, fe.PublicName, fe.UrlTemplate, fe.Reserved, fe.Id) _, err = stmt.Exec(fe.EnvironmentId, fe.PrivateShareId, fe.Token, fe.ZId, fe.PublicName, fe.UrlTemplate, fe.Reserved, fe.PermissionMode, fe.Id)
if err != nil { if err != nil {
return errors.Wrap(err, "error executing frontends update statement") return errors.Wrap(err, "error executing frontends update statement")
} }

View File

@ -0,0 +1,18 @@
package store
import (
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
)
func (str *Store) IsFrontendGrantedToAccount(acctId, frontendId int, trx *sqlx.Tx) (bool, error) {
stmt, err := trx.Prepare("select count(0) from frontend_grants where account_id = $1 AND frontend_id = $2")
if err != nil {
return false, errors.Wrap(err, "error preparing frontend_grants select statement")
}
var count int
if err := stmt.QueryRow(acctId, frontendId).Scan(&count); err != nil {
return false, errors.Wrap(err, "error querying frontend_grants count")
}
return count > 0, nil
}

View File

@ -22,6 +22,7 @@ type ResourceCountClass interface {
GetShares() int GetShares() int
GetReservedShares() int GetReservedShares() int
GetUniqueNames() int GetUniqueNames() int
GetShareFrontends() int
} }
type BandwidthClass interface { type BandwidthClass interface {
@ -37,11 +38,13 @@ type BandwidthClass interface {
type LimitClass struct { type LimitClass struct {
Model Model
Label *string
BackendMode *sdk.BackendMode BackendMode *sdk.BackendMode
Environments int Environments int
Shares int Shares int
ReservedShares int ReservedShares int
UniqueNames int UniqueNames int
ShareFrontends int
PeriodMinutes int PeriodMinutes int
RxBytes int64 RxBytes int64
TxBytes int64 TxBytes int64
@ -77,6 +80,10 @@ func (lc LimitClass) GetUniqueNames() int {
return lc.UniqueNames return lc.UniqueNames
} }
func (lc LimitClass) GetShareFrontends() int {
return lc.ShareFrontends
}
func (lc LimitClass) GetBackendMode() sdk.BackendMode { func (lc LimitClass) GetBackendMode() sdk.BackendMode {
if lc.BackendMode == nil { if lc.BackendMode == nil {
return "" return ""
@ -105,7 +112,12 @@ func (lc LimitClass) GetLimitAction() LimitAction {
} }
func (lc LimitClass) String() string { func (lc LimitClass) String() string {
out := fmt.Sprintf("LimitClass<#%d", lc.Id) out := "LimitClass<"
if lc.Label != nil && *lc.Label != "" {
out += "'" + *lc.Label + "'"
} else {
out += fmt.Sprintf("#%d", lc.Id)
}
if lc.BackendMode != nil { if lc.BackendMode != nil {
out += fmt.Sprintf(", backendMode: '%s'", *lc.BackendMode) out += fmt.Sprintf(", backendMode: '%s'", *lc.BackendMode)
} }
@ -121,6 +133,9 @@ func (lc LimitClass) String() string {
if lc.UniqueNames > Unlimited { if lc.UniqueNames > Unlimited {
out += fmt.Sprintf(", uniqueNames: %d", lc.UniqueNames) out += fmt.Sprintf(", uniqueNames: %d", lc.UniqueNames)
} }
if lc.ShareFrontends > Unlimited {
out += fmt.Sprintf(", shareFrontends: %d", lc.ShareFrontends)
}
if lc.RxBytes > Unlimited || lc.TxBytes > Unlimited || lc.TotalBytes > Unlimited { if lc.RxBytes > Unlimited || lc.TxBytes > Unlimited || lc.TotalBytes > Unlimited {
out += fmt.Sprintf(", periodMinutes: %d", lc.PeriodMinutes) out += fmt.Sprintf(", periodMinutes: %d", lc.PeriodMinutes)
} }
@ -140,12 +155,12 @@ func (lc LimitClass) String() string {
var _ BandwidthClass = (*LimitClass)(nil) var _ BandwidthClass = (*LimitClass)(nil)
func (str *Store) CreateLimitClass(lc *LimitClass, trx *sqlx.Tx) (int, error) { func (str *Store) CreateLimitClass(lc *LimitClass, trx *sqlx.Tx) (int, error) {
stmt, err := trx.Prepare("insert into limit_classes (backend_mode, environments, shares, reserved_shares, unique_names, period_minutes, rx_bytes, tx_bytes, total_bytes, limit_action) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) returning id") stmt, err := trx.Prepare("insert into limit_classes (label, backend_mode, environments, shares, reserved_shares, unique_names, share_frontends, period_minutes, rx_bytes, tx_bytes, total_bytes, limit_action) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) returning id")
if err != nil { if err != nil {
return 0, errors.Wrap(err, "error preparing limit_classes insert statement") return 0, errors.Wrap(err, "error preparing limit_classes insert statement")
} }
var id int var id int
if err := stmt.QueryRow(lc.BackendMode, lc.Environments, lc.Shares, lc.ReservedShares, lc.UniqueNames, lc.PeriodMinutes, lc.RxBytes, lc.TxBytes, lc.TotalBytes, lc.LimitAction).Scan(&id); err != nil { if err := stmt.QueryRow(lc.Label, lc.BackendMode, lc.Environments, lc.Shares, lc.ReservedShares, lc.UniqueNames, lc.ShareFrontends, lc.PeriodMinutes, lc.RxBytes, lc.TxBytes, lc.TotalBytes, lc.LimitAction).Scan(&id); err != nil {
return 0, errors.Wrap(err, "error executing limit_classes insert statement") return 0, errors.Wrap(err, "error executing limit_classes insert statement")
} }
return id, nil return id, nil

View File

@ -7,14 +7,6 @@ const (
WarningLimitAction LimitAction = "warning" WarningLimitAction LimitAction = "warning"
) )
type LimitScope string
const (
AccountLimitScope LimitScope = "account"
EnvironmentLimitScope LimitScope = "environment"
ShareLimitScope LimitScope = "share"
)
type PermissionMode string type PermissionMode string
const ( const (

View File

@ -18,7 +18,6 @@ type Share struct {
Reserved bool Reserved bool
UniqueName bool UniqueName bool
PermissionMode PermissionMode PermissionMode PermissionMode
Deleted bool
} }
func (str *Store) CreateShare(envId int, shr *Share, tx *sqlx.Tx) (int, error) { func (str *Store) CreateShare(envId int, shr *Share, tx *sqlx.Tx) (int, error) {

View File

@ -0,0 +1,3 @@
-- +migrate Up
alter table limit_classes add column share_frontends int not null default (-1);

View File

@ -0,0 +1,17 @@
-- +migrate Up
alter table frontends add column permission_mode permission_mode_type not null default('open');
create table frontend_grants (
id serial primary key,
account_id integer references accounts (id) not null,
frontend_id integer references frontends (id) not null,
created_at timestamptz not null default(current_timestamp),
updated_at timestamptz not null default(current_timestamp),
deleted boolean not null default(false)
);
create index frontend_grants_account_id_idx on frontend_grants (account_id);
create index frontend_grants_frontend_id_idx on frontend_grants (frontend_id);

View File

@ -0,0 +1,3 @@
-- +migrate Up
alter table limit_classes add column label varchar(32);

View File

@ -0,0 +1,3 @@
-- +migrate Up
alter table limit_classes add column share_frontends int not null default (-1);

View File

@ -0,0 +1,17 @@
-- +migrate Up
alter table frontends add column permission_mode string not null default('open');
create table frontend_grants (
id integer primary key,
account_id integer references accounts (id) not null,
frontend_id integer references frontends (id) not null,
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)
);
create index frontend_grants_account_id_idx on frontend_grants (account_id);
create index frontend_grants_frontend_id_idx on frontend_grants (frontend_id);

View File

@ -0,0 +1,3 @@
-- +migrate Up
alter table limit_classes add column label varchar(32);

View File

@ -90,9 +90,9 @@ ZROK_OAUTH_GOOGLE_CLIENT_ID=abcd1234
ZROK_OAUTH_GOOGLE_CLIENT_SECRET=abcd1234 ZROK_OAUTH_GOOGLE_CLIENT_SECRET=abcd1234
# zrok version, e.g., 1.0.0 # zrok version, e.g., 1.0.0
ZROK_IMAGE_TAG=latest ZROK_CLI_TAG=latest
# ziti version, e.g., 1.0.0 # ziti version, e.g., 1.0.0
ZITI_IMAGE_TAG=latest ZITI_CLI_TAG=latest
``` ```
### Start the Docker Compose Project ### Start the Docker Compose Project

View File

@ -4,7 +4,7 @@
# /___|_| \___/|_|\_\ # /___|_| \___/|_|\_\
# controller configuration # controller configuration
v: 3 v: 4
admin: admin:
# generate these admin tokens from a source of randomness, e.g. # generate these admin tokens from a source of randomness, e.g.
# LC_ALL=C tr -dc _A-Z-a-z-0-9 < /dev/urandom | head -c32 # LC_ALL=C tr -dc _A-Z-a-z-0-9 < /dev/urandom | head -c32

View File

@ -20,7 +20,7 @@ The limits agent is responsible for controlling the number of resources in use (
### Types of Limits ### Types of Limits
Limits can be specified that control the number of environments, shares, reserved shares, and unique names that can be created by an account. Limits that control the allowed number of resources are called _resource count limits_. Limits can be specified that control the number of environments, shares, reserved shares, unique names, and frontends per-share that can be created by an account. Limits that control the allowed number of resources are called _resource count limits_.
Limits can be specified to control the amount of data that can be transferred within a time period. Limits that control the amount of data that can be transferred are called _bandwidth limits_. Limits can be specified to control the amount of data that can be transferred within a time period. Limits that control the amount of data that can be transferred are called _bandwidth limits_.
@ -40,6 +40,7 @@ limits:
shares: -1 shares: -1
reserved_shares: -1 reserved_shares: -1
unique_names: -1 unique_names: -1
share_frontends: -1
bandwidth: bandwidth:
period: 5m period: 5m
warning: warning:
@ -64,7 +65,7 @@ The `cycle` value controls how frequently the limits agent will evaluate enforce
### Global Resouce Count Limits ### Global Resouce Count Limits
The `environments`, `shares`, `reserved_shares`, and `unique_names` specify the resource count limits, globally for the service instance. The `environments`, `shares`, `reserved_shares`, `unique_names`, and `share_frontends` specify the resource count limits, globally for the service instance.
These resource counts will be applied to all users in the service instance by default. These resource counts will be applied to all users in the service instance by default.
@ -91,11 +92,13 @@ Limit classes are created by creating a record in the `limit_classes` table in t
```sql ```sql
CREATE TABLE public.limit_classes ( CREATE TABLE public.limit_classes (
id integer NOT NULL, id integer NOT NULL,
label VARCHAR(32),
backend_mode public.backend_mode, backend_mode public.backend_mode,
environments integer DEFAULT '-1'::integer NOT NULL, environments integer DEFAULT '-1'::integer NOT NULL,
shares integer DEFAULT '-1'::integer NOT NULL, shares integer DEFAULT '-1'::integer NOT NULL,
reserved_shares integer DEFAULT '-1'::integer NOT NULL, reserved_shares integer DEFAULT '-1'::integer NOT NULL,
unique_names integer DEFAULT '-1'::integer NOT NULL, unique_names integer DEFAULT '-1'::integer NOT NULL,
share_frontends integer DEFAULT '-1'::integer NOT NULL,
period_minutes integer DEFAULT 1440 NOT NULL, period_minutes integer DEFAULT 1440 NOT NULL,
rx_bytes bigint DEFAULT '-1'::integer NOT NULL, rx_bytes bigint DEFAULT '-1'::integer NOT NULL,
tx_bytes bigint DEFAULT '-1'::integer NOT NULL, tx_bytes bigint DEFAULT '-1'::integer NOT NULL,
@ -130,7 +133,7 @@ Create a row in this table linking the `account_id` to the `limit_class_id` to a
To support overriding the resource count limits defined in the global limits configuration, a site administrator can create a limit class by inserting a row into the `limit_classes` table structured like this: To support overriding the resource count limits defined in the global limits configuration, a site administrator can create a limit class by inserting a row into the `limit_classes` table structured like this:
```sql ```sql
insert into limit_classes (environments, shares, reserved_shares, unique_names) values (1, 1, 1, 1); insert into limit_classes (environments, shares, reserved_shares, unique_names, share_frontends) values (1, 1, 1, 1, 1);
``` ```
This creates a limit class that sets the `environments`, `shares`, `reserved_shares`, and `unique_names` all to `1`. This creates a limit class that sets the `environments`, `shares`, `reserved_shares`, and `unique_names` all to `1`.

View File

@ -124,6 +124,7 @@ func (t *FilesystemTarget) WriteStream(path string, stream io.Reader, mode os.Fi
if err != nil { if err != nil {
return err return err
} }
defer f.Close()
_, err = io.Copy(f, stream) _, err = io.Copy(f, stream)
if err != nil { if err != nil {
return err return err

View File

@ -13,6 +13,7 @@ type Root interface {
Client() (*rest_client_zrok.Zrok, error) Client() (*rest_client_zrok.Zrok, error)
ApiEndpoint() (string, string) ApiEndpoint() (string, string)
DefaultFrontend() (string, string)
IsEnabled() bool IsEnabled() bool
Environment() *Environment Environment() *Environment
@ -35,6 +36,7 @@ type Environment struct {
type Config struct { type Config struct {
ApiEndpoint string ApiEndpoint string
DefaultFrontend string
} }
type Metadata struct { type Metadata struct {

View File

@ -85,6 +85,24 @@ func (r *Root) ApiEndpoint() (string, string) {
return apiEndpoint, from return apiEndpoint, from
} }
func (r *Root) DefaultFrontend() (string, string) {
defaultFrontend := "public"
from := "binary"
if r.Config() != nil && r.Config().DefaultFrontend != "" {
defaultFrontend = r.Config().DefaultFrontend
from = "config"
}
env := os.Getenv("ZROK_DEFAULT_FRONTEND")
if env != "" {
defaultFrontend = env
from = "ZROK_DEFAULT_FRONTEND"
}
return defaultFrontend, from
}
func (r *Root) Environment() *env_core.Environment { func (r *Root) Environment() *env_core.Environment {
return r.env return r.env
} }

View File

@ -85,6 +85,24 @@ func (r *Root) ApiEndpoint() (string, string) {
return apiEndpoint, from return apiEndpoint, from
} }
func (r *Root) DefaultFrontend() (string, string) {
defaultFrontend := "public"
from := "binary"
if r.Config() != nil && r.Config().DefaultFrontend != "" {
defaultFrontend = r.Config().DefaultFrontend
from = "config"
}
env := os.Getenv("ZROK_DEFAULT_FRONTEND")
if env != "" {
defaultFrontend = env
from = "ZROK_DEFAULT_FRONTEND"
}
return defaultFrontend, from
}
func (r *Root) Environment() *env_core.Environment { func (r *Root) Environment() *env_core.Environment {
return r.env return r.env
} }

View File

@ -224,12 +224,13 @@ func loadConfig() (*env_core.Config, error) {
} }
out := &env_core.Config{ out := &env_core.Config{
ApiEndpoint: cfg.ApiEndpoint, ApiEndpoint: cfg.ApiEndpoint,
DefaultFrontend: cfg.DefaultFrontend,
} }
return out, nil return out, nil
} }
func saveConfig(cfg *env_core.Config) error { func saveConfig(cfg *env_core.Config) error {
in := &config{ApiEndpoint: cfg.ApiEndpoint} in := &config{ApiEndpoint: cfg.ApiEndpoint, DefaultFrontend: cfg.DefaultFrontend}
data, err := json.MarshalIndent(in, "", " ") data, err := json.MarshalIndent(in, "", " ")
if err != nil { if err != nil {
return errors.Wrap(err, "error marshaling config") return errors.Wrap(err, "error marshaling config")
@ -324,6 +325,7 @@ type metadata struct {
type config struct { type config struct {
ApiEndpoint string `json:"api_endpoint"` ApiEndpoint string `json:"api_endpoint"`
DefaultFrontend string `json:"default_frontend"`
} }
type environment struct { type environment struct {

View File

@ -83,6 +83,7 @@ limits:
shares: -1 shares: -1
reserved_shares: -1 reserved_shares: -1
unique_names: -1 unique_names: -1
share_frontends: -1
bandwidth: bandwidth:
period: 5m period: 5m
warning: warning:

View File

@ -7,9 +7,12 @@ package rest_model_zrok
import ( import (
"context" "context"
"encoding/json"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
"github.com/go-openapi/swag" "github.com/go-openapi/swag"
"github.com/go-openapi/validate"
) )
// CreateFrontendRequest create frontend request // CreateFrontendRequest create frontend request
@ -17,6 +20,10 @@ import (
// swagger:model createFrontendRequest // swagger:model createFrontendRequest
type CreateFrontendRequest struct { type CreateFrontendRequest struct {
// permission mode
// Enum: [open closed]
PermissionMode string `json:"permissionMode,omitempty"`
// public name // public name
PublicName string `json:"public_name,omitempty"` PublicName string `json:"public_name,omitempty"`
@ -29,6 +36,57 @@ type CreateFrontendRequest struct {
// Validate validates this create frontend request // Validate validates this create frontend request
func (m *CreateFrontendRequest) Validate(formats strfmt.Registry) error { func (m *CreateFrontendRequest) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validatePermissionMode(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
var createFrontendRequestTypePermissionModePropEnum []interface{}
func init() {
var res []string
if err := json.Unmarshal([]byte(`["open","closed"]`), &res); err != nil {
panic(err)
}
for _, v := range res {
createFrontendRequestTypePermissionModePropEnum = append(createFrontendRequestTypePermissionModePropEnum, v)
}
}
const (
// CreateFrontendRequestPermissionModeOpen captures enum value "open"
CreateFrontendRequestPermissionModeOpen string = "open"
// CreateFrontendRequestPermissionModeClosed captures enum value "closed"
CreateFrontendRequestPermissionModeClosed string = "closed"
)
// prop value enum
func (m *CreateFrontendRequest) validatePermissionModeEnum(path, location string, value string) error {
if err := validate.EnumCase(path, location, value, createFrontendRequestTypePermissionModePropEnum, true); err != nil {
return err
}
return nil
}
func (m *CreateFrontendRequest) 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 return nil
} }

View File

@ -1200,6 +1200,13 @@ func init() {
"createFrontendRequest": { "createFrontendRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
"permissionMode": {
"type": "string",
"enum": [
"open",
"closed"
]
},
"public_name": { "public_name": {
"type": "string" "type": "string"
}, },
@ -2956,6 +2963,13 @@ func init() {
"createFrontendRequest": { "createFrontendRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
"permissionMode": {
"type": "string",
"enum": [
"open",
"closed"
]
},
"public_name": { "public_name": {
"type": "string" "type": "string"
}, },

View File

@ -1 +1 @@
7.4.0 7.6.0

View File

@ -16,6 +16,7 @@ export class CreateFrontendRequest {
'zId'?: string; 'zId'?: string;
'urlTemplate'?: string; 'urlTemplate'?: string;
'publicName'?: string; 'publicName'?: string;
'permissionMode'?: CreateFrontendRequest.PermissionModeEnum;
static discriminator: string | undefined = undefined; static discriminator: string | undefined = undefined;
@ -34,6 +35,11 @@ export class CreateFrontendRequest {
"name": "publicName", "name": "publicName",
"baseName": "public_name", "baseName": "public_name",
"type": "string" "type": "string"
},
{
"name": "permissionMode",
"baseName": "permissionMode",
"type": "CreateFrontendRequest.PermissionModeEnum"
} ]; } ];
static getAttributeTypeMap() { static getAttributeTypeMap() {
@ -41,3 +47,9 @@ export class CreateFrontendRequest {
} }
} }
export namespace CreateFrontendRequest {
export enum PermissionModeEnum {
Open = <any> 'open',
Closed = <any> 'closed'
}
}

View File

@ -108,6 +108,7 @@ let primitives = [
]; ];
let enumsMap: {[index: string]: any} = { let enumsMap: {[index: string]: any} = {
"CreateFrontendRequest.PermissionModeEnum": CreateFrontendRequest.PermissionModeEnum,
"ShareRequest.ShareModeEnum": ShareRequest.ShareModeEnum, "ShareRequest.ShareModeEnum": ShareRequest.ShareModeEnum,
"ShareRequest.BackendModeEnum": ShareRequest.BackendModeEnum, "ShareRequest.BackendModeEnum": ShareRequest.BackendModeEnum,
"ShareRequest.OauthProviderEnum": ShareRequest.OauthProviderEnum, "ShareRequest.OauthProviderEnum": ShareRequest.OauthProviderEnum,

View File

@ -120,7 +120,8 @@ export namespace ShareRequest {
UdpTunnel = <any> 'udpTunnel', UdpTunnel = <any> 'udpTunnel',
Caddy = <any> 'caddy', Caddy = <any> 'caddy',
Drive = <any> 'drive', Drive = <any> 'drive',
Socks = <any> 'socks' Socks = <any> 'socks',
Vpn = <any> 'vpn'
} }
export enum OauthProviderEnum { export enum OauthProviderEnum {
Github = <any> 'github', Github = <any> 'github',

View File

@ -30,20 +30,23 @@ class CreateFrontendRequest(object):
swagger_types = { swagger_types = {
'z_id': 'str', 'z_id': 'str',
'url_template': 'str', 'url_template': 'str',
'public_name': 'str' 'public_name': 'str',
'permission_mode': 'str'
} }
attribute_map = { attribute_map = {
'z_id': 'zId', 'z_id': 'zId',
'url_template': 'url_template', 'url_template': 'url_template',
'public_name': 'public_name' 'public_name': 'public_name',
'permission_mode': 'permissionMode'
} }
def __init__(self, z_id=None, url_template=None, public_name=None): # noqa: E501 def __init__(self, z_id=None, url_template=None, public_name=None, permission_mode=None): # noqa: E501
"""CreateFrontendRequest - a model defined in Swagger""" # noqa: E501 """CreateFrontendRequest - a model defined in Swagger""" # noqa: E501
self._z_id = None self._z_id = None
self._url_template = None self._url_template = None
self._public_name = None self._public_name = None
self._permission_mode = None
self.discriminator = None self.discriminator = None
if z_id is not None: if z_id is not None:
self.z_id = z_id self.z_id = z_id
@ -51,6 +54,8 @@ class CreateFrontendRequest(object):
self.url_template = url_template self.url_template = url_template
if public_name is not None: if public_name is not None:
self.public_name = public_name self.public_name = public_name
if permission_mode is not None:
self.permission_mode = permission_mode
@property @property
def z_id(self): def z_id(self):
@ -115,6 +120,33 @@ class CreateFrontendRequest(object):
self._public_name = public_name self._public_name = public_name
@property
def permission_mode(self):
"""Gets the permission_mode of this CreateFrontendRequest. # noqa: E501
:return: The permission_mode of this CreateFrontendRequest. # noqa: E501
:rtype: str
"""
return self._permission_mode
@permission_mode.setter
def permission_mode(self, permission_mode):
"""Sets the permission_mode of this CreateFrontendRequest.
:param permission_mode: The permission_mode of this CreateFrontendRequest. # 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
def to_dict(self): def to_dict(self):
"""Returns the model properties as a dict""" """Returns the model properties as a dict"""
result = {} result = {}

View File

@ -766,6 +766,9 @@ definitions:
type: string type: string
public_name: public_name:
type: string type: string
permissionMode:
type: string
enum: ["open", "closed"]
createFrontendResponse: createFrontendResponse:
type: object type: object

685
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,14 +7,14 @@
"@emotion/styled": "^11.10.4", "@emotion/styled": "^11.10.4",
"@mdi/js": "^7.0.96", "@mdi/js": "^7.0.96",
"@mdi/react": "^1.6.1", "@mdi/react": "^1.6.1",
"@mui/material": "^5.10.4", "@mui/material": "^5.15.18",
"bootstrap": "^5.2.3", "bootstrap": "^5.2.3",
"dagre": "^0.8.5", "dagre": "^0.8.5",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"humanize-duration": "^3.27.3", "humanize-duration": "^3.27.3",
"moment": "^2.29.4", "moment": "^2.29.4",
"react": "^18.3.1", "react": "^18.3.1",
"react-bootstrap": "^2.7.0", "react-bootstrap": "^2.10.2",
"react-data-table-component": "^7.5.2", "react-data-table-component": "^7.5.2",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-force-graph": "^1.43.0", "react-force-graph": "^1.43.0",

View File

@ -53,6 +53,7 @@
* @property {string} zId * @property {string} zId
* @property {string} url_template * @property {string} url_template
* @property {string} public_name * @property {string} public_name
* @property {string} permissionMode
*/ */
/** /**

View File

@ -14,7 +14,7 @@ const ActionsTab = (props) => {
return ( return (
<div className={"actions-tab"}> <div className={"actions-tab"}>
<div id={"change-password"} style={{"padding-top": "10px"}}> <div id={"change-password"} style={{"paddingTop": "10px"}}>
<h3>Change Password?</h3> <h3>Change Password?</h3>
<p>Change the password used to log into the zrok web console.</p> <p>Change the password used to log into the zrok web console.</p>
<Button variant={"danger"} onClick={openChangePasswordModal}>Change Password</Button> <Button variant={"danger"} onClick={openChangePasswordModal}>Change Password</Button>