diff --git a/CHANGELOG.md b/CHANGELOG.md index 1356a9d8c..c7951df07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 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: 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) FIX: use controller config spec v4 in the Docker instance diff --git a/cmd/zrok/adminCreateFrontend.go b/cmd/zrok/adminCreateFrontend.go index 430ecc59c..20dc9831c 100644 --- a/cmd/zrok/adminCreateFrontend.go +++ b/cmd/zrok/adminCreateFrontend.go @@ -4,6 +4,7 @@ import ( "github.com/openziti/zrok/environment" "github.com/openziti/zrok/rest_client_zrok/admin" "github.com/openziti/zrok/rest_model_zrok" + "github.com/openziti/zrok/sdk/golang/sdk" "github.com/openziti/zrok/tui" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -15,7 +16,8 @@ func init() { } type adminCreateFrontendCommand struct { - cmd *cobra.Command + cmd *cobra.Command + closed bool } func newAdminCreateFrontendCommand() *adminCreateFrontendCommand { @@ -25,6 +27,7 @@ func newAdminCreateFrontendCommand() *adminCreateFrontendCommand { Args: cobra.ExactArgs(3), } command := &adminCreateFrontendCommand{cmd: cmd} + cmd.Flags().BoolVar(&command.closed, "closed", false, "Enabled closed permission mode") cmd.Run = command.run return command } @@ -44,11 +47,16 @@ func (cmd *adminCreateFrontendCommand) run(_ *cobra.Command, args []string) { panic(err) } + permissionMode := sdk.OpenPermissionMode + if cmd.closed { + permissionMode = sdk.ClosedPermissionMode + } req := admin.NewCreateFrontendParams() req.Body = &rest_model_zrok.CreateFrontendRequest{ - ZID: zId, - PublicName: publicName, - URLTemplate: urlTemplate, + ZID: zId, + PublicName: publicName, + URLTemplate: urlTemplate, + PermissionMode: string(permissionMode), } resp, err := zrok.Admin.CreateFrontend(req, mustGetAdminAuth()) diff --git a/controller/createFrontend.go b/controller/createFrontend.go index 457673870..882b753dd 100644 --- a/controller/createFrontend.go +++ b/controller/createFrontend.go @@ -2,7 +2,6 @@ package controller import ( "errors" - "github.com/go-openapi/runtime/middleware" "github.com/lib/pq" "github.com/mattn/go-sqlite3" @@ -57,11 +56,12 @@ func (h *createFrontendHandler) Handle(params admin.CreateFrontendParams, princi } fe := &store.Frontend{ - Token: feToken, - ZId: params.Body.ZID, - PublicName: ¶ms.Body.PublicName, - UrlTemplate: ¶ms.Body.URLTemplate, - Reserved: true, + Token: feToken, + ZId: params.Body.ZID, + PublicName: ¶ms.Body.PublicName, + UrlTemplate: ¶ms.Body.URLTemplate, + Reserved: true, + PermissionMode: store.PermissionMode(params.Body.PermissionMode), } if _, err := str.CreateGlobalFrontend(fe, tx); err != nil { perr := &pq.Error{} diff --git a/controller/share.go b/controller/share.go index f3a244bfd..b5dc286b7 100644 --- a/controller/share.go +++ b/controller/share.go @@ -116,6 +116,17 @@ func (h *shareHandler) Handle(params share.ShareParams, principal *rest_model_zr logrus.Error(err) 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 { frontendZIds = append(frontendZIds, sfe.ZId) frontendTemplates = append(frontendTemplates, *sfe.UrlTemplate) diff --git a/controller/store/frontend.go b/controller/store/frontend.go index 8d34145e2..d4b55faf4 100644 --- a/controller/store/frontend.go +++ b/controller/store/frontend.go @@ -14,28 +14,28 @@ type Frontend struct { PublicName *string UrlTemplate *string Reserved bool - Deleted bool + PermissionMode PermissionMode } 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 { return 0, errors.Wrap(err, "error preparing frontends insert statement") } 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 id, nil } 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 { return 0, errors.Wrap(err, "error preparing global frontends insert statement") } 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 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 { - 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) if err != nil { 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 { return errors.Wrap(err, "error executing frontends update statement") } diff --git a/controller/store/frontendGrant.go b/controller/store/frontendGrant.go new file mode 100644 index 000000000..d676653f6 --- /dev/null +++ b/controller/store/frontendGrant.go @@ -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 +} diff --git a/controller/store/share.go b/controller/store/share.go index c12aa5293..cd8257e90 100644 --- a/controller/store/share.go +++ b/controller/store/share.go @@ -18,7 +18,6 @@ type Share struct { Reserved bool UniqueName bool PermissionMode PermissionMode - Deleted bool } func (str *Store) CreateShare(envId int, shr *Share, tx *sqlx.Tx) (int, error) { diff --git a/controller/store/sql/postgresql/027_v0_4_32_frontend_grants.sql b/controller/store/sql/postgresql/027_v0_4_32_frontend_grants.sql new file mode 100644 index 000000000..ba4c3d519 --- /dev/null +++ b/controller/store/sql/postgresql/027_v0_4_32_frontend_grants.sql @@ -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); \ No newline at end of file diff --git a/controller/store/sql/sqlite3/027_v0_4_32_frontend_grants.sql b/controller/store/sql/sqlite3/027_v0_4_32_frontend_grants.sql new file mode 100644 index 000000000..1ad99dd31 --- /dev/null +++ b/controller/store/sql/sqlite3/027_v0_4_32_frontend_grants.sql @@ -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); \ No newline at end of file diff --git a/rest_model_zrok/create_frontend_request.go b/rest_model_zrok/create_frontend_request.go index fdf6b5157..183b65ea0 100644 --- a/rest_model_zrok/create_frontend_request.go +++ b/rest_model_zrok/create_frontend_request.go @@ -7,9 +7,12 @@ package rest_model_zrok import ( "context" + "encoding/json" + "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" + "github.com/go-openapi/validate" ) // CreateFrontendRequest create frontend request @@ -17,6 +20,10 @@ import ( // swagger:model createFrontendRequest type CreateFrontendRequest struct { + // permission mode + // Enum: [open closed] + PermissionMode string `json:"permissionMode,omitempty"` + // public name PublicName string `json:"public_name,omitempty"` @@ -29,6 +36,57 @@ type CreateFrontendRequest struct { // Validate validates this create frontend request 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 } diff --git a/rest_server_zrok/embedded_spec.go b/rest_server_zrok/embedded_spec.go index e74a41987..9afdfdd30 100644 --- a/rest_server_zrok/embedded_spec.go +++ b/rest_server_zrok/embedded_spec.go @@ -1200,6 +1200,13 @@ func init() { "createFrontendRequest": { "type": "object", "properties": { + "permissionMode": { + "type": "string", + "enum": [ + "open", + "closed" + ] + }, "public_name": { "type": "string" }, @@ -2956,6 +2963,13 @@ func init() { "createFrontendRequest": { "type": "object", "properties": { + "permissionMode": { + "type": "string", + "enum": [ + "open", + "closed" + ] + }, "public_name": { "type": "string" }, diff --git a/sdk/nodejs/sdk/src/zrok/api/.openapi-generator/VERSION b/sdk/nodejs/sdk/src/zrok/api/.openapi-generator/VERSION index ba7f754d0..93c8ddab9 100644 --- a/sdk/nodejs/sdk/src/zrok/api/.openapi-generator/VERSION +++ b/sdk/nodejs/sdk/src/zrok/api/.openapi-generator/VERSION @@ -1 +1 @@ -7.4.0 +7.6.0 diff --git a/sdk/nodejs/sdk/src/zrok/api/model/createFrontendRequest.ts b/sdk/nodejs/sdk/src/zrok/api/model/createFrontendRequest.ts index 75d9842e4..35b425ef8 100644 --- a/sdk/nodejs/sdk/src/zrok/api/model/createFrontendRequest.ts +++ b/sdk/nodejs/sdk/src/zrok/api/model/createFrontendRequest.ts @@ -16,6 +16,7 @@ export class CreateFrontendRequest { 'zId'?: string; 'urlTemplate'?: string; 'publicName'?: string; + 'permissionMode'?: CreateFrontendRequest.PermissionModeEnum; static discriminator: string | undefined = undefined; @@ -34,6 +35,11 @@ export class CreateFrontendRequest { "name": "publicName", "baseName": "public_name", "type": "string" + }, + { + "name": "permissionMode", + "baseName": "permissionMode", + "type": "CreateFrontendRequest.PermissionModeEnum" } ]; static getAttributeTypeMap() { @@ -41,3 +47,9 @@ export class CreateFrontendRequest { } } +export namespace CreateFrontendRequest { + export enum PermissionModeEnum { + Open = 'open', + Closed = 'closed' + } +} diff --git a/sdk/nodejs/sdk/src/zrok/api/model/models.ts b/sdk/nodejs/sdk/src/zrok/api/model/models.ts index c5fb91831..13a6a25ec 100644 --- a/sdk/nodejs/sdk/src/zrok/api/model/models.ts +++ b/sdk/nodejs/sdk/src/zrok/api/model/models.ts @@ -108,6 +108,7 @@ let primitives = [ ]; let enumsMap: {[index: string]: any} = { + "CreateFrontendRequest.PermissionModeEnum": CreateFrontendRequest.PermissionModeEnum, "ShareRequest.ShareModeEnum": ShareRequest.ShareModeEnum, "ShareRequest.BackendModeEnum": ShareRequest.BackendModeEnum, "ShareRequest.OauthProviderEnum": ShareRequest.OauthProviderEnum, diff --git a/sdk/nodejs/sdk/src/zrok/api/model/shareRequest.ts b/sdk/nodejs/sdk/src/zrok/api/model/shareRequest.ts index 83205cb02..086ba7d87 100644 --- a/sdk/nodejs/sdk/src/zrok/api/model/shareRequest.ts +++ b/sdk/nodejs/sdk/src/zrok/api/model/shareRequest.ts @@ -120,7 +120,8 @@ export namespace ShareRequest { UdpTunnel = 'udpTunnel', Caddy = 'caddy', Drive = 'drive', - Socks = 'socks' + Socks = 'socks', + Vpn = 'vpn' } export enum OauthProviderEnum { Github = 'github', diff --git a/sdk/python/sdk/zrok/zrok_api/models/create_frontend_request.py b/sdk/python/sdk/zrok/zrok_api/models/create_frontend_request.py index 946c0cccd..f6a0cf016 100644 --- a/sdk/python/sdk/zrok/zrok_api/models/create_frontend_request.py +++ b/sdk/python/sdk/zrok/zrok_api/models/create_frontend_request.py @@ -30,20 +30,23 @@ class CreateFrontendRequest(object): swagger_types = { 'z_id': 'str', 'url_template': 'str', - 'public_name': 'str' + 'public_name': 'str', + 'permission_mode': 'str' } attribute_map = { 'z_id': 'zId', '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 self._z_id = None self._url_template = None self._public_name = None + self._permission_mode = None self.discriminator = None if z_id is not None: self.z_id = z_id @@ -51,6 +54,8 @@ def __init__(self, z_id=None, url_template=None, public_name=None): # noqa: E50 self.url_template = url_template if public_name is not None: self.public_name = public_name + if permission_mode is not None: + self.permission_mode = permission_mode @property def z_id(self): @@ -115,6 +120,33 @@ def public_name(self, 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): """Returns the model properties as a dict""" result = {} diff --git a/specs/zrok.yml b/specs/zrok.yml index da0a1a36c..0f37b6f02 100644 --- a/specs/zrok.yml +++ b/specs/zrok.yml @@ -766,6 +766,9 @@ definitions: type: string public_name: type: string + permissionMode: + type: string + enum: ["open", "closed"] createFrontendResponse: type: object diff --git a/ui/src/api/types.js b/ui/src/api/types.js index 153ff7237..625431a66 100644 --- a/ui/src/api/types.js +++ b/ui/src/api/types.js @@ -53,6 +53,7 @@ * @property {string} zId * @property {string} url_template * @property {string} public_name + * @property {string} permissionMode */ /**