mirror of
https://github.com/gravitational/teleport
synced 2024-10-19 16:53:57 +00:00
Add static and dynamic web ui configuration options (#21945)
* Add UiConfig option to proxy service config * Add uiConfig to GRV_CONFIG window object and use in terminal constructor * Update configuration reference docs * Remove extra structs and change property to scrollbackLines * Update web/packages/teleport/src/config.ts Co-authored-by: Jeff Pihach <hatched@users.noreply.github.com> * Use a common struct and apply in applyProxyConfig * Remove webclient dependency for fileconf * Add missing doc comments * Set individual properties of cfg.Proxy.UI * Remove unneeded comment * Add nolint rule * Fix nolint * Add test for UIConfig * Cleanup tests * Fix TestUIConfig context * Allow dynamic config for web ui (#22097) * Update protofiles --------- Co-authored-by: Jeff Pihach <hatched@users.noreply.github.com>
This commit is contained in:
parent
880736463b
commit
d94fed7b0d
|
@ -2265,6 +2265,27 @@ func (c *Client) GetInstallers(ctx context.Context) ([]types.Installer, error) {
|
|||
return installers, nil
|
||||
}
|
||||
|
||||
// GetUIConfig gets the configuration for the UI served by the proxy service
|
||||
func (c *Client) GetUIConfig(ctx context.Context) (types.UIConfig, error) {
|
||||
resp, err := c.grpc.GetUIConfig(ctx, &emptypb.Empty{}, c.callOpts...)
|
||||
return resp, trail.FromGRPC(err)
|
||||
}
|
||||
|
||||
// SetUIConfig sets the configuration for the UI served by the proxy service
|
||||
func (c *Client) SetUIConfig(ctx context.Context, uic types.UIConfig) error {
|
||||
uicV1, ok := uic.(*types.UIConfigV1)
|
||||
if !ok {
|
||||
return trace.BadParameter("invalid type %T", uic)
|
||||
}
|
||||
_, err := c.grpc.SetUIConfig(ctx, uicV1, c.callOpts...)
|
||||
return trail.FromGRPC(err)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteUIConfig(ctx context.Context) error {
|
||||
_, err := c.grpc.DeleteUIConfig(ctx, &emptypb.Empty{}, c.callOpts...)
|
||||
return trail.FromGRPC(err)
|
||||
}
|
||||
|
||||
// GetInstaller gets the cluster installer resource
|
||||
func (c *Client) GetInstaller(ctx context.Context, name string) (types.Installer, error) {
|
||||
resp, err := c.grpc.GetInstaller(ctx, &types.ResourceRequest{Name: name}, c.callOpts...)
|
||||
|
|
|
@ -171,6 +171,10 @@ func EventToGRPC(in types.Event) (*proto.Event, error) {
|
|||
out.Resource = &proto.Event_Installer{
|
||||
Installer: r,
|
||||
}
|
||||
case *types.UIConfigV1:
|
||||
out.Resource = &proto.Event_UIConfig{
|
||||
UIConfig: r,
|
||||
}
|
||||
case *types.DatabaseServiceV1:
|
||||
out.Resource = &proto.Event_DatabaseService{
|
||||
DatabaseService: r,
|
||||
|
@ -311,6 +315,9 @@ func EventFromGRPC(in proto.Event) (*types.Event, error) {
|
|||
} else if r := in.GetInstaller(); r != nil {
|
||||
out.Resource = r
|
||||
return &out, nil
|
||||
} else if r := in.GetUIConfig(); r != nil {
|
||||
out.Resource = r
|
||||
return &out, nil
|
||||
} else if r := in.GetDatabaseService(); r != nil {
|
||||
out.Resource = r
|
||||
return &out, nil
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -55,12 +55,20 @@ type WebConfig struct {
|
|||
TunnelPublicAddress string `json:"tunnelPublicAddress,omitempty"`
|
||||
// RecoveryCodesEnabled is a flag that determines if recovery codes are enabled in the cluster.
|
||||
RecoveryCodesEnabled bool `json:"recoveryCodesEnabled,omitempty"`
|
||||
// UIConfig is the configuration for the web UI
|
||||
UI UIConfig `json:"ui,omitempty"`
|
||||
// IsDashboard is a flag that determines if the cluster is running as a "dashboard".
|
||||
// The web UI for dashboards provides functionality for downloading self-hosted licenses and
|
||||
// Teleport Enterprise binaries.
|
||||
IsDashboard bool `json:"isDashboard,omitempty"`
|
||||
}
|
||||
|
||||
// UIConfig provides config options for the web UI served by the proxy service.
|
||||
type UIConfig struct {
|
||||
// ScrollbackLines is the max number of lines the UI terminal can display in its history
|
||||
ScrollbackLines int `json:"scrollbackLines,omitempty"` //nolint:unused // marshaled in config/configuration.go for WebConfig
|
||||
}
|
||||
|
||||
// WebConfigAuthProvider describes auth. provider
|
||||
type WebConfigAuthProvider struct {
|
||||
// Name is this provider ID
|
||||
|
|
|
@ -124,6 +124,8 @@ message Event {
|
|||
types.WebSessionV2 SAMLIdPSession = 37 [(gogoproto.jsontag) = "saml_idp_session,omitempty"];
|
||||
// UserGroup is a UserGroup resource
|
||||
types.UserGroupV1 UserGroup = 38 [(gogoproto.jsontag) = "user_group,omitempty"];
|
||||
// UIConfig provides a way for users to adjust settings of the UI served by the proxy service.
|
||||
types.UIConfigV1 UIConfig = 39 [(gogoproto.jsontag) = "ui_config,omitempty"];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2604,6 +2606,13 @@ service AuthService {
|
|||
// ResetAuthPreference resets cluster auth preference to defaults.
|
||||
rpc ResetAuthPreference(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||
|
||||
// GetUIConfig gets the configuration for the UI served by the proxy service
|
||||
rpc GetUIConfig(google.protobuf.Empty) returns (types.UIConfigV1);
|
||||
// SetUIConfig sets the configuration for the UI served by the proxy service
|
||||
rpc SetUIConfig(types.UIConfigV1) returns (google.protobuf.Empty);
|
||||
// DeleteUIConfig deletes the custom configuration for the UI served by the proxy service
|
||||
rpc DeleteUIConfig(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||
|
||||
// GetEvents gets events from the audit log.
|
||||
rpc GetEvents(GetEventsRequest) returns (Events);
|
||||
// GetSessionEvents gets completed session events from the audit log.
|
||||
|
|
|
@ -4595,6 +4595,26 @@ message Participant {
|
|||
];
|
||||
}
|
||||
|
||||
// UIConfigV1 represents the configuration for the web UI served by the proxy service
|
||||
message UIConfigV1 {
|
||||
// Header is the resource header for the UI configuration.
|
||||
ResourceHeader Header = 1 [
|
||||
(gogoproto.nullable) = false,
|
||||
(gogoproto.jsontag) = "",
|
||||
(gogoproto.embed) = true
|
||||
];
|
||||
// Spec is the resource spec.
|
||||
UIConfigSpecV1 Spec = 5 [
|
||||
(gogoproto.nullable) = false,
|
||||
(gogoproto.jsontag) = "spec"
|
||||
];
|
||||
}
|
||||
|
||||
// UIConfigSpecV1 is the specification for a UIConfig
|
||||
message UIConfigSpecV1 {
|
||||
int32 ScrollbackLines = 1 [(gogoproto.jsontag) = "scrollback_lines"];
|
||||
}
|
||||
|
||||
// InstallerV1 represents an installer script resource. Used to
|
||||
// provide a script to install teleport on discovered nodes.
|
||||
message InstallerV1 {
|
||||
|
|
|
@ -198,6 +198,10 @@ const (
|
|||
// cluster audit configuration.
|
||||
MetaNameClusterAuditConfig = "cluster-audit-config"
|
||||
|
||||
// MetaNameUIConfig is the exact name of the singleton resource holding
|
||||
// proxy service UI configuration.
|
||||
MetaNameUIConfig = "ui-config"
|
||||
|
||||
// KindClusterNetworkingConfig is the resource that holds cluster networking configuration.
|
||||
KindClusterNetworkingConfig = "cluster_networking_config"
|
||||
|
||||
|
@ -290,6 +294,10 @@ const (
|
|||
// used to install teleport on discovered nodes
|
||||
KindInstaller = "installer"
|
||||
|
||||
// KindUIConfig is a resource that holds configuration for the UI
|
||||
// served by the proxy service
|
||||
KindUIConfig = "ui_config"
|
||||
|
||||
// KindClusterAlert is a resource that conveys a cluster-level alert message.
|
||||
KindClusterAlert = "cluster_alert"
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
128
api/types/ui_config.go
Normal file
128
api/types/ui_config.go
Normal file
|
@ -0,0 +1,128 @@
|
|||
/**
|
||||
* Copyright 2023 Gravitational, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
)
|
||||
|
||||
// UIConfig defines configuration for the web UI served
|
||||
// by the proxy service. This is a configuration resource,
|
||||
// never create more than one instance of it.
|
||||
type UIConfig interface {
|
||||
Resource
|
||||
|
||||
// GetScript returns the contents of the installer script
|
||||
GetScrollbackLines() int32
|
||||
// SetScript sets the installer script
|
||||
SetScrollbackLines(int32)
|
||||
|
||||
String() string
|
||||
}
|
||||
|
||||
func NewUIConfigV1() (*UIConfigV1, error) {
|
||||
uiconfig := &UIConfigV1{
|
||||
ResourceHeader: ResourceHeader{},
|
||||
}
|
||||
if err := uiconfig.CheckAndSetDefaults(); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return uiconfig, nil
|
||||
}
|
||||
|
||||
// CheckAndSetDefaults verifies the constraints for UIConfig.
|
||||
func (c *UIConfigV1) CheckAndSetDefaults() error {
|
||||
c.setStaticFields()
|
||||
if err := c.Metadata.CheckAndSetDefaults(); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
if c.Spec.ScrollbackLines < 0 {
|
||||
return trace.BadParameter("invalid scrollback lines value. Must be greater than or equal to 0.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetVersion returns resource version.
|
||||
func (c *UIConfigV1) GetVersion() string {
|
||||
return c.Version
|
||||
}
|
||||
|
||||
// GetName returns the name of the resource.
|
||||
func (c *UIConfigV1) GetName() string {
|
||||
return c.Metadata.Name
|
||||
}
|
||||
|
||||
// SetName sets the name of the resource.
|
||||
func (c *UIConfigV1) SetName(e string) {
|
||||
c.Metadata.Name = e
|
||||
}
|
||||
|
||||
// SetExpiry sets expiry time for the object.
|
||||
func (c *UIConfigV1) SetExpiry(expires time.Time) {
|
||||
c.Metadata.SetExpiry(expires)
|
||||
}
|
||||
|
||||
// Expiry returns object expiry setting.
|
||||
func (c *UIConfigV1) Expiry() time.Time {
|
||||
return c.Metadata.Expiry()
|
||||
}
|
||||
|
||||
// GetMetadata returns object metadata.
|
||||
func (c *UIConfigV1) GetMetadata() Metadata {
|
||||
return c.Metadata
|
||||
}
|
||||
|
||||
// GetResourceID returns resource ID.
|
||||
func (c *UIConfigV1) GetResourceID() int64 {
|
||||
return c.Metadata.ID
|
||||
}
|
||||
|
||||
// SetResourceID sets resource ID.
|
||||
func (c *UIConfigV1) SetResourceID(id int64) {
|
||||
c.Metadata.ID = id
|
||||
}
|
||||
|
||||
// GetKind returns resource kind.
|
||||
func (c *UIConfigV1) GetKind() string {
|
||||
return c.Kind
|
||||
}
|
||||
|
||||
// GetSubKind returns resource subkind.
|
||||
func (c *UIConfigV1) GetSubKind() string {
|
||||
return c.SubKind
|
||||
}
|
||||
|
||||
// SetSubKind sets resource subkind.
|
||||
func (c *UIConfigV1) SetSubKind(sk string) {
|
||||
c.SubKind = sk
|
||||
}
|
||||
|
||||
func (c *UIConfigV1) GetScrollbackLines() int32 {
|
||||
return c.Spec.ScrollbackLines
|
||||
}
|
||||
|
||||
func (c *UIConfigV1) SetScrollbackLines(lines int32) {
|
||||
c.Spec.ScrollbackLines = lines
|
||||
}
|
||||
|
||||
// setStaticFields sets static resource header and metadata fields.
|
||||
func (c *UIConfigV1) setStaticFields() {
|
||||
c.Kind = KindUIConfig
|
||||
c.Version = V1
|
||||
}
|
|
@ -188,6 +188,9 @@ type ReadProxyAccessPoint interface {
|
|||
// GetSessionRecordingConfig returns session recording configuration.
|
||||
GetSessionRecordingConfig(ctx context.Context, opts ...services.MarshalOption) (types.SessionRecordingConfig, error)
|
||||
|
||||
// GetUIConfig returns configuration for the UI served by the proxy service
|
||||
GetUIConfig(ctx context.Context) (types.UIConfig, error)
|
||||
|
||||
// GetRole returns role by name
|
||||
GetRole(ctx context.Context, name string) (types.Role, error)
|
||||
|
||||
|
@ -918,6 +921,9 @@ type Cache interface {
|
|||
// ListWindowsDesktopServices returns a paginated list of windows desktops.
|
||||
ListWindowsDesktopServices(ctx context.Context, req types.ListWindowsDesktopServicesRequest) (*types.ListWindowsDesktopServicesResponse, error)
|
||||
|
||||
// GetUIConfig gets the config for the UI served by the proxy service
|
||||
GetUIConfig(ctx context.Context) (types.UIConfig, error)
|
||||
|
||||
// GetInstaller gets installer resource for this cluster
|
||||
GetInstaller(ctx context.Context, name string) (types.Installer, error)
|
||||
|
||||
|
|
|
@ -3616,6 +3616,28 @@ func (a *ServerWithRoles) GetAuthPreference(ctx context.Context) (types.AuthPref
|
|||
return a.authServer.GetAuthPreference(ctx)
|
||||
}
|
||||
|
||||
func (a *ServerWithRoles) GetUIConfig(ctx context.Context) (types.UIConfig, error) {
|
||||
if err := a.action(apidefaults.Namespace, types.KindUIConfig, types.VerbRead); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
cfg, err := a.authServer.GetUIConfig(ctx)
|
||||
return cfg, trace.Wrap(err)
|
||||
}
|
||||
|
||||
func (a *ServerWithRoles) SetUIConfig(ctx context.Context, uic types.UIConfig) error {
|
||||
if err := a.action(apidefaults.Namespace, types.KindUIConfig, types.VerbUpdate, types.VerbCreate); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
return trace.Wrap(a.authServer.SetUIConfig(ctx, uic))
|
||||
}
|
||||
|
||||
func (a *ServerWithRoles) DeleteUIConfig(ctx context.Context) error {
|
||||
if err := a.action(apidefaults.Namespace, types.KindUIConfig, types.VerbDelete); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
return trace.Wrap(a.authServer.DeleteUIConfig(ctx))
|
||||
}
|
||||
|
||||
// GetInstaller retrieves an installer script resource
|
||||
func (a *ServerWithRoles) GetInstaller(ctx context.Context, name string) (types.Installer, error) {
|
||||
if err := a.action(apidefaults.Namespace, types.KindInstaller, types.VerbRead); err != nil {
|
||||
|
|
|
@ -4500,6 +4500,45 @@ func (g *GRPCServer) SetInstaller(ctx context.Context, req *types.InstallerV1) (
|
|||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (g *GRPCServer) SetUIConfig(ctx context.Context, req *types.UIConfigV1) (*emptypb.Empty, error) {
|
||||
// TODO (avatus) send an audit event when SetUIConfig is called
|
||||
auth, err := g.authenticate(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
if err := auth.SetUIConfig(ctx, req); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (g *GRPCServer) GetUIConfig(ctx context.Context, _ *emptypb.Empty) (*types.UIConfigV1, error) {
|
||||
auth, err := g.authenticate(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
uiconfig, err := auth.ServerWithRoles.GetUIConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
uiconfigv1, ok := uiconfig.(*types.UIConfigV1)
|
||||
if !ok {
|
||||
return nil, trace.BadParameter("unexpected type %T", uiconfig)
|
||||
}
|
||||
return uiconfigv1, nil
|
||||
}
|
||||
|
||||
func (g *GRPCServer) DeleteUIConfig(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
auth, err := g.authenticate(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
if err := auth.DeleteUIConfig(ctx); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
// GetInstaller retrieves the installer script resource
|
||||
func (g *GRPCServer) GetInstaller(ctx context.Context, req *types.ResourceRequest) (*types.InstallerV1, error) {
|
||||
auth, err := g.authenticate(ctx)
|
||||
|
|
|
@ -400,6 +400,7 @@ func (a *authorizer) authorizeRemoteBuiltinRole(r RemoteBuiltinRole) (*Context,
|
|||
types.NewRule(types.KindKubeService, services.RO()),
|
||||
types.NewRule(types.KindKubeServer, services.RO()),
|
||||
types.NewRule(types.KindInstaller, services.RO()),
|
||||
types.NewRule(types.KindUIConfig, services.RO()),
|
||||
types.NewRule(types.KindDatabaseService, services.RO()),
|
||||
// this rule allows remote proxy to update the cluster's certificate authorities
|
||||
// during certificates renewal
|
||||
|
@ -477,6 +478,7 @@ func roleSpecForProxy(clusterName string) types.RoleSpecV6 {
|
|||
types.NewRule(types.KindClusterAuditConfig, services.RO()),
|
||||
types.NewRule(types.KindClusterNetworkingConfig, services.RO()),
|
||||
types.NewRule(types.KindSessionRecordingConfig, services.RO()),
|
||||
types.NewRule(types.KindUIConfig, services.RO()),
|
||||
types.NewRule(types.KindStaticTokens, services.RO()),
|
||||
types.NewRule(types.KindTunnelConnection, services.RW()),
|
||||
types.NewRule(types.KindRemoteCluster, services.RO()),
|
||||
|
|
16
lib/cache/cache.go
vendored
16
lib/cache/cache.go
vendored
|
@ -78,6 +78,7 @@ func ForAuth(cfg Config) Config {
|
|||
{Kind: types.KindClusterNetworkingConfig},
|
||||
{Kind: types.KindClusterAuthPreference},
|
||||
{Kind: types.KindSessionRecordingConfig},
|
||||
{Kind: types.KindUIConfig},
|
||||
{Kind: types.KindStaticTokens},
|
||||
{Kind: types.KindToken},
|
||||
{Kind: types.KindUser},
|
||||
|
@ -126,6 +127,7 @@ func ForProxy(cfg Config) Config {
|
|||
{Kind: types.KindClusterNetworkingConfig},
|
||||
{Kind: types.KindClusterAuthPreference},
|
||||
{Kind: types.KindSessionRecordingConfig},
|
||||
{Kind: types.KindUIConfig},
|
||||
{Kind: types.KindUser},
|
||||
{Kind: types.KindRole},
|
||||
{Kind: types.KindNamespace},
|
||||
|
@ -1562,6 +1564,20 @@ func (c *Cache) GetClusterName(opts ...services.MarshalOption) (types.ClusterNam
|
|||
return rg.clusterConfig.GetClusterName(opts...)
|
||||
}
|
||||
|
||||
func (c *Cache) GetUIConfig(ctx context.Context) (types.UIConfig, error) {
|
||||
ctx, span := c.Tracer.Start(ctx, "cache/GetUIConfig")
|
||||
defer span.End()
|
||||
|
||||
rg, err := c.read()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
defer rg.Release()
|
||||
|
||||
uiconfig, err := rg.clusterConfig.GetUIConfig(ctx)
|
||||
return uiconfig, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// GetInstaller gets the installer script resource for the cluster
|
||||
func (c *Cache) GetInstaller(ctx context.Context, name string) (types.Installer, error) {
|
||||
ctx, span := c.Tracer.Start(ctx, "cache/GetInstaller")
|
||||
|
|
1
lib/cache/cache_test.go
vendored
1
lib/cache/cache_test.go
vendored
|
@ -2161,6 +2161,7 @@ func TestCacheWatchKindExistsInEvents(t *testing.T) {
|
|||
types.KindClusterNetworkingConfig: types.DefaultClusterNetworkingConfig(),
|
||||
types.KindClusterAuthPreference: types.DefaultAuthPreference(),
|
||||
types.KindSessionRecordingConfig: types.DefaultSessionRecordingConfig(),
|
||||
types.KindUIConfig: &types.UIConfigV1{},
|
||||
types.KindStaticTokens: &types.StaticTokensV2{},
|
||||
types.KindToken: &types.ProvisionTokenV2{},
|
||||
types.KindUser: &types.UserV2{},
|
||||
|
|
73
lib/cache/collections.go
vendored
73
lib/cache/collections.go
vendored
|
@ -94,6 +94,11 @@ func setupCollections(c *Cache, watches []types.WatchKind) (map[resourceKind]col
|
|||
return nil, trace.BadParameter("missing parameter ClusterConfig")
|
||||
}
|
||||
collections[resourceKind] = &installerConfig{watch: watch, Cache: c}
|
||||
case types.KindUIConfig:
|
||||
if c.ClusterConfig == nil {
|
||||
return nil, trace.BadParameter("missing parameter ClusterConfig")
|
||||
}
|
||||
collections[resourceKind] = &uiConfig{watch: watch, Cache: c}
|
||||
case types.KindUser:
|
||||
if c.Users == nil {
|
||||
return nil, trace.BadParameter("missing parameter Users")
|
||||
|
@ -2294,6 +2299,74 @@ func (c *clusterNetworkingConfig) watchKind() types.WatchKind {
|
|||
return c.watch
|
||||
}
|
||||
|
||||
type uiConfig struct {
|
||||
*Cache
|
||||
watch types.WatchKind
|
||||
}
|
||||
|
||||
func (c *uiConfig) erase(ctx context.Context) error {
|
||||
if err := c.clusterConfigCache.DeleteUIConfig(ctx); err != nil {
|
||||
if !trace.IsNotFound(err) {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *uiConfig) fetch(ctx context.Context) (apply func(ctx context.Context) error, err error) {
|
||||
var noConfig bool
|
||||
resource, err := c.ClusterConfig.GetUIConfig(ctx)
|
||||
if err != nil {
|
||||
if !trace.IsNotFound(err) {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
noConfig = true
|
||||
}
|
||||
return func(ctx context.Context) error {
|
||||
// either zero or one instance exists, so we either erase or
|
||||
// update, but not both.
|
||||
if noConfig {
|
||||
if err := c.erase(ctx); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := c.clusterConfigCache.SetUIConfig(ctx, resource); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
return nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *uiConfig) processEvent(ctx context.Context, event types.Event) error {
|
||||
switch event.Type {
|
||||
case types.OpDelete:
|
||||
err := c.clusterConfigCache.DeleteUIConfig(ctx)
|
||||
if err != nil {
|
||||
if !trace.IsNotFound(err) {
|
||||
c.Logger.WithError(err).Warn("Failed to delete resource.")
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
case types.OpPut:
|
||||
resource, ok := event.Resource.(types.UIConfig)
|
||||
if !ok {
|
||||
return trace.BadParameter("unexpected type %T", event.Resource)
|
||||
}
|
||||
if err := c.clusterConfigCache.SetUIConfig(ctx, resource); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
default:
|
||||
c.Logger.WithField("event", event.Type).Warn("Skipping unsupported event type.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *uiConfig) watchKind() types.WatchKind {
|
||||
return c.watch
|
||||
}
|
||||
|
||||
type sessionRecordingConfig struct {
|
||||
*Cache
|
||||
watch types.WatchKind
|
||||
|
|
|
@ -40,6 +40,7 @@ import (
|
|||
kyaml "k8s.io/apimachinery/pkg/util/yaml"
|
||||
|
||||
"github.com/gravitational/teleport"
|
||||
"github.com/gravitational/teleport/api/client/webclient"
|
||||
"github.com/gravitational/teleport/api/constants"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib"
|
||||
|
@ -872,6 +873,10 @@ func applyProxyConfig(fc *FileConfig, cfg *service.Config) error {
|
|||
cfg.Proxy.PeerAddr = *addr
|
||||
}
|
||||
|
||||
if fc.Proxy.UI != nil {
|
||||
cfg.Proxy.UI = webclient.UIConfig(*fc.Proxy.UI)
|
||||
}
|
||||
|
||||
// This is the legacy format. Continue to support it forever, but ideally
|
||||
// users now use the list format below.
|
||||
if fc.Proxy.KeyFile != "" || fc.Proxy.CertFile != "" {
|
||||
|
|
|
@ -1880,6 +1880,15 @@ type Proxy struct {
|
|||
//
|
||||
//nolint:revive // Because we want this to be IdP.
|
||||
IdP IdP `yaml:"idp,omitempty"`
|
||||
|
||||
// UI provides config options for the web UI
|
||||
UI *UIConfig `yaml:"ui,omitempty"`
|
||||
}
|
||||
|
||||
// UIConfig provides config options for the web UI served by the proxy service.
|
||||
type UIConfig struct {
|
||||
// ScrollbackLines is the max number of lines the UI terminal can display in its history
|
||||
ScrollbackLines int `yaml:"scrollback_lines,omitempty"`
|
||||
}
|
||||
|
||||
// ACME configures ACME protocol - automatic X.509 certificates
|
||||
|
|
|
@ -41,6 +41,7 @@ import (
|
|||
|
||||
"github.com/gravitational/teleport"
|
||||
"github.com/gravitational/teleport/api/breaker"
|
||||
"github.com/gravitational/teleport/api/client/webclient"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
azureutils "github.com/gravitational/teleport/api/utils/azure"
|
||||
"github.com/gravitational/teleport/lib/auth"
|
||||
|
@ -524,6 +525,9 @@ type ProxyConfig struct {
|
|||
|
||||
// DisableALPNSNIListener allows turning off the ALPN Proxy listener. Used in tests.
|
||||
DisableALPNSNIListener bool
|
||||
|
||||
// UI provides config options for the web UI
|
||||
UI webclient.UIConfig
|
||||
}
|
||||
|
||||
// ACME configures ACME automatic certificate renewal
|
||||
|
|
|
@ -3606,6 +3606,7 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error {
|
|||
Context: process.ExitContext(),
|
||||
StaticFS: fs,
|
||||
ClusterFeatures: process.getClusterFeatures(),
|
||||
UI: cfg.Proxy.UI,
|
||||
ProxySettings: proxySettings,
|
||||
PublicProxyAddr: process.proxyPublicAddr().Addr,
|
||||
ALPNHandler: alpnHandlerForWeb.HandleConnection,
|
||||
|
|
|
@ -43,6 +43,13 @@ type ClusterConfiguration interface {
|
|||
// DeleteStaticTokens deletes static tokens resource
|
||||
DeleteStaticTokens() error
|
||||
|
||||
// GetUIConfig gets the proxy service UI config from the backend
|
||||
GetUIConfig(context.Context) (types.UIConfig, error)
|
||||
// SetUIConfig sets the proxy service UI config from the backend
|
||||
SetUIConfig(context.Context, types.UIConfig) error
|
||||
// DeleteUIConfig deletes the proxy service UI config from the backend
|
||||
DeleteUIConfig(ctx context.Context) error
|
||||
|
||||
// GetAuthPreference gets types.AuthPreference from the backend.
|
||||
GetAuthPreference(context.Context) (types.AuthPreference, error)
|
||||
// SetAuthPreference sets types.AuthPreference from the backend.
|
||||
|
|
|
@ -379,6 +379,31 @@ func (s *ClusterConfigurationService) GetInstallers(ctx context.Context) ([]type
|
|||
return installers, nil
|
||||
}
|
||||
|
||||
func (s *ClusterConfigurationService) GetUIConfig(ctx context.Context) (types.UIConfig, error) {
|
||||
item, err := s.Get(ctx, backend.Key(clusterConfigPrefix, uiPrefix))
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return services.UnmarshalUIConfig(item.Value)
|
||||
}
|
||||
|
||||
func (s *ClusterConfigurationService) SetUIConfig(ctx context.Context, uic types.UIConfig) error {
|
||||
value, err := services.MarshalUIConfig(uic)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
_, err = s.Put(ctx, backend.Item{
|
||||
Key: backend.Key(clusterConfigPrefix, uiPrefix),
|
||||
Value: value,
|
||||
})
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
func (s *ClusterConfigurationService) DeleteUIConfig(ctx context.Context) error {
|
||||
return trace.Wrap(s.Delete(ctx, backend.Key(clusterConfigPrefix, uiPrefix)))
|
||||
}
|
||||
|
||||
// GetInstaller gets the script of the cluster from the backend.
|
||||
func (s *ClusterConfigurationService) GetInstaller(ctx context.Context, name string) (types.Installer, error) {
|
||||
item, err := s.Get(ctx, backend.Key(clusterConfigPrefix, scriptsPrefix, installerPrefix, name))
|
||||
|
@ -430,5 +455,6 @@ const (
|
|||
networkingPrefix = "networking"
|
||||
sessionRecordingPrefix = "session_recording"
|
||||
scriptsPrefix = "scripts"
|
||||
uiPrefix = "ui"
|
||||
installerPrefix = "installer"
|
||||
)
|
||||
|
|
|
@ -72,6 +72,8 @@ func (e *EventsService) NewWatcher(ctx context.Context, watch types.Watch) (type
|
|||
parser = newAuthPreferenceParser()
|
||||
case types.KindSessionRecordingConfig:
|
||||
parser = newSessionRecordingConfigParser()
|
||||
case types.KindUIConfig:
|
||||
parser = newUIConfigParser()
|
||||
case types.KindClusterName:
|
||||
parser = newClusterNameParser()
|
||||
case types.KindNamespace:
|
||||
|
@ -496,6 +498,40 @@ func (p *authPreferenceParser) parse(event backend.Event) (types.Resource, error
|
|||
}
|
||||
}
|
||||
|
||||
func newUIConfigParser() *uiConfigParser {
|
||||
return &uiConfigParser{
|
||||
baseParser: newBaseParser(backend.Key(clusterConfigPrefix, uiPrefix)),
|
||||
}
|
||||
}
|
||||
|
||||
type uiConfigParser struct {
|
||||
baseParser
|
||||
}
|
||||
|
||||
func (p *uiConfigParser) parse(event backend.Event) (types.Resource, error) {
|
||||
switch event.Type {
|
||||
case types.OpDelete:
|
||||
h, err := resourceHeader(event, types.KindUIConfig, types.V1, 0)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
h.SetName(types.MetaNameUIConfig)
|
||||
return h, nil
|
||||
case types.OpPut:
|
||||
ap, err := services.UnmarshalUIConfig(
|
||||
event.Item.Value,
|
||||
services.WithResourceID(event.Item.ID),
|
||||
services.WithExpires(event.Item.Expires),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return ap, nil
|
||||
default:
|
||||
return nil, trace.BadParameter("event %v is not supported", event.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func newSessionRecordingConfigParser() *sessionRecordingConfigParser {
|
||||
return &sessionRecordingConfigParser{
|
||||
baseParser: newBaseParser(backend.Key(clusterConfigPrefix, sessionRecordingPrefix)),
|
||||
|
|
|
@ -67,6 +67,7 @@ func NewPresetEditorRole() types.Role {
|
|||
types.NewRule(types.KindClusterName, RW()),
|
||||
types.NewRule(types.KindClusterNetworkingConfig, RW()),
|
||||
types.NewRule(types.KindSessionRecordingConfig, RW()),
|
||||
types.NewRule(types.KindUIConfig, RW()),
|
||||
types.NewRule(types.KindTrustedCluster, RW()),
|
||||
types.NewRule(types.KindRemoteCluster, RW()),
|
||||
types.NewRule(types.KindToken, RW()),
|
||||
|
|
|
@ -145,6 +145,8 @@ func ParseShortcut(in string) (string, error) {
|
|||
return types.KindTrustedCluster, nil
|
||||
case types.KindClusterAuthPreference, "cluster_authentication_preferences", "cap":
|
||||
return types.KindClusterAuthPreference, nil
|
||||
case types.KindUIConfig, "ui":
|
||||
return types.KindUIConfig, nil
|
||||
case types.KindClusterNetworkingConfig, "networking_config", "networking", "net_config", "netconfig":
|
||||
return types.KindClusterNetworkingConfig, nil
|
||||
case types.KindSessionRecordingConfig, "recording_config", "session_recording", "rec_config", "recconfig":
|
||||
|
|
|
@ -156,6 +156,7 @@ func RoleForUser(u types.User) types.Role {
|
|||
types.NewRule(types.KindClusterAuthPreference, RW()),
|
||||
types.NewRule(types.KindClusterNetworkingConfig, RW()),
|
||||
types.NewRule(types.KindSessionRecordingConfig, RW()),
|
||||
types.NewRule(types.KindUIConfig, RW()),
|
||||
types.NewRule(types.KindApp, RW()),
|
||||
types.NewRule(types.KindDatabase, RW()),
|
||||
types.NewRule(types.KindLock, RW()),
|
||||
|
|
76
lib/services/ui_config.go
Normal file
76
lib/services/ui_config.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* Copyright 2023 Gravitational, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"github.com/gravitational/trace"
|
||||
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
)
|
||||
|
||||
// UnmarshalUIConfig unmarshals the UIConfig resource from JSON.
|
||||
func UnmarshalUIConfig(data []byte, opts ...MarshalOption) (types.UIConfig, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, trace.BadParameter("missing resource data")
|
||||
}
|
||||
|
||||
cfg, err := CollectOptions(opts)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
var uiconfig types.UIConfigV1
|
||||
if err := utils.FastUnmarshal(data, &uiconfig); err != nil {
|
||||
return nil, trace.BadParameter(err.Error())
|
||||
}
|
||||
if err := uiconfig.CheckAndSetDefaults(); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if cfg.ID != 0 {
|
||||
uiconfig.SetResourceID(cfg.ID)
|
||||
}
|
||||
if !cfg.Expires.IsZero() {
|
||||
uiconfig.SetExpiry(cfg.Expires)
|
||||
}
|
||||
return &uiconfig, nil
|
||||
}
|
||||
|
||||
// MarshalUIConfig marshals the UIConfig resource to JSON.
|
||||
func MarshalUIConfig(uiconfig types.UIConfig, opts ...MarshalOption) ([]byte, error) {
|
||||
if err := uiconfig.CheckAndSetDefaults(); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
cfg, err := CollectOptions(opts)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
switch uiconfig := uiconfig.(type) {
|
||||
case *types.UIConfigV1:
|
||||
if !cfg.PreserveResourceID {
|
||||
// avoid modifying the original object
|
||||
// to prevent unexpected data races
|
||||
copy := *uiconfig
|
||||
copy.SetResourceID(0)
|
||||
uiconfig = ©
|
||||
}
|
||||
return utils.FastMarshal(uiconfig)
|
||||
default:
|
||||
return nil, trace.BadParameter("unrecognized uiconfig version %T", uiconfig)
|
||||
}
|
||||
}
|
|
@ -243,6 +243,9 @@ type Config struct {
|
|||
// HealthCheckAppServer is a function that checks if the proxy can handle
|
||||
// application requests.
|
||||
HealthCheckAppServer healthCheckAppServerFunc
|
||||
|
||||
// UI provides config options for the web UI
|
||||
UI webclient.UIConfig
|
||||
}
|
||||
|
||||
type APIHandler struct {
|
||||
|
@ -1303,6 +1306,7 @@ func (h *Handler) getWebConfig(w http.ResponseWriter, r *http.Request, p httprou
|
|||
IsCloud: h.ClusterFeatures.GetCloud(),
|
||||
TunnelPublicAddress: tunnelPublicAddr,
|
||||
RecoveryCodesEnabled: h.ClusterFeatures.GetRecoveryCodes(),
|
||||
UI: h.getUIConfig(r.Context()),
|
||||
IsDashboard: isDashboard(h.ClusterFeatures),
|
||||
}
|
||||
|
||||
|
@ -1327,6 +1331,18 @@ type JWKSResponse struct {
|
|||
Keys []jwt.JWK `json:"keys"`
|
||||
}
|
||||
|
||||
// getUiConfig will first try to get an ui_config set in the cache and then
|
||||
// return what was set by the file config. Returns nil if neither are set which
|
||||
// is fine, as the web UI can set its own defaults.
|
||||
func (h *Handler) getUIConfig(ctx context.Context) webclient.UIConfig {
|
||||
if uiConfig, err := h.cfg.AccessPoint.GetUIConfig(ctx); err == nil && uiConfig != nil {
|
||||
return webclient.UIConfig{
|
||||
ScrollbackLines: int(uiConfig.GetScrollbackLines()),
|
||||
}
|
||||
}
|
||||
return h.cfg.UI
|
||||
}
|
||||
|
||||
// jwks returns all public keys used to sign JWT tokens for this cluster.
|
||||
func (h *Handler) jwks(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
|
||||
clusterName, err := h.cfg.ProxyClient.GetDomainName(r.Context())
|
||||
|
|
|
@ -171,6 +171,8 @@ type webSuiteConfig struct {
|
|||
|
||||
disableDiskBasedRecording bool
|
||||
|
||||
uiConfig webclient.UIConfig
|
||||
|
||||
// Custom "HealthCheckAppServer" function. Can be used to avoid dialing app
|
||||
// services.
|
||||
HealthCheckAppServer healthCheckAppServerFunc
|
||||
|
@ -443,6 +445,7 @@ func newWebSuiteWithConfig(t *testing.T, cfg webSuiteConfig) *WebSuite {
|
|||
SessionControl: proxySessionController,
|
||||
Router: router,
|
||||
HealthCheckAppServer: cfg.HealthCheckAppServer,
|
||||
UI: cfg.uiConfig,
|
||||
}
|
||||
|
||||
if handlerConfig.HealthCheckAppServer == nil {
|
||||
|
@ -1237,6 +1240,30 @@ func TestNewTerminalHandler(t *testing.T) {
|
|||
require.NotNil(t, term.log)
|
||||
}
|
||||
|
||||
func TestUIConfig(t *testing.T) {
|
||||
uiConfig := webclient.UIConfig{
|
||||
ScrollbackLines: 555,
|
||||
}
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
s := newWebSuiteWithConfig(t, webSuiteConfig{uiConfig: uiConfig})
|
||||
clt := s.client(t)
|
||||
endpoint := clt.Endpoint("web", "config.js")
|
||||
re, err := clt.Get(ctx, endpoint, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.HasPrefix(string(re.Bytes()), "var GRV_CONFIG"))
|
||||
t.Cleanup(cancel)
|
||||
|
||||
// Response is type application/javascript, we need to strip off the variable name
|
||||
// and the semicolon at the end, then we are left with json like object.
|
||||
var cfg webclient.WebConfig
|
||||
str := strings.ReplaceAll(string(re.Bytes()), "var GRV_CONFIG = ", "")
|
||||
err = json.Unmarshal([]byte(str[:len(str)-1]), &cfg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uiConfig, cfg.UI)
|
||||
}
|
||||
|
||||
func TestResizeTerminal(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := newWebSuiteWithConfig(t, webSuiteConfig{disableDiskBasedRecording: true})
|
||||
|
|
|
@ -155,8 +155,8 @@ func (s *serverCollection) writeText(w io.Writer) error {
|
|||
t = asciitable.MakeTable(headers, rows...)
|
||||
} else {
|
||||
t = asciitable.MakeTableWithTruncatedColumn(headers, rows, "Labels")
|
||||
|
||||
}
|
||||
|
||||
_, err := t.AsBuffer().WriteTo(w)
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
@ -552,6 +552,21 @@ func (c *authPrefCollection) writeText(w io.Writer) error {
|
|||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
type uiConfigCollection struct {
|
||||
uiconfig types.UIConfig
|
||||
}
|
||||
|
||||
func (c *uiConfigCollection) resources() (r []types.Resource) {
|
||||
return []types.Resource{c.uiconfig}
|
||||
}
|
||||
|
||||
func (c *uiConfigCollection) writeText(w io.Writer) error {
|
||||
t := asciitable.MakeTable([]string{"Scrollback Lines"})
|
||||
t.AddRow([]string{string(c.uiconfig.GetScrollbackLines())})
|
||||
_, err := t.AsBuffer().WriteTo(w)
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
type netConfigCollection struct {
|
||||
netConfig types.ClusterNetworkingConfig
|
||||
}
|
||||
|
|
|
@ -109,6 +109,7 @@ func (rc *ResourceCommand) Initialize(app *kingpin.Application, config *service.
|
|||
types.KindClusterAuthPreference: rc.createAuthPreference,
|
||||
types.KindClusterNetworkingConfig: rc.createClusterNetworkingConfig,
|
||||
types.KindSessionRecordingConfig: rc.createSessionRecordingConfig,
|
||||
types.KindUIConfig: rc.createUIConfig,
|
||||
types.KindLock: rc.createLock,
|
||||
types.KindNetworkRestrictions: rc.createNetworkRestrictions,
|
||||
types.KindApp: rc.createApp,
|
||||
|
@ -639,6 +640,15 @@ func (rc *ResourceCommand) createInstaller(ctx context.Context, client auth.Clie
|
|||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
func (rc *ResourceCommand) createUIConfig(ctx context.Context, client auth.ClientI, raw services.UnknownResource) error {
|
||||
uic, err := services.UnmarshalUIConfig(raw.Raw)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
return trace.Wrap(client.SetUIConfig(ctx, uic))
|
||||
}
|
||||
|
||||
func (rc *ResourceCommand) createNode(ctx context.Context, client auth.ClientI, raw services.UnknownResource) error {
|
||||
server, err := services.UnmarshalServer(raw.Raw, types.KindNode)
|
||||
if err != nil {
|
||||
|
@ -788,6 +798,7 @@ func (rc *ResourceCommand) Delete(ctx context.Context, client auth.ClientI) (err
|
|||
types.KindClusterNetworkingConfig,
|
||||
types.KindSessionRecordingConfig,
|
||||
types.KindInstaller,
|
||||
types.KindUIConfig,
|
||||
}
|
||||
if !slices.Contains(singletonResources, rc.ref.Kind) && (rc.ref.Kind == "" || rc.ref.Name == "") {
|
||||
return trace.BadParameter("provide a full resource name to delete, for example:\n$ tctl rm cluster/east\n")
|
||||
|
@ -995,6 +1006,12 @@ func (rc *ResourceCommand) Delete(ctx context.Context, client auth.ClientI) (err
|
|||
return trace.NotFound("kubernetes server %q not found", rc.ref.Name)
|
||||
}
|
||||
fmt.Printf("kubernetes server %q has been deleted\n", rc.ref.Name)
|
||||
case types.KindUIConfig:
|
||||
err := client.DeleteUIConfig(ctx)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
fmt.Printf("%s has been deleted\n", types.KindUIConfig)
|
||||
case types.KindInstaller:
|
||||
err := client.DeleteInstaller(ctx, rc.ref.Name)
|
||||
if err != nil {
|
||||
|
@ -1545,6 +1562,15 @@ func (rc *ResourceCommand) getCollection(ctx context.Context, client auth.Client
|
|||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return &installerCollection{installers: []types.Installer{inst}}, nil
|
||||
case types.KindUIConfig:
|
||||
if rc.ref.Name != "" {
|
||||
return nil, trace.BadParameter("only simple `tctl get %v` can be used", types.KindUIConfig)
|
||||
}
|
||||
uiconfig, err := client.GetUIConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return &uiConfigCollection{uiconfig}, nil
|
||||
case types.KindDatabaseService:
|
||||
resourceName := rc.ref.Name
|
||||
listReq := proto.ListResourcesRequest{
|
||||
|
|
|
@ -44,6 +44,10 @@ const cfg = {
|
|||
|
||||
baseUrl: window.location.origin,
|
||||
|
||||
ui: {
|
||||
scrollbackLines: 1000,
|
||||
},
|
||||
|
||||
auth: {
|
||||
localAuthEnabled: true,
|
||||
allowPasswordless: false,
|
||||
|
@ -554,6 +558,10 @@ const cfg = {
|
|||
});
|
||||
},
|
||||
|
||||
getUIConfig() {
|
||||
return cfg.ui;
|
||||
},
|
||||
|
||||
init(backendConfig = {}) {
|
||||
mergeDeep(this, backendConfig);
|
||||
},
|
||||
|
|
|
@ -20,6 +20,8 @@ import { debounce, isInteger } from 'shared/utils/highbar';
|
|||
import { WebLinksAddon } from 'xterm-addon-web-links';
|
||||
import Logger from 'shared/libs/logger';
|
||||
|
||||
import cfg from 'teleport/config';
|
||||
|
||||
import { TermEvent } from './enums';
|
||||
import Tty from './tty';
|
||||
|
||||
|
@ -49,7 +51,9 @@ export default class TtyTerminal {
|
|||
this._el = el;
|
||||
this._fontFamily = fontFamily || undefined;
|
||||
this._fontSize = fontSize || 14;
|
||||
this._scrollBack = scrollBack;
|
||||
// Passing scrollback will overwrite the default config. This is to support ttyplayer.
|
||||
// Default to the config when not passed anything, which is the normal usecase
|
||||
this._scrollBack = scrollBack || cfg.ui.scrollbackLines;
|
||||
this.tty = tty;
|
||||
this.term = null;
|
||||
|
||||
|
@ -63,7 +67,7 @@ export default class TtyTerminal {
|
|||
lineHeight: 1,
|
||||
fontFamily: this._fontFamily,
|
||||
fontSize: this._fontSize,
|
||||
scrollback: this._scrollBack || 1000,
|
||||
scrollback: this._scrollBack,
|
||||
cursorBlink: false,
|
||||
allowTransparency: true,
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue