diff --git a/.github/ISSUE_TEMPLATE/webtestplan.md b/.github/ISSUE_TEMPLATE/webtestplan.md index a0414c040908..2f905c9f3ec6 100644 --- a/.github/ISSUE_TEMPLATE/webtestplan.md +++ b/.github/ISSUE_TEMPLATE/webtestplan.md @@ -259,6 +259,99 @@ spec: - [ ] Verify that root is marked with a `root` pill - [ ] Verify that cluster dropdown menu items goes to the correct route +## Application Access + +### Required Applications + +Create two apps running locally, a frontend app and a backend app. The frontend app should +make an API request to the backend app at its teleport public_addr + +
+ You can use this example app if you don't have a frontend/backend setup + + ```go + package main + + import ( + "encoding/json" + "fmt" + "log" + "net/http" + ) + + // change to your cluster addr + const clusterName = "avatus.sh" + + func main() { + // handler for the html page. this is the "client". + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + html := fmt.Sprintf(html, clusterName) + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(html)) + }) + + // Handler for the API endpoint + http.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", fmt.Sprintf("https://client.%s", clusterName)) + w.Header().Set("Access-Control-Allow-Credentials", "true") + data := map[string]string{"hello": "world"} + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(data) + }) + + log.Println("Server starting on http://localhost:8080") + log.Fatal(http.ListenAndServe(":8080", nil)) + } + + const html = ` + + + + + + API Data Fetcher + + +
+
+ + + + ` +``` +
+ +Update your app service to serve the apps like this (update your public addr to what makes sense for your cluster) +``` +app_service: + enabled: "yes" + debug_app: true + apps: + - name: client + uri: http://localhost:8080 + public_addr: client.avatus.sh + required_apps: + - api + - name: api + uri: http://localhost:8080 + public_addr: api.avatus.sh + cors: + allowed_origins: + - https://client.avatus.sh +``` + +Launch your cluster and make sure you are logged out of your api by going to `https://api.avatus.sh/teleport-logout` + +- [ ] Launch the client app and you should see `{"hello":"world"}` response +- [ ] You should see no CORS issues in the console + ## Access Requests Not available for OSS diff --git a/api/client/client.go b/api/client/client.go index 8980e42ac16e..fc08ad53467c 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -53,6 +53,7 @@ import ( "github.com/gravitational/teleport/api/client/accessmonitoringrules" crownjewelapi "github.com/gravitational/teleport/api/client/crownjewel" "github.com/gravitational/teleport/api/client/discoveryconfig" + "github.com/gravitational/teleport/api/client/dynamicwindows" "github.com/gravitational/teleport/api/client/externalauditstorage" kubewaitingcontainerclient "github.com/gravitational/teleport/api/client/kubewaitingcontainer" "github.com/gravitational/teleport/api/client/okta" @@ -74,6 +75,7 @@ import ( dbobjectimportrulev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/dbobjectimportrule/v1" devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1" discoveryconfigv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/discoveryconfig/v1" + dynamicwindowsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/dynamicwindows/v1" externalauditstoragev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/externalauditstorage/v1" integrationpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1" kubeproto "github.com/gravitational/teleport/api/gen/proto/go/teleport/kube/v1" @@ -2657,6 +2659,10 @@ func (c *Client) SearchSessionEvents(ctx context.Context, fromUTC time.Time, toU return decodedEvents, response.LastKey, nil } +func (c *Client) DynamicDesktopClient() *dynamicwindows.Client { + return dynamicwindows.NewClient(dynamicwindowsv1.NewDynamicWindowsServiceClient(c.conn)) +} + // ClusterConfigClient returns an unadorned Cluster Configuration client, using the underlying // Auth gRPC connection. func (c *Client) ClusterConfigClient() clusterconfigpb.ClusterConfigServiceClient { diff --git a/api/client/dynamicwindows/dynamicwindows.go b/api/client/dynamicwindows/dynamicwindows.go new file mode 100644 index 000000000000..6c158a39e424 --- /dev/null +++ b/api/client/dynamicwindows/dynamicwindows.go @@ -0,0 +1,93 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package dynamicwindows + +import ( + "context" + + "github.com/gravitational/trace" + + dynamicwindows "github.com/gravitational/teleport/api/gen/proto/go/teleport/dynamicwindows/v1" + "github.com/gravitational/teleport/api/types" +) + +// Client is a DynamicWindowsDesktop client. +type Client struct { + grpcClient dynamicwindows.DynamicWindowsServiceClient +} + +// NewClient creates a new StaticHostUser client. +func NewClient(grpcClient dynamicwindows.DynamicWindowsServiceClient) *Client { + return &Client{ + grpcClient: grpcClient, + } +} + +func (c *Client) GetDynamicWindowsDesktop(ctx context.Context, name string) (types.DynamicWindowsDesktop, error) { + desktop, err := c.grpcClient.GetDynamicWindowsDesktop(ctx, &dynamicwindows.GetDynamicWindowsDesktopRequest{ + Name: name, + }) + return desktop, trace.Wrap(err) +} + +func (c *Client) ListDynamicWindowsDesktop(ctx context.Context, pageSize int, pageToken string) ([]types.DynamicWindowsDesktop, string, error) { + resp, err := c.grpcClient.ListDynamicWindowsDesktops(ctx, &dynamicwindows.ListDynamicWindowsDesktopsRequest{ + PageSize: int32(pageSize), + PageToken: pageToken, + }) + if err != nil { + return nil, "", trace.Wrap(err) + } + desktops := make([]types.DynamicWindowsDesktop, 0, len(resp.GetDesktops())) + for _, desktop := range resp.GetDesktops() { + desktops = append(desktops, desktop) + } + return desktops, resp.GetNextPageToken(), nil +} + +func (c *Client) CreateDynamicWindowsDesktop(ctx context.Context, desktop types.DynamicWindowsDesktop) (types.DynamicWindowsDesktop, error) { + switch desktop := desktop.(type) { + case *types.DynamicWindowsDesktopV1: + desktop, err := c.grpcClient.CreateDynamicWindowsDesktop(ctx, &dynamicwindows.CreateDynamicWindowsDesktopRequest{ + Desktop: desktop, + }) + return desktop, trace.Wrap(err) + default: + return nil, trace.BadParameter("unknown desktop type: %T", desktop) + } +} + +func (c *Client) UpdateDynamicWindowsDesktop(ctx context.Context, desktop types.DynamicWindowsDesktop) (types.DynamicWindowsDesktop, error) { + switch desktop := desktop.(type) { + case *types.DynamicWindowsDesktopV1: + desktop, err := c.grpcClient.UpdateDynamicWindowsDesktop(ctx, &dynamicwindows.UpdateDynamicWindowsDesktopRequest{ + Desktop: desktop, + }) + return desktop, trace.Wrap(err) + default: + return nil, trace.BadParameter("unknown desktop type: %T", desktop) + } +} + +func (c *Client) DeleteDynamicWindowsDesktop(ctx context.Context, name string) error { + _, err := c.grpcClient.DeleteDynamicWindowsDesktop(ctx, &dynamicwindows.DeleteDynamicWindowsDesktopRequest{ + Name: name, + }) + return trace.Wrap(err) +} diff --git a/api/client/events.go b/api/client/events.go index e856a77d26d2..0cce9664d248 100644 --- a/api/client/events.go +++ b/api/client/events.go @@ -254,6 +254,10 @@ func EventToGRPC(in types.Event) (*proto.Event, error) { out.Resource = &proto.Event_WindowsDesktop{ WindowsDesktop: r, } + case *types.DynamicWindowsDesktopV1: + out.Resource = &proto.Event_DynamicWindowsDesktop{ + DynamicWindowsDesktop: r, + } case *types.InstallerV1: out.Resource = &proto.Event_Installer{ Installer: r, @@ -444,6 +448,9 @@ func EventFromGRPC(in *proto.Event) (*types.Event, error) { } else if r := in.GetWindowsDesktop(); r != nil { out.Resource = r return &out, nil + } else if r := in.GetDynamicWindowsDesktop(); r != nil { + out.Resource = r + return &out, nil } else if r := in.GetKubernetesServer(); r != nil { out.Resource = r return &out, nil diff --git a/api/client/proto/joinservice.pb.go b/api/client/proto/joinservice.pb.go index a819e18f0c46..78690405a47e 100644 --- a/api/client/proto/joinservice.pb.go +++ b/api/client/proto/joinservice.pb.go @@ -27,7 +27,8 @@ var _ = math.Inf // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package -// TODO(nklaassen): Document me. +// RegisterUsingIAMMethodRequest is a request for registration via the IAM join +// method. type RegisterUsingIAMMethodRequest struct { // RegisterUsingTokenRequest holds registration parameters common to all // join methods. diff --git a/api/gen/proto/go/teleport/autoupdate/v1/autoupdate.pb.go b/api/gen/proto/go/teleport/autoupdate/v1/autoupdate.pb.go index 6213401019f8..e01283cc8241 100644 --- a/api/gen/proto/go/teleport/autoupdate/v1/autoupdate.pb.go +++ b/api/gen/proto/go/teleport/autoupdate/v1/autoupdate.pb.go @@ -186,7 +186,8 @@ type AutoUpdateConfigSpec struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Tools *AutoUpdateConfigSpecTools `protobuf:"bytes,2,opt,name=tools,proto3" json:"tools,omitempty"` + Tools *AutoUpdateConfigSpecTools `protobuf:"bytes,2,opt,name=tools,proto3" json:"tools,omitempty"` + Agents *AutoUpdateConfigSpecAgents `protobuf:"bytes,3,opt,name=agents,proto3" json:"agents,omitempty"` } func (x *AutoUpdateConfigSpec) Reset() { @@ -226,6 +227,13 @@ func (x *AutoUpdateConfigSpec) GetTools() *AutoUpdateConfigSpecTools { return nil } +func (x *AutoUpdateConfigSpec) GetAgents() *AutoUpdateConfigSpecAgents { + if x != nil { + return x.Agents + } + return nil +} + // AutoUpdateConfigSpecTools encodes the parameters for client tools auto updates. type AutoUpdateConfigSpecTools struct { state protoimpl.MessageState @@ -287,8 +295,8 @@ type AutoUpdateConfigSpecAgents struct { // Once the window is over, the group transitions to the done state. Existing agents won't be updated until the next // maintenance window. MaintenanceWindowDuration *durationpb.Duration `protobuf:"bytes,3,opt,name=maintenance_window_duration,json=maintenanceWindowDuration,proto3" json:"maintenance_window_duration,omitempty"` - // agent_schedules specifies schedules for updates of grouped agents. - AgentSchedules *AgentAutoUpdateSchedules `protobuf:"bytes,5,opt,name=agent_schedules,json=agentSchedules,proto3" json:"agent_schedules,omitempty"` + // schedules specifies schedules for updates of grouped agents. + Schedules *AgentAutoUpdateSchedules `protobuf:"bytes,6,opt,name=schedules,proto3" json:"schedules,omitempty"` } func (x *AutoUpdateConfigSpecAgents) Reset() { @@ -342,9 +350,9 @@ func (x *AutoUpdateConfigSpecAgents) GetMaintenanceWindowDuration() *durationpb. return nil } -func (x *AutoUpdateConfigSpecAgents) GetAgentSchedules() *AgentAutoUpdateSchedules { +func (x *AutoUpdateConfigSpecAgents) GetSchedules() *AgentAutoUpdateSchedules { if x != nil { - return x.AgentSchedules + return x.Schedules } return nil } @@ -1068,167 +1076,173 @@ var file_teleport_autoupdate_v1_autoupdate_proto_rawDesc = []byte{ 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x22, - 0x77, 0x0a, 0x14, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x53, 0x70, 0x65, 0x63, 0x12, 0x47, 0x0a, 0x05, 0x74, 0x6f, 0x6f, 0x6c, 0x73, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, - 0x74, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, - 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x53, 0x70, 0x65, 0x63, 0x54, 0x6f, 0x6f, 0x6c, 0x73, 0x52, 0x05, 0x74, 0x6f, 0x6f, 0x6c, 0x73, - 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x52, 0x10, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x5f, 0x61, 0x75, - 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x22, 0x2f, 0x0a, 0x19, 0x41, 0x75, 0x74, 0x6f, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x53, 0x70, 0x65, 0x63, - 0x54, 0x6f, 0x6f, 0x6c, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x22, 0x82, 0x02, 0x0a, 0x1a, 0x41, 0x75, - 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x53, 0x70, - 0x65, 0x63, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, - 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x59, 0x0a, 0x1b, 0x6d, 0x61, 0x69, 0x6e, - 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x5f, 0x64, - 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x19, 0x6d, 0x61, 0x69, 0x6e, 0x74, 0x65, - 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x44, 0x75, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x59, 0x0a, 0x0f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x63, 0x68, - 0x65, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x74, - 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x52, 0x0e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x22, 0x62, - 0x0a, 0x18, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x46, 0x0a, 0x07, 0x72, 0x65, - 0x67, 0x75, 0x6c, 0x61, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x74, 0x65, - 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x07, 0x72, 0x65, 0x67, 0x75, 0x6c, - 0x61, 0x72, 0x22, 0x7a, 0x0a, 0x14, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, - 0x0a, 0x04, 0x64, 0x61, 0x79, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, - 0x79, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x68, 0x6f, 0x75, 0x72, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x48, 0x6f, 0x75, - 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x77, 0x61, 0x69, 0x74, 0x5f, 0x64, 0x61, 0x79, 0x73, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x77, 0x61, 0x69, 0x74, 0x44, 0x61, 0x79, 0x73, 0x22, 0xd9, - 0x01, 0x0a, 0x11, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x56, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x75, 0x62, 0x5f, - 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x4b, - 0x69, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, - 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, - 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x41, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, - 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, - 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x22, 0xc3, 0x01, 0x0a, 0x15, 0x41, - 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x53, 0x70, 0x65, 0x63, 0x12, 0x48, 0x0a, 0x05, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, - 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, - 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x70, - 0x65, 0x63, 0x54, 0x6f, 0x6f, 0x6c, 0x73, 0x52, 0x05, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x12, 0x4b, - 0x0a, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, + 0xc3, 0x01, 0x0a, 0x14, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x53, 0x70, 0x65, 0x63, 0x12, 0x47, 0x0a, 0x05, 0x74, 0x6f, 0x6f, 0x6c, + 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, + 0x2e, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x53, 0x70, 0x65, 0x63, 0x54, 0x6f, 0x6f, 0x6c, 0x73, 0x52, 0x05, 0x74, 0x6f, 0x6f, 0x6c, + 0x73, 0x12, 0x4a, 0x0a, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x32, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x75, 0x74, + 0x6f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x6f, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x53, 0x70, 0x65, 0x63, 0x41, + 0x67, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x4a, 0x04, 0x08, + 0x01, 0x10, 0x02, 0x52, 0x10, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x22, 0x2f, 0x0a, 0x19, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x53, 0x70, 0x65, 0x63, 0x54, 0x6f, 0x6f, + 0x6c, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x22, 0x8e, 0x02, 0x0a, 0x1a, 0x41, 0x75, 0x74, 0x6f, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x53, 0x70, 0x65, 0x63, 0x41, + 0x67, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x74, 0x72, + 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x74, 0x72, + 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x59, 0x0a, 0x1b, 0x6d, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x6e, + 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x5f, 0x64, 0x75, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x19, 0x6d, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x6e, 0x61, 0x6e, + 0x63, 0x65, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x4e, 0x0a, 0x09, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, + 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x67, 0x65, + 0x6e, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x63, 0x68, 0x65, + 0x64, 0x75, 0x6c, 0x65, 0x73, 0x52, 0x09, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x73, + 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x52, 0x0f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x63, + 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x22, 0x62, 0x0a, 0x18, 0x41, 0x67, 0x65, 0x6e, 0x74, + 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, + 0x6c, 0x65, 0x73, 0x12, 0x46, 0x0a, 0x07, 0x72, 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x61, 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x67, + 0x65, 0x6e, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x52, 0x07, 0x72, 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x22, 0x7a, 0x0a, 0x14, 0x41, + 0x67, 0x65, 0x6e, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x47, 0x72, + 0x6f, 0x75, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x79, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x79, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x73, + 0x74, 0x61, 0x72, 0x74, 0x5f, 0x68, 0x6f, 0x75, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x48, 0x6f, 0x75, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x77, 0x61, + 0x69, 0x74, 0x5f, 0x64, 0x61, 0x79, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x77, + 0x61, 0x69, 0x74, 0x44, 0x61, 0x79, 0x73, 0x22, 0xd9, 0x01, 0x0a, 0x11, 0x41, 0x75, 0x74, 0x6f, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, + 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, + 0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x75, 0x62, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x12, 0x41, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x70, 0x65, 0x63, 0x41, 0x67, 0x65, - 0x6e, 0x74, 0x73, 0x52, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x4a, 0x04, 0x08, 0x01, 0x10, - 0x02, 0x52, 0x0d, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x22, 0x43, 0x0a, 0x1a, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x56, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x70, 0x65, 0x63, 0x54, 0x6f, 0x6f, 0x6c, 0x73, 0x12, 0x25, - 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x56, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x99, 0x01, 0x0a, 0x1b, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x70, 0x65, 0x63, 0x41, - 0x67, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x74, - 0x61, 0x72, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x61, - 0x72, 0x67, 0x65, 0x74, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0d, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x12, 0x0a, - 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, - 0x65, 0x22, 0xb1, 0x02, 0x0a, 0x16, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x6c, 0x6c, 0x6f, 0x75, 0x74, 0x12, 0x12, 0x0a, 0x04, - 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, - 0x12, 0x19, 0x0a, 0x08, 0x73, 0x75, 0x62, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, - 0x72, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, - 0x46, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, - 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x6c, 0x6c, 0x6f, 0x75, 0x74, 0x53, 0x70, 0x65, - 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x12, 0x4c, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, + 0x74, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, + 0x70, 0x65, 0x63, 0x22, 0xc3, 0x01, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x70, 0x65, 0x63, 0x12, 0x48, 0x0a, + 0x05, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x70, 0x65, 0x63, 0x54, 0x6f, 0x6f, 0x6c, 0x73, + 0x52, 0x05, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x12, 0x4b, 0x0a, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, - 0x52, 0x6f, 0x6c, 0x6c, 0x6f, 0x75, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0xc9, 0x01, 0x0a, 0x1a, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, + 0x2e, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x53, 0x70, 0x65, 0x63, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x06, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x73, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x52, 0x0d, 0x74, 0x6f, 0x6f, 0x6c, + 0x73, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x43, 0x0a, 0x1a, 0x41, 0x75, 0x74, + 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x70, + 0x65, 0x63, 0x54, 0x6f, 0x6f, 0x6c, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, + 0x74, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x99, + 0x01, 0x0a, 0x1b, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x70, 0x65, 0x63, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x23, + 0x0a, 0x0d, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x74, 0x61, 0x72, 0x74, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x61, 0x72, + 0x67, 0x65, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x63, + 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x63, + 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x22, 0xb1, 0x02, 0x0a, 0x16, 0x41, + 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x6f, + 0x6c, 0x6c, 0x6f, 0x75, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x75, 0x62, + 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, + 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x38, + 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, + 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x46, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, + 0x6f, 0x6c, 0x6c, 0x6f, 0x75, 0x74, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, + 0x12, 0x4c, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x6f, + 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x6c, 0x6c, 0x6f, 0x75, 0x74, - 0x53, 0x70, 0x65, 0x63, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x76, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x74, 0x61, - 0x72, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x61, 0x72, - 0x67, 0x65, 0x74, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0d, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x27, 0x0a, 0x0f, - 0x61, 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, - 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, - 0x79, 0x22, 0x71, 0x0a, 0x1c, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, - 0x67, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x6c, 0x6c, 0x6f, 0x75, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x12, 0x51, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x39, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x75, 0x74, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0xc9, + 0x01, 0x0a, 0x1a, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x67, 0x65, + 0x6e, 0x74, 0x52, 0x6f, 0x6c, 0x6c, 0x6f, 0x75, 0x74, 0x53, 0x70, 0x65, 0x63, 0x12, 0x23, 0x0a, + 0x0d, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x74, 0x61, 0x72, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x61, 0x72, 0x67, + 0x65, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x63, 0x68, + 0x65, 0x64, 0x75, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x63, 0x68, + 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x61, 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, + 0x61, 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x1a, + 0x0a, 0x08, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x22, 0x71, 0x0a, 0x1c, 0x41, 0x75, + 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x6c, + 0x6c, 0x6f, 0x75, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x51, 0x0a, 0x06, 0x67, 0x72, + 0x6f, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x67, + 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x6c, 0x6c, 0x6f, 0x75, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x22, 0xaf, 0x02, + 0x0a, 0x21, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x67, 0x65, 0x6e, + 0x74, 0x52, 0x6f, 0x6c, 0x6c, 0x6f, 0x75, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x47, 0x72, + 0x6f, 0x75, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, + 0x6d, 0x65, 0x12, 0x47, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x31, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x6f, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x6c, 0x6c, 0x6f, 0x75, - 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x06, 0x67, 0x72, - 0x6f, 0x75, 0x70, 0x73, 0x22, 0xaf, 0x02, 0x0a, 0x21, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x6c, 0x6c, 0x6f, 0x75, 0x74, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x39, - 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, - 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x47, 0x0a, 0x05, 0x73, 0x74, 0x61, - 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x31, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, - 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x67, 0x65, 0x6e, - 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, - 0x74, 0x65, 0x12, 0x44, 0x0a, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x6c, 0x61, 0x73, 0x74, - 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x2a, 0xf7, 0x01, 0x0a, 0x19, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x12, 0x2d, 0x0a, 0x29, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x55, 0x50, 0x44, - 0x41, 0x54, 0x45, 0x5f, 0x41, 0x47, 0x45, 0x4e, 0x54, 0x5f, 0x47, 0x52, 0x4f, 0x55, 0x50, 0x5f, - 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, - 0x44, 0x10, 0x00, 0x12, 0x2b, 0x0a, 0x27, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x55, 0x50, 0x44, 0x41, + 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x44, 0x0a, 0x10, 0x6c, + 0x61, 0x73, 0x74, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, + 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6c, + 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x2a, + 0xf7, 0x01, 0x0a, 0x19, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x67, + 0x65, 0x6e, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x2d, 0x0a, + 0x29, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x41, 0x47, 0x45, + 0x4e, 0x54, 0x5f, 0x47, 0x52, 0x4f, 0x55, 0x50, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x2b, 0x0a, 0x27, + 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x41, 0x47, 0x45, 0x4e, + 0x54, 0x5f, 0x47, 0x52, 0x4f, 0x55, 0x50, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, + 0x53, 0x54, 0x41, 0x52, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x28, 0x0a, 0x24, 0x41, 0x55, 0x54, + 0x4f, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x41, 0x47, 0x45, 0x4e, 0x54, 0x5f, 0x47, + 0x52, 0x4f, 0x55, 0x50, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, + 0x45, 0x10, 0x02, 0x12, 0x26, 0x0a, 0x22, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x41, 0x47, 0x45, 0x4e, 0x54, 0x5f, 0x47, 0x52, 0x4f, 0x55, 0x50, 0x5f, 0x53, - 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x54, 0x41, 0x52, 0x54, 0x45, 0x44, 0x10, 0x01, - 0x12, 0x28, 0x0a, 0x24, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, - 0x41, 0x47, 0x45, 0x4e, 0x54, 0x5f, 0x47, 0x52, 0x4f, 0x55, 0x50, 0x5f, 0x53, 0x54, 0x41, 0x54, - 0x45, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x10, 0x02, 0x12, 0x26, 0x0a, 0x22, 0x41, 0x55, - 0x54, 0x4f, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x41, 0x47, 0x45, 0x4e, 0x54, 0x5f, - 0x47, 0x52, 0x4f, 0x55, 0x50, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x44, 0x4f, 0x4e, 0x45, - 0x10, 0x03, 0x12, 0x2c, 0x0a, 0x28, 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, - 0x45, 0x5f, 0x41, 0x47, 0x45, 0x4e, 0x54, 0x5f, 0x47, 0x52, 0x4f, 0x55, 0x50, 0x5f, 0x53, 0x54, - 0x41, 0x54, 0x45, 0x5f, 0x52, 0x4f, 0x4c, 0x4c, 0x45, 0x44, 0x42, 0x41, 0x43, 0x4b, 0x10, 0x04, - 0x42, 0x56, 0x5a, 0x54, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, - 0x72, 0x61, 0x76, 0x69, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x6c, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, - 0x61, 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x61, 0x75, - 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x54, 0x41, 0x54, 0x45, 0x5f, 0x44, 0x4f, 0x4e, 0x45, 0x10, 0x03, 0x12, 0x2c, 0x0a, 0x28, 0x41, + 0x55, 0x54, 0x4f, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x41, 0x47, 0x45, 0x4e, 0x54, + 0x5f, 0x47, 0x52, 0x4f, 0x55, 0x50, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x52, 0x4f, 0x4c, + 0x4c, 0x45, 0x44, 0x42, 0x41, 0x43, 0x4b, 0x10, 0x04, 0x42, 0x56, 0x5a, 0x54, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x76, 0x69, 0x74, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x2f, + 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x61, 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x61, 0x75, 0x74, 0x6f, 0x75, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1269,25 +1283,26 @@ var file_teleport_autoupdate_v1_autoupdate_proto_depIdxs = []int32{ 15, // 0: teleport.autoupdate.v1.AutoUpdateConfig.metadata:type_name -> teleport.header.v1.Metadata 2, // 1: teleport.autoupdate.v1.AutoUpdateConfig.spec:type_name -> teleport.autoupdate.v1.AutoUpdateConfigSpec 3, // 2: teleport.autoupdate.v1.AutoUpdateConfigSpec.tools:type_name -> teleport.autoupdate.v1.AutoUpdateConfigSpecTools - 16, // 3: teleport.autoupdate.v1.AutoUpdateConfigSpecAgents.maintenance_window_duration:type_name -> google.protobuf.Duration - 5, // 4: teleport.autoupdate.v1.AutoUpdateConfigSpecAgents.agent_schedules:type_name -> teleport.autoupdate.v1.AgentAutoUpdateSchedules - 6, // 5: teleport.autoupdate.v1.AgentAutoUpdateSchedules.regular:type_name -> teleport.autoupdate.v1.AgentAutoUpdateGroup - 15, // 6: teleport.autoupdate.v1.AutoUpdateVersion.metadata:type_name -> teleport.header.v1.Metadata - 8, // 7: teleport.autoupdate.v1.AutoUpdateVersion.spec:type_name -> teleport.autoupdate.v1.AutoUpdateVersionSpec - 9, // 8: teleport.autoupdate.v1.AutoUpdateVersionSpec.tools:type_name -> teleport.autoupdate.v1.AutoUpdateVersionSpecTools - 10, // 9: teleport.autoupdate.v1.AutoUpdateVersionSpec.agents:type_name -> teleport.autoupdate.v1.AutoUpdateVersionSpecAgents - 15, // 10: teleport.autoupdate.v1.AutoUpdateAgentRollout.metadata:type_name -> teleport.header.v1.Metadata - 12, // 11: teleport.autoupdate.v1.AutoUpdateAgentRollout.spec:type_name -> teleport.autoupdate.v1.AutoUpdateAgentRolloutSpec - 13, // 12: teleport.autoupdate.v1.AutoUpdateAgentRollout.status:type_name -> teleport.autoupdate.v1.AutoUpdateAgentRolloutStatus - 14, // 13: teleport.autoupdate.v1.AutoUpdateAgentRolloutStatus.groups:type_name -> teleport.autoupdate.v1.AutoUpdateAgentRolloutStatusGroup - 17, // 14: teleport.autoupdate.v1.AutoUpdateAgentRolloutStatusGroup.start_time:type_name -> google.protobuf.Timestamp - 0, // 15: teleport.autoupdate.v1.AutoUpdateAgentRolloutStatusGroup.state:type_name -> teleport.autoupdate.v1.AutoUpdateAgentGroupState - 17, // 16: teleport.autoupdate.v1.AutoUpdateAgentRolloutStatusGroup.last_update_time:type_name -> google.protobuf.Timestamp - 17, // [17:17] is the sub-list for method output_type - 17, // [17:17] is the sub-list for method input_type - 17, // [17:17] is the sub-list for extension type_name - 17, // [17:17] is the sub-list for extension extendee - 0, // [0:17] is the sub-list for field type_name + 4, // 3: teleport.autoupdate.v1.AutoUpdateConfigSpec.agents:type_name -> teleport.autoupdate.v1.AutoUpdateConfigSpecAgents + 16, // 4: teleport.autoupdate.v1.AutoUpdateConfigSpecAgents.maintenance_window_duration:type_name -> google.protobuf.Duration + 5, // 5: teleport.autoupdate.v1.AutoUpdateConfigSpecAgents.schedules:type_name -> teleport.autoupdate.v1.AgentAutoUpdateSchedules + 6, // 6: teleport.autoupdate.v1.AgentAutoUpdateSchedules.regular:type_name -> teleport.autoupdate.v1.AgentAutoUpdateGroup + 15, // 7: teleport.autoupdate.v1.AutoUpdateVersion.metadata:type_name -> teleport.header.v1.Metadata + 8, // 8: teleport.autoupdate.v1.AutoUpdateVersion.spec:type_name -> teleport.autoupdate.v1.AutoUpdateVersionSpec + 9, // 9: teleport.autoupdate.v1.AutoUpdateVersionSpec.tools:type_name -> teleport.autoupdate.v1.AutoUpdateVersionSpecTools + 10, // 10: teleport.autoupdate.v1.AutoUpdateVersionSpec.agents:type_name -> teleport.autoupdate.v1.AutoUpdateVersionSpecAgents + 15, // 11: teleport.autoupdate.v1.AutoUpdateAgentRollout.metadata:type_name -> teleport.header.v1.Metadata + 12, // 12: teleport.autoupdate.v1.AutoUpdateAgentRollout.spec:type_name -> teleport.autoupdate.v1.AutoUpdateAgentRolloutSpec + 13, // 13: teleport.autoupdate.v1.AutoUpdateAgentRollout.status:type_name -> teleport.autoupdate.v1.AutoUpdateAgentRolloutStatus + 14, // 14: teleport.autoupdate.v1.AutoUpdateAgentRolloutStatus.groups:type_name -> teleport.autoupdate.v1.AutoUpdateAgentRolloutStatusGroup + 17, // 15: teleport.autoupdate.v1.AutoUpdateAgentRolloutStatusGroup.start_time:type_name -> google.protobuf.Timestamp + 0, // 16: teleport.autoupdate.v1.AutoUpdateAgentRolloutStatusGroup.state:type_name -> teleport.autoupdate.v1.AutoUpdateAgentGroupState + 17, // 17: teleport.autoupdate.v1.AutoUpdateAgentRolloutStatusGroup.last_update_time:type_name -> google.protobuf.Timestamp + 18, // [18:18] is the sub-list for method output_type + 18, // [18:18] is the sub-list for method input_type + 18, // [18:18] is the sub-list for extension type_name + 18, // [18:18] is the sub-list for extension extendee + 0, // [0:18] is the sub-list for field type_name } func init() { file_teleport_autoupdate_v1_autoupdate_proto_init() } diff --git a/api/proto/teleport/autoupdate/v1/autoupdate.proto b/api/proto/teleport/autoupdate/v1/autoupdate.proto index 8bd11fd8cdee..5c7527d0177c 100644 --- a/api/proto/teleport/autoupdate/v1/autoupdate.proto +++ b/api/proto/teleport/autoupdate/v1/autoupdate.proto @@ -38,6 +38,7 @@ message AutoUpdateConfigSpec { reserved 1; reserved "tools_autoupdate"; // ToolsAutoupdate is replaced by tools.mode. AutoUpdateConfigSpecTools tools = 2; + AutoUpdateConfigSpecAgents agents = 3; } // AutoUpdateConfigSpecTools encodes the parameters for client tools auto updates. @@ -48,6 +49,8 @@ message AutoUpdateConfigSpecTools { // AutoUpdateConfigSpecAgents encodes the parameters of automatic agent updates. message AutoUpdateConfigSpecAgents { + reserved 5; + reserved "agent_schedules"; // mode specifies whether agent autoupdates are enabled, disabled, or paused. string mode = 1; // strategy to use for updating the agents. @@ -56,8 +59,8 @@ message AutoUpdateConfigSpecAgents { // Once the window is over, the group transitions to the done state. Existing agents won't be updated until the next // maintenance window. google.protobuf.Duration maintenance_window_duration = 3; - // agent_schedules specifies schedules for updates of grouped agents. - AgentAutoUpdateSchedules agent_schedules = 5; + // schedules specifies schedules for updates of grouped agents. + AgentAutoUpdateSchedules schedules = 6; } // AgentAutoUpdateSchedules specifies update scheduled for grouped agents. diff --git a/api/proto/teleport/legacy/client/proto/joinservice.proto b/api/proto/teleport/legacy/client/proto/joinservice.proto index 68b35f06df33..2f173401fdd2 100644 --- a/api/proto/teleport/legacy/client/proto/joinservice.proto +++ b/api/proto/teleport/legacy/client/proto/joinservice.proto @@ -21,7 +21,8 @@ import "teleport/legacy/types/types.proto"; option go_package = "github.com/gravitational/teleport/api/client/proto"; -// TODO(nklaassen): Document me. +// RegisterUsingIAMMethodRequest is a request for registration via the IAM join +// method. message RegisterUsingIAMMethodRequest { // RegisterUsingTokenRequest holds registration parameters common to all // join methods. diff --git a/api/proto/teleport/legacy/types/types.proto b/api/proto/teleport/legacy/types/types.proto index 3014206024d3..cf5042e3b21c 100644 --- a/api/proto/teleport/legacy/types/types.proto +++ b/api/proto/teleport/legacy/types/types.proto @@ -2095,7 +2095,8 @@ message AuthPreferenceSpecV2 { HardwareKey HardwareKey = 19 [(gogoproto.jsontag) = "hardware_key,omitempty"]; // SignatureAlgorithmSuite is the configured signature algorithm suite for the cluster. - // The current default value is "legacy". This field is not yet fully supported. + // If unspecified, the current default value is "legacy". + // 1 is "legacy", 2 is "balanced-v1", 3 is "fips-v1", 4 is "hsm-v1". SignatureAlgorithmSuite signature_algorithm_suite = 20; // SecondFactors is a list of supported second factor types. @@ -6518,6 +6519,14 @@ message PluginAWSICSettings { // Provisioning holds settings for provisioing users and groups into AWS AWSICProvisioningSpec provisioning_spec = 4; + + // AccessListDefaultOwners is a list of default owners for Access List created for + // user groups imported from AWS Idenity Center. + repeated string access_list_default_owners = 5; + + // SAMLIdPServiceProviderName is the name of a SAML service provider created + // for the Identity Center. + string saml_idp_service_provider_name = 6; } // AWSICProvisioningSpec holds provisioning-specific Identity Center settings diff --git a/api/types/constants.go b/api/types/constants.go index 85dbe7c14038..87c0335586bf 100644 --- a/api/types/constants.go +++ b/api/types/constants.go @@ -423,6 +423,9 @@ const ( // KindWindowsDesktop is a Windows desktop host. KindWindowsDesktop = "windows_desktop" + // KindDynamicWindowsDesktop is a dynamic Windows desktop host. + KindDynamicWindowsDesktop = "dynamic_windows_desktop" + // KindRecoveryCodes is a resource that holds users recovery codes. KindRecoveryCodes = "recovery_codes" @@ -564,6 +567,20 @@ const ( // KindStaticHostUser is a host user to be created on matching SSH nodes. KindStaticHostUser = "static_host_user" + // KindIdentityCenterAccount describes an Identity-Center managed AWS Account + KindIdentityCenterAccount = "aws_ic_account" + + // KindIdentityCenterPermissionSet describes an AWS Identity Center Permission Set + KindIdentityCenterPermissionSet = "aws_ic_permission_set" + + // KindIdentityCenterPermissionSet describes an AWS Principal Assignment, representing + // a collection Account Assignments assigned to a Teleport User or AccessList + KindIdentityCenterPrincipalAssignment = "aws_ic_principal_assignment" + + // KindIdentityCenterAccountAssignment describes an AWS Account and Permission Set + // pair that can be requested by a Teleport User. + KindIdentityCenterAccountAssignment = "aws_ic_account_assignment" + // MetaNameAccessGraphSettings is the exact name of the singleton resource holding // access graph settings. MetaNameAccessGraphSettings = "access-graph-settings" diff --git a/api/types/derived.gen.go b/api/types/derived.gen.go index 6f2cd5467ee9..0f96c2afaa43 100644 --- a/api/types/derived.gen.go +++ b/api/types/derived.gen.go @@ -77,12 +77,20 @@ func deriveTeleportEqualDatabaseV3(this, that *DatabaseV3) bool { deriveTeleportEqual_11(&this.Spec, &that.Spec) } +// deriveTeleportEqualDynamicWindowsDesktopV1 returns whether this and that are equal. +func deriveTeleportEqualDynamicWindowsDesktopV1(this, that *DynamicWindowsDesktopV1) bool { + return (this == nil && that == nil) || + this != nil && that != nil && + deriveTeleportEqualResourceHeader(&this.ResourceHeader, &that.ResourceHeader) && + deriveTeleportEqual_12(&this.Spec, &that.Spec) +} + // deriveTeleportEqualWindowsDesktopV3 returns whether this and that are equal. func deriveTeleportEqualWindowsDesktopV3(this, that *WindowsDesktopV3) bool { return (this == nil && that == nil) || this != nil && that != nil && deriveTeleportEqualResourceHeader(&this.ResourceHeader, &that.ResourceHeader) && - deriveTeleportEqual_12(&this.Spec, &that.Spec) + deriveTeleportEqual_13(&this.Spec, &that.Spec) } // deriveTeleportEqualKubeAzure returns whether this and that are equal. @@ -121,7 +129,7 @@ func deriveTeleportEqualKubernetesClusterV3(this, that *KubernetesClusterV3) boo this.SubKind == that.SubKind && this.Version == that.Version && deriveTeleportEqualMetadata(&this.Metadata, &that.Metadata) && - deriveTeleportEqual_13(&this.Spec, &that.Spec) + deriveTeleportEqual_14(&this.Spec, &that.Spec) } // deriveTeleportEqualKubernetesServerV3 returns whether this and that are equal. @@ -132,7 +140,7 @@ func deriveTeleportEqualKubernetesServerV3(this, that *KubernetesServerV3) bool this.SubKind == that.SubKind && this.Version == that.Version && deriveTeleportEqualMetadata(&this.Metadata, &that.Metadata) && - deriveTeleportEqual_14(&this.Spec, &that.Spec) + deriveTeleportEqual_15(&this.Spec, &that.Spec) } // deriveTeleportEqualOktaAssignmentV1 returns whether this and that are equal. @@ -140,7 +148,7 @@ func deriveTeleportEqualOktaAssignmentV1(this, that *OktaAssignmentV1) bool { return (this == nil && that == nil) || this != nil && that != nil && deriveTeleportEqualResourceHeader(&this.ResourceHeader, &that.ResourceHeader) && - deriveTeleportEqual_15(&this.Spec, &that.Spec) + deriveTeleportEqual_16(&this.Spec, &that.Spec) } // deriveTeleportEqualResourceHeader returns whether this and that are equal. @@ -169,7 +177,7 @@ func deriveTeleportEqualUserGroupV1(this, that *UserGroupV1) bool { return (this == nil && that == nil) || this != nil && that != nil && deriveTeleportEqualResourceHeader(&this.ResourceHeader, &that.ResourceHeader) && - deriveTeleportEqual_16(&this.Spec, &that.Spec) + deriveTeleportEqual_17(&this.Spec, &that.Spec) } // deriveTeleportEqual returns whether this and that are equal. @@ -178,15 +186,15 @@ func deriveTeleportEqual(this, that *AppSpecV3) bool { this != nil && that != nil && this.URI == that.URI && this.PublicAddr == that.PublicAddr && - deriveTeleportEqual_17(this.DynamicLabels, that.DynamicLabels) && + deriveTeleportEqual_18(this.DynamicLabels, that.DynamicLabels) && this.InsecureSkipVerify == that.InsecureSkipVerify && - deriveTeleportEqual_18(this.Rewrite, that.Rewrite) && - deriveTeleportEqual_19(this.AWS, that.AWS) && + deriveTeleportEqual_19(this.Rewrite, that.Rewrite) && + deriveTeleportEqual_20(this.AWS, that.AWS) && this.Cloud == that.Cloud && - deriveTeleportEqual_20(this.UserGroups, that.UserGroups) && + deriveTeleportEqual_21(this.UserGroups, that.UserGroups) && this.Integration == that.Integration && - deriveTeleportEqual_20(this.RequiredAppNames, that.RequiredAppNames) && - deriveTeleportEqual_21(this.CORS, that.CORS) + deriveTeleportEqual_21(this.RequiredAppNames, that.RequiredAppNames) && + deriveTeleportEqual_22(this.CORS, that.CORS) } // deriveTeleportEqual_ returns whether this and that are equal. @@ -204,9 +212,9 @@ func deriveTeleportEqual_1(this, that *RDS) bool { this.ClusterID == that.ClusterID && this.ResourceID == that.ResourceID && this.IAMAuth == that.IAMAuth && - deriveTeleportEqual_20(this.Subnets, that.Subnets) && + deriveTeleportEqual_21(this.Subnets, that.Subnets) && this.VPCID == that.VPCID && - deriveTeleportEqual_20(this.SecurityGroups, that.SecurityGroups) + deriveTeleportEqual_21(this.SecurityGroups, that.SecurityGroups) } // deriveTeleportEqual_2 returns whether this and that are equal. @@ -214,7 +222,7 @@ func deriveTeleportEqual_2(this, that *ElastiCache) bool { return (this == nil && that == nil) || this != nil && that != nil && this.ReplicationGroupID == that.ReplicationGroupID && - deriveTeleportEqual_20(this.UserGroupIDs, that.UserGroupIDs) && + deriveTeleportEqual_21(this.UserGroupIDs, that.UserGroupIDs) && this.TransitEncryptionEnabled == that.TransitEncryptionEnabled && this.EndpointType == that.EndpointType } @@ -307,73 +315,83 @@ func deriveTeleportEqual_11(this, that *DatabaseSpecV3) bool { this.Protocol == that.Protocol && this.URI == that.URI && this.CACert == that.CACert && - deriveTeleportEqual_17(this.DynamicLabels, that.DynamicLabels) && + deriveTeleportEqual_18(this.DynamicLabels, that.DynamicLabels) && deriveTeleportEqualAWS(&this.AWS, &that.AWS) && deriveTeleportEqualGCPCloudSQL(&this.GCP, &that.GCP) && deriveTeleportEqualAzure(&this.Azure, &that.Azure) && - deriveTeleportEqual_22(&this.TLS, &that.TLS) && - deriveTeleportEqual_23(&this.AD, &that.AD) && - deriveTeleportEqual_24(&this.MySQL, &that.MySQL) && - deriveTeleportEqual_25(this.AdminUser, that.AdminUser) && - deriveTeleportEqual_26(&this.MongoAtlas, &that.MongoAtlas) && - deriveTeleportEqual_27(&this.Oracle, &that.Oracle) + deriveTeleportEqual_23(&this.TLS, &that.TLS) && + deriveTeleportEqual_24(&this.AD, &that.AD) && + deriveTeleportEqual_25(&this.MySQL, &that.MySQL) && + deriveTeleportEqual_26(this.AdminUser, that.AdminUser) && + deriveTeleportEqual_27(&this.MongoAtlas, &that.MongoAtlas) && + deriveTeleportEqual_28(&this.Oracle, &that.Oracle) } // deriveTeleportEqual_12 returns whether this and that are equal. -func deriveTeleportEqual_12(this, that *WindowsDesktopSpecV3) bool { +func deriveTeleportEqual_12(this, that *DynamicWindowsDesktopSpecV1) bool { return (this == nil && that == nil) || this != nil && that != nil && this.Addr == that.Addr && this.Domain == that.Domain && - this.HostID == that.HostID && this.NonAD == that.NonAD && - deriveTeleportEqual_28(this.ScreenSize, that.ScreenSize) + deriveTeleportEqual_29(this.ScreenSize, that.ScreenSize) } // deriveTeleportEqual_13 returns whether this and that are equal. -func deriveTeleportEqual_13(this, that *KubernetesClusterSpecV3) bool { +func deriveTeleportEqual_13(this, that *WindowsDesktopSpecV3) bool { return (this == nil && that == nil) || this != nil && that != nil && - deriveTeleportEqual_17(this.DynamicLabels, that.DynamicLabels) && + this.Addr == that.Addr && + this.Domain == that.Domain && + this.HostID == that.HostID && + this.NonAD == that.NonAD && + deriveTeleportEqual_29(this.ScreenSize, that.ScreenSize) +} + +// deriveTeleportEqual_14 returns whether this and that are equal. +func deriveTeleportEqual_14(this, that *KubernetesClusterSpecV3) bool { + return (this == nil && that == nil) || + this != nil && that != nil && + deriveTeleportEqual_18(this.DynamicLabels, that.DynamicLabels) && bytes.Equal(this.Kubeconfig, that.Kubeconfig) && deriveTeleportEqualKubeAzure(&this.Azure, &that.Azure) && deriveTeleportEqualKubeAWS(&this.AWS, &that.AWS) && deriveTeleportEqualKubeGCP(&this.GCP, &that.GCP) } -// deriveTeleportEqual_14 returns whether this and that are equal. -func deriveTeleportEqual_14(this, that *KubernetesServerSpecV3) bool { +// deriveTeleportEqual_15 returns whether this and that are equal. +func deriveTeleportEqual_15(this, that *KubernetesServerSpecV3) bool { return (this == nil && that == nil) || this != nil && that != nil && this.Version == that.Version && this.Hostname == that.Hostname && this.HostID == that.HostID && - deriveTeleportEqual_29(&this.Rotation, &that.Rotation) && + deriveTeleportEqual_30(&this.Rotation, &that.Rotation) && deriveTeleportEqualKubernetesClusterV3(this.Cluster, that.Cluster) && - deriveTeleportEqual_20(this.ProxyIDs, that.ProxyIDs) + deriveTeleportEqual_21(this.ProxyIDs, that.ProxyIDs) } -// deriveTeleportEqual_15 returns whether this and that are equal. -func deriveTeleportEqual_15(this, that *OktaAssignmentSpecV1) bool { +// deriveTeleportEqual_16 returns whether this and that are equal. +func deriveTeleportEqual_16(this, that *OktaAssignmentSpecV1) bool { return (this == nil && that == nil) || this != nil && that != nil && this.User == that.User && - deriveTeleportEqual_30(this.Targets, that.Targets) && + deriveTeleportEqual_31(this.Targets, that.Targets) && this.CleanupTime.Equal(that.CleanupTime) && this.Status == that.Status && this.LastTransition.Equal(that.LastTransition) && this.Finalized == that.Finalized } -// deriveTeleportEqual_16 returns whether this and that are equal. -func deriveTeleportEqual_16(this, that *UserGroupSpecV1) bool { +// deriveTeleportEqual_17 returns whether this and that are equal. +func deriveTeleportEqual_17(this, that *UserGroupSpecV1) bool { return (this == nil && that == nil) || this != nil && that != nil && - deriveTeleportEqual_20(this.Applications, that.Applications) + deriveTeleportEqual_21(this.Applications, that.Applications) } -// deriveTeleportEqual_17 returns whether this and that are equal. -func deriveTeleportEqual_17(this, that map[string]CommandLabelV2) bool { +// deriveTeleportEqual_18 returns whether this and that are equal. +func deriveTeleportEqual_18(this, that map[string]CommandLabelV2) bool { if this == nil || that == nil { return this == nil && that == nil } @@ -385,31 +403,31 @@ func deriveTeleportEqual_17(this, that map[string]CommandLabelV2) bool { if !ok { return false } - if !(deriveTeleportEqual_31(&v, &thatv)) { + if !(deriveTeleportEqual_32(&v, &thatv)) { return false } } return true } -// deriveTeleportEqual_18 returns whether this and that are equal. -func deriveTeleportEqual_18(this, that *Rewrite) bool { +// deriveTeleportEqual_19 returns whether this and that are equal. +func deriveTeleportEqual_19(this, that *Rewrite) bool { return (this == nil && that == nil) || this != nil && that != nil && - deriveTeleportEqual_20(this.Redirect, that.Redirect) && - deriveTeleportEqual_32(this.Headers, that.Headers) && + deriveTeleportEqual_21(this.Redirect, that.Redirect) && + deriveTeleportEqual_33(this.Headers, that.Headers) && this.JWTClaims == that.JWTClaims } -// deriveTeleportEqual_19 returns whether this and that are equal. -func deriveTeleportEqual_19(this, that *AppAWS) bool { +// deriveTeleportEqual_20 returns whether this and that are equal. +func deriveTeleportEqual_20(this, that *AppAWS) bool { return (this == nil && that == nil) || this != nil && that != nil && this.ExternalID == that.ExternalID } -// deriveTeleportEqual_20 returns whether this and that are equal. -func deriveTeleportEqual_20(this, that []string) bool { +// deriveTeleportEqual_21 returns whether this and that are equal. +func deriveTeleportEqual_21(this, that []string) bool { if this == nil || that == nil { return this == nil && that == nil } @@ -424,20 +442,20 @@ func deriveTeleportEqual_20(this, that []string) bool { return true } -// deriveTeleportEqual_21 returns whether this and that are equal. -func deriveTeleportEqual_21(this, that *CORSPolicy) bool { +// deriveTeleportEqual_22 returns whether this and that are equal. +func deriveTeleportEqual_22(this, that *CORSPolicy) bool { return (this == nil && that == nil) || this != nil && that != nil && - deriveTeleportEqual_20(this.AllowedOrigins, that.AllowedOrigins) && - deriveTeleportEqual_20(this.AllowedMethods, that.AllowedMethods) && - deriveTeleportEqual_20(this.AllowedHeaders, that.AllowedHeaders) && + deriveTeleportEqual_21(this.AllowedOrigins, that.AllowedOrigins) && + deriveTeleportEqual_21(this.AllowedMethods, that.AllowedMethods) && + deriveTeleportEqual_21(this.AllowedHeaders, that.AllowedHeaders) && this.AllowCredentials == that.AllowCredentials && this.MaxAge == that.MaxAge && - deriveTeleportEqual_20(this.ExposedHeaders, that.ExposedHeaders) + deriveTeleportEqual_21(this.ExposedHeaders, that.ExposedHeaders) } -// deriveTeleportEqual_22 returns whether this and that are equal. -func deriveTeleportEqual_22(this, that *DatabaseTLS) bool { +// deriveTeleportEqual_23 returns whether this and that are equal. +func deriveTeleportEqual_23(this, that *DatabaseTLS) bool { return (this == nil && that == nil) || this != nil && that != nil && this.Mode == that.Mode && @@ -446,8 +464,8 @@ func deriveTeleportEqual_22(this, that *DatabaseTLS) bool { this.TrustSystemCertPool == that.TrustSystemCertPool } -// deriveTeleportEqual_23 returns whether this and that are equal. -func deriveTeleportEqual_23(this, that *AD) bool { +// deriveTeleportEqual_24 returns whether this and that are equal. +func deriveTeleportEqual_24(this, that *AD) bool { return (this == nil && that == nil) || this != nil && that != nil && this.KeytabFile == that.KeytabFile && @@ -458,45 +476,45 @@ func deriveTeleportEqual_23(this, that *AD) bool { this.KDCHostName == that.KDCHostName } -// deriveTeleportEqual_24 returns whether this and that are equal. -func deriveTeleportEqual_24(this, that *MySQLOptions) bool { +// deriveTeleportEqual_25 returns whether this and that are equal. +func deriveTeleportEqual_25(this, that *MySQLOptions) bool { return (this == nil && that == nil) || this != nil && that != nil && this.ServerVersion == that.ServerVersion } -// deriveTeleportEqual_25 returns whether this and that are equal. -func deriveTeleportEqual_25(this, that *DatabaseAdminUser) bool { +// deriveTeleportEqual_26 returns whether this and that are equal. +func deriveTeleportEqual_26(this, that *DatabaseAdminUser) bool { return (this == nil && that == nil) || this != nil && that != nil && this.Name == that.Name && this.DefaultDatabase == that.DefaultDatabase } -// deriveTeleportEqual_26 returns whether this and that are equal. -func deriveTeleportEqual_26(this, that *MongoAtlas) bool { +// deriveTeleportEqual_27 returns whether this and that are equal. +func deriveTeleportEqual_27(this, that *MongoAtlas) bool { return (this == nil && that == nil) || this != nil && that != nil && this.Name == that.Name } -// deriveTeleportEqual_27 returns whether this and that are equal. -func deriveTeleportEqual_27(this, that *OracleOptions) bool { +// deriveTeleportEqual_28 returns whether this and that are equal. +func deriveTeleportEqual_28(this, that *OracleOptions) bool { return (this == nil && that == nil) || this != nil && that != nil && this.AuditUser == that.AuditUser } -// deriveTeleportEqual_28 returns whether this and that are equal. -func deriveTeleportEqual_28(this, that *Resolution) bool { +// deriveTeleportEqual_29 returns whether this and that are equal. +func deriveTeleportEqual_29(this, that *Resolution) bool { return (this == nil && that == nil) || this != nil && that != nil && this.Width == that.Width && this.Height == that.Height } -// deriveTeleportEqual_29 returns whether this and that are equal. -func deriveTeleportEqual_29(this, that *Rotation) bool { +// deriveTeleportEqual_30 returns whether this and that are equal. +func deriveTeleportEqual_30(this, that *Rotation) bool { return (this == nil && that == nil) || this != nil && that != nil && this.State == that.State && @@ -506,11 +524,11 @@ func deriveTeleportEqual_29(this, that *Rotation) bool { this.Started.Equal(that.Started) && this.GracePeriod == that.GracePeriod && this.LastRotated.Equal(that.LastRotated) && - deriveTeleportEqual_33(&this.Schedule, &that.Schedule) + deriveTeleportEqual_34(&this.Schedule, &that.Schedule) } -// deriveTeleportEqual_30 returns whether this and that are equal. -func deriveTeleportEqual_30(this, that []*OktaAssignmentTargetV1) bool { +// deriveTeleportEqual_31 returns whether this and that are equal. +func deriveTeleportEqual_31(this, that []*OktaAssignmentTargetV1) bool { if this == nil || that == nil { return this == nil && that == nil } @@ -518,24 +536,24 @@ func deriveTeleportEqual_30(this, that []*OktaAssignmentTargetV1) bool { return false } for i := 0; i < len(this); i++ { - if !(deriveTeleportEqual_34(this[i], that[i])) { + if !(deriveTeleportEqual_35(this[i], that[i])) { return false } } return true } -// deriveTeleportEqual_31 returns whether this and that are equal. -func deriveTeleportEqual_31(this, that *CommandLabelV2) bool { +// deriveTeleportEqual_32 returns whether this and that are equal. +func deriveTeleportEqual_32(this, that *CommandLabelV2) bool { return (this == nil && that == nil) || this != nil && that != nil && this.Period == that.Period && - deriveTeleportEqual_20(this.Command, that.Command) && + deriveTeleportEqual_21(this.Command, that.Command) && this.Result == that.Result } -// deriveTeleportEqual_32 returns whether this and that are equal. -func deriveTeleportEqual_32(this, that []*Header) bool { +// deriveTeleportEqual_33 returns whether this and that are equal. +func deriveTeleportEqual_33(this, that []*Header) bool { if this == nil || that == nil { return this == nil && that == nil } @@ -543,15 +561,15 @@ func deriveTeleportEqual_32(this, that []*Header) bool { return false } for i := 0; i < len(this); i++ { - if !(deriveTeleportEqual_35(this[i], that[i])) { + if !(deriveTeleportEqual_36(this[i], that[i])) { return false } } return true } -// deriveTeleportEqual_33 returns whether this and that are equal. -func deriveTeleportEqual_33(this, that *RotationSchedule) bool { +// deriveTeleportEqual_34 returns whether this and that are equal. +func deriveTeleportEqual_34(this, that *RotationSchedule) bool { return (this == nil && that == nil) || this != nil && that != nil && this.UpdateClients.Equal(that.UpdateClients) && @@ -559,16 +577,16 @@ func deriveTeleportEqual_33(this, that *RotationSchedule) bool { this.Standby.Equal(that.Standby) } -// deriveTeleportEqual_34 returns whether this and that are equal. -func deriveTeleportEqual_34(this, that *OktaAssignmentTargetV1) bool { +// deriveTeleportEqual_35 returns whether this and that are equal. +func deriveTeleportEqual_35(this, that *OktaAssignmentTargetV1) bool { return (this == nil && that == nil) || this != nil && that != nil && this.Type == that.Type && this.Id == that.Id } -// deriveTeleportEqual_35 returns whether this and that are equal. -func deriveTeleportEqual_35(this, that *Header) bool { +// deriveTeleportEqual_36 returns whether this and that are equal. +func deriveTeleportEqual_36(this, that *Header) bool { return (this == nil && that == nil) || this != nil && that != nil && this.Name == that.Name && diff --git a/api/types/desktop.go b/api/types/desktop.go index 23a8140a4ded..a6455484e2da 100644 --- a/api/types/desktop.go +++ b/api/types/desktop.go @@ -127,6 +127,174 @@ func (s *WindowsDesktopServiceV3) MatchSearch(values []string) bool { return MatchSearch(fieldVals, values, nil) } +// DynamicWindowsDesktop represents a Windows desktop host that is automatically discovered by Windows Desktop Service. +type DynamicWindowsDesktop interface { + // ResourceWithLabels provides common resource methods. + ResourceWithLabels + // GetAddr returns the network address of this host. + GetAddr() string + // GetDomain returns the ActiveDirectory domain of this host. + GetDomain() string + // NonAD checks whether this is a standalone host that + // is not joined to an Active Directory domain. + NonAD() bool + // GetScreenSize returns the desired size of the screen to use for sessions + // to this host. Returns (0, 0) if no screen size is set, which means to + // use the size passed by the client over TDP. + GetScreenSize() (width, height uint32) + // Copy returns a copy of this dynamic Windows desktop + Copy() *DynamicWindowsDesktopV1 +} + +var _ DynamicWindowsDesktop = &DynamicWindowsDesktopV1{} + +// NewDynamicWindowsDesktopV1 creates a new DynamicWindowsDesktopV1 resource. +func NewDynamicWindowsDesktopV1(name string, labels map[string]string, spec DynamicWindowsDesktopSpecV1) (*DynamicWindowsDesktopV1, error) { + d := &DynamicWindowsDesktopV1{ + ResourceHeader: ResourceHeader{ + Metadata: Metadata{ + Name: name, + Labels: labels, + }, + }, + Spec: spec, + } + if err := d.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + return d, nil +} + +func (d *DynamicWindowsDesktopV1) setStaticFields() { + d.Kind = KindDynamicWindowsDesktop + d.Version = V1 +} + +// CheckAndSetDefaults checks and sets default values for any missing fields. +func (d *DynamicWindowsDesktopV1) CheckAndSetDefaults() error { + if d.Spec.Addr == "" { + return trace.BadParameter("DynamicWindowsDesktopV1.Spec missing Addr field") + } + + if err := checkNameAndScreenSize(d.GetName(), d.Spec.ScreenSize); err != nil { + return trace.Wrap(err) + } + + d.setStaticFields() + if err := d.ResourceHeader.CheckAndSetDefaults(); err != nil { + return trace.Wrap(err) + } + + return nil +} + +func (d *DynamicWindowsDesktopV1) GetScreenSize() (width, height uint32) { + if d.Spec.ScreenSize == nil { + return 0, 0 + } + return d.Spec.ScreenSize.Width, d.Spec.ScreenSize.Height +} + +// NonAD checks whether host is part of Active Directory +func (d *DynamicWindowsDesktopV1) NonAD() bool { + return d.Spec.NonAD +} + +// GetAddr returns the network address of this host. +func (d *DynamicWindowsDesktopV1) GetAddr() string { + return d.Spec.Addr +} + +// GetDomain returns the Active Directory domain of this host. +func (d *DynamicWindowsDesktopV1) GetDomain() string { + return d.Spec.Domain +} + +// MatchSearch goes through select field values and tries to +// match against the list of search values. +func (d *DynamicWindowsDesktopV1) MatchSearch(values []string) bool { + fieldVals := append(utils.MapToStrings(d.GetAllLabels()), d.GetName(), d.GetAddr()) + return MatchSearch(fieldVals, values, nil) +} + +// Copy returns a deep copy of this dynamic Windows desktop object. +func (d *DynamicWindowsDesktopV1) Copy() *DynamicWindowsDesktopV1 { + return utils.CloneProtoMsg(d) +} + +// IsEqual determines if two dynamic Windows desktop resources are equivalent to one another. +func (d *DynamicWindowsDesktopV1) IsEqual(i DynamicWindowsDesktop) bool { + if other, ok := i.(*DynamicWindowsDesktopV1); ok { + return deriveTeleportEqualDynamicWindowsDesktopV1(d, other) + } + return false +} + +// DynamicWindowsDesktops represents a list of Windows desktops. +type DynamicWindowsDesktops []DynamicWindowsDesktop + +// Len returns the slice length. +func (s DynamicWindowsDesktops) Len() int { return len(s) } + +// Less compares desktops by name and host ID. +func (s DynamicWindowsDesktops) Less(i, j int) bool { + return s[i].GetName() < s[j].GetName() +} + +// Swap swaps two windows desktops. +func (s DynamicWindowsDesktops) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// SortByCustom custom sorts by given sort criteria. +func (s DynamicWindowsDesktops) SortByCustom(sortBy SortBy) error { + if sortBy.Field == "" { + return nil + } + + isDesc := sortBy.IsDesc + switch sortBy.Field { + case ResourceMetadataName: + sort.SliceStable(s, func(i, j int) bool { + return stringCompare(s[i].GetName(), s[j].GetName(), isDesc) + }) + case ResourceSpecAddr: + sort.SliceStable(s, func(i, j int) bool { + return stringCompare(s[i].GetAddr(), s[j].GetAddr(), isDesc) + }) + default: + return trace.NotImplemented("sorting by field %q for resource %q is not supported", sortBy.Field, KindDynamicWindowsDesktop) + } + + return nil +} + +// AsResources returns dynamic windows desktops as type resources with labels. +func (s DynamicWindowsDesktops) AsResources() []ResourceWithLabels { + resources := make([]ResourceWithLabels, 0, len(s)) + for _, server := range s { + resources = append(resources, ResourceWithLabels(server)) + } + return resources +} + +// GetFieldVals returns list of select field values. +func (s DynamicWindowsDesktops) GetFieldVals(field string) ([]string, error) { + vals := make([]string, 0, len(s)) + switch field { + case ResourceMetadataName: + for _, server := range s { + vals = append(vals, server.GetName()) + } + case ResourceSpecAddr: + for _, server := range s { + vals = append(vals, server.GetAddr()) + } + default: + return nil, trace.NotImplemented("getting field %q for resource %q is not supported", field, KindDynamicWindowsDesktop) + } + + return vals, nil +} + // WindowsDesktop represents a Windows desktop host. type WindowsDesktop interface { // ResourceWithLabels provides common resource methods. @@ -180,11 +348,8 @@ func (d *WindowsDesktopV3) CheckAndSetDefaults() error { return trace.BadParameter("WindowsDesktopV3.Spec missing Addr field") } - // We use SNI to identify the desktop to route a connection to, - // and '.' will add an extra subdomain, preventing Teleport from - // correctly establishing TLS connections. - if name := d.GetName(); strings.Contains(name, ".") { - return trace.BadParameter("invalid name %q: desktop names cannot contain periods", name) + if err := checkNameAndScreenSize(d.GetName(), d.Spec.ScreenSize); err != nil { + return trace.Wrap(err) } d.setStaticFields() @@ -192,13 +357,6 @@ func (d *WindowsDesktopV3) CheckAndSetDefaults() error { return trace.Wrap(err) } - if d.Spec.ScreenSize != nil { - if d.Spec.ScreenSize.Width > MaxRDPScreenWidth || d.Spec.ScreenSize.Height > MaxRDPScreenHeight { - return trace.BadParameter("invalid screen size %dx%d (maximum %dx%d)", - d.Spec.ScreenSize.Width, d.Spec.ScreenSize.Height, MaxRDPScreenWidth, MaxRDPScreenHeight) - } - } - return nil } @@ -351,6 +509,12 @@ type ListWindowsDesktopsRequest struct { SearchKeywords []string } +// ListDynamicWindowsDesktopsResponse is a response type to ListDynamicWindowsDesktops. +type ListDynamicWindowsDesktopsResponse struct { + Desktops []DynamicWindowsDesktop + NextKey string +} + // ListWindowsDesktopServicesResponse is a response type to ListWindowsDesktopServices. type ListWindowsDesktopServicesResponse struct { DesktopServices []WindowsDesktopService @@ -364,3 +528,18 @@ type ListWindowsDesktopServicesRequest struct { Labels map[string]string SearchKeywords []string } + +func checkNameAndScreenSize(name string, screenSize *Resolution) error { + // We use SNI to identify the desktop to route a connection to, + // and '.' will add an extra subdomain, preventing Teleport from + // correctly establishing TLS connections. + if strings.Contains(name, ".") { + return trace.BadParameter("invalid name %q: desktop names cannot contain periods", name) + } + + if screenSize != nil && (screenSize.Width > MaxRDPScreenWidth || screenSize.Height > MaxRDPScreenHeight) { + return trace.BadParameter("screen size %dx%d too big (maximum %dx%d)", + screenSize.Width, screenSize.Height, MaxRDPScreenWidth, MaxRDPScreenHeight) + } + return nil +} diff --git a/api/types/role.go b/api/types/role.go index a36d7242cd86..25902d8e5c08 100644 --- a/api/types/role.go +++ b/api/types/role.go @@ -1855,6 +1855,8 @@ func (r *RoleV6) GetLabelMatchers(rct RoleConditionType, kind string) (LabelMatc return LabelMatchers{cond.DatabaseServiceLabels, cond.DatabaseServiceLabelsExpression}, nil case KindWindowsDesktop: return LabelMatchers{cond.WindowsDesktopLabels, cond.WindowsDesktopLabelsExpression}, nil + case KindDynamicWindowsDesktop: + return LabelMatchers{cond.WindowsDesktopLabels, cond.WindowsDesktopLabelsExpression}, nil case KindWindowsDesktopService: return LabelMatchers{cond.WindowsDesktopLabels, cond.WindowsDesktopLabelsExpression}, nil case KindUserGroup: diff --git a/api/types/types.pb.go b/api/types/types.pb.go index 773dfea65c92..ffcd029a4f18 100644 --- a/api/types/types.pb.go +++ b/api/types/types.pb.go @@ -6175,7 +6175,8 @@ type AuthPreferenceSpecV2 struct { // HardwareKey are the settings for hardware key support. HardwareKey *HardwareKey `protobuf:"bytes,19,opt,name=HardwareKey,proto3" json:"hardware_key,omitempty"` // SignatureAlgorithmSuite is the configured signature algorithm suite for the cluster. - // The current default value is "legacy". This field is not yet fully supported. + // If unspecified, the current default value is "legacy". + // 1 is "legacy", 2 is "balanced-v1", 3 is "fips-v1", 4 is "hsm-v1". SignatureAlgorithmSuite SignatureAlgorithmSuite `protobuf:"varint,20,opt,name=signature_algorithm_suite,json=signatureAlgorithmSuite,proto3,enum=types.SignatureAlgorithmSuite" json:"signature_algorithm_suite,omitempty"` // SecondFactors is a list of supported second factor types. SecondFactors []SecondFactorType `protobuf:"varint,21,rep,packed,name=SecondFactors,proto3,enum=types.SecondFactorType" json:"second_factors,omitempty"` @@ -16689,10 +16690,16 @@ type PluginAWSICSettings struct { // InstanceARN is the arn of the Identity Center instance to manage Arn string `protobuf:"bytes,3,opt,name=arn,proto3" json:"arn,omitempty"` // Provisioning holds settings for provisioing users and groups into AWS - ProvisioningSpec *AWSICProvisioningSpec `protobuf:"bytes,4,opt,name=provisioning_spec,json=provisioningSpec,proto3" json:"provisioning_spec,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + ProvisioningSpec *AWSICProvisioningSpec `protobuf:"bytes,4,opt,name=provisioning_spec,json=provisioningSpec,proto3" json:"provisioning_spec,omitempty"` + // AccessListDefaultOwners is a list of default owners for Access List created for + // user groups imported from AWS Idenity Center. + AccessListDefaultOwners []string `protobuf:"bytes,5,rep,name=access_list_default_owners,json=accessListDefaultOwners,proto3" json:"access_list_default_owners,omitempty"` + // SAMLIdPServiceProviderName is the name of a SAML service provider created + // for the Identity Center. + SamlIdpServiceProviderName string `protobuf:"bytes,6,opt,name=saml_idp_service_provider_name,json=samlIdpServiceProviderName,proto3" json:"saml_idp_service_provider_name,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *PluginAWSICSettings) Reset() { *m = PluginAWSICSettings{} } @@ -20650,7 +20657,7 @@ func init() { func init() { proto.RegisterFile("teleport/legacy/types/types.proto", fileDescriptor_9198ee693835762e) } var fileDescriptor_9198ee693835762e = []byte{ - // 28794 bytes of a gzipped FileDescriptorProto + // 28842 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0xbd, 0x6b, 0x90, 0x1c, 0x49, 0x7a, 0x18, 0xb6, 0xdd, 0x3d, 0x8f, 0x9e, 0x6f, 0x5e, 0x3d, 0x39, 0x03, 0x60, 0x30, 0xfb, 0x68, 0x6c, 0xed, 0x2e, 0x16, 0xbb, 0xb7, 0x0b, 0x1c, 0x06, 0xb7, 0xb8, 0xdb, 0xdb, 0xd7, 0xf5, 0x3c, @@ -22128,329 +22135,332 @@ var fileDescriptor_9198ee693835762e = []byte{ 0x99, 0x18, 0x5e, 0x0b, 0xfd, 0x7b, 0xcb, 0x54, 0x4b, 0x4b, 0x1d, 0x31, 0xba, 0x60, 0xcd, 0x2e, 0xc2, 0x7c, 0x2a, 0xe4, 0x46, 0xe2, 0xc3, 0x6d, 0xcf, 0x89, 0x22, 0x33, 0x40, 0xd3, 0x8b, 0x30, 0xa5, 0x86, 0x21, 0x88, 0x3d, 0xc1, 0xec, 0x49, 0x82, 0x89, 0x59, 0x4e, 0xd5, 0x75, 0x55, 0xa3, - 0x32, 0x65, 0xf2, 0x21, 0x64, 0xd7, 0xff, 0x9f, 0xbd, 0xab, 0xf9, 0x71, 0xe4, 0xb8, 0xee, 0xd3, - 0x24, 0x67, 0x86, 0xf3, 0x38, 0x1f, 0x3d, 0xb5, 0xa3, 0xdd, 0xd1, 0xce, 0x7e, 0x68, 0x5b, 0xbb, - 0xeb, 0x5d, 0xca, 0x92, 0xbd, 0xab, 0xd8, 0xd2, 0x2a, 0x91, 0xe5, 0x1e, 0xb2, 0x39, 0xe4, 0x0e, - 0xbf, 0xd4, 0x4d, 0xce, 0x78, 0x25, 0xdb, 0x1d, 0x8a, 0xec, 0x99, 0x61, 0xc4, 0x69, 0xd2, 0x64, - 0x53, 0xeb, 0x15, 0x02, 0xe4, 0x0b, 0xb0, 0x81, 0x24, 0x8e, 0x13, 0x27, 0x40, 0x8c, 0x20, 0x40, - 0x0e, 0x11, 0x82, 0x1c, 0xf2, 0x17, 0x24, 0xa7, 0xdc, 0x04, 0x18, 0x06, 0x7c, 0xc8, 0x29, 0x01, - 0x84, 0x44, 0x40, 0x72, 0x48, 0x72, 0x0b, 0xe2, 0x83, 0x4f, 0x41, 0xbd, 0xaa, 0xea, 0xae, 0xfe, - 0x20, 0x77, 0x56, 0x2b, 0x25, 0x31, 0xe0, 0xd3, 0x0c, 0xab, 0xde, 0xab, 0xae, 0xef, 0x7a, 0xf5, - 0xea, 0xbd, 0xdf, 0x23, 0x2f, 0x02, 0xf1, 0xe5, 0x64, 0x7f, 0x61, 0xf2, 0x0f, 0x6e, 0x8a, 0x1c, - 0x7f, 0x45, 0x05, 0xa2, 0xee, 0xb9, 0x04, 0x61, 0x9e, 0x0a, 0xdd, 0x7d, 0xd7, 0x73, 0x8e, 0x99, - 0xc8, 0x2e, 0x37, 0x72, 0x43, 0x4a, 0xe7, 0x1a, 0x9a, 0x25, 0x16, 0x83, 0x9b, 0x7f, 0x8b, 0xff, - 0xa2, 0x8b, 0xb5, 0x33, 0x16, 0xca, 0x07, 0xfa, 0x2f, 0xa9, 0xc0, 0x26, 0x06, 0x16, 0x98, 0xf4, - 0x87, 0x18, 0x9f, 0x00, 0x0f, 0xfd, 0x4c, 0xe8, 0x0a, 0x84, 0xb5, 0x68, 0x4a, 0x44, 0xf4, 0xd4, - 0x37, 0xd5, 0x51, 0x24, 0x85, 0xd7, 0xfe, 0x6d, 0x78, 0x26, 0x91, 0x81, 0xee, 0xa7, 0x68, 0xc9, - 0x13, 0x88, 0x02, 0xcb, 0xf4, 0x37, 0x95, 0x05, 0xae, 0xc1, 0xea, 0x3b, 0x4e, 0x67, 0xec, 0x8c, - 0xf9, 0x41, 0xc5, 0x47, 0x84, 0xa5, 0xc9, 0xe7, 0xd4, 0x5f, 0xa5, 0xc4, 0x2d, 0x60, 0x77, 0x38, - 0xf4, 0x26, 0xde, 0xb8, 0x33, 0x0a, 0xe9, 0x1f, 0xc8, 0x29, 0x3c, 0x8b, 0x87, 0xe4, 0x5d, 0xc4, - 0x8d, 0x1f, 0x8e, 0x85, 0xa3, 0x7c, 0x57, 0x18, 0x9f, 0xe6, 0xee, 0x7e, 0x21, 0x7c, 0x8c, 0xeb, - 0x94, 0x5a, 0x97, 0x89, 0xa9, 0xd0, 0x2a, 0x95, 0x5a, 0x5e, 0x30, 0x2f, 0xb0, 0x32, 0x63, 0x54, - 0xa4, 0x9c, 0x50, 0xf1, 0xa8, 0x02, 0x62, 0x37, 0x68, 0x45, 0xb8, 0x54, 0xb9, 0x7d, 0xe4, 0x2b, - 0xb0, 0xd2, 0xef, 0xc9, 0xe1, 0xd1, 0xa2, 0x57, 0xdf, 0x4a, 0x8f, 0x41, 0xb4, 0x06, 0x65, 0x94, - 0x17, 0xcc, 0x6c, 0x9f, 0xa7, 0xee, 0xae, 0x85, 0x34, 0x35, 0xda, 0xae, 0x10, 0x38, 0xe3, 0x6c, - 0x64, 0x1d, 0x52, 0xfe, 0x42, 0x4c, 0xf5, 0x7b, 0x74, 0xb2, 0x4c, 0x24, 0x90, 0x58, 0x93, 0xff, - 0xd2, 0x7e, 0x1d, 0x6e, 0x9d, 0xb5, 0x8f, 0xe8, 0x44, 0x9f, 0xd1, 0xe1, 0x2b, 0xe6, 0x66, 0x27, - 0xd6, 0x6f, 0xd7, 0x40, 0xc6, 0xb8, 0xec, 0x8b, 0x01, 0x17, 0x69, 0xed, 0x71, 0x5f, 0xfb, 0xed, - 0x34, 0xac, 0x87, 0x75, 0x53, 0xe4, 0x05, 0xc8, 0xf8, 0xc5, 0xae, 0xfb, 0x6f, 0x28, 0x32, 0x11, - 0x2d, 0xdc, 0x44, 0x22, 0x2a, 0x04, 0xe0, 0x93, 0xab, 0x7d, 0x2a, 0x3f, 0x73, 0x98, 0xab, 0x98, - 0x28, 0x9e, 0x37, 0xee, 0xc3, 0x3a, 0x5a, 0xcb, 0xe0, 0xe9, 0xe2, 0xf5, 0xb9, 0xc6, 0x73, 0xbe, - 0xd2, 0x3a, 0xfb, 0xe1, 0x47, 0x57, 0x17, 0x50, 0x3f, 0xbd, 0x4a, 0x79, 0xe9, 0x0e, 0x4f, 0x33, - 0x25, 0xd5, 0x43, 0x66, 0xb6, 0xea, 0x81, 0x37, 0x65, 0x86, 0xea, 0x61, 0x71, 0x8e, 0xea, 0x21, - 0xe0, 0x94, 0x55, 0x0f, 0xa8, 0x80, 0x5a, 0x9e, 0xa5, 0x80, 0x0a, 0x78, 0x98, 0x02, 0xea, 0x3a, - 0x6f, 0xee, 0xb8, 0xf3, 0xd0, 0xc6, 0x7e, 0xe0, 0xf1, 0x10, 0xb0, 0x21, 0x66, 0xe7, 0x21, 0x3e, - 0x4e, 0xef, 0xae, 0x80, 0x78, 0xd1, 0xd6, 0xfe, 0x58, 0x89, 0xdc, 0xdb, 0xc5, 0x50, 0xdc, 0x80, - 0xf5, 0xfe, 0x29, 0xbd, 0x50, 0x38, 0x3d, 0x49, 0x32, 0x5c, 0x33, 0xd7, 0x44, 0x2a, 0x93, 0x0e, - 0x3f, 0x07, 0x1b, 0x3e, 0x19, 0x17, 0x90, 0xd0, 0xd3, 0xc5, 0xf4, 0xb9, 0x39, 0x6c, 0xc3, 0x0b, - 0xb0, 0xe9, 0x13, 0xf2, 0xbb, 0x17, 0x13, 0x0e, 0xd7, 0x4c, 0x55, 0x64, 0xf0, 0x70, 0xfb, 0x13, - 0xed, 0x38, 0x2a, 0x27, 0x7c, 0x46, 0xb5, 0xd2, 0xfe, 0x3e, 0x1d, 0xba, 0xd3, 0x88, 0xcf, 0xec, - 0x42, 0x8e, 0x1e, 0x56, 0xbc, 0x93, 0xf8, 0xb6, 0x72, 0x6d, 0x46, 0xf7, 0x73, 0x9b, 0x00, 0xcb, - 0x6a, 0x98, 0x30, 0x99, 0x0c, 0x85, 0x89, 0x80, 0xcd, 0xce, 0x63, 0x26, 0xa7, 0xe2, 0xf4, 0x13, - 0xc5, 0xb1, 0x3d, 0x24, 0x3f, 0xbf, 0x38, 0x21, 0x54, 0xd2, 0xd9, 0x87, 0xe7, 0xb2, 0xff, 0x4b, - 0x7c, 0xa0, 0x0d, 0xa8, 0x02, 0x98, 0x84, 0x0b, 0x4f, 0x27, 0x48, 0x3a, 0xb1, 0xc2, 0xb1, 0x97, - 0xb0, 0x64, 0x75, 0x2a, 0xfe, 0x15, 0xc5, 0x1a, 0xb0, 0x8a, 0x37, 0x0a, 0x51, 0x60, 0x26, 0x41, - 0x85, 0x15, 0x6f, 0x7c, 0xa1, 0x52, 0x33, 0x73, 0x94, 0x4f, 0x14, 0x73, 0x02, 0xcf, 0xca, 0xf7, - 0x80, 0x70, 0x25, 0x17, 0x05, 0x0a, 0xe5, 0xdc, 0x1e, 0x08, 0xae, 0x0b, 0x58, 0xd5, 0xf3, 0x9d, - 0x70, 0x02, 0x27, 0xd3, 0x4e, 0xe0, 0xe2, 0xec, 0x21, 0x99, 0x13, 0xe1, 0x24, 0x10, 0x59, 0x52, - 0xb2, 0xc8, 0x22, 0xdf, 0x0a, 0xd2, 0xa1, 0x5b, 0x81, 0xf6, 0x97, 0x69, 0x78, 0xfe, 0x0c, 0xc3, - 0x35, 0xe7, 0x9b, 0x5f, 0x85, 0x1c, 0x53, 0xbc, 0xb3, 0xed, 0x93, 0x19, 0xad, 0xf8, 0x71, 0x4a, - 0xdf, 0xf5, 0x3a, 0x7c, 0xaf, 0xa3, 0x42, 0x65, 0xb0, 0xdf, 0xc1, 0xc4, 0xff, 0x9f, 0xfc, 0x2a, - 0x6c, 0xb0, 0x0d, 0x8d, 0x99, 0xf5, 0x1c, 0x4d, 0x07, 0x67, 0xd8, 0xd1, 0x76, 0x84, 0x0f, 0x42, - 0x84, 0x15, 0x37, 0x39, 0xdc, 0x31, 0x2c, 0x3f, 0x8d, 0xb4, 0x20, 0x87, 0x64, 0x47, 0x9d, 0xfe, - 0xe0, 0x4c, 0xc6, 0xf0, 0xc2, 0xc3, 0x41, 0x66, 0x63, 0xd6, 0x88, 0x34, 0xa1, 0x84, 0xbf, 0xc9, - 0x4d, 0xd8, 0x70, 0xa7, 0xa7, 0xf4, 0x22, 0xc5, 0xe6, 0x02, 0x7f, 0x3d, 0x5d, 0x34, 0xd7, 0xdc, - 0xe9, 0xa9, 0x3e, 0x1a, 0xe1, 0x90, 0xe2, 0x33, 0xeb, 0x26, 0xa5, 0x63, 0xab, 0x56, 0x50, 0x2e, - 0x21, 0x25, 0x2d, 0x80, 0xad, 0x5b, 0x4e, 0xbb, 0x05, 0xcc, 0xe8, 0x86, 0x47, 0x78, 0x61, 0x3f, - 0xb4, 0x9f, 0xa6, 0x84, 0xb4, 0x3c, 0x7b, 0xde, 0xff, 0x62, 0x88, 0x12, 0x86, 0xe8, 0x16, 0xa8, - 0xb4, 0xeb, 0x83, 0x4d, 0xc5, 0x1f, 0xa3, 0x75, 0x77, 0x7a, 0xea, 0xf7, 0x9d, 0xdc, 0xf1, 0x4b, - 0x72, 0xc7, 0xbf, 0x22, 0xa4, 0xe9, 0xc4, 0xed, 0x61, 0x76, 0x97, 0x6b, 0xff, 0x99, 0x86, 0x9b, - 0x67, 0xdb, 0x04, 0x7e, 0x31, 0x6e, 0x09, 0xe3, 0x16, 0x51, 0x74, 0x2c, 0xc6, 0x14, 0x1d, 0x09, - 0x6b, 0x6f, 0x29, 0x69, 0xed, 0xc5, 0xd4, 0x2a, 0xcb, 0x09, 0x6a, 0x95, 0xc4, 0x05, 0x9a, 0x7d, - 0xcc, 0x02, 0x5d, 0x91, 0xe7, 0xc9, 0xbf, 0xa5, 0xc4, 0xf5, 0x27, 0x2c, 0xda, 0xbf, 0x0d, 0xe7, - 0x84, 0x68, 0xcf, 0x4e, 0x8e, 0x40, 0x5b, 0x96, 0xbb, 0x7b, 0x3b, 0x49, 0xa8, 0x47, 0xb2, 0x04, - 0xc1, 0x7b, 0x93, 0x8b, 0xf3, 0x41, 0xfe, 0xff, 0x1f, 0x41, 0x9e, 0x3c, 0x80, 0xf3, 0x88, 0x8f, - 0xdc, 0x95, 0xf5, 0x7c, 0xf6, 0xd8, 0x39, 0xe2, 0xf3, 0xe1, 0x5a, 0x4c, 0xec, 0xed, 0x77, 0xa5, - 0xea, 0x98, 0xce, 0x51, 0x79, 0xc1, 0xdc, 0x9a, 0x24, 0xa4, 0x47, 0xef, 0x08, 0x7f, 0xa3, 0x80, - 0xf6, 0xf8, 0xfe, 0xc2, 0x6b, 0x6e, 0xb4, 0xc3, 0xe9, 0x35, 0x57, 0xea, 0xbd, 0xe7, 0x61, 0x6d, - 0xec, 0x1c, 0x8d, 0x9d, 0xc9, 0x49, 0xe8, 0x02, 0xb7, 0xca, 0x13, 0x45, 0xc7, 0x08, 0x94, 0xb6, - 0x27, 0x12, 0xb2, 0x05, 0x93, 0x56, 0xf2, 0xaf, 0x7e, 0x89, 0xe3, 0x40, 0x67, 0x93, 0x5c, 0x41, - 0xf6, 0xe3, 0x7e, 0x26, 0x9b, 0x52, 0xd3, 0x26, 0xc7, 0x92, 0x3b, 0xea, 0x0f, 0x1c, 0xed, 0x6f, - 0x15, 0x21, 0x11, 0x24, 0x75, 0x1e, 0x79, 0x5b, 0x32, 0x86, 0x4b, 0xc7, 0xc4, 0x90, 0x24, 0x16, - 0xd9, 0x6e, 0x88, 0xc3, 0x9b, 0x61, 0x42, 0x08, 0xde, 0x0c, 0x53, 0x9e, 0xc2, 0xa2, 0x87, 0x5f, - 0x80, 0xef, 0x89, 0x17, 0x75, 0xba, 0xe7, 0x1d, 0xdc, 0x21, 0xb7, 0x61, 0x99, 0x3d, 0xa2, 0x8b, - 0xea, 0x6e, 0x84, 0xaa, 0x7b, 0x70, 0xc7, 0x14, 0xf9, 0xda, 0x0f, 0x7d, 0x2d, 0x74, 0xac, 0x11, - 0x07, 0x77, 0xc8, 0x2b, 0x67, 0x33, 0x6e, 0xcb, 0x0a, 0xe3, 0x36, 0xdf, 0xb0, 0xed, 0xd5, 0x90, - 0x61, 0xdb, 0xf5, 0xf9, 0xbd, 0xc5, 0xdf, 0x0e, 0x18, 0x9c, 0x57, 0x00, 0xf3, 0xf2, 0x53, 0x05, - 0x2e, 0xcf, 0xe5, 0x20, 0x97, 0x20, 0xab, 0x37, 0x2b, 0xad, 0x60, 0x7c, 0xe9, 0x9a, 0x11, 0x29, - 0x64, 0x0f, 0x56, 0x76, 0x3b, 0x93, 0x7e, 0x97, 0x4e, 0xe3, 0x44, 0x65, 0x5e, 0xac, 0x58, 0x9f, - 0xbc, 0xbc, 0x60, 0x06, 0xbc, 0xc4, 0x86, 0x4d, 0x5c, 0x0b, 0xa1, 0xd0, 0x29, 0xe9, 0x04, 0xb5, - 0x41, 0xac, 0xc0, 0x18, 0x1b, 0xdd, 0x67, 0x62, 0x89, 0xd1, 0x25, 0xf8, 0x9e, 0x90, 0x45, 0x66, - 0x57, 0xf0, 0x09, 0x70, 0x09, 0x6f, 0x41, 0xb6, 0x29, 0x5e, 0xf5, 0x24, 0x6b, 0x50, 0xf1, 0x82, - 0x67, 0xfa, 0xb9, 0xda, 0xef, 0x2b, 0xe2, 0x6e, 0xff, 0xf8, 0x86, 0x48, 0x51, 0x67, 0x7a, 0xf3, - 0xa3, 0xce, 0xf4, 0x3e, 0x61, 0xd4, 0x19, 0xed, 0xaf, 0x39, 0x6a, 0x70, 0xa5, 0xd7, 0x8c, 0x04, - 0x1b, 0x7d, 0x5a, 0xab, 0x5e, 0x23, 0x34, 0x3b, 0x9f, 0x97, 0xa2, 0x96, 0xc5, 0xbf, 0x35, 0xdb, - 0xb8, 0x57, 0x9a, 0xaa, 0x7f, 0x9a, 0x86, 0x4b, 0xf3, 0xd8, 0x13, 0xe3, 0xa2, 0x2a, 0x4f, 0x16, - 0x17, 0xf5, 0x36, 0x64, 0x59, 0x9a, 0x6f, 0xb2, 0x8a, 0x1d, 0xce, 0x59, 0x69, 0x87, 0x8b, 0x6c, - 0xf2, 0x3c, 0x2c, 0xe9, 0x05, 0x2b, 0x08, 0xd5, 0x83, 0xb6, 0x65, 0x9d, 0xee, 0x04, 0xad, 0x96, - 0x78, 0x16, 0xf9, 0x66, 0x3c, 0x3a, 0x15, 0x8f, 0xd1, 0xb3, 0x23, 0x75, 0x48, 0x0c, 0xd0, 0x1b, - 0xeb, 0x1b, 0x00, 0x50, 0x73, 0x4c, 0x57, 0x33, 0x1e, 0xe9, 0x4a, 0x83, 0xa5, 0xe6, 0xd8, 0x99, - 0x38, 0x9e, 0x6c, 0xf7, 0x35, 0xc2, 0x14, 0x93, 0xe7, 0x70, 0xab, 0xac, 0xce, 0x23, 0xe6, 0x84, - 0xbb, 0x24, 0x03, 0x23, 0xa0, 0x19, 0x17, 0x4d, 0x36, 0x25, 0x12, 0xca, 0x50, 0xed, 0x4c, 0xdd, - 0xee, 0x49, 0xdb, 0xac, 0x72, 0x51, 0x83, 0x31, 0x0c, 0x30, 0x95, 0x36, 0x70, 0x62, 0x4a, 0x24, - 0xda, 0x77, 0x15, 0xd8, 0x4a, 0x6a, 0x07, 0xb9, 0x04, 0x19, 0x37, 0x31, 0x10, 0x97, 0xcb, 0x7c, - 0x07, 0x73, 0xf4, 0xaf, 0x7d, 0x34, 0x1c, 0x9f, 0x76, 0x3c, 0xd9, 0x3a, 0x4e, 0x4a, 0x36, 0x81, - 0xfe, 0x28, 0xe1, 0xff, 0xe4, 0xaa, 0xd8, 0xa3, 0xd3, 0xb1, 0xd0, 0x5d, 0xf8, 0x47, 0xd3, 0x01, - 0x2a, 0xbd, 0x66, 0x63, 0xc4, 0x00, 0xa5, 0x5f, 0x86, 0x0c, 0xad, 0x56, 0x64, 0xf6, 0xd2, 0xf9, - 0xa3, 0xd7, 0xaa, 0x9c, 0x88, 0xd5, 0x6a, 0xd2, 0x39, 0x1d, 0x98, 0x48, 0xac, 0x1d, 0xc2, 0x7a, - 0x98, 0x82, 0x18, 0x61, 0x08, 0xc2, 0xdc, 0x5d, 0x95, 0x97, 0xb4, 0x3b, 0x1c, 0x32, 0x0b, 0xed, - 0xdd, 0x67, 0xff, 0xf1, 0xa3, 0xab, 0x40, 0x7f, 0x32, 0x9e, 0x24, 0x88, 0x42, 0xed, 0xfb, 0x29, - 0xd8, 0x0a, 0x9c, 0x42, 0xc5, 0x1a, 0xfa, 0xb9, 0xf5, 0x50, 0xd2, 0x43, 0x1e, 0x34, 0x42, 0xd0, - 0x8a, 0x37, 0x70, 0x8e, 0xe1, 0xfe, 0x1e, 0x6c, 0xcf, 0xa2, 0x27, 0x2f, 0xc0, 0x0a, 0xe2, 0x88, - 0x8c, 0x3a, 0x5d, 0x47, 0xde, 0xfb, 0x5c, 0x91, 0x68, 0x06, 0xf9, 0xda, 0x8f, 0x15, 0xb8, 0xc8, - 0xed, 0x8a, 0x6b, 0x9d, 0xbe, 0xeb, 0x39, 0x6e, 0xc7, 0xed, 0x3a, 0x9f, 0x8e, 0x87, 0xdd, 0x5e, - 0x68, 0x1f, 0xbb, 0x11, 0x36, 0x1f, 0x8f, 0x7d, 0x6d, 0x76, 0x6b, 0xc9, 0x6d, 0xc4, 0xc6, 0xe9, - 0xb2, 0xc9, 0x9b, 0x61, 0x1e, 0xcd, 0x2e, 0x4d, 0x90, 0x3d, 0x9a, 0x91, 0x42, 0xfb, 0x0d, 0xb8, - 0x32, 0xff, 0x03, 0xe4, 0x1b, 0xb0, 0x86, 0xc1, 0x56, 0xda, 0xa3, 0xe3, 0x71, 0xa7, 0xe7, 0x08, - 0x55, 0x98, 0xd0, 0x44, 0xca, 0x79, 0x0c, 0xea, 0x87, 0x7b, 0xd8, 0x1e, 0x63, 0x18, 0x17, 0xce, - 0x14, 0x32, 0xde, 0x97, 0x4b, 0xd3, 0x7e, 0x53, 0x01, 0x12, 0x2f, 0x83, 0x7c, 0x19, 0x56, 0xdb, - 0xad, 0x82, 0xe5, 0x75, 0xc6, 0x5e, 0x79, 0x38, 0x1d, 0x73, 0x9c, 0x1d, 0xe6, 0x70, 0xe9, 0x75, - 0xe9, 0x56, 0x32, 0xf6, 0xec, 0x93, 0xe1, 0x74, 0x6c, 0x86, 0xe8, 0x30, 0x4a, 0x88, 0xe3, 0xbc, - 0xdb, 0xeb, 0x3c, 0x0a, 0x47, 0x09, 0xe1, 0x69, 0xa1, 0x28, 0x21, 0x3c, 0x4d, 0xfb, 0x40, 0x81, - 0x1d, 0x61, 0xfb, 0xd3, 0x4b, 0xa8, 0x4b, 0x01, 0x61, 0x05, 0xc6, 0x02, 0xd8, 0x71, 0x9e, 0x48, - 0xbb, 0x29, 0x90, 0x37, 0xb0, 0x82, 0x28, 0xdb, 0x32, 0x5e, 0xf2, 0x55, 0xc8, 0x58, 0xde, 0x70, - 0x74, 0x06, 0xe8, 0x0d, 0xd5, 0x1f, 0x51, 0x6f, 0x38, 0xc2, 0x22, 0x90, 0x53, 0x73, 0x60, 0x4b, - 0xae, 0x9c, 0xa8, 0x31, 0xa9, 0xc1, 0x32, 0xc7, 0x58, 0x8a, 0x3c, 0xf3, 0xcd, 0x69, 0xd3, 0xee, - 0x86, 0xc0, 0xf7, 0xe0, 0xc0, 0x82, 0xa6, 0x28, 0x43, 0xfb, 0x03, 0x05, 0x72, 0x54, 0xda, 0xc0, - 0x5b, 0xdc, 0xd3, 0x4e, 0xe9, 0xb0, 0xe0, 0x28, 0x5e, 0x89, 0xfd, 0xe2, 0xcf, 0x74, 0x1a, 0x7f, - 0x09, 0x36, 0x22, 0x0c, 0x44, 0x43, 0xcf, 0xee, 0x41, 0xbf, 0xdb, 0x61, 0x41, 0x07, 0xd8, 0x13, - 0x6b, 0x28, 0x4d, 0xfb, 0x5d, 0x05, 0xb6, 0xe8, 0x9d, 0xbf, 0x82, 0xea, 0x5e, 0x73, 0x3a, 0x10, - 0xeb, 0x9d, 0x4a, 0x50, 0xc2, 0x88, 0x8c, 0x79, 0x9d, 0x32, 0x09, 0x8a, 0xa7, 0x99, 0x7e, 0x2e, - 0x29, 0x43, 0x96, 0x9f, 0x2f, 0x13, 0x8e, 0x07, 0x78, 0x45, 0x52, 0x26, 0x04, 0x05, 0x73, 0x22, - 0xda, 0x12, 0xdc, 0xc2, 0x38, 0x8f, 0xe9, 0x73, 0x6b, 0xff, 0xa5, 0xc0, 0x85, 0x19, 0x3c, 0xe4, - 0x75, 0x58, 0x44, 0x8f, 0x18, 0x3e, 0x7a, 0x97, 0x66, 0x7c, 0xc2, 0xeb, 0x9e, 0x1c, 0xdc, 0x61, - 0x07, 0xd1, 0x29, 0xfd, 0x61, 0x32, 0x2e, 0xf2, 0x36, 0xac, 0xe8, 0xbd, 0x1e, 0xbf, 0xce, 0xa4, - 0x42, 0xd7, 0x99, 0x19, 0x5f, 0x7c, 0xc9, 0xa7, 0x67, 0xd7, 0x19, 0x66, 0x9b, 0xdd, 0xeb, 0xd9, - 0xdc, 0xdb, 0x27, 0x28, 0xef, 0xe2, 0xaf, 0xc0, 0x7a, 0x98, 0xf8, 0x89, 0x1c, 0x14, 0x7e, 0xa8, - 0x80, 0x1a, 0xae, 0xc3, 0x67, 0x83, 0x4c, 0x92, 0x34, 0xcc, 0x8f, 0x99, 0x54, 0x7f, 0x94, 0x82, - 0x67, 0x12, 0x7b, 0x98, 0xbc, 0x08, 0x4b, 0xfa, 0x68, 0x54, 0x29, 0xf2, 0x59, 0xc5, 0x25, 0x24, - 0xd4, 0x12, 0x87, 0x6e, 0x7b, 0x8c, 0x88, 0xbc, 0x0c, 0x59, 0x9c, 0x99, 0x94, 0x21, 0x15, 0x20, - 0xf3, 0x31, 0x25, 0x4a, 0x04, 0x99, 0x4f, 0x10, 0x92, 0x12, 0xac, 0x73, 0x90, 0x02, 0xd3, 0x39, - 0x76, 0xbe, 0xed, 0x43, 0x44, 0x23, 0x8a, 0xb5, 0x50, 0x3d, 0xdb, 0x63, 0x96, 0x27, 0xbb, 0xe9, - 0x87, 0xb9, 0x48, 0x15, 0x54, 0x2c, 0x53, 0x2e, 0x89, 0xc1, 0x03, 0x22, 0x6c, 0x04, 0xab, 0xc4, - 0x8c, 0xb2, 0x62, 0x9c, 0xfe, 0x70, 0xe9, 0x93, 0x49, 0xff, 0xd8, 0x3d, 0x75, 0x5c, 0xef, 0xb3, - 0x1b, 0xae, 0xe0, 0x1b, 0x67, 0x1a, 0xae, 0x3f, 0xc9, 0xb0, 0xc5, 0x1c, 0x65, 0xa3, 0x12, 0x8d, - 0x84, 0x08, 0x8b, 0x12, 0x0d, 0xc6, 0xd0, 0x66, 0x6e, 0xf8, 0x45, 0x58, 0x66, 0xf0, 0x08, 0x62, - 0x65, 0x5c, 0x4e, 0xac, 0x02, 0xa3, 0x39, 0xb8, 0xc3, 0xc4, 0x17, 0xe6, 0x9a, 0x33, 0x31, 0x05, - 0x2b, 0x39, 0x80, 0x5c, 0x61, 0xe0, 0x74, 0xdc, 0xe9, 0xa8, 0x75, 0xb6, 0xd7, 0xc3, 0x6d, 0xde, - 0x96, 0xd5, 0x2e, 0x63, 0xc3, 0x57, 0x47, 0xdc, 0xc9, 0xe5, 0x82, 0x48, 0xcb, 0xb7, 0xd6, 0x67, - 0x91, 0xcf, 0xbf, 0x38, 0xa7, 0x7f, 0xa2, 0x89, 0xc8, 0x17, 0x76, 0x45, 0xe1, 0xe6, 0xfc, 0x36, - 0xac, 0x57, 0x3b, 0x13, 0xaf, 0x35, 0xee, 0xb8, 0x13, 0x84, 0x55, 0x3b, 0x03, 0xec, 0xcc, 0x8e, - 0x08, 0xd9, 0x89, 0x3a, 0x46, 0xcf, 0x67, 0x65, 0x1a, 0xcc, 0x70, 0x71, 0x54, 0x5e, 0x2a, 0xf5, - 0xdd, 0xce, 0xa0, 0xff, 0xbe, 0x70, 0x6a, 0x62, 0xf2, 0xd2, 0x91, 0x48, 0x34, 0x83, 0x7c, 0xed, - 0xeb, 0xb1, 0x71, 0x63, 0xb5, 0xcc, 0xc1, 0x32, 0x77, 0x79, 0x65, 0x2e, 0xa0, 0x4d, 0xa3, 0x5e, - 0xac, 0xd4, 0xf7, 0x54, 0x85, 0xac, 0x03, 0x34, 0xcd, 0x46, 0xc1, 0xb0, 0x2c, 0xfa, 0x3b, 0x45, - 0x7f, 0x73, 0xff, 0xd0, 0x52, 0xbb, 0xaa, 0xa6, 0x25, 0x17, 0xd1, 0x8c, 0xf6, 0x23, 0x05, 0xce, - 0x27, 0x0f, 0x25, 0x69, 0x01, 0x3a, 0x09, 0xf3, 0x77, 0xe4, 0x2f, 0xcf, 0x1d, 0xf7, 0xc4, 0xe4, - 0xa8, 0xb3, 0xb1, 0xc7, 0x9c, 0x58, 0x53, 0xe2, 0xb1, 0x28, 0x88, 0x13, 0xdf, 0xef, 0x69, 0x05, - 0xd8, 0x9e, 0x55, 0x46, 0xb8, 0xa9, 0x1b, 0x90, 0xd3, 0x9b, 0xcd, 0x6a, 0xa5, 0xa0, 0xb7, 0x2a, - 0x8d, 0xba, 0xaa, 0x90, 0x15, 0x58, 0xdc, 0x33, 0x1b, 0xed, 0xa6, 0x9a, 0xd2, 0x7e, 0xa0, 0xc0, - 0x5a, 0x25, 0x30, 0xf2, 0x78, 0xda, 0xc5, 0xf7, 0x5a, 0x68, 0xf1, 0x6d, 0xfb, 0xee, 0xf4, 0xfe, - 0x07, 0xce, 0xb4, 0xf2, 0xfe, 0x41, 0x81, 0xcd, 0x18, 0x0f, 0xb1, 0x60, 0x59, 0x3f, 0xb4, 0x1a, - 0x95, 0x62, 0x81, 0xd7, 0xec, 0x6a, 0x60, 0x47, 0x82, 0x11, 0x53, 0x62, 0x5f, 0x61, 0x2e, 0x68, - 0x0f, 0x27, 0xf6, 0xb0, 0xdf, 0x93, 0xa2, 0x1d, 0x96, 0x17, 0x4c, 0x51, 0x12, 0x9e, 0x64, 0xef, - 0x4f, 0xc7, 0x0e, 0x16, 0x9b, 0x0a, 0x29, 0x42, 0xfd, 0xf4, 0x78, 0xc1, 0x68, 0x9e, 0xdc, 0xa1, - 0xf9, 0xf1, 0xa2, 0x83, 0xf2, 0x76, 0xd7, 0x20, 0xc7, 0x6f, 0x2d, 0x78, 0x21, 0xf8, 0x9e, 0x02, - 0xdb, 0xb3, 0xea, 0x4a, 0x2f, 0x42, 0x61, 0x7f, 0xd4, 0xf3, 0x3e, 0x02, 0x7a, 0xd8, 0x11, 0x55, - 0x90, 0x91, 0x37, 0x20, 0x57, 0x99, 0x4c, 0xa6, 0xce, 0xd8, 0x7a, 0xb9, 0x6d, 0x56, 0xf8, 0x04, - 0xb9, 0xfc, 0xef, 0x1f, 0x5d, 0xbd, 0x80, 0x46, 0xc4, 0x63, 0x7b, 0xf2, 0xb2, 0x3d, 0x1d, 0xf7, - 0x43, 0x68, 0xd1, 0x32, 0x87, 0xf6, 0x1d, 0x05, 0x2e, 0xce, 0x6e, 0x24, 0x3d, 0x65, 0x5a, 0x54, - 0x36, 0x0f, 0x5c, 0xfa, 0xf0, 0x94, 0x41, 0x79, 0x3d, 0xe2, 0xd3, 0xe7, 0x13, 0x52, 0x26, 0x3f, - 0x92, 0x70, 0x2a, 0x16, 0x3e, 0x34, 0xcc, 0x24, 0x08, 0xb5, 0xff, 0x48, 0xc1, 0x79, 0x3a, 0x81, - 0x06, 0xce, 0x64, 0xa2, 0x4f, 0xbd, 0x13, 0xc7, 0xf5, 0xb8, 0x48, 0x45, 0x5e, 0x81, 0xa5, 0x93, - 0x27, 0x53, 0x1f, 0x32, 0x72, 0x42, 0x00, 0x37, 0x65, 0x61, 0xde, 0x4c, 0xff, 0x27, 0xd7, 0x40, - 0x0e, 0xd8, 0x9a, 0x46, 0xc8, 0xb8, 0xd4, 0xb6, 0x62, 0xae, 0x8c, 0xfc, 0xd8, 0x8a, 0xaf, 0xc2, - 0x22, 0xaa, 0x0c, 0xf8, 0xf6, 0x28, 0xc4, 0xda, 0xe4, 0xda, 0xa1, 0x42, 0xc1, 0x64, 0x0c, 0xe4, - 0x0b, 0x00, 0x01, 0xda, 0x36, 0xdf, 0xff, 0xc4, 0x55, 0xda, 0x07, 0xdc, 0x36, 0x57, 0x4e, 0x8f, - 0x3a, 0x1c, 0xc2, 0x3a, 0x0f, 0x9b, 0xa2, 0x5b, 0x46, 0x02, 0x69, 0x8a, 0xbf, 0x6c, 0x6d, 0xb0, - 0x8c, 0xca, 0x48, 0xa0, 0x4d, 0x5d, 0x8f, 0x05, 0x9d, 0x44, 0xc0, 0xc9, 0x48, 0x64, 0xc9, 0xeb, - 0xb1, 0xc8, 0x92, 0x59, 0x46, 0x25, 0x87, 0x8f, 0xd4, 0xfe, 0x35, 0x05, 0x2b, 0x87, 0x54, 0xf0, - 0xc0, 0xeb, 0xf4, 0xfc, 0xeb, 0xf9, 0x5d, 0xc8, 0x55, 0x87, 0x1d, 0xfe, 0x84, 0xc0, 0xad, 0x82, - 0x99, 0x9f, 0xdc, 0x60, 0xd8, 0x11, 0xaf, 0x11, 0x13, 0x53, 0x26, 0x7a, 0x8c, 0x8f, 0xdf, 0x7d, - 0x58, 0x62, 0x4f, 0x3a, 0x5c, 0x53, 0x24, 0x44, 0x4f, 0xbf, 0x46, 0x2f, 0xb1, 0x6c, 0x49, 0xeb, - 0xcd, 0x9e, 0x85, 0x64, 0x39, 0x88, 0xe3, 0xe6, 0x49, 0xca, 0x83, 0xc5, 0xb3, 0x29, 0x0f, 0x24, - 0x7c, 0xa0, 0xa5, 0xb3, 0xe0, 0x03, 0x5d, 0xbc, 0x07, 0x39, 0xa9, 0x3e, 0x4f, 0x24, 0x89, 0xfe, - 0x56, 0x0a, 0xd6, 0xb0, 0x55, 0xbe, 0x7d, 0xc7, 0xcf, 0xa7, 0x2a, 0xe4, 0xb5, 0x90, 0x2a, 0x64, - 0x5b, 0x1e, 0x2f, 0xd6, 0xb2, 0x39, 0x3a, 0x90, 0xfb, 0xb0, 0x19, 0x23, 0x24, 0x5f, 0x82, 0x45, - 0x5a, 0x7d, 0x71, 0x75, 0x54, 0xa3, 0x33, 0x20, 0xc0, 0x92, 0xa4, 0x0d, 0x9f, 0x98, 0x8c, 0x5a, - 0xfb, 0x6f, 0x05, 0x56, 0x39, 0x94, 0xbb, 0x7b, 0x34, 0x7c, 0x6c, 0x77, 0xde, 0x8c, 0x76, 0x27, - 0xf3, 0x58, 0xe7, 0xdd, 0xf9, 0xbf, 0xdd, 0x89, 0xf7, 0x42, 0x9d, 0x78, 0xc1, 0x47, 0x96, 0x12, - 0xcd, 0x99, 0xd3, 0x87, 0x7f, 0x87, 0x58, 0x8b, 0x61, 0x42, 0xf2, 0x4d, 0x58, 0xa9, 0x3b, 0x0f, - 0x43, 0x37, 0xb0, 0x9b, 0x33, 0x0a, 0x7d, 0xc9, 0x27, 0x64, 0x6b, 0x0a, 0x0f, 0x2f, 0xd7, 0x79, - 0x68, 0xc7, 0x5e, 0x93, 0x82, 0x22, 0xe9, 0x25, 0x2c, 0xcc, 0xf6, 0x24, 0x53, 0x9f, 0xbb, 0x28, - 0x21, 0x08, 0xc3, 0x77, 0xd3, 0x00, 0x81, 0x77, 0x07, 0x5d, 0x80, 0xa1, 0x87, 0x74, 0xa1, 0xbc, - 0xc6, 0x24, 0x79, 0x8e, 0x8b, 0xf7, 0xf5, 0x9b, 0x5c, 0xc9, 0x9a, 0x9a, 0x8d, 0xfc, 0x85, 0xea, - 0xd6, 0x02, 0x77, 0x27, 0xe8, 0x39, 0x83, 0x0e, 0xdb, 0xdb, 0xd3, 0xbb, 0xd7, 0x11, 0xe8, 0xd1, - 0x4f, 0x9d, 0x11, 0x93, 0x13, 0x9d, 0x0e, 0x8a, 0x94, 0x20, 0xe6, 0x31, 0x95, 0x79, 0x32, 0x8f, - 0xa9, 0x26, 0xac, 0xf4, 0xdd, 0xf7, 0x1c, 0xd7, 0x1b, 0x8e, 0x1f, 0xa1, 0x66, 0x39, 0x50, 0x59, - 0xd1, 0x2e, 0xa8, 0x88, 0x3c, 0x36, 0x0e, 0x78, 0x30, 0xfa, 0xf4, 0xf2, 0x30, 0xf8, 0x89, 0xbe, - 0xc7, 0xd7, 0xa2, 0xba, 0x74, 0x3f, 0x93, 0x5d, 0x52, 0x97, 0xef, 0x67, 0xb2, 0x59, 0x75, 0xe5, - 0x7e, 0x26, 0xbb, 0xa2, 0x82, 0x29, 0xbd, 0xd5, 0xf8, 0x6f, 0x31, 0xd2, 0xf3, 0x49, 0xf8, 0x69, - 0x44, 0xfb, 0x59, 0x0a, 0x48, 0xbc, 0x1a, 0xe4, 0x35, 0xc8, 0xb1, 0x0d, 0xd6, 0x1e, 0x4f, 0xbe, - 0xc5, 0x2d, 0x68, 0x19, 0x94, 0x85, 0x94, 0x2c, 0x43, 0x59, 0xb0, 0x64, 0x73, 0xf2, 0xad, 0x01, - 0xf9, 0x06, 0x9c, 0xc3, 0xee, 0x1d, 0x39, 0xe3, 0xfe, 0xb0, 0x67, 0x23, 0xee, 0x60, 0x67, 0xc0, - 0xe3, 0x67, 0xbd, 0x88, 0x81, 0x1e, 0xe3, 0xd9, 0x33, 0x86, 0x01, 0x9d, 0x38, 0x9a, 0x48, 0xd9, - 0x64, 0x84, 0xa4, 0x05, 0xaa, 0xcc, 0x7f, 0x34, 0x1d, 0x0c, 0xf8, 0xc8, 0xe6, 0xe9, 0xa5, 0x35, - 0x9a, 0x37, 0xa3, 0xe0, 0xf5, 0xa0, 0xe0, 0xd2, 0x74, 0x30, 0x20, 0xaf, 0x00, 0x0c, 0x5d, 0xfb, - 0xb4, 0x3f, 0x99, 0xb0, 0xf7, 0x0a, 0xdf, 0xdf, 0x2c, 0x48, 0x95, 0x07, 0x63, 0xe8, 0xd6, 0x58, - 0x22, 0xf9, 0x25, 0x40, 0x0f, 0x58, 0x74, 0x0d, 0x67, 0x16, 0x2a, 0x1c, 0x11, 0x5f, 0x24, 0x86, - 0xdd, 0xdb, 0x8e, 0x1d, 0xab, 0xff, 0xbe, 0x30, 0xf9, 0x7e, 0x0b, 0x36, 0xb9, 0x6d, 0xe8, 0x61, - 0xdf, 0x3b, 0xe1, 0xd2, 0xf2, 0xd3, 0x88, 0xda, 0x92, 0xb8, 0xfc, 0x4f, 0x19, 0x00, 0xfd, 0xd0, - 0x12, 0xa8, 0x2b, 0xb7, 0x61, 0x91, 0xde, 0x01, 0x84, 0x2e, 0x01, 0x35, 0xb1, 0x58, 0xae, 0xac, - 0x89, 0x45, 0x0a, 0xba, 0x1a, 0x4d, 0x34, 0xe0, 0x16, 0x7a, 0x04, 0x5c, 0x8d, 0xcc, 0xa6, 0x3b, - 0x84, 0x7a, 0xc9, 0xa9, 0x48, 0x15, 0x20, 0xc0, 0x41, 0xe1, 0xb7, 0xd2, 0xcd, 0x00, 0x50, 0x80, - 0x67, 0x70, 0xe4, 0xed, 0x00, 0x4b, 0x45, 0x9e, 0x3e, 0x01, 0x19, 0xd9, 0x87, 0x4c, 0xab, 0xe3, - 0x7b, 0x53, 0xcd, 0x40, 0x87, 0x79, 0x8e, 0xc7, 0x37, 0x0b, 0x10, 0x62, 0xd6, 0xbd, 0x4e, 0x28, - 0x0c, 0x24, 0x16, 0x42, 0x0c, 0x58, 0xe2, 0xb1, 0x6b, 0x67, 0xa0, 0x8a, 0xf1, 0xd0, 0xb5, 0x1c, - 0x4b, 0x14, 0x13, 0x65, 0x99, 0x82, 0x47, 0xa9, 0xbd, 0x0b, 0x69, 0xcb, 0xaa, 0x71, 0x9f, 0xe8, - 0xb5, 0xe0, 0x86, 0x61, 0x59, 0x35, 0x11, 0x98, 0xfd, 0x54, 0x62, 0xa3, 0xc4, 0xe4, 0x97, 0x21, - 0x27, 0x89, 0xcf, 0x1c, 0x4d, 0x00, 0xfb, 0x40, 0xb2, 0x9f, 0x97, 0x37, 0x0d, 0x89, 0x9a, 0x54, - 0x41, 0xdd, 0x9f, 0xbe, 0xe3, 0xe8, 0xa3, 0x11, 0x3a, 0xb2, 0xbc, 0xe7, 0x8c, 0x99, 0xd8, 0x96, - 0x0d, 0x60, 0x38, 0xd1, 0x0f, 0xa8, 0x27, 0x72, 0x65, 0x7d, 0x4a, 0x94, 0x93, 0x34, 0x61, 0xd3, - 0x72, 0xbc, 0xe9, 0x88, 0xd9, 0x5c, 0x94, 0x86, 0x63, 0x7a, 0xa1, 0x60, 0xd8, 0x03, 0x88, 0x58, - 0x38, 0xa1, 0x99, 0xc2, 0xd0, 0xe5, 0x68, 0x38, 0x8e, 0x5c, 0x2e, 0xe2, 0xcc, 0x9a, 0x23, 0x0f, - 0x39, 0x3d, 0x55, 0xc3, 0xd7, 0x14, 0x3c, 0x55, 0xc5, 0x35, 0x25, 0xb8, 0x9c, 0x7c, 0x21, 0x01, - 0x1f, 0x07, 0x1f, 0xbf, 0x24, 0x7c, 0x9c, 0x10, 0x2a, 0xce, 0x07, 0x19, 0x09, 0xa2, 0x8d, 0x8f, - 0xc5, 0xeb, 0x00, 0xf7, 0x87, 0x7d, 0xb7, 0xe6, 0x78, 0x27, 0xc3, 0x9e, 0x04, 0xd3, 0x93, 0xfb, - 0xb5, 0x61, 0xdf, 0xb5, 0x4f, 0x31, 0xf9, 0x67, 0x1f, 0x5d, 0x95, 0x88, 0x4c, 0xe9, 0x7f, 0xf2, - 0x79, 0x58, 0xa1, 0xbf, 0x5a, 0x81, 0xe5, 0x08, 0x53, 0x3b, 0x22, 0x37, 0x03, 0x32, 0x0f, 0x08, - 0xc8, 0x3d, 0x84, 0xee, 0xef, 0x8f, 0x3c, 0x49, 0x78, 0x15, 0x38, 0xfd, 0xfd, 0x91, 0x17, 0x45, - 0xdd, 0x94, 0x88, 0x49, 0xd9, 0xaf, 0xba, 0x88, 0xb6, 0xc1, 0x23, 0x04, 0xa0, 0x6e, 0x8d, 0xcf, - 0x35, 0x5b, 0xc0, 0xfd, 0xc9, 0x71, 0x11, 0x23, 0x6c, 0x58, 0x09, 0xab, 0x5c, 0x64, 0x8f, 0x21, - 0x5c, 0xa8, 0x15, 0x81, 0xe5, 0x7b, 0x76, 0x17, 0x93, 0x43, 0x95, 0xf0, 0x89, 0xc9, 0x2e, 0x6c, - 0x30, 0x19, 0xdf, 0x8f, 0xda, 0xc5, 0x45, 0x5c, 0xdc, 0xdb, 0x82, 0xb0, 0x5e, 0xf2, 0xe7, 0x23, - 0x0c, 0xa4, 0x04, 0x8b, 0x78, 0x21, 0xe4, 0x96, 0xdf, 0x3b, 0xf2, 0x4d, 0x38, 0xba, 0x8e, 0x70, - 0x5f, 0xc1, 0x3b, 0xb0, 0xbc, 0xaf, 0x20, 0x29, 0xf9, 0x1a, 0x80, 0xe1, 0x8e, 0x87, 0x83, 0x01, - 0x02, 0x52, 0x66, 0xf1, 0x2a, 0x75, 0x39, 0xbc, 0x1e, 0xb1, 0x94, 0x80, 0x88, 0x83, 0x27, 0xe1, - 0x6f, 0x3b, 0x02, 0x5b, 0x29, 0x95, 0xa5, 0x55, 0x60, 0x89, 0x2d, 0x46, 0x04, 0x77, 0xe5, 0x70, - 0xf5, 0x12, 0x34, 0x28, 0x03, 0x77, 0xe5, 0xe9, 0x71, 0x70, 0x57, 0x89, 0x41, 0xdb, 0x87, 0xad, - 0xa4, 0x86, 0x85, 0xae, 0xb0, 0xca, 0x59, 0xaf, 0xb0, 0x7f, 0x91, 0x86, 0x55, 0x2c, 0x4d, 0xec, - 0xc2, 0x3a, 0xac, 0x59, 0xd3, 0x77, 0x7c, 0xe4, 0x13, 0xb1, 0x1b, 0x63, 0xfd, 0x26, 0x72, 0x86, - 0xfc, 0x4c, 0x15, 0xe2, 0x20, 0x06, 0xac, 0x8b, 0x93, 0x60, 0x4f, 0x58, 0x93, 0xfb, 0xb8, 0xaa, - 0x02, 0xbd, 0x2b, 0x1e, 0xb5, 0x30, 0xc2, 0x14, 0x9c, 0x07, 0xe9, 0x27, 0x39, 0x0f, 0x32, 0x67, - 0x3a, 0x0f, 0xde, 0x86, 0x55, 0xf1, 0x35, 0xdc, 0xc9, 0x17, 0x9f, 0x6e, 0x27, 0x0f, 0x15, 0x46, - 0xaa, 0xfe, 0x8e, 0xbe, 0x34, 0x77, 0x47, 0xc7, 0xb7, 0x3f, 0xb1, 0xca, 0x62, 0x81, 0xc8, 0x79, - 0x19, 0x18, 0xd6, 0x6b, 0xaf, 0xd0, 0xfc, 0x04, 0xa7, 0xe4, 0x97, 0x60, 0xa5, 0x3a, 0x14, 0xcf, - 0x3e, 0x92, 0xbe, 0x7d, 0x20, 0x12, 0x65, 0x71, 0xc1, 0xa7, 0xf4, 0x4f, 0xb7, 0xf4, 0xa7, 0x71, - 0xba, 0xdd, 0x03, 0xe0, 0x6e, 0x0a, 0x41, 0x38, 0x1e, 0x5c, 0x32, 0xc2, 0xc7, 0x3c, 0xac, 0xf6, - 0x97, 0x88, 0xe9, 0xee, 0xc4, 0x2d, 0x4a, 0xf4, 0x6e, 0x77, 0x38, 0x75, 0xbd, 0x50, 0xfc, 0x4a, - 0x8e, 0x71, 0x41, 0x8f, 0x04, 0xcc, 0x93, 0xb7, 0x87, 0x08, 0xdb, 0xa7, 0x3b, 0x20, 0xe4, 0x4d, - 0xdf, 0x20, 0x6e, 0x6e, 0x38, 0x7f, 0x2d, 0xd6, 0x43, 0x33, 0xcd, 0xe0, 0xb4, 0x1f, 0x29, 0x32, - 0xa8, 0xf5, 0x27, 0x18, 0xea, 0x57, 0x01, 0xfc, 0x77, 0x77, 0x31, 0xd6, 0xec, 0xbe, 0xe4, 0xa7, - 0xca, 0xbd, 0x1c, 0xd0, 0x4a, 0xad, 0x49, 0x7f, 0x5a, 0xad, 0x69, 0x41, 0xae, 0xf1, 0xae, 0xd7, - 0x09, 0x0c, 0x35, 0xc0, 0xf2, 0x25, 0x59, 0xdc, 0x99, 0xd2, 0xbb, 0x37, 0xf0, 0x6c, 0x08, 0xe4, - 0xe0, 0x19, 0x22, 0xb0, 0xc4, 0xa8, 0xbd, 0x09, 0x1b, 0xb2, 0x1f, 0xe7, 0x23, 0xb7, 0x4b, 0xbe, - 0xc2, 0x20, 0xf6, 0x94, 0xd0, 0x8d, 0x45, 0x22, 0xa2, 0x3b, 0xee, 0x23, 0xb7, 0xcb, 0xe4, 0x9f, - 0xce, 0x43, 0xb9, 0xae, 0x78, 0xc7, 0xfb, 0x89, 0x02, 0x24, 0x4e, 0x2e, 0xef, 0x26, 0xca, 0xff, - 0x81, 0x74, 0x19, 0x91, 0xca, 0x32, 0x4f, 0x22, 0x95, 0xe5, 0xff, 0x50, 0x81, 0x8d, 0x8a, 0x5e, - 0xe3, 0x08, 0xd4, 0xec, 0xfd, 0xe0, 0x1a, 0x5c, 0xae, 0xe8, 0x35, 0xbb, 0xd9, 0xa8, 0x56, 0x0a, - 0x0f, 0xec, 0x44, 0x60, 0xc9, 0xcb, 0xf0, 0x6c, 0x9c, 0x24, 0x78, 0x67, 0xb8, 0x04, 0xdb, 0xf1, - 0x6c, 0x01, 0x3e, 0x99, 0xcc, 0x2c, 0x70, 0x2a, 0xd3, 0xf9, 0x37, 0x60, 0x43, 0x00, 0x2d, 0xb6, - 0xaa, 0x16, 0x42, 0x39, 0x6f, 0x40, 0xee, 0xc0, 0x30, 0x2b, 0xa5, 0x07, 0x76, 0xa9, 0x5d, 0xad, - 0xaa, 0x0b, 0x64, 0x0d, 0x56, 0x78, 0x42, 0x41, 0x57, 0x15, 0xb2, 0x0a, 0xd9, 0x4a, 0xdd, 0x32, - 0x0a, 0x6d, 0xd3, 0x50, 0x53, 0xf9, 0x37, 0x60, 0xbd, 0x39, 0xee, 0xbf, 0xd7, 0xf1, 0x9c, 0x7d, - 0xe7, 0x11, 0x3e, 0x13, 0x2c, 0x43, 0xda, 0xd4, 0x0f, 0xd5, 0x05, 0x02, 0xb0, 0xd4, 0xdc, 0x2f, - 0x58, 0x77, 0xee, 0xa8, 0x0a, 0xc9, 0xc1, 0xf2, 0x5e, 0xa1, 0x69, 0xef, 0xd7, 0x2c, 0x35, 0x45, - 0x7f, 0xe8, 0x87, 0x16, 0xfe, 0x48, 0xe7, 0xbf, 0x08, 0x9b, 0x28, 0x2b, 0x54, 0xfb, 0x13, 0xcf, - 0x71, 0x9d, 0x31, 0xd6, 0x61, 0x15, 0xb2, 0x96, 0x43, 0x17, 0xb9, 0xe7, 0xb0, 0x0a, 0xd4, 0xa6, - 0x03, 0xaf, 0x3f, 0x1a, 0x38, 0xdf, 0x56, 0x95, 0xfc, 0x3d, 0xd8, 0x30, 0x87, 0x53, 0xaf, 0xef, - 0x1e, 0x5b, 0x1e, 0xa5, 0x38, 0x7e, 0x44, 0x9e, 0x81, 0xcd, 0x76, 0x5d, 0xaf, 0xed, 0x56, 0xf6, - 0xda, 0x8d, 0xb6, 0x65, 0xd7, 0xf4, 0x56, 0xa1, 0xcc, 0x1e, 0x29, 0x6a, 0x0d, 0xab, 0x65, 0x9b, - 0x46, 0xc1, 0xa8, 0xb7, 0x54, 0x25, 0xff, 0x7d, 0x54, 0x7b, 0x74, 0x87, 0x6e, 0xaf, 0xd4, 0xc1, - 0xd0, 0xde, 0xb4, 0xc2, 0x1a, 0x5c, 0xb1, 0x8c, 0x42, 0xa3, 0x5e, 0xb4, 0x4b, 0x7a, 0xa1, 0xd5, - 0x30, 0x93, 0x90, 0x4d, 0x2f, 0xc2, 0xf9, 0x04, 0x9a, 0x46, 0xab, 0xa9, 0x2a, 0xe4, 0x2a, 0xec, - 0x24, 0xe4, 0x1d, 0x1a, 0xbb, 0x7a, 0xbb, 0x55, 0xae, 0xab, 0xa9, 0x19, 0xcc, 0x96, 0xd5, 0x50, - 0xd3, 0xf9, 0xdf, 0x53, 0x60, 0xbd, 0x3d, 0xe1, 0x16, 0xc2, 0x6d, 0xf4, 0xf3, 0x7b, 0x0e, 0x2e, - 0xb5, 0x2d, 0xc3, 0xb4, 0x5b, 0x8d, 0x7d, 0xa3, 0x6e, 0xb7, 0x2d, 0x7d, 0x2f, 0x5a, 0x9b, 0xab, - 0xb0, 0x23, 0x51, 0x98, 0x46, 0xa1, 0x71, 0x60, 0x98, 0x76, 0x53, 0xb7, 0xac, 0xc3, 0x86, 0x59, - 0x54, 0x15, 0xfa, 0xc5, 0x04, 0x82, 0x5a, 0x49, 0x67, 0xb5, 0x09, 0xe5, 0xd5, 0x8d, 0x43, 0xbd, - 0x6a, 0xef, 0x36, 0x5a, 0x6a, 0x3a, 0x5f, 0xa3, 0x47, 0x2f, 0xe2, 0x0b, 0x32, 0xbb, 0xb6, 0x2c, - 0x64, 0xea, 0x8d, 0xba, 0x11, 0x7d, 0xda, 0x5a, 0x85, 0xac, 0xde, 0x6c, 0x9a, 0x8d, 0x03, 0x9c, - 0x62, 0x00, 0x4b, 0x45, 0xa3, 0x4e, 0x6b, 0x96, 0xa6, 0x39, 0x4d, 0xb3, 0x51, 0x6b, 0xb4, 0x8c, - 0xa2, 0x9a, 0xc9, 0x9b, 0x62, 0x09, 0x8b, 0x42, 0xbb, 0x43, 0xf6, 0x8e, 0x54, 0x34, 0x4a, 0x7a, - 0xbb, 0xda, 0xe2, 0x43, 0xf4, 0xc0, 0x36, 0x8d, 0x37, 0xdb, 0x86, 0xd5, 0xb2, 0x54, 0x85, 0xa8, - 0xb0, 0x5a, 0x37, 0x8c, 0xa2, 0x65, 0x9b, 0xc6, 0x41, 0xc5, 0x38, 0x54, 0x53, 0xb4, 0x4c, 0xf6, - 0x3f, 0xfd, 0x42, 0xfe, 0x03, 0x05, 0x08, 0xc3, 0x66, 0x14, 0x80, 0xff, 0x38, 0x63, 0xae, 0xc0, - 0xc5, 0x32, 0x1d, 0x6a, 0x6c, 0x5a, 0xad, 0x51, 0x8c, 0x76, 0xd9, 0x79, 0x20, 0x91, 0xfc, 0x46, - 0xa9, 0xa4, 0x2a, 0x64, 0x07, 0xce, 0x45, 0xd2, 0x8b, 0x66, 0xa3, 0xa9, 0xa6, 0x2e, 0xa6, 0xb2, - 0x0a, 0xb9, 0x10, 0xcb, 0xdc, 0x37, 0x8c, 0xa6, 0x9a, 0xa6, 0x43, 0x14, 0xc9, 0x10, 0x4b, 0x82, - 0xb1, 0x67, 0xf2, 0xdf, 0x51, 0xe0, 0x3c, 0xab, 0xa6, 0x58, 0x5f, 0x7e, 0x55, 0x2f, 0xc1, 0x36, - 0x47, 0x9c, 0x4d, 0xaa, 0xe8, 0x16, 0xa8, 0xa1, 0x5c, 0x56, 0xcd, 0x67, 0x60, 0x33, 0x94, 0x8a, - 0xf5, 0x48, 0xd1, 0xdd, 0x23, 0x94, 0xbc, 0x6b, 0x58, 0x2d, 0xdb, 0x28, 0x95, 0x1a, 0x66, 0x8b, - 0x55, 0x24, 0x9d, 0xd7, 0x60, 0xb3, 0xe0, 0x8c, 0x3d, 0x7a, 0x2b, 0x72, 0x27, 0xfd, 0xa1, 0x8b, - 0x55, 0x58, 0x83, 0x15, 0xe3, 0x6b, 0x2d, 0xa3, 0x6e, 0x55, 0x1a, 0x75, 0x75, 0x21, 0x7f, 0x29, - 0x42, 0x23, 0xd6, 0xb1, 0x65, 0x95, 0xd5, 0x85, 0x7c, 0x07, 0xd6, 0x84, 0x2d, 0x2e, 0x9b, 0x15, - 0x57, 0xe0, 0xa2, 0x98, 0x6b, 0xb8, 0xa3, 0x44, 0x9b, 0xb0, 0x0d, 0x5b, 0xf1, 0x7c, 0xa3, 0xa5, - 0x2a, 0x74, 0x14, 0x22, 0x39, 0x34, 0x3d, 0x95, 0xff, 0x1d, 0x05, 0xd6, 0xfc, 0xf7, 0x0c, 0xd4, - 0xa0, 0x5e, 0x85, 0x9d, 0x5a, 0x49, 0xb7, 0x8b, 0xc6, 0x41, 0xa5, 0x60, 0xd8, 0xfb, 0x95, 0x7a, - 0x31, 0xf2, 0x91, 0x67, 0xe1, 0x99, 0x04, 0x02, 0xfc, 0xca, 0x36, 0x6c, 0x45, 0xb3, 0x5a, 0x74, - 0xa9, 0xa6, 0x68, 0xd7, 0x47, 0x73, 0xfc, 0x75, 0x9a, 0xce, 0xff, 0xb9, 0x02, 0xdb, 0x3c, 0xa2, - 0x32, 0x7f, 0x59, 0x61, 0x50, 0xfb, 0x88, 0x45, 0x99, 0x87, 0x9b, 0x2d, 0xb3, 0x6d, 0xb5, 0x8c, - 0xa2, 0x60, 0xa7, 0x93, 0xb6, 0x62, 0x1a, 0x35, 0xa3, 0xde, 0x8a, 0xd4, 0xed, 0x05, 0xf8, 0xdc, - 0x1c, 0xda, 0x7a, 0xa3, 0x25, 0x7e, 0xd3, 0xb5, 0xfa, 0x39, 0x78, 0x7e, 0x0e, 0xb1, 0x4f, 0x98, - 0xca, 0x1f, 0xc0, 0xba, 0xa5, 0xd7, 0xaa, 0xa5, 0xe1, 0xb8, 0xeb, 0xe8, 0x53, 0xef, 0xc4, 0x25, - 0x3b, 0x70, 0xa1, 0xd4, 0x30, 0x0b, 0x86, 0x8d, 0x2d, 0x88, 0x54, 0xe2, 0x1c, 0x6c, 0xc8, 0x99, - 0x0f, 0x0c, 0xba, 0xba, 0x08, 0xac, 0xcb, 0x89, 0xf5, 0x86, 0x9a, 0xca, 0x7f, 0x1d, 0x56, 0x43, - 0x61, 0x89, 0x2e, 0xc0, 0x39, 0xf9, 0x77, 0xd3, 0x71, 0x7b, 0x7d, 0xf7, 0x58, 0x5d, 0x88, 0x66, - 0x98, 0x53, 0xd7, 0xa5, 0x19, 0xb8, 0xdd, 0xc8, 0x19, 0x2d, 0x67, 0x7c, 0xda, 0x77, 0x3b, 0x9e, - 0xd3, 0x53, 0x53, 0xf9, 0x97, 0x60, 0x2d, 0x04, 0x86, 0x4a, 0xe7, 0x55, 0xb5, 0xc1, 0xcf, 0x87, - 0x9a, 0x51, 0xac, 0xb4, 0x6b, 0xea, 0x22, 0xdd, 0x68, 0xca, 0x95, 0xbd, 0xb2, 0x0a, 0xf9, 0x1f, - 0x28, 0xf4, 0x86, 0x82, 0xfd, 0x5e, 0x2b, 0xe9, 0x62, 0x26, 0xd2, 0x55, 0xc0, 0x20, 0x96, 0x0d, - 0xcb, 0x62, 0x0f, 0xce, 0x97, 0x60, 0x9b, 0xff, 0xb0, 0xf5, 0x7a, 0xd1, 0x2e, 0xeb, 0x66, 0xf1, - 0x50, 0x37, 0xe9, 0xd2, 0x78, 0xa0, 0xa6, 0x70, 0xbd, 0x4b, 0x29, 0x76, 0xab, 0xd1, 0x2e, 0x94, - 0xd5, 0x34, 0x5d, 0x5e, 0xa1, 0xf4, 0x66, 0xa5, 0xae, 0x66, 0x70, 0xf7, 0x88, 0x51, 0x63, 0xb1, - 0x34, 0x7f, 0x31, 0xff, 0xb1, 0x02, 0x17, 0xac, 0xfe, 0xb1, 0xdb, 0xf1, 0xa6, 0x63, 0x47, 0x1f, - 0x1c, 0x0f, 0xc7, 0x7d, 0xef, 0xe4, 0xd4, 0x9a, 0xf6, 0x3d, 0x87, 0xdc, 0x86, 0x1b, 0x56, 0x65, - 0xaf, 0xae, 0xb7, 0xe8, 0xea, 0xd7, 0xab, 0x7b, 0x0d, 0xb3, 0xd2, 0x2a, 0xd7, 0x6c, 0xab, 0x5d, - 0x89, 0x2d, 0x8c, 0xeb, 0xf0, 0xdc, 0x6c, 0xd2, 0xaa, 0xb1, 0xa7, 0x17, 0x1e, 0xa8, 0xca, 0xfc, - 0x02, 0x77, 0xf5, 0xaa, 0x5e, 0x2f, 0x18, 0x45, 0xfb, 0xe0, 0x8e, 0x9a, 0x22, 0x37, 0xe0, 0xda, - 0x6c, 0xd2, 0x52, 0xa5, 0x69, 0x51, 0xb2, 0xf4, 0xfc, 0xef, 0x96, 0xad, 0x1a, 0xa5, 0xca, 0xe4, - 0xfb, 0xa0, 0x46, 0xdd, 0xd3, 0x63, 0xe6, 0x0d, 0x66, 0xbb, 0x5e, 0x67, 0x67, 0xc0, 0x06, 0xe4, - 0x1a, 0xad, 0xb2, 0x61, 0x72, 0xa4, 0x6f, 0x84, 0xf6, 0x6e, 0xd7, 0xe9, 0xb4, 0x6a, 0x98, 0x95, - 0xb7, 0xf0, 0x30, 0xd8, 0x86, 0x2d, 0xab, 0xaa, 0x17, 0xf6, 0x71, 0xc6, 0x57, 0xea, 0x76, 0xa1, - 0xac, 0xd7, 0xeb, 0x46, 0x55, 0x85, 0xfc, 0x9f, 0x29, 0xcc, 0xce, 0x20, 0xc9, 0x8f, 0x8d, 0x7c, - 0x1e, 0x6e, 0x35, 0xf6, 0x5b, 0xba, 0xdd, 0xac, 0xb6, 0xf7, 0x2a, 0x75, 0xdb, 0x7a, 0x50, 0x2f, - 0x08, 0xc1, 0xa5, 0x10, 0xdf, 0x2f, 0x6f, 0xc1, 0xf5, 0xb9, 0xd4, 0x01, 0x26, 0xf7, 0x4d, 0xd0, - 0xe6, 0x52, 0xf2, 0x86, 0xe4, 0x7f, 0xac, 0xc0, 0xce, 0x9c, 0xb7, 0x59, 0xf2, 0x22, 0xdc, 0x2e, - 0x1b, 0x7a, 0xb1, 0x6a, 0x58, 0x16, 0x2e, 0x23, 0xa3, 0xde, 0xe2, 0x66, 0x10, 0x89, 0xbb, 0xe1, - 0x6d, 0xb8, 0x31, 0x9f, 0x3c, 0x38, 0x57, 0x6f, 0xc1, 0xf5, 0xf9, 0xa4, 0xfc, 0x9c, 0x4d, 0xd1, - 0xdd, 0x68, 0x3e, 0xa5, 0x7f, 0x3e, 0xa7, 0xf3, 0xdf, 0x53, 0xe0, 0x7c, 0xb2, 0x82, 0x84, 0xd6, - 0xad, 0x52, 0xb7, 0x5a, 0x7a, 0xb5, 0x6a, 0x37, 0x75, 0x53, 0xaf, 0xd9, 0x46, 0xdd, 0x6c, 0x54, - 0xab, 0x49, 0xe7, 0xd2, 0x75, 0x78, 0x6e, 0x36, 0xa9, 0x55, 0x30, 0x2b, 0x4d, 0xba, 0xf5, 0x6a, - 0x70, 0x65, 0x36, 0x95, 0x51, 0x29, 0x18, 0x6a, 0x6a, 0xf7, 0xf5, 0x0f, 0xff, 0xe5, 0xca, 0xc2, - 0x87, 0x1f, 0x5f, 0x51, 0x7e, 0xf2, 0xf1, 0x15, 0xe5, 0x9f, 0x3f, 0xbe, 0xa2, 0xbc, 0xf5, 0xc2, - 0xd9, 0xc2, 0x59, 0xa0, 0xd0, 0xfe, 0xce, 0x12, 0xda, 0xfd, 0xbc, 0xfc, 0x3f, 0x01, 0x00, 0x00, - 0xff, 0xff, 0x04, 0x36, 0x74, 0xce, 0xa5, 0xa8, 0x01, 0x00, + 0x32, 0x65, 0xf2, 0x21, 0x64, 0xd7, 0xff, 0x9f, 0xbd, 0xab, 0xf9, 0x8d, 0xe4, 0xb8, 0xee, 0xec, + 0x99, 0x21, 0x39, 0x7c, 0xc3, 0x8f, 0x66, 0xed, 0x6a, 0x97, 0x22, 0x77, 0xb9, 0xda, 0xd6, 0xee, + 0x6a, 0x77, 0x64, 0x49, 0xde, 0x55, 0x64, 0x69, 0xe5, 0xc8, 0x72, 0x73, 0xa6, 0x87, 0xd3, 0xcb, + 0xf9, 0x52, 0xf7, 0x0c, 0xe9, 0x95, 0x6c, 0x77, 0x5a, 0x9c, 0x26, 0x39, 0xd1, 0x70, 0x66, 0x3c, + 0x1f, 0x5a, 0xaf, 0x10, 0x20, 0x5f, 0x80, 0x0d, 0x24, 0x71, 0x9c, 0x38, 0x01, 0x62, 0x04, 0x01, + 0x72, 0x88, 0x10, 0xe4, 0x90, 0xbf, 0x20, 0xc9, 0x25, 0x37, 0x01, 0x86, 0x01, 0x1f, 0x72, 0x4a, + 0x00, 0x21, 0x11, 0x90, 0x1c, 0x92, 0xdc, 0x82, 0xf8, 0xe0, 0x53, 0x50, 0xaf, 0xaa, 0xba, 0xab, + 0x3f, 0x66, 0x96, 0xab, 0x95, 0x92, 0x18, 0xf0, 0x89, 0x9c, 0xaa, 0xf7, 0xaa, 0xeb, 0xbb, 0x5e, + 0xbd, 0x7a, 0xef, 0xf7, 0xc8, 0x0b, 0x40, 0x7c, 0x39, 0xd9, 0x5f, 0x98, 0xfc, 0x83, 0xeb, 0x22, + 0xc7, 0x5f, 0x51, 0xfc, 0xb3, 0x7f, 0x97, 0x82, 0x73, 0x09, 0xc2, 0x3c, 0x15, 0xba, 0x3b, 0xbd, + 0xb1, 0x77, 0xcc, 0x44, 0x76, 0xb9, 0x91, 0x6b, 0x52, 0x3a, 0xd7, 0xd0, 0x2c, 0xb0, 0x18, 0xdc, + 0xfc, 0x5b, 0xfc, 0x17, 0x5d, 0xac, 0xee, 0x50, 0x28, 0x1f, 0xe8, 0xbf, 0xc4, 0x84, 0x75, 0x0c, + 0x2c, 0x30, 0xea, 0xf4, 0x31, 0x3e, 0x01, 0x1e, 0xfa, 0x99, 0xd0, 0x15, 0x08, 0x6b, 0xd1, 0x90, + 0x88, 0xe8, 0xa9, 0x6f, 0xa9, 0x83, 0x48, 0x0a, 0xf9, 0x32, 0x6c, 0x4a, 0x7b, 0xbb, 0x13, 0x99, + 0xea, 0x68, 0xeb, 0x6d, 0x5d, 0x74, 0xfd, 0x5d, 0xbe, 0x18, 0x9a, 0xf4, 0x3b, 0xb0, 0x8d, 0x83, + 0xd8, 0x69, 0x0f, 0x9c, 0x58, 0x24, 0x0a, 0x6c, 0x2a, 0x83, 0x6e, 0xdf, 0xa4, 0x54, 0x66, 0x7b, + 0x10, 0x09, 0x4a, 0x41, 0x5b, 0xcd, 0xbb, 0xef, 0x1d, 0x78, 0x2a, 0xb1, 0xc6, 0x74, 0x43, 0x47, + 0x53, 0xa2, 0x40, 0x16, 0x59, 0xa4, 0xbf, 0xa9, 0x30, 0x72, 0x15, 0x96, 0xdf, 0xf5, 0xdc, 0xa1, + 0x37, 0xe4, 0x27, 0x25, 0x9f, 0x12, 0x2c, 0x4d, 0x3e, 0x28, 0xff, 0x32, 0x25, 0xae, 0x21, 0x3b, + 0xfd, 0xfe, 0x78, 0x34, 0x1e, 0xba, 0x83, 0x90, 0x02, 0x84, 0x9c, 0xc2, 0xd3, 0x78, 0x4a, 0xdf, + 0x41, 0xe0, 0xfa, 0xfe, 0x50, 0x78, 0xea, 0x1f, 0x0a, 0xeb, 0xd7, 0xdc, 0x9d, 0x97, 0xc2, 0x72, + 0x84, 0x4e, 0xa9, 0x75, 0x99, 0x98, 0x4a, 0xcd, 0x52, 0xa9, 0xe5, 0x39, 0xeb, 0x22, 0x2b, 0x33, + 0x46, 0x45, 0xca, 0x09, 0x15, 0x8f, 0x6a, 0x40, 0x76, 0x82, 0x56, 0x84, 0x4b, 0x95, 0xdb, 0x47, + 0xbe, 0x02, 0x4b, 0x9d, 0xb6, 0x1c, 0x9f, 0x2d, 0x7a, 0xf7, 0x36, 0xdb, 0x0c, 0x23, 0x36, 0x28, + 0xa3, 0x3c, 0x67, 0x65, 0x3b, 0x3c, 0x75, 0x67, 0x25, 0xa4, 0x2a, 0xd2, 0x76, 0x84, 0xc4, 0x1b, + 0x67, 0x23, 0xab, 0x90, 0xf2, 0x77, 0x82, 0x54, 0xa7, 0x4d, 0x67, 0xeb, 0x48, 0x42, 0xa9, 0xb5, + 0xf8, 0x2f, 0xed, 0xd7, 0xe0, 0xe6, 0x59, 0xfb, 0x88, 0xae, 0xb4, 0x29, 0x1d, 0xbe, 0x64, 0xad, + 0xbb, 0xb1, 0x7e, 0xbb, 0x0a, 0x32, 0xc8, 0x66, 0x47, 0x0c, 0xb8, 0x48, 0x6b, 0x0d, 0x3b, 0xda, + 0x6f, 0xa5, 0x61, 0x35, 0xac, 0x1c, 0x23, 0xcf, 0x43, 0xc6, 0x2f, 0x76, 0xd5, 0x7f, 0xc4, 0x91, + 0x89, 0x68, 0xe1, 0x16, 0x12, 0x51, 0x29, 0x04, 0xdf, 0x7c, 0x9d, 0x53, 0xf9, 0x9d, 0xc5, 0x5a, + 0xc6, 0x44, 0xf1, 0xbe, 0x72, 0x0f, 0x56, 0xd1, 0x5c, 0x07, 0x8f, 0xb7, 0x71, 0x87, 0xab, 0x5c, + 0x67, 0x6b, 0xcd, 0xb3, 0x1f, 0x7d, 0x7c, 0x65, 0x0e, 0x15, 0xe4, 0xcb, 0x94, 0x97, 0x1e, 0x31, + 0x34, 0x53, 0xd2, 0x7d, 0x64, 0xa6, 0xeb, 0x3e, 0x78, 0x53, 0xa6, 0xe8, 0x3e, 0xe6, 0x67, 0xe8, + 0x3e, 0x02, 0x4e, 0x59, 0xf7, 0x81, 0x1a, 0xb0, 0xc5, 0x69, 0x1a, 0xb0, 0x80, 0x87, 0x69, 0xc0, + 0xae, 0xf1, 0xe6, 0x0e, 0xdd, 0x07, 0x0e, 0xf6, 0x03, 0x5f, 0xd5, 0xd8, 0x10, 0xcb, 0x7d, 0x80, + 0xaf, 0xe3, 0x3b, 0x4b, 0x20, 0x9e, 0xd4, 0xb5, 0x3f, 0x52, 0x22, 0x8a, 0x03, 0x31, 0x14, 0xd7, + 0x61, 0xb5, 0x73, 0x4a, 0x6f, 0x34, 0x5e, 0x5b, 0x12, 0x4d, 0x57, 0xac, 0x15, 0x91, 0xca, 0xc4, + 0xd3, 0xe7, 0x60, 0xcd, 0x27, 0xe3, 0x12, 0x1a, 0xba, 0xda, 0x58, 0x3e, 0x37, 0xc7, 0x8d, 0x78, + 0x1e, 0xd6, 0x7d, 0x42, 0x7e, 0xf9, 0x63, 0xd2, 0xe9, 0x8a, 0xa5, 0x8a, 0x0c, 0x1e, 0xef, 0x7f, + 0xa4, 0x1d, 0x47, 0x05, 0x95, 0xcf, 0xa9, 0x56, 0xda, 0xdf, 0xa7, 0x43, 0x97, 0x2a, 0xf1, 0x99, + 0x1d, 0xc8, 0xd1, 0xd3, 0x92, 0x77, 0x12, 0xdf, 0x56, 0xae, 0x4e, 0xe9, 0x7e, 0x6e, 0x94, 0x60, + 0xdb, 0x75, 0x0b, 0x46, 0xa3, 0xbe, 0xb0, 0x51, 0x70, 0x98, 0x40, 0xc0, 0x04, 0x65, 0x9c, 0x7e, + 0xa2, 0x38, 0xb6, 0x87, 0xe4, 0x67, 0x17, 0x27, 0xa4, 0x5a, 0x3a, 0xfb, 0x50, 0x30, 0xf0, 0x7f, + 0x89, 0x0f, 0xb4, 0x00, 0x75, 0x10, 0xa3, 0x70, 0xe1, 0xe9, 0x04, 0x51, 0x2b, 0x56, 0x38, 0xf6, + 0x12, 0x96, 0xac, 0x4e, 0xc4, 0xbf, 0xa2, 0x58, 0x03, 0x96, 0xf1, 0x4a, 0x23, 0x0a, 0xcc, 0x24, + 0xe8, 0xd0, 0xe2, 0x8d, 0x2f, 0x98, 0x55, 0x2b, 0x47, 0xf9, 0x44, 0x31, 0x27, 0xf0, 0xb4, 0x7c, + 0x11, 0x09, 0x57, 0x72, 0x5e, 0xc0, 0x60, 0xce, 0xec, 0x81, 0xe0, 0xbe, 0x82, 0x55, 0xbd, 0xe0, + 0x86, 0x13, 0x38, 0x99, 0x76, 0x02, 0x9b, 0xd3, 0x87, 0x64, 0x46, 0x88, 0x95, 0x40, 0x66, 0x4a, + 0xc9, 0x32, 0x93, 0x7c, 0x2d, 0x49, 0x87, 0xae, 0x25, 0xda, 0x5f, 0xa4, 0xe1, 0xd9, 0x33, 0x0c, + 0xd7, 0x8c, 0x6f, 0x7e, 0x15, 0x72, 0x4c, 0xf3, 0xcf, 0xb6, 0x4f, 0x66, 0x35, 0xe3, 0x07, 0x4a, + 0x7d, 0x6f, 0xec, 0xf2, 0xbd, 0x8e, 0x4a, 0xb5, 0xc1, 0x7e, 0x07, 0x23, 0xff, 0x7f, 0xf2, 0x2b, + 0xb0, 0xc6, 0x36, 0x34, 0x66, 0x57, 0x74, 0x34, 0xe9, 0x9e, 0x61, 0x47, 0xdb, 0x12, 0x4e, 0x10, + 0x11, 0x56, 0xdc, 0xe4, 0x70, 0xc7, 0xb0, 0xfd, 0x34, 0xd2, 0x84, 0x1c, 0x92, 0x1d, 0xb9, 0x9d, + 0xee, 0x99, 0xac, 0xf1, 0x85, 0x8b, 0x85, 0xcc, 0xc6, 0xcc, 0x21, 0x69, 0x42, 0x09, 0x7f, 0x93, + 0x1b, 0xb0, 0xd6, 0x9b, 0x9c, 0xd2, 0x9b, 0x1c, 0x9b, 0x0b, 0xfc, 0xf9, 0x76, 0xde, 0x5a, 0xe9, + 0x4d, 0x4e, 0xf5, 0xc1, 0x00, 0x87, 0x14, 0xdf, 0x79, 0xd7, 0x29, 0x1d, 0x5b, 0xb5, 0x82, 0x72, + 0x01, 0x29, 0x69, 0x01, 0x6c, 0xdd, 0x72, 0xda, 0xf3, 0xc0, 0xac, 0x7e, 0x78, 0x88, 0x19, 0xf6, + 0x43, 0xfb, 0x69, 0x4a, 0x88, 0xeb, 0xd3, 0xe7, 0xfd, 0x2f, 0x86, 0x28, 0x61, 0x88, 0x6e, 0x82, + 0x4a, 0xbb, 0x3e, 0xd8, 0x54, 0xfc, 0x31, 0x5a, 0xed, 0x4d, 0x4e, 0xfd, 0xbe, 0x93, 0x3b, 0x7e, + 0x41, 0xee, 0xf8, 0x57, 0x85, 0x38, 0x9f, 0xb8, 0x3d, 0x4c, 0xef, 0x72, 0xed, 0x3f, 0xd3, 0x70, + 0xe3, 0x6c, 0x9b, 0xc0, 0x2f, 0xc6, 0x2d, 0x61, 0xdc, 0x22, 0x9a, 0x96, 0xf9, 0x98, 0xa6, 0x25, + 0x61, 0xed, 0x2d, 0x24, 0xad, 0xbd, 0x98, 0x5e, 0x67, 0x31, 0x41, 0xaf, 0x93, 0xb8, 0x40, 0xb3, + 0x8f, 0x58, 0xa0, 0x4b, 0xf2, 0x3c, 0xf9, 0x37, 0xff, 0xfe, 0x15, 0x16, 0xed, 0xdf, 0x81, 0x73, + 0x42, 0xb4, 0x67, 0x27, 0x47, 0xa0, 0xae, 0xcb, 0xdd, 0xb9, 0x95, 0x24, 0xd4, 0x23, 0x59, 0x82, + 0xe0, 0xbd, 0xce, 0xc5, 0xf9, 0x20, 0xff, 0xff, 0x8f, 0x20, 0x4f, 0xee, 0xc3, 0x05, 0x04, 0x68, + 0x3e, 0x94, 0x15, 0x8d, 0xce, 0xd0, 0x3b, 0xe2, 0xf3, 0xe1, 0x6a, 0x4c, 0xec, 0xed, 0x1c, 0x4a, + 0xd5, 0xb1, 0xbc, 0xa3, 0xf2, 0x9c, 0x75, 0x7e, 0x94, 0x90, 0x1e, 0xbd, 0x23, 0xfc, 0xb5, 0x02, + 0xda, 0xa3, 0xfb, 0x0b, 0xef, 0xd9, 0xd1, 0x0e, 0xa7, 0xf7, 0x6c, 0xa9, 0xf7, 0x9e, 0x85, 0x95, + 0xa1, 0x77, 0x34, 0xf4, 0x46, 0x27, 0xa1, 0x0b, 0xdc, 0x32, 0x4f, 0x14, 0x1d, 0x23, 0x60, 0xe2, + 0x1e, 0x4b, 0xc8, 0x16, 0x4c, 0x5a, 0xc9, 0xbf, 0xfa, 0x25, 0x8e, 0x03, 0x9d, 0x4d, 0x72, 0x05, + 0xd9, 0x8f, 0x7b, 0x99, 0x6c, 0x4a, 0x4d, 0x5b, 0x1c, 0xcc, 0xee, 0xa8, 0xd3, 0xf5, 0xb4, 0xbf, + 0x51, 0x84, 0x44, 0x90, 0xd4, 0x79, 0xe4, 0x1d, 0xc9, 0x1a, 0x2f, 0x1d, 0x13, 0x43, 0x92, 0x58, + 0x64, 0xc3, 0x25, 0x8e, 0xaf, 0x86, 0x09, 0x21, 0x7c, 0x35, 0x4c, 0x79, 0x02, 0x93, 0x22, 0x7e, + 0x01, 0xbe, 0x2b, 0x9e, 0xf4, 0xe9, 0x9e, 0xb7, 0x7f, 0x9b, 0xdc, 0x82, 0x45, 0xf6, 0x8a, 0x2f, + 0xaa, 0xbb, 0x16, 0xaa, 0xee, 0xfe, 0x6d, 0x4b, 0xe4, 0x6b, 0x3f, 0xf4, 0xd5, 0xe0, 0xb1, 0x46, + 0xec, 0xdf, 0x26, 0xaf, 0x9e, 0xcd, 0xba, 0x2e, 0x2b, 0xac, 0xeb, 0x7c, 0xcb, 0xba, 0xd7, 0x42, + 0x96, 0x75, 0xd7, 0x66, 0xf7, 0x16, 0x7f, 0xbc, 0x60, 0x78, 0x62, 0x01, 0xce, 0xcc, 0x4f, 0x15, + 0xb8, 0x3c, 0x93, 0x83, 0x5c, 0x82, 0xac, 0xde, 0x30, 0x9b, 0xc1, 0xf8, 0xd2, 0x35, 0x23, 0x52, + 0xc8, 0x2e, 0x2c, 0xed, 0xb8, 0xa3, 0xce, 0x21, 0x9d, 0xc6, 0x89, 0xda, 0xc4, 0x58, 0xb1, 0x3e, + 0x79, 0x79, 0xce, 0x0a, 0x78, 0x89, 0x03, 0xeb, 0xb8, 0x16, 0x42, 0xb1, 0x5b, 0xd2, 0x09, 0x6a, + 0x83, 0x58, 0x81, 0x31, 0x36, 0xba, 0xcf, 0xc4, 0x12, 0xa3, 0x4b, 0xf0, 0x7d, 0x21, 0x8b, 0x4c, + 0xaf, 0xe0, 0x63, 0x00, 0x23, 0xde, 0x84, 0x6c, 0x43, 0x3c, 0x2b, 0x4a, 0xe6, 0xa8, 0xe2, 0x09, + 0xd1, 0xf2, 0x73, 0xb5, 0xdf, 0x53, 0xc4, 0xdd, 0xfe, 0xd1, 0x0d, 0x91, 0xc2, 0xde, 0xb4, 0x67, + 0x87, 0xbd, 0x69, 0x7f, 0xca, 0xb0, 0x37, 0xda, 0x5f, 0x71, 0xd8, 0x62, 0xb3, 0xdd, 0x88, 0x28, + 0x96, 0x9e, 0xd4, 0xac, 0xd8, 0x08, 0xcd, 0xce, 0x67, 0xa5, 0xb0, 0x69, 0xf1, 0x6f, 0x4d, 0xb7, + 0x2e, 0x96, 0xa6, 0xea, 0x9f, 0xa4, 0xe1, 0xd2, 0x2c, 0xf6, 0xc4, 0xc0, 0xac, 0xca, 0xe3, 0x05, + 0x66, 0xbd, 0x05, 0x59, 0x96, 0xe6, 0xdb, 0xcc, 0x62, 0x87, 0x73, 0x56, 0xda, 0xe1, 0x22, 0x9b, + 0x3c, 0x0b, 0x0b, 0x7a, 0xc1, 0x0e, 0x62, 0x05, 0xa1, 0x71, 0x9b, 0x7b, 0x38, 0x42, 0xb3, 0x29, + 0x9e, 0x45, 0xbe, 0x19, 0x0f, 0x8f, 0xc5, 0x83, 0x04, 0x6d, 0x49, 0x1d, 0x12, 0x43, 0x14, 0xc7, + 0xfa, 0x06, 0x08, 0xd8, 0x1c, 0x54, 0xd6, 0x8a, 0x87, 0xda, 0xd2, 0x60, 0xa1, 0x31, 0xf4, 0x46, + 0xde, 0x58, 0x36, 0x3c, 0x1b, 0x60, 0x8a, 0xc5, 0x73, 0xb8, 0x59, 0x98, 0xfb, 0x90, 0x79, 0x01, + 0x2f, 0xc8, 0xc8, 0x0c, 0x68, 0x47, 0x46, 0x93, 0x2d, 0x89, 0x84, 0x32, 0x54, 0xdc, 0x49, 0xef, + 0xf0, 0xa4, 0x65, 0x55, 0xb8, 0xa8, 0xc1, 0x18, 0xba, 0x98, 0x4a, 0x1b, 0x38, 0xb2, 0x24, 0x12, + 0xed, 0xbb, 0x0a, 0x9c, 0x4f, 0x6a, 0x07, 0xb9, 0x04, 0x99, 0x5e, 0x62, 0x24, 0xb0, 0x1e, 0x73, + 0x5e, 0xcc, 0xd1, 0xbf, 0xce, 0x51, 0x7f, 0x78, 0xea, 0x8e, 0x65, 0xf3, 0x3c, 0x29, 0xd9, 0x02, + 0xfa, 0xa3, 0x84, 0xff, 0x93, 0x2b, 0x62, 0x8f, 0x4e, 0xc7, 0x62, 0x87, 0xe1, 0x1f, 0x4d, 0x07, + 0x30, 0xdb, 0x8d, 0xfa, 0x80, 0x21, 0x5a, 0xbf, 0x0c, 0x19, 0x5a, 0xad, 0xc8, 0xec, 0xa5, 0xf3, + 0x47, 0xaf, 0x56, 0x38, 0x11, 0xab, 0xd5, 0xc8, 0x3d, 0xed, 0x5a, 0x48, 0xac, 0x1d, 0xc0, 0x6a, + 0x98, 0x82, 0x18, 0x61, 0x0c, 0xc4, 0xdc, 0x1d, 0x95, 0x97, 0xb4, 0xd3, 0xef, 0x33, 0x13, 0xf1, + 0x9d, 0xa7, 0xff, 0xf1, 0xe3, 0x2b, 0x40, 0x7f, 0x32, 0x9e, 0x24, 0x8c, 0x44, 0xed, 0xfb, 0x29, + 0x38, 0x1f, 0x78, 0xa5, 0x8a, 0x35, 0xf4, 0x73, 0xeb, 0x22, 0xa5, 0x87, 0x5c, 0x78, 0x84, 0xa0, + 0x15, 0x6f, 0xe0, 0x0c, 0xcf, 0x81, 0x5d, 0xd8, 0x98, 0x46, 0x4f, 0x9e, 0x87, 0x25, 0x04, 0x32, + 0x19, 0xb8, 0x87, 0x9e, 0xbc, 0xf7, 0xf5, 0x44, 0xa2, 0x15, 0xe4, 0x6b, 0x3f, 0x56, 0x60, 0x93, + 0x1b, 0x36, 0x57, 0xdd, 0x4e, 0x6f, 0xec, 0xf5, 0xdc, 0xde, 0xa1, 0xf7, 0xd9, 0xb8, 0xf8, 0xed, + 0x86, 0xf6, 0xb1, 0xeb, 0x61, 0xfb, 0xf5, 0xd8, 0xd7, 0xa6, 0xb7, 0x96, 0xdc, 0x42, 0x70, 0x9e, + 0x43, 0x36, 0x79, 0x33, 0xcc, 0xa5, 0xba, 0x47, 0x13, 0x64, 0x97, 0x6a, 0xa4, 0xd0, 0x7e, 0x1d, + 0xb6, 0x67, 0x7f, 0x80, 0x7c, 0x03, 0x56, 0x30, 0xda, 0x4b, 0x6b, 0x70, 0x3c, 0x74, 0xdb, 0x9e, + 0x50, 0x85, 0x09, 0x4d, 0xa4, 0x9c, 0xc7, 0xb0, 0x86, 0xb8, 0x8b, 0xef, 0x31, 0xc6, 0x91, 0xe1, + 0x4c, 0x21, 0xef, 0x01, 0xb9, 0x34, 0xed, 0x37, 0x14, 0x20, 0xf1, 0x32, 0xc8, 0x97, 0x60, 0xb9, + 0xd5, 0x2c, 0xd8, 0x63, 0x77, 0x38, 0x2e, 0xf7, 0x27, 0x43, 0x0e, 0xf4, 0xc3, 0x3c, 0x3e, 0xc7, + 0x87, 0x74, 0x2b, 0x19, 0x8e, 0x9d, 0x93, 0xfe, 0x64, 0x68, 0x85, 0xe8, 0x30, 0x4c, 0x89, 0xe7, + 0xbd, 0xd7, 0x76, 0x1f, 0x86, 0xc3, 0x94, 0xf0, 0xb4, 0x50, 0x98, 0x12, 0x9e, 0xa6, 0x7d, 0xa8, + 0xc0, 0x96, 0x30, 0x3e, 0x6a, 0x27, 0xd4, 0xa5, 0x80, 0xb8, 0x06, 0x43, 0x81, 0x2c, 0x39, 0x4b, + 0xa4, 0x5d, 0x17, 0xd0, 0x1f, 0x58, 0x41, 0x94, 0x6d, 0x19, 0x2f, 0xf9, 0x2a, 0x64, 0xec, 0x71, + 0x7f, 0x70, 0x06, 0xec, 0x0f, 0xd5, 0x1f, 0xd1, 0x71, 0x7f, 0x80, 0x45, 0x20, 0xa7, 0xe6, 0xc1, + 0x79, 0xb9, 0x72, 0xa2, 0xc6, 0xa4, 0x0a, 0x8b, 0x1c, 0xe4, 0x29, 0xf2, 0xce, 0x38, 0xa3, 0x4d, + 0x3b, 0x6b, 0x02, 0x60, 0x84, 0x23, 0x1b, 0x5a, 0xa2, 0x0c, 0xed, 0xf7, 0x15, 0xc8, 0x51, 0x69, + 0x03, 0x6f, 0x71, 0x4f, 0x3a, 0xa5, 0xc3, 0x82, 0xa3, 0x78, 0xa6, 0xf6, 0x8b, 0x3f, 0xd3, 0x69, + 0xfc, 0x0a, 0xac, 0x45, 0x18, 0x88, 0x86, 0xae, 0xe5, 0xdd, 0xce, 0xa1, 0xcb, 0xa2, 0x1e, 0xb0, + 0x37, 0xde, 0x50, 0x9a, 0xf6, 0x3b, 0x0a, 0x9c, 0xa7, 0x77, 0x7e, 0x13, 0xd5, 0xbd, 0xd6, 0xa4, + 0x2b, 0xd6, 0x3b, 0x95, 0xa0, 0x84, 0x15, 0x1b, 0x73, 0x7b, 0x65, 0x12, 0x14, 0x4f, 0xb3, 0xfc, + 0x5c, 0x52, 0x86, 0x2c, 0x3f, 0x5f, 0x46, 0x1c, 0x90, 0x70, 0x5b, 0x52, 0x26, 0x04, 0x05, 0x73, + 0x22, 0xda, 0x12, 0xdc, 0xc2, 0x38, 0x8f, 0xe5, 0x73, 0x6b, 0xff, 0xa5, 0xc0, 0xc5, 0x29, 0x3c, + 0xe4, 0x0d, 0x98, 0x47, 0x97, 0x1c, 0x3e, 0x7a, 0x97, 0xa6, 0x7c, 0x62, 0x7c, 0x78, 0xb2, 0x7f, + 0x9b, 0x1d, 0x44, 0xa7, 0xf4, 0x87, 0xc5, 0xb8, 0xc8, 0x3b, 0xb0, 0xa4, 0xb7, 0xdb, 0xfc, 0x3a, + 0x93, 0x0a, 0x5d, 0x67, 0xa6, 0x7c, 0xf1, 0x45, 0x9f, 0x9e, 0x5d, 0x67, 0x98, 0x71, 0x78, 0xbb, + 0xed, 0x70, 0x77, 0xa3, 0xa0, 0xbc, 0xcd, 0x5f, 0x86, 0xd5, 0x30, 0xf1, 0x63, 0x79, 0x48, 0xfc, + 0x50, 0x01, 0x35, 0x5c, 0x87, 0xcf, 0x07, 0x1a, 0x25, 0x69, 0x98, 0x1f, 0x31, 0xa9, 0xfe, 0x30, + 0x05, 0x4f, 0x25, 0xf6, 0x30, 0x79, 0x01, 0x16, 0xf4, 0xc1, 0xc0, 0x2c, 0xf2, 0x59, 0xc5, 0x25, + 0x24, 0xd4, 0x12, 0x87, 0x6e, 0x7b, 0x8c, 0x88, 0xbc, 0x0c, 0x59, 0x9c, 0x99, 0x94, 0x21, 0x15, + 0x40, 0x03, 0x32, 0x25, 0x4a, 0x04, 0x1a, 0x50, 0x10, 0x92, 0x12, 0xac, 0x72, 0x94, 0x04, 0xcb, + 0x3b, 0xf6, 0xbe, 0xed, 0x63, 0x54, 0x23, 0x8c, 0xb6, 0x50, 0x3d, 0x3b, 0x43, 0x96, 0x27, 0xe3, + 0x04, 0x84, 0xb9, 0x48, 0x05, 0x54, 0x2c, 0x53, 0x2e, 0x89, 0xe1, 0x13, 0x22, 0x6e, 0x05, 0xab, + 0xc4, 0x94, 0xb2, 0x62, 0x9c, 0xfe, 0x70, 0xe9, 0xa3, 0x51, 0xe7, 0xb8, 0x77, 0xea, 0xf5, 0xc6, + 0x9f, 0xdf, 0x70, 0x05, 0xdf, 0x38, 0xd3, 0x70, 0xfd, 0x71, 0x86, 0x2d, 0xe6, 0x28, 0x1b, 0x95, + 0x68, 0x24, 0x48, 0x5a, 0x94, 0x68, 0x30, 0x88, 0x37, 0xc3, 0x01, 0x28, 0xc2, 0x22, 0xc3, 0x67, + 0x10, 0x2b, 0xe3, 0x72, 0x62, 0x15, 0x18, 0xcd, 0xfe, 0x6d, 0x26, 0xbe, 0x30, 0xdf, 0xa0, 0x91, + 0x25, 0x58, 0xc9, 0x3e, 0xe4, 0x0a, 0x5d, 0xcf, 0xed, 0x4d, 0x06, 0xcd, 0xb3, 0xbd, 0x1e, 0x6e, + 0xf0, 0xb6, 0x2c, 0x1f, 0x32, 0x36, 0x7c, 0x75, 0xc4, 0x9d, 0x5c, 0x2e, 0x88, 0x34, 0x7d, 0x77, + 0x01, 0x16, 0x7a, 0xfd, 0x8b, 0x33, 0xfa, 0x27, 0x9a, 0x88, 0x7c, 0x61, 0x5f, 0x18, 0xee, 0x4f, + 0xe0, 0xc0, 0x6a, 0xc5, 0x1d, 0x8d, 0x9b, 0x43, 0xb7, 0x37, 0x42, 0x5c, 0xb7, 0x33, 0xe0, 0xde, + 0x6c, 0x89, 0x98, 0xa1, 0xa8, 0x63, 0x1c, 0xfb, 0xac, 0x4c, 0x83, 0x19, 0x2e, 0x8e, 0xca, 0x4b, + 0xa5, 0x4e, 0xcf, 0xed, 0x76, 0x3e, 0x10, 0x5e, 0x55, 0x4c, 0x5e, 0x3a, 0x12, 0x89, 0x56, 0x90, + 0xaf, 0x7d, 0x3d, 0x36, 0x6e, 0xac, 0x96, 0x39, 0x58, 0xe4, 0x3e, 0xb7, 0xcc, 0x07, 0xb5, 0x61, + 0xd4, 0x8a, 0x66, 0x6d, 0x57, 0x55, 0xc8, 0x2a, 0x40, 0xc3, 0xaa, 0x17, 0x0c, 0xdb, 0xa6, 0xbf, + 0x53, 0xf4, 0x37, 0x77, 0x50, 0x2d, 0xb5, 0x2a, 0x6a, 0x5a, 0xf2, 0x51, 0xcd, 0x68, 0x3f, 0x52, + 0xe0, 0x42, 0xf2, 0x50, 0x92, 0x26, 0xa0, 0x97, 0x32, 0x7f, 0x47, 0xfe, 0xd2, 0xcc, 0x71, 0x4f, + 0x4c, 0x8e, 0x7a, 0x3b, 0x8f, 0x99, 0x17, 0x6d, 0x4a, 0x3c, 0x16, 0x05, 0x81, 0xea, 0x3b, 0x6d, + 0xad, 0x00, 0x1b, 0xd3, 0xca, 0x08, 0x37, 0x75, 0x0d, 0x72, 0x7a, 0xa3, 0x51, 0x31, 0x0b, 0x7a, + 0xd3, 0xac, 0xd7, 0x54, 0x85, 0x2c, 0xc1, 0xfc, 0xae, 0x55, 0x6f, 0x35, 0xd4, 0x94, 0xf6, 0x03, + 0x05, 0x56, 0xcc, 0xc0, 0xca, 0xe4, 0x49, 0x17, 0xdf, 0xeb, 0xa1, 0xc5, 0xb7, 0xe1, 0xfb, 0xf3, + 0xfb, 0x1f, 0x38, 0xd3, 0xca, 0xfb, 0x07, 0x05, 0xd6, 0x63, 0x3c, 0xc4, 0x86, 0x45, 0xfd, 0xc0, + 0xae, 0x9b, 0xc5, 0x02, 0xaf, 0xd9, 0x95, 0xc0, 0x90, 0x05, 0x43, 0xb6, 0xc4, 0xbe, 0xc2, 0x7c, + 0xe0, 0x1e, 0x8c, 0x9c, 0x7e, 0xa7, 0x2d, 0x85, 0x5b, 0x2c, 0xcf, 0x59, 0xa2, 0x24, 0x3c, 0xc9, + 0x3e, 0x98, 0x0c, 0x3d, 0x2c, 0x36, 0x15, 0x52, 0x84, 0xfa, 0xe9, 0xf1, 0x82, 0xd1, 0x3e, 0xda, + 0xa5, 0xf9, 0xf1, 0xa2, 0x83, 0xf2, 0x76, 0x56, 0x20, 0xc7, 0x6f, 0x2d, 0x78, 0x21, 0xf8, 0x9e, + 0x02, 0x1b, 0xd3, 0xea, 0x4a, 0x2f, 0x42, 0x61, 0x87, 0xd8, 0x0b, 0x3e, 0x04, 0x7b, 0xd8, 0x13, + 0x56, 0x90, 0x91, 0x37, 0x21, 0x67, 0x8e, 0x46, 0x13, 0x6f, 0x68, 0xbf, 0xdc, 0xb2, 0x4c, 0x3e, + 0x41, 0x2e, 0xff, 0xfb, 0xc7, 0x57, 0x2e, 0xa2, 0x15, 0xf3, 0xd0, 0x19, 0xbd, 0xec, 0x4c, 0x86, + 0x9d, 0x10, 0x5c, 0xb5, 0xcc, 0xa1, 0x7d, 0x47, 0x81, 0xcd, 0xe9, 0x8d, 0xa4, 0xa7, 0x4c, 0x93, + 0xca, 0xe6, 0x81, 0x4f, 0x21, 0x9e, 0x32, 0x28, 0xaf, 0x47, 0x9c, 0x0a, 0x7d, 0x42, 0xca, 0xe4, + 0x87, 0x32, 0x4e, 0xc5, 0xe2, 0x97, 0x86, 0x99, 0x04, 0xa1, 0xf6, 0x1f, 0x29, 0xb8, 0x40, 0x27, + 0x50, 0xd7, 0x1b, 0x8d, 0xf4, 0xc9, 0xf8, 0xc4, 0xeb, 0x8d, 0xb9, 0x48, 0x45, 0x5e, 0x85, 0x85, + 0x93, 0xc7, 0x53, 0x1f, 0x32, 0x72, 0x42, 0x00, 0x37, 0x65, 0x61, 0x5f, 0x4d, 0xff, 0x27, 0x57, + 0x41, 0x8e, 0x18, 0x9b, 0x46, 0xcc, 0xba, 0xd4, 0x86, 0x62, 0x2d, 0x0d, 0xfc, 0xe0, 0x8e, 0xaf, + 0xc1, 0x3c, 0xaa, 0x0c, 0xf8, 0xf6, 0x28, 0xc4, 0xda, 0xe4, 0xda, 0xa1, 0x42, 0xc1, 0x62, 0x0c, + 0xe4, 0x25, 0x80, 0x00, 0xee, 0x9b, 0xef, 0x7f, 0xe2, 0x2a, 0xed, 0x23, 0x7e, 0x5b, 0x4b, 0xa7, + 0x47, 0x2e, 0xc7, 0xd0, 0xce, 0xc3, 0xba, 0xe8, 0x96, 0x81, 0x80, 0xba, 0xe2, 0x2f, 0x5b, 0x6b, + 0x2c, 0xc3, 0x1c, 0x08, 0xb8, 0xab, 0x6b, 0xb1, 0xa8, 0x97, 0x88, 0x78, 0x19, 0x09, 0x6d, 0x79, + 0x2d, 0x16, 0xda, 0x32, 0xcb, 0xa8, 0xe4, 0xf8, 0x95, 0xda, 0xbf, 0xa6, 0x60, 0xe9, 0x80, 0x0a, + 0x1e, 0x78, 0x9d, 0x9e, 0x7d, 0x3d, 0xbf, 0x03, 0xb9, 0x4a, 0xdf, 0xe5, 0x4f, 0x08, 0xdc, 0x2c, + 0x99, 0x39, 0xea, 0x75, 0xfb, 0xae, 0x78, 0x8d, 0x18, 0x59, 0x32, 0xd1, 0x23, 0x9c, 0x0c, 0xef, + 0xc1, 0x02, 0x7b, 0xd2, 0xe1, 0x9a, 0x22, 0x21, 0x7a, 0xfa, 0x35, 0x7a, 0x91, 0x65, 0x4b, 0x5a, + 0x6f, 0xf6, 0x2c, 0x24, 0xcb, 0x41, 0x1c, 0xb8, 0x4f, 0x52, 0x1e, 0xcc, 0x9f, 0x4d, 0x79, 0x20, + 0x01, 0x14, 0x2d, 0x9c, 0x05, 0xa0, 0x68, 0xf3, 0x2e, 0xe4, 0xa4, 0xfa, 0x3c, 0x96, 0x24, 0xfa, + 0x9b, 0x29, 0x58, 0xc1, 0x56, 0xf9, 0xf6, 0x1d, 0x3f, 0x9f, 0xaa, 0x90, 0xd7, 0x43, 0xaa, 0x90, + 0x0d, 0x79, 0xbc, 0x58, 0xcb, 0x66, 0xe8, 0x40, 0xee, 0xc1, 0x7a, 0x8c, 0x90, 0xbc, 0x02, 0xf3, + 0xb4, 0xfa, 0xe2, 0xea, 0xa8, 0x46, 0x67, 0x40, 0x00, 0x66, 0x49, 0x1b, 0x3e, 0xb2, 0x18, 0xb5, + 0xf6, 0xdf, 0x0a, 0x2c, 0x73, 0x2c, 0xf9, 0xde, 0x51, 0xff, 0x91, 0xdd, 0x79, 0x23, 0xda, 0x9d, + 0xcc, 0x65, 0x9e, 0x77, 0xe7, 0xff, 0x76, 0x27, 0xde, 0x0d, 0x75, 0xe2, 0x45, 0x1f, 0xda, 0x4a, + 0x34, 0x67, 0x46, 0x1f, 0xfe, 0x2d, 0x82, 0x3d, 0x86, 0x09, 0xc9, 0x37, 0x61, 0xa9, 0xe6, 0x3d, + 0x08, 0xdd, 0xc0, 0x6e, 0x4c, 0x29, 0xf4, 0x45, 0x9f, 0x90, 0xad, 0x29, 0x3c, 0xbc, 0x7a, 0xde, + 0x03, 0x27, 0xf6, 0x9a, 0x14, 0x14, 0x49, 0x2f, 0x61, 0x61, 0xb6, 0xc7, 0x99, 0xfa, 0xdc, 0x47, + 0x0a, 0x51, 0x20, 0xbe, 0x9b, 0x06, 0x08, 0xdc, 0x4b, 0xe8, 0x02, 0x0c, 0x3d, 0xa4, 0x0b, 0xe5, + 0x35, 0x26, 0xc9, 0x73, 0x5c, 0xbc, 0xaf, 0xdf, 0xe0, 0x4a, 0xd6, 0xd4, 0x74, 0xe8, 0x31, 0x54, + 0xb7, 0x16, 0xb8, 0x3f, 0x43, 0xdb, 0xeb, 0xba, 0x6c, 0x6f, 0x4f, 0xef, 0x5c, 0x43, 0xa4, 0x49, + 0x3f, 0x75, 0x4a, 0x50, 0x50, 0xf4, 0x7a, 0x28, 0x52, 0x82, 0x98, 0xcb, 0x56, 0xe6, 0xf1, 0x5c, + 0xb6, 0x1a, 0xb0, 0xd4, 0xe9, 0xbd, 0xef, 0xf5, 0xc6, 0xfd, 0xe1, 0x43, 0xd4, 0x2c, 0x07, 0x2a, + 0x2b, 0xda, 0x05, 0xa6, 0xc8, 0x63, 0xe3, 0x80, 0x07, 0xa3, 0x4f, 0x2f, 0x0f, 0x83, 0x9f, 0xe8, + 0xbb, 0x9c, 0xcd, 0xab, 0x0b, 0xf7, 0x32, 0xd9, 0x05, 0x75, 0xf1, 0x5e, 0x26, 0x9b, 0x55, 0x97, + 0xee, 0x65, 0xb2, 0x4b, 0x2a, 0x58, 0xd2, 0x5b, 0x8d, 0xff, 0x16, 0x23, 0x3d, 0x9f, 0x84, 0x9f, + 0x46, 0xb4, 0x9f, 0xa5, 0x80, 0xc4, 0xab, 0x41, 0x5e, 0x87, 0x1c, 0xdb, 0x60, 0x9d, 0xe1, 0xe8, + 0x5b, 0xdc, 0x82, 0x96, 0x61, 0x69, 0x48, 0xc9, 0x32, 0x96, 0x06, 0x4b, 0xb6, 0x46, 0xdf, 0xea, + 0x92, 0x6f, 0xc0, 0x39, 0xec, 0xde, 0x81, 0x37, 0xec, 0xf4, 0xdb, 0x0e, 0x02, 0x1f, 0xba, 0x5d, + 0x1e, 0xc0, 0xeb, 0x05, 0x8c, 0x34, 0x19, 0xcf, 0x9e, 0x32, 0x0c, 0xe8, 0x45, 0xd2, 0x40, 0xca, + 0x06, 0x23, 0x24, 0x4d, 0x50, 0x65, 0xfe, 0xa3, 0x49, 0xb7, 0xcb, 0x47, 0x36, 0x4f, 0x2f, 0xad, + 0xd1, 0xbc, 0x29, 0x05, 0xaf, 0x06, 0x05, 0x97, 0x26, 0xdd, 0x2e, 0x79, 0x15, 0xa0, 0xdf, 0x73, + 0x4e, 0x3b, 0xa3, 0x11, 0x7b, 0xaf, 0xf0, 0x1d, 0xde, 0x82, 0x54, 0x79, 0x30, 0xfa, 0xbd, 0x2a, + 0x4b, 0x24, 0xbf, 0x04, 0xe8, 0x82, 0x8b, 0xbe, 0xe9, 0xcc, 0x42, 0x85, 0x43, 0xf2, 0x8b, 0xc4, + 0xb0, 0x7f, 0xdd, 0xb1, 0x67, 0x77, 0x3e, 0x10, 0xd6, 0xcb, 0x6f, 0xc3, 0x3a, 0xb7, 0x0d, 0x3d, + 0xe8, 0x8c, 0x4f, 0xb8, 0xb4, 0xfc, 0x24, 0xa2, 0xb6, 0x24, 0x2e, 0xff, 0x53, 0x06, 0x40, 0x3f, + 0xb0, 0x05, 0xec, 0xcb, 0x2d, 0x98, 0xa7, 0x77, 0x00, 0xa1, 0x4b, 0x40, 0x4d, 0x2c, 0x96, 0x2b, + 0x6b, 0x62, 0x91, 0x82, 0xae, 0x46, 0x0b, 0x2d, 0xc8, 0x85, 0x1e, 0x01, 0x57, 0x23, 0x33, 0x2a, + 0x0f, 0xc1, 0x6e, 0x72, 0x2a, 0x52, 0x01, 0x08, 0x80, 0x58, 0xf8, 0xad, 0x74, 0x3d, 0x40, 0x34, + 0xe0, 0x19, 0x1c, 0xfa, 0x3b, 0x00, 0x73, 0x91, 0xa7, 0x4f, 0x40, 0x46, 0xf6, 0x20, 0xd3, 0x74, + 0x7d, 0x77, 0xae, 0x29, 0xf0, 0x34, 0xcf, 0xf0, 0x00, 0x6b, 0x01, 0x44, 0xcd, 0xea, 0xd8, 0x0d, + 0xc5, 0xa1, 0xc4, 0x42, 0x88, 0x01, 0x0b, 0x3c, 0x78, 0xee, 0x14, 0x58, 0x33, 0x1e, 0x3b, 0x97, + 0x83, 0x99, 0x62, 0xa2, 0x2c, 0x53, 0xf0, 0x30, 0xb9, 0x77, 0x20, 0x6d, 0xdb, 0x55, 0xee, 0x94, + 0xbd, 0x12, 0xdc, 0x30, 0x6c, 0xbb, 0x2a, 0x22, 0xc3, 0x9f, 0x4a, 0x6c, 0x94, 0x98, 0x7c, 0x19, + 0x72, 0x92, 0xf8, 0xcc, 0xe1, 0x0c, 0xb0, 0x0f, 0x24, 0x03, 0x7e, 0x79, 0xd3, 0x90, 0xa8, 0x49, + 0x05, 0xd4, 0xbd, 0xc9, 0xbb, 0x9e, 0x3e, 0x18, 0xa0, 0x27, 0xcd, 0xfb, 0xde, 0x90, 0x89, 0x6d, + 0xd9, 0x00, 0x07, 0x14, 0x1d, 0x91, 0xda, 0x22, 0x57, 0xd6, 0xa7, 0x44, 0x39, 0x49, 0x03, 0xd6, + 0x6d, 0x6f, 0x3c, 0x19, 0x30, 0x9b, 0x8b, 0x52, 0x7f, 0x48, 0x2f, 0x14, 0x0c, 0xfc, 0x00, 0x21, + 0x13, 0x47, 0x34, 0x53, 0x18, 0xba, 0x1c, 0xf5, 0x87, 0x91, 0xcb, 0x45, 0x9c, 0x59, 0xf3, 0xe4, + 0x21, 0xa7, 0xa7, 0x6a, 0xf8, 0x9a, 0x82, 0xa7, 0xaa, 0xb8, 0xa6, 0x04, 0x97, 0x93, 0x97, 0x12, + 0x00, 0x7a, 0xf0, 0xf1, 0x4b, 0x02, 0xe8, 0x09, 0xc1, 0xf2, 0x7c, 0x98, 0x91, 0x30, 0xe2, 0xf8, + 0x58, 0xbc, 0x01, 0x70, 0xaf, 0xdf, 0xe9, 0x55, 0xbd, 0xf1, 0x49, 0xbf, 0x2d, 0xe1, 0x04, 0xe5, + 0x7e, 0xb5, 0xdf, 0xe9, 0x39, 0xa7, 0x98, 0xfc, 0xb3, 0x8f, 0xaf, 0x48, 0x44, 0x96, 0xf4, 0x3f, + 0xf9, 0x02, 0x2c, 0xd1, 0x5f, 0xcd, 0xc0, 0x72, 0x84, 0xa9, 0x1d, 0x91, 0x9b, 0x21, 0xa9, 0x07, + 0x04, 0xe4, 0x2e, 0xc6, 0x0e, 0xe8, 0x0c, 0xc6, 0x92, 0xf0, 0x2a, 0x02, 0x05, 0x74, 0x06, 0xe3, + 0x28, 0xec, 0xa7, 0x44, 0x4c, 0xca, 0x7e, 0xd5, 0x45, 0xb8, 0x0f, 0x1e, 0xa2, 0x00, 0x75, 0x6b, + 0x7c, 0xae, 0x39, 0x02, 0x6f, 0x50, 0x0e, 0xcc, 0x18, 0x61, 0xc3, 0x4a, 0xd8, 0xe5, 0x22, 0x7b, + 0x0c, 0xe1, 0x42, 0xad, 0x88, 0x6c, 0xdf, 0x76, 0x0e, 0x31, 0x39, 0x54, 0x09, 0x9f, 0x98, 0xec, + 0xc0, 0x1a, 0x93, 0xf1, 0xfd, 0xb0, 0x61, 0x5c, 0xc4, 0xc5, 0xbd, 0x2d, 0x88, 0x2b, 0x26, 0x7f, + 0x3e, 0xc2, 0x40, 0x4a, 0x30, 0x8f, 0x17, 0x42, 0x6e, 0xf9, 0xbd, 0x25, 0xdf, 0x84, 0xa3, 0xeb, + 0x08, 0xf7, 0x15, 0xbc, 0x03, 0xcb, 0xfb, 0x0a, 0x92, 0x92, 0xaf, 0x01, 0x18, 0xbd, 0x61, 0xbf, + 0xdb, 0x45, 0x44, 0xcc, 0x2c, 0x5e, 0xa5, 0x2e, 0x87, 0xd7, 0x23, 0x96, 0x12, 0x10, 0x71, 0xf4, + 0x26, 0xfc, 0xed, 0x44, 0x70, 0x33, 0xa5, 0xb2, 0x34, 0x13, 0x16, 0xd8, 0x62, 0x44, 0x74, 0x59, + 0x8e, 0x97, 0x2f, 0x61, 0x93, 0x32, 0x74, 0x59, 0x9e, 0x1e, 0x47, 0x97, 0x95, 0x18, 0xb4, 0x3d, + 0x38, 0x9f, 0xd4, 0xb0, 0xd0, 0x15, 0x56, 0x39, 0xeb, 0x15, 0xf6, 0xcf, 0xd3, 0xb0, 0x8c, 0xa5, + 0x89, 0x5d, 0x58, 0x87, 0x15, 0x7b, 0xf2, 0xae, 0x0f, 0xbd, 0x22, 0x76, 0x63, 0xac, 0xdf, 0x48, + 0xce, 0x90, 0x9f, 0xa9, 0x42, 0x1c, 0xc4, 0x80, 0x55, 0x71, 0x12, 0xec, 0x0a, 0x6b, 0x72, 0x1f, + 0xd8, 0x55, 0xc0, 0x87, 0xc5, 0xc3, 0x26, 0x46, 0x98, 0x82, 0xf3, 0x20, 0xfd, 0x38, 0xe7, 0x41, + 0xe6, 0x4c, 0xe7, 0xc1, 0x3b, 0xb0, 0x2c, 0xbe, 0x86, 0x3b, 0xf9, 0xfc, 0x93, 0xed, 0xe4, 0xa1, + 0xc2, 0x48, 0xc5, 0xdf, 0xd1, 0x17, 0x66, 0xee, 0xe8, 0xf8, 0xf6, 0x27, 0x56, 0x59, 0x2c, 0x12, + 0x3a, 0x2f, 0x03, 0xe3, 0x8a, 0xed, 0x16, 0x1a, 0x9f, 0xe2, 0x94, 0x7c, 0x05, 0x96, 0x2a, 0x7d, + 0xf1, 0xec, 0x23, 0xe9, 0xdb, 0xbb, 0x22, 0x51, 0x16, 0x17, 0x7c, 0x4a, 0xff, 0x74, 0x4b, 0x7f, + 0x16, 0xa7, 0xdb, 0x5d, 0x00, 0xee, 0xa6, 0x10, 0xc4, 0x03, 0xc2, 0x25, 0x23, 0x9c, 0xdc, 0xc3, + 0x6a, 0x7f, 0x89, 0x98, 0xee, 0x4e, 0xdc, 0xa2, 0x44, 0x3f, 0x3c, 0xec, 0x4f, 0x7a, 0xe3, 0x50, + 0x00, 0x4d, 0xe1, 0x94, 0xe5, 0xf2, 0x3c, 0x79, 0x7b, 0x88, 0xb0, 0x7d, 0xb6, 0x03, 0x42, 0xde, + 0xf2, 0x0d, 0xe2, 0x16, 0x67, 0xf5, 0x90, 0x16, 0xeb, 0xa1, 0xa9, 0x66, 0x70, 0xda, 0x8f, 0x14, + 0x19, 0x55, 0xfb, 0x53, 0x0c, 0xf5, 0x6b, 0x00, 0xfe, 0xbb, 0xbb, 0x18, 0x6b, 0x76, 0x5f, 0xf2, + 0x53, 0xe5, 0x5e, 0x0e, 0x68, 0xa5, 0xd6, 0xa4, 0x3f, 0xab, 0xd6, 0x34, 0x21, 0x57, 0x7f, 0x6f, + 0xec, 0x06, 0x86, 0x1a, 0x60, 0xfb, 0x92, 0x2c, 0xee, 0x4c, 0xe9, 0x9d, 0xeb, 0x78, 0x36, 0x04, + 0x72, 0xf0, 0x14, 0x11, 0x58, 0x62, 0xd4, 0xde, 0x82, 0x35, 0xd9, 0x91, 0xf4, 0x61, 0xef, 0x90, + 0x7c, 0x85, 0x61, 0xfc, 0x29, 0xa1, 0x1b, 0x8b, 0x44, 0x44, 0x77, 0xdc, 0x87, 0xbd, 0x43, 0x26, + 0xff, 0xb8, 0x0f, 0xe4, 0xba, 0xe2, 0x1d, 0xef, 0x27, 0x0a, 0x90, 0x38, 0xb9, 0xbc, 0x9b, 0x28, + 0xff, 0x07, 0xd2, 0x65, 0x44, 0x2a, 0xcb, 0x3c, 0x8e, 0x54, 0x96, 0xff, 0x03, 0x05, 0xd6, 0x4c, + 0xbd, 0xca, 0x21, 0xb0, 0xd9, 0xfb, 0xc1, 0x55, 0xb8, 0x6c, 0xea, 0x55, 0xa7, 0x51, 0xaf, 0x98, + 0x85, 0xfb, 0x4e, 0x22, 0xb2, 0xe5, 0x65, 0x78, 0x3a, 0x4e, 0x12, 0xbc, 0x33, 0x5c, 0x82, 0x8d, + 0x78, 0xb6, 0x40, 0xbf, 0x4c, 0x66, 0x16, 0x40, 0x99, 0xe9, 0xfc, 0x9b, 0xb0, 0x26, 0x90, 0x1e, + 0x9b, 0x15, 0x1b, 0xb1, 0xa4, 0xd7, 0x20, 0xb7, 0x6f, 0x58, 0x66, 0xe9, 0xbe, 0x53, 0x6a, 0x55, + 0x2a, 0xea, 0x1c, 0x59, 0x81, 0x25, 0x9e, 0x50, 0xd0, 0x55, 0x85, 0x2c, 0x43, 0xd6, 0xac, 0xd9, + 0x46, 0xa1, 0x65, 0x19, 0x6a, 0x2a, 0xff, 0x26, 0xac, 0x36, 0x86, 0x9d, 0xf7, 0xdd, 0xb1, 0xb7, + 0xe7, 0x3d, 0xc4, 0x67, 0x82, 0x45, 0x48, 0x5b, 0xfa, 0x81, 0x3a, 0x47, 0x00, 0x16, 0x1a, 0x7b, + 0x05, 0xfb, 0xf6, 0x6d, 0x55, 0x21, 0x39, 0x58, 0xdc, 0x2d, 0x34, 0x9c, 0xbd, 0xaa, 0xad, 0xa6, + 0xe8, 0x0f, 0xfd, 0xc0, 0xc6, 0x1f, 0xe9, 0xfc, 0x17, 0x61, 0x1d, 0x65, 0x85, 0x4a, 0x67, 0x34, + 0xf6, 0x7a, 0xde, 0x10, 0xeb, 0xb0, 0x0c, 0x59, 0xdb, 0xa3, 0x8b, 0x7c, 0xec, 0xb1, 0x0a, 0x54, + 0x27, 0xdd, 0x71, 0x67, 0xd0, 0xf5, 0xbe, 0xad, 0x2a, 0xf9, 0xbb, 0xb0, 0x66, 0xf5, 0x27, 0xe3, + 0x4e, 0xef, 0xd8, 0x1e, 0x53, 0x8a, 0xe3, 0x87, 0xe4, 0x29, 0x58, 0x6f, 0xd5, 0xf4, 0xea, 0x8e, + 0xb9, 0xdb, 0xaa, 0xb7, 0x6c, 0xa7, 0xaa, 0x37, 0x0b, 0x65, 0xf6, 0x48, 0x51, 0xad, 0xdb, 0x4d, + 0xc7, 0x32, 0x0a, 0x46, 0xad, 0xa9, 0x2a, 0xf9, 0xef, 0xa3, 0xda, 0xe3, 0xb0, 0xdf, 0x6b, 0x97, + 0x5c, 0x8c, 0x2d, 0x4e, 0x2b, 0xac, 0xc1, 0xb6, 0x6d, 0x14, 0xea, 0xb5, 0xa2, 0x53, 0xd2, 0x0b, + 0xcd, 0xba, 0x95, 0x04, 0xad, 0xba, 0x09, 0x17, 0x12, 0x68, 0xea, 0xcd, 0x86, 0xaa, 0x90, 0x2b, + 0xb0, 0x95, 0x90, 0x77, 0x60, 0xec, 0xe8, 0xad, 0x66, 0xb9, 0xa6, 0xa6, 0xa6, 0x30, 0xdb, 0x76, + 0x5d, 0x4d, 0xe7, 0x7f, 0x57, 0x81, 0xd5, 0xd6, 0x88, 0x5b, 0x08, 0xb7, 0xd0, 0xcf, 0xef, 0x19, + 0xb8, 0xd4, 0xb2, 0x0d, 0xcb, 0x69, 0xd6, 0xf7, 0x8c, 0x9a, 0xd3, 0xb2, 0xf5, 0xdd, 0x68, 0x6d, + 0xae, 0xc0, 0x96, 0x44, 0x61, 0x19, 0x85, 0xfa, 0xbe, 0x61, 0x39, 0x0d, 0xdd, 0xb6, 0x0f, 0xea, + 0x56, 0x51, 0x55, 0xe8, 0x17, 0x13, 0x08, 0xaa, 0x25, 0x9d, 0xd5, 0x26, 0x94, 0x57, 0x33, 0x0e, + 0xf4, 0x8a, 0xb3, 0x53, 0x6f, 0xaa, 0xe9, 0x7c, 0x95, 0x1e, 0xbd, 0x08, 0x70, 0xc8, 0xec, 0xda, + 0xb2, 0x90, 0xa9, 0xd5, 0x6b, 0x46, 0xf4, 0x69, 0x6b, 0x19, 0xb2, 0x7a, 0xa3, 0x61, 0xd5, 0xf7, + 0x71, 0x8a, 0x01, 0x2c, 0x14, 0x8d, 0x1a, 0xad, 0x59, 0x9a, 0xe6, 0x34, 0xac, 0x7a, 0xb5, 0xde, + 0x34, 0x8a, 0x6a, 0x26, 0x6f, 0x89, 0x25, 0x2c, 0x0a, 0x3d, 0xec, 0xb3, 0x77, 0xa4, 0xa2, 0x51, + 0xd2, 0x5b, 0x95, 0x26, 0x1f, 0xa2, 0xfb, 0x8e, 0x65, 0xbc, 0xd5, 0x32, 0xec, 0xa6, 0xad, 0x2a, + 0x44, 0x85, 0xe5, 0x9a, 0x61, 0x14, 0x6d, 0xc7, 0x32, 0xf6, 0x4d, 0xe3, 0x40, 0x4d, 0xd1, 0x32, + 0xd9, 0xff, 0xf4, 0x0b, 0xf9, 0x0f, 0x15, 0x20, 0x0c, 0x1c, 0x52, 0x44, 0x1c, 0xc0, 0x19, 0xb3, + 0x0d, 0x9b, 0x65, 0x3a, 0xd4, 0xd8, 0xb4, 0x6a, 0xbd, 0x18, 0xed, 0xb2, 0x0b, 0x40, 0x22, 0xf9, + 0xf5, 0x52, 0x49, 0x55, 0xc8, 0x16, 0x9c, 0x8b, 0xa4, 0x17, 0xad, 0x7a, 0x43, 0x4d, 0x6d, 0xa6, + 0xb2, 0x0a, 0xb9, 0x18, 0xcb, 0xdc, 0x33, 0x8c, 0x86, 0x9a, 0xa6, 0x43, 0x14, 0xc9, 0x10, 0x4b, + 0x82, 0xb1, 0x67, 0xf2, 0xdf, 0x51, 0xe0, 0x02, 0xab, 0xa6, 0x58, 0x5f, 0x7e, 0x55, 0x2f, 0xc1, + 0x06, 0x87, 0xbc, 0x4d, 0xaa, 0xe8, 0x79, 0x50, 0x43, 0xb9, 0xac, 0x9a, 0x4f, 0xc1, 0x7a, 0x28, + 0x15, 0xeb, 0x91, 0xa2, 0xbb, 0x47, 0x28, 0x79, 0xc7, 0xb0, 0x9b, 0x8e, 0x51, 0x2a, 0xd5, 0xad, + 0x26, 0xab, 0x48, 0x3a, 0xaf, 0xc1, 0x7a, 0xc1, 0x1b, 0x8e, 0xe9, 0xad, 0xa8, 0x37, 0xea, 0xf4, + 0x7b, 0x58, 0x85, 0x15, 0x58, 0x32, 0xbe, 0xd6, 0x34, 0x6a, 0xb6, 0x59, 0xaf, 0xa9, 0x73, 0xf9, + 0x4b, 0x11, 0x1a, 0xb1, 0x8e, 0x6d, 0xbb, 0xac, 0xce, 0xe5, 0x5d, 0x58, 0x11, 0xb6, 0xb8, 0x6c, + 0x56, 0x6c, 0xc3, 0xa6, 0x98, 0x6b, 0xb8, 0xa3, 0x44, 0x9b, 0xb0, 0x01, 0xe7, 0xe3, 0xf9, 0x46, + 0x53, 0x55, 0xe8, 0x28, 0x44, 0x72, 0x68, 0x7a, 0x2a, 0xff, 0xdb, 0x0a, 0xac, 0xf8, 0xef, 0x19, + 0xa8, 0x41, 0xbd, 0x02, 0x5b, 0xd5, 0x92, 0xee, 0x14, 0x8d, 0x7d, 0xb3, 0x60, 0x38, 0x7b, 0x66, + 0xad, 0x18, 0xf9, 0xc8, 0xd3, 0xf0, 0x54, 0x02, 0x01, 0x7e, 0x65, 0x03, 0xce, 0x47, 0xb3, 0x9a, + 0x74, 0xa9, 0xa6, 0x68, 0xd7, 0x47, 0x73, 0xfc, 0x75, 0x9a, 0xce, 0xff, 0x99, 0x02, 0x1b, 0x3c, + 0xa4, 0x33, 0x7f, 0x59, 0x61, 0x58, 0xff, 0x08, 0x86, 0x99, 0x87, 0x1b, 0x4d, 0xab, 0x65, 0x37, + 0x8d, 0xa2, 0x60, 0xa7, 0x93, 0xd6, 0xb4, 0x8c, 0xaa, 0x51, 0x6b, 0x46, 0xea, 0xf6, 0x3c, 0x3c, + 0x37, 0x83, 0xb6, 0x56, 0x6f, 0x8a, 0xdf, 0x74, 0xad, 0x3e, 0x07, 0xcf, 0xce, 0x20, 0xf6, 0x09, + 0x53, 0xf9, 0x7d, 0x58, 0xb5, 0xf5, 0x6a, 0xa5, 0xd4, 0x1f, 0x1e, 0x7a, 0xfa, 0x64, 0x7c, 0xd2, + 0x23, 0x5b, 0x70, 0xb1, 0x54, 0xb7, 0x0a, 0x86, 0x83, 0x2d, 0x88, 0x54, 0xe2, 0x1c, 0xac, 0xc9, + 0x99, 0xf7, 0x0d, 0xba, 0xba, 0x08, 0xac, 0xca, 0x89, 0xb5, 0xba, 0x9a, 0xca, 0x7f, 0x1d, 0x96, + 0x43, 0x71, 0x91, 0x2e, 0xc2, 0x39, 0xf9, 0x77, 0xc3, 0xeb, 0xb5, 0x3b, 0xbd, 0x63, 0x75, 0x2e, + 0x9a, 0x61, 0x4d, 0x7a, 0x3d, 0x9a, 0x81, 0xdb, 0x8d, 0x9c, 0xd1, 0xf4, 0x86, 0xa7, 0x9d, 0x9e, + 0x3b, 0xf6, 0xda, 0x6a, 0x2a, 0xff, 0x22, 0xac, 0x84, 0xd0, 0x58, 0xe9, 0xbc, 0xaa, 0xd4, 0xf9, + 0xf9, 0x50, 0x35, 0x8a, 0x66, 0xab, 0xaa, 0xce, 0xd3, 0x8d, 0xa6, 0x6c, 0xee, 0x96, 0x55, 0xc8, + 0xff, 0x40, 0xa1, 0x37, 0x14, 0xec, 0xf7, 0x6a, 0x49, 0x17, 0x33, 0x91, 0xae, 0x02, 0x86, 0xf1, + 0x6c, 0xd8, 0x36, 0x7b, 0x70, 0xbe, 0x04, 0x1b, 0xfc, 0x87, 0xa3, 0xd7, 0x8a, 0x4e, 0x59, 0xb7, + 0x8a, 0x07, 0xba, 0x45, 0x97, 0xc6, 0x7d, 0x35, 0x85, 0xeb, 0x5d, 0x4a, 0x71, 0x9a, 0xf5, 0x56, + 0xa1, 0xac, 0xa6, 0xe9, 0xf2, 0x0a, 0xa5, 0x37, 0xcc, 0x9a, 0x9a, 0xc1, 0xdd, 0x23, 0x46, 0x8d, + 0xc5, 0xd2, 0xfc, 0xf9, 0xfc, 0x27, 0x0a, 0x5c, 0xb4, 0x3b, 0xc7, 0x3d, 0x77, 0x3c, 0x19, 0x7a, + 0x7a, 0xf7, 0xb8, 0x3f, 0xec, 0x8c, 0x4f, 0x4e, 0xed, 0x49, 0x67, 0xec, 0x91, 0x5b, 0x70, 0xdd, + 0x36, 0x77, 0x6b, 0x7a, 0x93, 0xae, 0x7e, 0xbd, 0xb2, 0x5b, 0xb7, 0xcc, 0x66, 0xb9, 0xea, 0xd8, + 0x2d, 0x33, 0xb6, 0x30, 0xae, 0xc1, 0x33, 0xd3, 0x49, 0x2b, 0xc6, 0xae, 0x5e, 0xb8, 0xaf, 0x2a, + 0xb3, 0x0b, 0xdc, 0xd1, 0x2b, 0x7a, 0xad, 0x60, 0x14, 0x9d, 0xfd, 0xdb, 0x6a, 0x8a, 0x5c, 0x87, + 0xab, 0xd3, 0x49, 0x4b, 0x66, 0xc3, 0xa6, 0x64, 0xe9, 0xd9, 0xdf, 0x2d, 0xdb, 0x55, 0x4a, 0x95, + 0xc9, 0x77, 0x40, 0x8d, 0xba, 0xa7, 0xc7, 0xcc, 0x1b, 0xac, 0x56, 0xad, 0xc6, 0xce, 0x80, 0x35, + 0xc8, 0xd5, 0x9b, 0x65, 0xc3, 0xe2, 0x50, 0xe3, 0x88, 0x2d, 0xde, 0xaa, 0xd1, 0x69, 0x55, 0xb7, + 0xcc, 0xb7, 0xf1, 0x30, 0xd8, 0x80, 0xf3, 0x76, 0x45, 0x2f, 0xec, 0xe1, 0x8c, 0x37, 0x6b, 0x4e, + 0xa1, 0xac, 0xd7, 0x6a, 0x46, 0x45, 0x85, 0xfc, 0x9f, 0x2a, 0xcc, 0xce, 0x20, 0xc9, 0x8f, 0x8d, + 0x7c, 0x01, 0x6e, 0xd6, 0xf7, 0x9a, 0xba, 0xd3, 0xa8, 0xb4, 0x76, 0xcd, 0x9a, 0x63, 0xdf, 0xaf, + 0x15, 0x84, 0xe0, 0x52, 0x88, 0xef, 0x97, 0x37, 0xe1, 0xda, 0x4c, 0xea, 0x00, 0x14, 0xfc, 0x06, + 0x68, 0x33, 0x29, 0x79, 0x43, 0xf2, 0x3f, 0x56, 0x60, 0x6b, 0xc6, 0xdb, 0x2c, 0x79, 0x01, 0x6e, + 0x95, 0x0d, 0xbd, 0x58, 0x31, 0x6c, 0x1b, 0x97, 0x91, 0x51, 0x6b, 0x72, 0x33, 0x88, 0xc4, 0xdd, + 0xf0, 0x16, 0x5c, 0x9f, 0x4d, 0x1e, 0x9c, 0xab, 0x37, 0xe1, 0xda, 0x6c, 0x52, 0x7e, 0xce, 0xa6, + 0xe8, 0x6e, 0x34, 0x9b, 0xd2, 0x3f, 0x9f, 0xd3, 0xf9, 0xef, 0x29, 0x70, 0x21, 0x59, 0x41, 0x42, + 0xeb, 0x66, 0xd6, 0xec, 0xa6, 0x5e, 0xa9, 0x38, 0x0d, 0xdd, 0xd2, 0xab, 0x8e, 0x51, 0xb3, 0xea, + 0x95, 0x4a, 0xd2, 0xb9, 0x74, 0x0d, 0x9e, 0x99, 0x4e, 0x6a, 0x17, 0x2c, 0xb3, 0x41, 0xb7, 0x5e, + 0x0d, 0xb6, 0xa7, 0x53, 0x19, 0x66, 0xc1, 0x50, 0x53, 0x3b, 0x6f, 0x7c, 0xf4, 0x2f, 0xdb, 0x73, + 0x1f, 0x7d, 0xb2, 0xad, 0xfc, 0xe4, 0x93, 0x6d, 0xe5, 0x9f, 0x3f, 0xd9, 0x56, 0xde, 0x7e, 0xfe, + 0x6c, 0xf1, 0x34, 0x50, 0x68, 0x7f, 0x77, 0x01, 0xed, 0x7e, 0x5e, 0xfe, 0x9f, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xee, 0xd6, 0xab, 0xfe, 0x26, 0xa9, 0x01, 0x00, } func (this *PluginSpecV1) Equal(that interface{}) bool { @@ -23555,6 +23565,17 @@ func (this *PluginAWSICSettings) Equal(that interface{}) bool { if !this.ProvisioningSpec.Equal(that1.ProvisioningSpec) { return false } + if len(this.AccessListDefaultOwners) != len(that1.AccessListDefaultOwners) { + return false + } + for i := range this.AccessListDefaultOwners { + if this.AccessListDefaultOwners[i] != that1.AccessListDefaultOwners[i] { + return false + } + } + if this.SamlIdpServiceProviderName != that1.SamlIdpServiceProviderName { + return false + } if !bytes.Equal(this.XXX_unrecognized, that1.XXX_unrecognized) { return false } @@ -43590,6 +43611,22 @@ func (m *PluginAWSICSettings) MarshalToSizedBuffer(dAtA []byte) (int, error) { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } + if len(m.SamlIdpServiceProviderName) > 0 { + i -= len(m.SamlIdpServiceProviderName) + copy(dAtA[i:], m.SamlIdpServiceProviderName) + i = encodeVarintTypes(dAtA, i, uint64(len(m.SamlIdpServiceProviderName))) + i-- + dAtA[i] = 0x32 + } + if len(m.AccessListDefaultOwners) > 0 { + for iNdEx := len(m.AccessListDefaultOwners) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.AccessListDefaultOwners[iNdEx]) + copy(dAtA[i:], m.AccessListDefaultOwners[iNdEx]) + i = encodeVarintTypes(dAtA, i, uint64(len(m.AccessListDefaultOwners[iNdEx]))) + i-- + dAtA[i] = 0x2a + } + } if m.ProvisioningSpec != nil { { size, err := m.ProvisioningSpec.MarshalToSizedBuffer(dAtA[:i]) @@ -56479,6 +56516,16 @@ func (m *PluginAWSICSettings) Size() (n int) { l = m.ProvisioningSpec.Size() n += 1 + l + sovTypes(uint64(l)) } + if len(m.AccessListDefaultOwners) > 0 { + for _, s := range m.AccessListDefaultOwners { + l = len(s) + n += 1 + l + sovTypes(uint64(l)) + } + } + l = len(m.SamlIdpServiceProviderName) + if l > 0 { + n += 1 + l + sovTypes(uint64(l)) + } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } @@ -116906,6 +116953,70 @@ func (m *PluginAWSICSettings) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field AccessListDefaultOwners", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.AccessListDefaultOwners = append(m.AccessListDefaultOwners, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex + case 6: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field SamlIdpServiceProviderName", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.SamlIdpServiceProviderName = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipTypes(dAtA[iNdEx:]) diff --git a/docs/pages/admin-guides/teleport-policy/integrations/aws-sync.mdx b/docs/pages/admin-guides/teleport-policy/integrations/aws-sync.mdx index cb35dc9c2c06..2aae7cd963fb 100644 --- a/docs/pages/admin-guides/teleport-policy/integrations/aws-sync.mdx +++ b/docs/pages/admin-guides/teleport-policy/integrations/aws-sync.mdx @@ -60,12 +60,13 @@ graphical representation thereof. ## Prerequisites - A running Teleport Enterprise cluster v14.3.9/v15.2.0 or later. -- For self-hosted clusters, an updated `license.pem` with Teleport Policy enabled. -- For self-hosted clusters, a running Access Graph node v1.17.0 or later. -Check [Access Graph page](../teleport-policy.mdx) for details on +- Teleport Policy enabled for your account. +- For self-hosted clusters: + - Ensure that an up-to-date `license.pem` is used in the Auth Service configuration. + - A running Access Graph node v1.17.0 or later. +Check the [Teleport Policy page](../teleport-policy.mdx) for details on how to set up Access Graph. -- The node running the Access Graph service must be reachable -from Teleport Auth Service and Discovery Service. + - The node running the Access Graph service must be reachable from the Teleport Auth Service. ## Step 1/2. Configure Discovery Service (Self-hosted only) diff --git a/docs/pages/admin-guides/teleport-policy/integrations/entra-id.mdx b/docs/pages/admin-guides/teleport-policy/integrations/entra-id.mdx index 67d9736ed8ff..da9b9e7feff9 100644 --- a/docs/pages/admin-guides/teleport-policy/integrations/entra-id.mdx +++ b/docs/pages/admin-guides/teleport-policy/integrations/entra-id.mdx @@ -35,11 +35,12 @@ These resources are then visualized using the graph representation detailed in t - A running Teleport Enterprise cluster v15.4.2/v16.0.0 or later. - Teleport Identity and Teleport Policy enabled for your account. - - For self-hosted clusters, ensure that an up-to-date `license.pem` is used in the Auth Service configuration. -- For self-hosted clusters, a running Access Graph node v1.21.3 or later. +- For self-hosted clusters: + - Ensure that an up-to-date `license.pem` is used in the Auth Service configuration. + - A running Access Graph node v1.21.3 or later. Check the [Teleport Policy page](../teleport-policy.mdx) for details on how to set up Access Graph. -- The node running the Access Graph service must be reachable from the Teleport Auth Service. + - The node running the Access Graph service must be reachable from the Teleport Auth Service. - Your user must have privileged administrator permissions in the Azure account To verify that Access Graph is set up correctly for your cluster, sign in to the Teleport Web UI and navigate to the Management tab. diff --git a/docs/pages/admin-guides/teleport-policy/integrations/gitlab.mdx b/docs/pages/admin-guides/teleport-policy/integrations/gitlab.mdx index 83cc19350707..3a25ef7ad225 100644 --- a/docs/pages/admin-guides/teleport-policy/integrations/gitlab.mdx +++ b/docs/pages/admin-guides/teleport-policy/integrations/gitlab.mdx @@ -46,13 +46,14 @@ graphical representation thereof. ## Prerequisites - A running Teleport Enterprise cluster v14.3.20/v15.3.1/v16.0.0 or later. -- For self-hosted clusters, an updated `license.pem` with Teleport Policy enabled. -- For self-hosted clusters, a running Access Graph node v1.21.4 or later. -Check [Access Graph page](../teleport-policy.mdx) for details on -how to set up Access Graph. -- For self-hosted clusters, the node running the Access Graph service must be reachable -from Teleport Auth Service. +- Teleport Policy enabled for your account. - A GitLab instance running GitLab v9.0 or later. +- For self-hosted clusters: + - Ensure that an up-to-date `license.pem` is used in the Auth Service configuration. + - A running Access Graph node v1.21.4 or later. +Check the [Teleport Policy page](../teleport-policy.mdx) for details on +how to set up Access Graph. + - The node running the Access Graph service must be reachable from the Teleport Auth Service. ## Step 1/3. Create GitLab token diff --git a/docs/pages/admin-guides/teleport-policy/integrations/ssh-keys-scan.mdx b/docs/pages/admin-guides/teleport-policy/integrations/ssh-keys-scan.mdx index 8aa1b8eac451..8c50d3ad2da9 100644 --- a/docs/pages/admin-guides/teleport-policy/integrations/ssh-keys-scan.mdx +++ b/docs/pages/admin-guides/teleport-policy/integrations/ssh-keys-scan.mdx @@ -70,15 +70,16 @@ It also never sends the private key path or any other sensitive information. ## Prerequisites - A running Teleport Enterprise cluster v15.4.16/v16.2.0 or later. -- For self-hosted clusters, an updated `license.pem` with Teleport Policy enabled. -- For self-hosted clusters, a running Access Graph node v1.22.0 or later. -Check [Access Graph page](../teleport-policy.mdx) for details on -how to set up Access Graph. -- For self-hosted clusters, the node running the Access Graph service must be reachable -from Teleport Auth Service. +- Teleport Policy enabled for your account. - A Linux/macOS server running the Teleport SSH Service. - Devices enrolled in the [Teleport Device Trust feature](../../access-controls/device-trust.mdx). - For Jamf Pro integration, devices must be enrolled in Jamf Pro and have the signed `tsh` binary installed. +- For self-hosted clusters: + - Ensure that an up-to-date `license.pem` is used in the Auth Service configuration. + - A running Access Graph node v1.22.0 or later. +Check the [Teleport Policy page](../teleport-policy.mdx) for details on +how to set up Access Graph. + - The node running the Access Graph service must be reachable from the Teleport Auth Service. ## Step 1/3. Enable SSH Key Scanning diff --git a/docs/pages/includes/metrics.mdx b/docs/pages/includes/metrics.mdx index d1a64ba4bf24..e875f84e7596 100644 --- a/docs/pages/includes/metrics.mdx +++ b/docs/pages/includes/metrics.mdx @@ -15,8 +15,8 @@ | `backend_batch_write_seconds` | histogram | cache | Latency for backend batch write operations. | | `backend_read_requests_total` | counter | cache | Number of read requests to the backend. | | `backend_read_seconds` | histogram | cache | Latency for read operations. | -| `backend_requests` | counter | cache | Number of requests to the backend (reads, writes, and keepalives). | -| `backend_write_requests_total` | counter | cache | Number of write requests to the backend. | +| `backend_requests` | counter | cache | Number of requests to the backend (reads, writes, and keepalives). | +| `backend_write_requests_total` | counter | cache | Number of write requests to the backend. | | `backend_write_seconds` | histogram | cache | Latency for backend write operations. | | `cluster_name_not_found_total` | counter | Teleport Auth | Number of times a cluster was not found. | | `dynamo_requests_total` | counter | DynamoDB | Total number of requests to the DynamoDB API. | @@ -31,7 +31,7 @@ | `etcd_backend_write_requests` | counter | etcd | Number of write requests to the database. | | `etcd_backend_write_seconds` | histogram | etcd | Latency for etcd write operations. | | `teleport_etcd_events` | counter | etcd | Total number of etcd events processed. | -| `teleport_etcd_event_backpressure` | counter | etcd | Total number of times event processing encountered backpressure. | +| `teleport_etcd_event_backpressure` | counter | etcd | Total number of times event processing encountered backpressure. | | `firestore_events_backend_batch_read_requests` | counter | GCP Cloud Firestore | Number of batch read requests to Cloud Firestore events. | | `firestore_events_backend_batch_read_seconds` | histogram | GCP Cloud Firestore | Latency for Cloud Firestore events batch read operations. | | `firestore_events_backend_batch_write_requests` | counter | GCP Cloud Firestore | Number of batch write requests to Cloud Firestore events. | @@ -59,17 +59,18 @@ | `teleport_audit_parquetlog_last_processed_timestamp` | gauge | Teleport Audit Log | Number of last processing time in Parquet-format audit log. | | `teleport_audit_parquetlog_age_oldest_processed_message` | gauge | Teleport Audit Log | Number of age of oldest event in Parquet-format audit log. | | `teleport_audit_parquetlog_errors_from_collect_count` | counter | Teleport Audit Log | Number of collect failures in Parquet-format audit log. | +| `teleport_connected_resources` | gauge | Teleport Auth | Number and type of resources connected via keepalives. x | | `teleport_postgres_events_backend_write_requests` | counter | Postgres (Events) | Number of write requests to postgres events, labeled with the request `status` (success or failure). | | `teleport_postgres_events_backend_batch_read_requests` | counter | Postgres (Events) | Number of batch read requests to postgres events, labeled with the request `status` (success or failure). | | `teleport_postgres_events_backend_batch_delete_requests` | counter | Postgres (Events) | Number of batch delete requests to postgres events, labeled with the request `status` (success or failure). | | `teleport_postgres_events_backend_write_seconds` | histogram | Postgres (Events) | Latency for postgres events write operations, in seconds. | | `teleport_postgres_events_backend_batch_read_seconds` | histogram | Postgres (Events) | Latency for postgres events batch read operations, in seconds. | | `teleport_postgres_events_backend_batch_delete_seconds` | histogram | Postgres (Events) | Latency for postgres events batch delete operations, in seconds. | -| `teleport_connected_resources` | gauge | Teleport Auth | Number and type of resources connected via keepalives. | | `teleport_registered_servers` | gauge | Teleport Auth | The number of Teleport services that are connected to an Auth Service instance grouped by version. | | `teleport_registered_servers_by_install_methods` | gauge | Teleport Auth | The number of Teleport services that are connected to an Auth Service instance grouped by install methods. | -| `user_login_total` | counter | Teleport Auth | Number of user logins. | +| `teleport_roles_total` | gauge | Teleport Auth | The number of roles that exist in the cluster. | | `teleport_migrations` | gauge | Teleport Auth | Tracks for each migration if it is active (1) or not (0). | +| `user_login_total` | counter | Teleport Auth | Number of user logins. | | `watcher_event_sizes` | histogram | cache | Overall size of events emitted. | | `watcher_events` | histogram | cache | Per resource size of events emitted. | @@ -110,13 +111,13 @@ | `remote_clusters` | gauge | Teleport Proxy | Number of inbound connections from leaf clusters. | | `teleport_connect_to_node_attempts_total` | counter | Teleport Proxy | Number of SSH connection attempts to a SSH Service. Use with `failed_connect_to_node_attempts_total` to get the failure rate. | | `teleport_reverse_tunnels_connected` | gauge | Teleport Proxy | Number of reverse SSH tunnels connected to the Teleport Proxy Service by Teleport instances. | -| `trusted_clusters` | gauge | Teleport Proxy | Number of outbound connections to leaf clusters. | | `teleport_proxy_db_connection_setup_time_seconds` | histogram | Teleport Proxy | Time to establish connection to DB service from Proxy service. | | `teleport_proxy_db_connection_dial_attempts_total` | counter | Teleport Proxy | Number of dial attempts from Proxy to DB service made. | | `teleport_proxy_db_connection_dial_failures_total` | counter | Teleport Proxy | Number of failed dial attempts from Proxy to DB service made. | | `teleport_proxy_db_attempted_servers_total` | histogram | Teleport Proxy | Number of servers processed during connection attempt to the DB service from Proxy service. | | `teleport_proxy_db_connection_tls_config_time_seconds` | histogram | Teleport Proxy | Time to fetch TLS configuration for the connection to DB service from Proxy service. | | `teleport_proxy_db_active_connections_total` | gauge | Teleport Proxy | Number of currently active connections to DB service from Proxy service. | +| `trusted_clusters` | gauge | Teleport Proxy | Number of outbound connections to leaf clusters. | ## Database Service diff --git a/docs/pages/reference/terraform-provider/data-sources/auth_preference.mdx b/docs/pages/reference/terraform-provider/data-sources/auth_preference.mdx index cfbd8e789ff2..b6c7dfd44ff0 100644 --- a/docs/pages/reference/terraform-provider/data-sources/auth_preference.mdx +++ b/docs/pages/reference/terraform-provider/data-sources/auth_preference.mdx @@ -42,7 +42,7 @@ Optional: - `require_session_mfa` (Number) RequireMFAType is the type of MFA requirement enforced for this cluster. 0 is "OFF", 1 is "SESSION", 2 is "SESSION_AND_HARDWARE_KEY", 3 is "HARDWARE_KEY_TOUCH", 4 is "HARDWARE_KEY_PIN", 5 is "HARDWARE_KEY_TOUCH_AND_PIN". - `second_factor` (String) SecondFactor is the type of mult-factor. - `second_factors` (List of Number) SecondFactors is a list of supported second factor types. -- `signature_algorithm_suite` (Number) SignatureAlgorithmSuite is the configured signature algorithm suite for the cluster. The current default value is "legacy". This field is not yet fully supported. +- `signature_algorithm_suite` (Number) SignatureAlgorithmSuite is the configured signature algorithm suite for the cluster. If unspecified, the current default value is "legacy". 1 is "legacy", 2 is "balanced-v1", 3 is "fips-v1", 4 is "hsm-v1". - `type` (String) Type is the type of authentication. - `u2f` (Attributes) U2F are the settings for the U2F device. (see [below for nested schema](#nested-schema-for-specu2f)) - `webauthn` (Attributes) Webauthn are the settings for server-side Web Authentication support. (see [below for nested schema](#nested-schema-for-specwebauthn)) diff --git a/docs/pages/reference/terraform-provider/resources/auth_preference.mdx b/docs/pages/reference/terraform-provider/resources/auth_preference.mdx index 4de75147ae76..fbc49794bc64 100644 --- a/docs/pages/reference/terraform-provider/resources/auth_preference.mdx +++ b/docs/pages/reference/terraform-provider/resources/auth_preference.mdx @@ -61,7 +61,7 @@ Optional: - `require_session_mfa` (Number) RequireMFAType is the type of MFA requirement enforced for this cluster. 0 is "OFF", 1 is "SESSION", 2 is "SESSION_AND_HARDWARE_KEY", 3 is "HARDWARE_KEY_TOUCH", 4 is "HARDWARE_KEY_PIN", 5 is "HARDWARE_KEY_TOUCH_AND_PIN". - `second_factor` (String) SecondFactor is the type of mult-factor. - `second_factors` (List of Number) SecondFactors is a list of supported second factor types. -- `signature_algorithm_suite` (Number) SignatureAlgorithmSuite is the configured signature algorithm suite for the cluster. The current default value is "legacy". This field is not yet fully supported. +- `signature_algorithm_suite` (Number) SignatureAlgorithmSuite is the configured signature algorithm suite for the cluster. If unspecified, the current default value is "legacy". 1 is "legacy", 2 is "balanced-v1", 3 is "fips-v1", 4 is "hsm-v1". - `type` (String) Type is the type of authentication. - `u2f` (Attributes) U2F are the settings for the U2F device. (see [below for nested schema](#nested-schema-for-specu2f)) - `webauthn` (Attributes) Webauthn are the settings for server-side Web Authentication support. (see [below for nested schema](#nested-schema-for-specwebauthn)) diff --git a/integration/hsm/hsm_test.go b/integration/hsm/hsm_test.go index 3a5ea29eed74..1d6d03f56738 100644 --- a/integration/hsm/hsm_test.go +++ b/integration/hsm/hsm_test.go @@ -590,7 +590,10 @@ func TestHSMRevert(t *testing.T) { clock.Advance(2 * defaults.HighResPollingPeriod) assert.EventuallyWithT(t, func(t *assert.CollectT) { alerts, err = auth1.process.GetAuthServer().GetClusterAlerts(ctx, types.GetClusterAlertsRequest{}) - require.NoError(t, err) + assert.NoError(t, err) assert.Empty(t, alerts) + + // Keep advancing the clock to make sure the rotation ticker gets fired + clock.Advance(2 * defaults.HighResPollingPeriod) }, 5*time.Second, 100*time.Millisecond) } diff --git a/integrations/access/accessmonitoring/access_monitoring_rules.go b/integrations/access/accessmonitoring/access_monitoring_rules.go index 72eb921f3f4e..7fb2c045d57e 100644 --- a/integrations/access/accessmonitoring/access_monitoring_rules.go +++ b/integrations/access/accessmonitoring/access_monitoring_rules.go @@ -48,6 +48,7 @@ type RuleHandler struct { pluginName string fetchRecipientCallback func(ctx context.Context, recipient string) (*common.Recipient, error) + onCacheUpdateCallback func(Operation types.OpType, name string, rule *accessmonitoringrulesv1.AccessMonitoringRule) error } // RuleMap is a concurrent map for access monitoring rules. @@ -65,6 +66,8 @@ type RuleHandlerConfig struct { // FetchRecipientCallback is a callback that maps recipient strings to plugin Recipients. FetchRecipientCallback func(ctx context.Context, recipient string) (*common.Recipient, error) + // OnCacheUpdateCallback is a callback that is called when a rule in the cache is created or updated. + OnCacheUpdateCallback func(Operation types.OpType, name string, rule *accessmonitoringrulesv1.AccessMonitoringRule) error } // NewRuleHandler returns a new RuleHandler. @@ -77,6 +80,7 @@ func NewRuleHandler(conf RuleHandlerConfig) *RuleHandler { pluginType: conf.PluginType, pluginName: conf.PluginName, fetchRecipientCallback: conf.FetchRecipientCallback, + onCacheUpdateCallback: conf.OnCacheUpdateCallback, } } @@ -93,6 +97,9 @@ func (amrh *RuleHandler) InitAccessMonitoringRulesCache(ctx context.Context) err continue } amrh.accessMonitoringRules.rules[amr.GetMetadata().Name] = amr + if amrh.onCacheUpdateCallback != nil { + amrh.onCacheUpdateCallback(types.OpPut, amr.GetMetadata().Name, amr) + } } return nil } @@ -123,6 +130,9 @@ func (amrh *RuleHandler) HandleAccessMonitoringRule(ctx context.Context, event t return nil } amrh.accessMonitoringRules.rules[req.Metadata.Name] = req + if amrh.onCacheUpdateCallback != nil { + amrh.onCacheUpdateCallback(types.OpPut, req.GetMetadata().Name, req) + } return nil case types.OpDelete: delete(amrh.accessMonitoringRules.rules, event.Resource.GetName()) diff --git a/integrations/access/msteams/app.go b/integrations/access/msteams/app.go index 885d9ca8de65..0f964705c6b9 100644 --- a/integrations/access/msteams/app.go +++ b/integrations/access/msteams/app.go @@ -17,12 +17,14 @@ package msteams import ( "context" "log/slog" + "slices" "time" "github.com/gravitational/trace" "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/access/accessmonitoring" "github.com/gravitational/teleport/integrations/access/common" "github.com/gravitational/teleport/integrations/access/common/teleport" "github.com/gravitational/teleport/integrations/lib" @@ -40,7 +42,7 @@ const ( // initTimeout is used to bound execution time of health check and teleport version check. initTimeout = time.Second * 10 // handlerTimeout is used to bound the execution time of watcher event handler. - handlerTimeout = time.Second * 5 + handlerTimeout = time.Second * 15 ) // App contains global application state. @@ -53,7 +55,8 @@ type App struct { watcherJob lib.ServiceJob pd *pd.CompareAndSwap[PluginData] - log *slog.Logger + log *slog.Logger + accessMonitoringRules *accessmonitoring.RuleHandler *lib.Process } @@ -85,13 +88,11 @@ func (a *App) Run(ctx context.Context) error { } a.Process = lib.NewProcess(ctx) - a.watcherJob, err = a.newWatcherJob() if err != nil { return trace.Wrap(err) } a.SpawnCriticalJob(a.mainJob) - a.SpawnCriticalJob(a.watcherJob) select { case <-ctx.Done(): @@ -116,10 +117,14 @@ func (a *App) init(ctx context.Context) error { ctx, cancel := context.WithTimeout(ctx, initTimeout) defer cancel() - var err error - a.apiClient, err = common.GetTeleportClient(ctx, a.conf.Teleport) - if err != nil { - return trace.Wrap(err) + if a.conf.Client != nil { + a.apiClient = a.conf.Client + } else { + var err error + a.apiClient, err = common.GetTeleportClient(ctx, a.conf.Teleport) + if err != nil { + return trace.Wrap(err) + } } a.pd = pd.NewCAS( @@ -145,6 +150,24 @@ func (a *App) init(ctx context.Context) error { return trace.Wrap(err) } + a.accessMonitoringRules = accessmonitoring.NewRuleHandler(accessmonitoring.RuleHandlerConfig{ + Client: a.apiClient, + PluginName: pluginName, + // Map msteams.RecipientData onto the common recipient type used + // by the access monitoring rules watcher. + FetchRecipientCallback: func(ctx context.Context, name string) (*common.Recipient, error) { + msTeamsRecipient, err := a.bot.FetchRecipient(ctx, name) + if err != nil { + return nil, trace.Wrap(err) + } + return &common.Recipient{ + Name: name, + ID: msTeamsRecipient.ID, + Kind: string(msTeamsRecipient.Kind), + }, nil + }, + }) + return a.initBot(ctx) } @@ -187,27 +210,52 @@ func (a *App) initBot(ctx context.Context) error { return nil } -// newWatcherJob creates WatcherJob -func (a *App) newWatcherJob() (lib.ServiceJob, error) { - return watcherjob.NewJob( +// run starts the main process +func (a *App) run(ctx context.Context) error { + + process := lib.MustGetProcess(ctx) + + watchKinds := []types.WatchKind{ + {Kind: types.KindAccessRequest}, + {Kind: types.KindAccessMonitoringRule}, + } + acceptedWatchKinds := make([]string, 0, len(watchKinds)) + watcherJob, err := watcherjob.NewJobWithConfirmedWatchKinds( a.apiClient, watcherjob.Config{ - Watch: types.Watch{ - Kinds: []types.WatchKind{{Kind: types.KindAccessRequest}}, - }, + Watch: types.Watch{Kinds: watchKinds, AllowPartialSuccess: true}, EventFuncTimeout: handlerTimeout, }, a.onWatcherEvent, + func(ws types.WatchStatus) { + for _, watchKind := range ws.GetKinds() { + acceptedWatchKinds = append(acceptedWatchKinds, watchKind.Kind) + } + }, ) -} - -// run starts the main process -func (a *App) run(ctx context.Context) error { - ok, err := a.watcherJob.WaitReady(ctx) if err != nil { return trace.Wrap(err) } + process.SpawnCriticalJob(watcherJob) + + ok, err := watcherJob.WaitReady(ctx) + if err != nil { + return trace.Wrap(err) + } + if len(acceptedWatchKinds) == 0 { + return trace.BadParameter("failed to initialize watcher for all the required resources: %+v", + watchKinds) + } + // Check if KindAccessMonitoringRule resources are being watched, + // the role the plugin is running as may not have access. + if slices.Contains(acceptedWatchKinds, types.KindAccessMonitoringRule) { + if err := a.accessMonitoringRules.InitAccessMonitoringRulesCache(ctx); err != nil { + return trace.Wrap(err, "initializing Access Monitoring Rule cache") + } + } + a.watcherJob = watcherJob + a.watcherJob.SetReady(ok) if ok { a.log.InfoContext(ctx, "Plugin is ready") } else { @@ -243,6 +291,10 @@ func (a *App) checkTeleportVersion(ctx context.Context) (proto.PingResponse, err // onWatcherEvent called when an access request event is received func (a *App) onWatcherEvent(ctx context.Context, event types.Event) error { kind := event.Resource.GetKind() + if kind == types.KindAccessMonitoringRule { + return trace.Wrap(a.accessMonitoringRules.HandleAccessMonitoringRule(ctx, event)) + } + if kind != types.KindAccessRequest { return trace.Errorf("unexpected kind %s", kind) } @@ -480,6 +532,14 @@ func (a *App) getMessageRecipients(ctx context.Context, req types.AccessRequest) recipientSet := stringset.New() a.log.DebugContext(ctx, "Getting suggested reviewer recipients") + accessRuleRecipients := a.accessMonitoringRules.RecipientsFromAccessMonitoringRules(ctx, req) + accessRuleRecipients.ForEach(func(r common.Recipient) { + recipientSet.Add(r.Name) + }) + if recipientSet.Len() != 0 { + return recipientSet.ToSlice() + } + var validEmailsSuggReviewers []string for _, reviewer := range req.GetSuggestedReviewers() { if !lib.IsEmail(reviewer) { diff --git a/integrations/access/msteams/config.go b/integrations/access/msteams/config.go index bffb57e95e91..8aca1fa074ab 100644 --- a/integrations/access/msteams/config.go +++ b/integrations/access/msteams/config.go @@ -22,6 +22,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/integrations/access/common" + "github.com/gravitational/teleport/integrations/access/common/teleport" "github.com/gravitational/teleport/integrations/access/msteams/msapi" "github.com/gravitational/teleport/integrations/lib" "github.com/gravitational/teleport/integrations/lib/logger" @@ -29,6 +30,8 @@ import ( // Config represents plugin configuration type Config struct { + // Client is the Teleport API client. + Client teleport.Client Teleport lib.TeleportConfig Recipients common.RawRecipientsMap `toml:"role_to_recipients"` Log logger.Config diff --git a/integrations/access/msteams/testlib/suite.go b/integrations/access/msteams/testlib/suite.go index eaa9e138a9a1..3e652bea1aa8 100644 --- a/integrations/access/msteams/testlib/suite.go +++ b/integrations/access/msteams/testlib/suite.go @@ -28,6 +28,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + accessmonitoringrulesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/accessmonitoringrules/v1" + v1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/integrations/access/common" "github.com/gravitational/teleport/integrations/access/msteams" @@ -37,7 +39,7 @@ import ( "github.com/gravitational/teleport/integrations/lib/testing/integration" ) -// MsTeamsBaseSuite is the Slack access plugin test suite. +// MsTeamsBaseSuite is the MsTeams access plugin test suite. // It implements the testify.TestingSuite interface. type MsTeamsBaseSuite struct { *integration.AccessRequestSuite @@ -51,16 +53,19 @@ type MsTeamsBaseSuite struct { reviewer2TeamsUser msapi.User } -// SetupTest starts a fake Slack, generates the plugin configuration, and loads -// the fixtures in Slack. It runs for each test. +// SetupTest starts a fake MsTeams, generates the plugin configuration, and loads +// the fixtures in MsTeams. It runs for each test. func (s *MsTeamsBaseSuite) SetupTest() { t := s.T() + + err := logger.Setup(logger.Config{Severity: "debug"}) + require.NoError(t, err) s.raceNumber = runtime.GOMAXPROCS(0) s.fakeTeams = NewFakeTeams(s.raceNumber) t.Cleanup(s.fakeTeams.Close) - // We need requester users as well, the slack plugin sends messages to users + // We need requester users as well, the MsTeams plugin sends messages to users // when their access request got approved. s.requesterOSSTeamsUser = s.fakeTeams.StoreUser(msapi.User{Name: "Requester OSS", Mail: integration.RequesterOSSUserName}) s.requester1TeamsUser = s.fakeTeams.StoreUser(msapi.User{Name: "Requester Ent", Mail: integration.Requester1UserName}) @@ -71,16 +76,16 @@ func (s *MsTeamsBaseSuite) SetupTest() { var conf msteams.Config conf.Teleport = s.TeleportConfig() + apiClient, err := common.GetTeleportClient(context.Background(), s.TeleportConfig()) + require.NoError(t, err) + conf.Client = apiClient conf.MSAPI = s.fakeTeams.Config conf.MSAPI.SetBaseURLs(s.fakeTeams.URL(), s.fakeTeams.URL(), s.fakeTeams.URL()) - conf.Log = logger.Config{ - Severity: "debug", - } s.appConfig = &conf } -// startApp starts the Slack plugin, waits for it to become ready and returns. +// startApp starts the MsTeams plugin, waits for it to become ready and returns. func (s *MsTeamsBaseSuite) startApp() { s.T().Helper() t := s.T() @@ -414,7 +419,9 @@ func (s *MsTeamsSuiteEnterprise) TestRace() { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) t.Cleanup(cancel) - s.appConfig.Log.Severity = "debug" // Turn off noisy debug logging + err := logger.Setup(logger.Config{Severity: "info"}) // Turn off noisy debug logging + require.NoError(t, err) + s.startApp() var ( @@ -527,3 +534,57 @@ func (s *MsTeamsSuiteEnterprise) TestRace() { return next }) } + +func (s *MsTeamsSuiteOSS) TestRecipientsFromAccessMonitoringRule() { + t := s.T() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + t.Cleanup(cancel) + + s.startApp() + + _, err := s.ClientByName(integration.RulerUserName). + AccessMonitoringRulesClient(). + CreateAccessMonitoringRule(ctx, &accessmonitoringrulesv1.AccessMonitoringRule{ + Kind: types.KindAccessMonitoringRule, + Version: types.V1, + Metadata: &v1.Metadata{ + Name: "test-msteams-amr", + }, + Spec: &accessmonitoringrulesv1.AccessMonitoringRuleSpec{ + Subjects: []string{types.KindAccessRequest}, + Condition: "!is_empty(access_request.spec.roles)", + Notification: &accessmonitoringrulesv1.Notification{ + Name: "msteams", + Recipients: []string{ + s.reviewer1TeamsUser.ID, + s.reviewer2TeamsUser.Mail, + }, + }, + }, + }) + assert.NoError(t, err) + + // Test execution: create an access request + req := s.CreateAccessRequest(ctx, integration.RequesterOSSUserName, nil) + + s.checkPluginData(ctx, req.GetName(), func(data msteams.PluginData) bool { + return len(data.TeamsData) > 0 + }) + + title := "Access Request " + req.GetName() + msgs, err := s.getNewMessages(ctx, 2) + require.NoError(t, err) + + var body1 testTeamsMessage + require.NoError(t, json.Unmarshal([]byte(msgs[0].Body), &body1)) + body1.checkTitle(t, title) + require.Equal(t, msgs[0].RecipientID, s.reviewer1TeamsUser.ID) + + var body2 testTeamsMessage + require.NoError(t, json.Unmarshal([]byte(msgs[1].Body), &body2)) + body1.checkTitle(t, title) + require.Equal(t, msgs[1].RecipientID, s.reviewer2TeamsUser.ID) + + assert.NoError(t, s.ClientByName(integration.RulerUserName). + AccessMonitoringRulesClient().DeleteAccessMonitoringRule(ctx, "test-msteams-amr")) +} diff --git a/integrations/access/pagerduty/app.go b/integrations/access/pagerduty/app.go index 972ed1bffce1..5eadcc5147cd 100644 --- a/integrations/access/pagerduty/app.go +++ b/integrations/access/pagerduty/app.go @@ -78,7 +78,6 @@ func NewApp(conf Config) (*App, error) { teleport: conf.Client, statusSink: conf.StatusSink, } - app.mainJob = lib.NewServiceJob(app.run) return app, nil @@ -173,7 +172,7 @@ func (a *App) init(ctx context.Context) error { } } - a.accessMonitoringRules = accessmonitoring.NewRuleHandler(accessmonitoring.RuleHandlerConfig{ + amrhConf := accessmonitoring.RuleHandlerConfig{ Client: a.teleport, PluginType: types.PluginTypePagerDuty, PluginName: pluginName, @@ -184,7 +183,11 @@ func (a *App) init(ctx context.Context) error { Kind: common.RecipientKindSchedule, }, nil }, - }) + } + if a.conf.OnAccessMonitoringRuleCacheUpdateCallback != nil { + amrhConf.OnCacheUpdateCallback = a.conf.OnAccessMonitoringRuleCacheUpdateCallback + } + a.accessMonitoringRules = accessmonitoring.NewRuleHandler(amrhConf) if pong, err = a.checkTeleportVersion(ctx); err != nil { return trace.Wrap(err) diff --git a/integrations/access/pagerduty/config.go b/integrations/access/pagerduty/config.go index f76e9d2f955f..8bf7060652b0 100644 --- a/integrations/access/pagerduty/config.go +++ b/integrations/access/pagerduty/config.go @@ -24,6 +24,8 @@ import ( "github.com/gravitational/trace" "github.com/pelletier/go-toml" + accessmonitoringrulesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/accessmonitoringrules/v1" + "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/integrations/access/common" "github.com/gravitational/teleport/integrations/access/common/teleport" "github.com/gravitational/teleport/integrations/lib" @@ -47,6 +49,10 @@ type Config struct { // TeleportUser is the name of the Teleport user that will act // as the access request approver TeleportUser string + + // OnAccessMonitoringRuleCacheUpdateCallback is used for checking when + // the Rule cache is updated in tests + OnAccessMonitoringRuleCacheUpdateCallback func(Operation types.OpType, name string, rule *accessmonitoringrulesv1.AccessMonitoringRule) error } type PagerdutyConfig struct { diff --git a/integrations/access/pagerduty/testlib/suite.go b/integrations/access/pagerduty/testlib/suite.go index c379c85219a5..b68c4d7e8d70 100644 --- a/integrations/access/pagerduty/testlib/suite.go +++ b/integrations/access/pagerduty/testlib/suite.go @@ -430,6 +430,15 @@ func (s *PagerdutySuiteOSS) TestRecipientsFromAccessMonitoringRule() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) + const ruleName = "test-pagerduty-amr" + var collectedNames []string + var mu sync.Mutex + s.appConfig.OnAccessMonitoringRuleCacheUpdateCallback = func(_ types.OpType, name string, _ *accessmonitoringrulesv1.AccessMonitoringRule) error { + mu.Lock() + collectedNames = append(collectedNames, name) + mu.Unlock() + return nil + } s.startApp() _, err := s.ClientByName(integration.RulerUserName). @@ -438,7 +447,7 @@ func (s *PagerdutySuiteOSS) TestRecipientsFromAccessMonitoringRule() { Kind: types.KindAccessMonitoringRule, Version: types.V1, Metadata: &v1.Metadata{ - Name: "test-pagerduty-amr", + Name: ruleName, }, Spec: &accessmonitoringrulesv1.AccessMonitoringRuleSpec{ Subjects: []string{types.KindAccessRequest}, @@ -453,6 +462,14 @@ func (s *PagerdutySuiteOSS) TestRecipientsFromAccessMonitoringRule() { }) assert.NoError(t, err) + // Incident creation may happen before plugins Access Monitoring Rule cache + // has been updated with new rule. Retry until the new cache picks up the rule. + require.EventuallyWithT(t, func(t *assert.CollectT) { + mu.Lock() + require.Contains(t, collectedNames, ruleName) + mu.Unlock() + }, 3*time.Second, time.Millisecond*100, "new access monitoring rule did not begin applying") + // Test execution: create an access request req := s.CreateAccessRequest(ctx, integration.RequesterOSSUserName, nil) @@ -463,16 +480,16 @@ func (s *PagerdutySuiteOSS) TestRecipientsFromAccessMonitoringRule() { }) incident, err := s.fakePagerduty.CheckNewIncident(ctx) - require.NoError(t, err, "no new incidents stored") - + assert.NoError(t, err, "no new incidents stored") assert.Equal(t, incident.ID, pluginData.IncidentID) - assert.Equal(t, s.pdNotifyService2.ID, pluginData.ServiceID) assert.Equal(t, pagerduty.PdIncidentKeyPrefix+"/"+req.GetName(), incident.IncidentKey) assert.Equal(t, "triggered", incident.Status) + assert.Equal(t, s.pdNotifyService2.ID, pluginData.ServiceID) + assert.NoError(t, s.ClientByName(integration.RulerUserName). - AccessMonitoringRulesClient().DeleteAccessMonitoringRule(ctx, "test-pagerduty-amr")) + AccessMonitoringRulesClient().DeleteAccessMonitoringRule(ctx, ruleName)) } func (s *PagerdutyBaseSuite) assertNewEvent(ctx context.Context, watcher types.Watcher, opType types.OpType, resourceKind, resourceName string) types.Event { diff --git a/integrations/terraform/tfschema/types_terraform.go b/integrations/terraform/tfschema/types_terraform.go index cfbac365857e..abb411d8bcad 100644 --- a/integrations/terraform/tfschema/types_terraform.go +++ b/integrations/terraform/tfschema/types_terraform.go @@ -1405,7 +1405,7 @@ func GenSchemaAuthPreferenceV2(ctx context.Context) (github_com_hashicorp_terraf Type: github_com_hashicorp_terraform_plugin_framework_types.ListType{ElemType: github_com_hashicorp_terraform_plugin_framework_types.Int64Type}, }, "signature_algorithm_suite": { - Description: "SignatureAlgorithmSuite is the configured signature algorithm suite for the cluster. The current default value is \"legacy\". This field is not yet fully supported.", + Description: "SignatureAlgorithmSuite is the configured signature algorithm suite for the cluster. If unspecified, the current default value is \"legacy\". 1 is \"legacy\", 2 is \"balanced-v1\", 3 is \"fips-v1\", 4 is \"hsm-v1\".", Optional: true, Type: github_com_hashicorp_terraform_plugin_framework_types.Int64Type, }, diff --git a/lib/auth/accesspoint/accesspoint.go b/lib/auth/accesspoint/accesspoint.go index 158243d12655..3cf7c1d2e86a 100644 --- a/lib/auth/accesspoint/accesspoint.go +++ b/lib/auth/accesspoint/accesspoint.go @@ -106,6 +106,7 @@ type Config struct { WindowsDesktops services.WindowsDesktops AutoUpdateService services.AutoUpdateServiceGetter ProvisioningStates services.ProvisioningStates + IdentityCenter services.IdentityCenter } func (c *Config) CheckAndSetDefaults() error { diff --git a/lib/auth/auth.go b/lib/auth/auth.go index ffffea18d4e7..85881e3c026d 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -247,6 +247,12 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (*Server, error) { if cfg.WindowsDesktops == nil { cfg.WindowsDesktops = local.NewWindowsDesktopService(cfg.Backend) } + if cfg.DynamicWindowsDesktops == nil { + cfg.DynamicWindowsDesktops, err = local.NewDynamicWindowsDesktopService(cfg.Backend) + if err != nil { + return nil, trace.Wrap(err) + } + } if cfg.SAMLIdPServiceProviders == nil { cfg.SAMLIdPServiceProviders, err = local.NewSAMLIdPServiceProviderService(cfg.Backend) if err != nil { @@ -347,7 +353,14 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (*Server, error) { if cfg.ProvisioningStates == nil { cfg.ProvisioningStates, err = local.NewProvisioningStateService(cfg.Backend) if err != nil { - return nil, trace.Wrap(err, "Creating provisioning state service") + return nil, trace.Wrap(err) + } + } + if cfg.IdentityCenter == nil { + svcCfg := local.IdentityCenterServiceConfig{Backend: cfg.Backend} + cfg.IdentityCenter, err = local.NewIdentityCenterService(svcCfg) + if err != nil { + return nil, trace.Wrap(err) } } if cfg.CloudClients == nil { @@ -443,6 +456,7 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (*Server, error) { AuditLogSessionStreamer: cfg.AuditLog, Events: cfg.Events, WindowsDesktops: cfg.WindowsDesktops, + DynamicWindowsDesktops: cfg.DynamicWindowsDesktops, SAMLIdPServiceProviders: cfg.SAMLIdPServiceProviders, UserGroups: cfg.UserGroups, SessionTrackerService: cfg.SessionTrackerService, @@ -644,6 +658,7 @@ type Services struct { services.Databases services.DatabaseServices services.WindowsDesktops + services.DynamicWindowsDesktops services.SAMLIdPServiceProviders services.UserGroups services.SessionTrackerService @@ -676,6 +691,7 @@ type Services struct { services.StaticHostUser services.AutoUpdateService services.ProvisioningStates + services.IdentityCenter } // GetWebSession returns existing web session described by req. @@ -738,6 +754,14 @@ var ( }, ) + roleCount = prometheus.NewGauge( + prometheus.GaugeOpts{ + Namespace: teleport.MetricNamespace, + Name: "roles_total", + Help: "Number of roles that exist in the cluster", + }, + ) + registeredAgents = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: teleport.MetricNamespace, @@ -822,6 +846,7 @@ var ( accessRequestsCreatedMetric, registeredAgentsInstallMethod, userCertificatesGeneratedMetric, + roleCount, } ) @@ -1292,94 +1317,122 @@ func (a *Server) periodicSyncUpgradeWindowStartHour() { } } +// periodicIntervalKey is used to uniquely identify the subintervals registered with +// the interval.MultiInterval instance that we use for managing periodics operations. + +type periodicIntervalKey int + +const ( + heartbeatCheckKey periodicIntervalKey = 1 + iota + rotationCheckKey + metricsKey + releaseCheckKey + localReleaseCheckKey + instancePeriodicsKey + dynamicLabelsCheckKey + notificationsCleanupKey + desktopCheckKey + upgradeWindowCheckKey + roleCountKey +) + // runPeriodicOperations runs some periodic bookkeeping operations // performed by auth server func (a *Server) runPeriodicOperations() { - ctx := context.TODO() + firstReleaseCheck := utils.FullJitter(time.Hour * 6) + + // this environment variable is "unstable" since it will be deprecated + // by an upcoming tctl command. currently exists for testing purposes only. + if os.Getenv("TELEPORT_UNSTABLE_VC_SYNC_ON_START") == "yes" { + firstReleaseCheck = utils.HalfJitter(time.Second * 10) + } + // run periodic functions with a semi-random period // to avoid contention on the database in case if there are multiple // auth servers running - so they don't compete trying // to update the same resources. r := insecurerand.New(insecurerand.NewSource(a.GetClock().Now().UnixNano())) period := defaults.HighResPollingPeriod + time.Duration(r.Intn(int(defaults.HighResPollingPeriod/time.Second)))*time.Second - log.Debugf("Ticking with period: %v.", period) - a.lock.RLock() - ticker := a.clock.NewTicker(period) - a.lock.RUnlock() - // Create a ticker with jitter - heartbeatCheckTicker := interval.New(interval.Config{ - Duration: apidefaults.ServerKeepAliveTTL() * 2, - Jitter: retryutils.NewSeventhJitter(), - }) - promTicker := interval.New(interval.Config{ - FirstDuration: 5 * time.Second, - Duration: defaults.PrometheusScrapeInterval, - Jitter: retryutils.NewSeventhJitter(), - }) - missedKeepAliveCount := 0 - defer ticker.Stop() - defer heartbeatCheckTicker.Stop() - defer promTicker.Stop() - firstReleaseCheck := utils.FullJitter(time.Hour * 6) + ticker := interval.NewMulti( + a.GetClock(), + interval.SubInterval[periodicIntervalKey]{ + Key: rotationCheckKey, + Duration: period, + }, + interval.SubInterval[periodicIntervalKey]{ + Key: metricsKey, + Duration: defaults.PrometheusScrapeInterval, + FirstDuration: 5 * time.Second, + Jitter: retryutils.NewSeventhJitter(), + }, + interval.SubInterval[periodicIntervalKey]{ + Key: instancePeriodicsKey, + Duration: 9 * time.Minute, + FirstDuration: utils.HalfJitter(time.Minute), + Jitter: retryutils.NewSeventhJitter(), + }, + interval.SubInterval[periodicIntervalKey]{ + Key: notificationsCleanupKey, + Duration: 48 * time.Hour, + FirstDuration: utils.FullJitter(time.Hour), + Jitter: retryutils.NewSeventhJitter(), + }, + interval.SubInterval[periodicIntervalKey]{ + Key: roleCountKey, + Duration: 12 * time.Hour, + FirstDuration: utils.FullJitter(time.Minute), + Jitter: retryutils.NewSeventhJitter(), + }, + ) - // this environment variable is "unstable" since it will be deprecated - // by an upcoming tctl command. currently exists for testing purposes only. - if os.Getenv("TELEPORT_UNSTABLE_VC_SYNC_ON_START") == "yes" { - firstReleaseCheck = utils.HalfJitter(time.Second * 10) - } + defer ticker.Stop() - // note the use of FullJitter for the releases check interval. this lets us ensure - // that frequent restarts don't prevent checks from happening despite the infrequent - // effective check rate. - releaseCheck := interval.New(interval.Config{ - Duration: time.Hour * 24, - FirstDuration: firstReleaseCheck, - Jitter: retryutils.NewFullJitter(), - }) - defer releaseCheck.Stop() - - // more frequent release check that just re-calculates alerts based on previously - // pulled versioning info. - localReleaseCheck := interval.New(interval.Config{ - Duration: time.Minute * 10, - FirstDuration: utils.HalfJitter(time.Second * 10), - Jitter: retryutils.NewHalfJitter(), - }) - defer localReleaseCheck.Stop() + missedKeepAliveCount := 0 - instancePeriodics := interval.New(interval.Config{ - Duration: time.Minute * 9, - FirstDuration: utils.HalfJitter(time.Minute), - Jitter: retryutils.NewSeventhJitter(), - }) - defer instancePeriodics.Stop() + // Prevent some periodic operations from running for dashboard tenants. + if !services.IsDashboard(*modules.GetModules().Features().ToProto()) { + ticker.Push(interval.SubInterval[periodicIntervalKey]{ + Key: dynamicLabelsCheckKey, + Duration: dynamicLabelCheckPeriod, + FirstDuration: utils.HalfJitter(10 * time.Second), + Jitter: retryutils.NewSeventhJitter(), + }) + ticker.Push(interval.SubInterval[periodicIntervalKey]{ + Key: heartbeatCheckKey, + Duration: apidefaults.ServerKeepAliveTTL() * 2, + Jitter: retryutils.NewSeventhJitter(), + }) + ticker.Push(interval.SubInterval[periodicIntervalKey]{ + Key: releaseCheckKey, + Duration: 24 * time.Hour, + FirstDuration: firstReleaseCheck, + // note the use of FullJitter for the releases check interval. this lets us ensure + // that frequent restarts don't prevent checks from happening despite the infrequent + // effective check rate. + Jitter: retryutils.NewFullJitter(), + }) + // more frequent release check that just re-calculates alerts based on previously + // pulled versioning info. + ticker.Push(interval.SubInterval[periodicIntervalKey]{ + Key: localReleaseCheckKey, + Duration: 10 * time.Minute, + FirstDuration: utils.HalfJitter(10 * time.Second), + Jitter: retryutils.NewHalfJitter(), + }) + } - var ossDesktopsCheck <-chan time.Time if modules.GetModules().IsOSSBuild() { - ossDesktopsCheck = interval.New(interval.Config{ + ticker.Push(interval.SubInterval[periodicIntervalKey]{ + Key: desktopCheckKey, Duration: OSSDesktopsCheckPeriod, - FirstDuration: utils.HalfJitter(time.Second * 10), + FirstDuration: utils.HalfJitter(10 * time.Second), Jitter: retryutils.NewHalfJitter(), - }).Next() - } else if err := a.DeleteClusterAlert(ctx, OSSDesktopsAlertID); err != nil && !trace.IsNotFound(err) { + }) + } else if err := a.DeleteClusterAlert(a.closeCtx, OSSDesktopsAlertID); err != nil && !trace.IsNotFound(err) { log.Warnf("Can't delete OSS non-AD desktops limit alert: %v", err) } - dynamicLabelsCheck := interval.New(interval.Config{ - Duration: dynamicLabelCheckPeriod, - FirstDuration: utils.HalfJitter(time.Second * 10), - Jitter: retryutils.NewSeventhJitter(), - }) - defer dynamicLabelsCheck.Stop() - - notificationsCleanup := interval.New(interval.Config{ - Duration: 48 * time.Hour, - FirstDuration: utils.FullJitter(time.Hour), - Jitter: retryutils.NewSeventhJitter(), - }) - defer notificationsCleanup.Stop() - // isolate the schedule of potentially long-running refreshRemoteClusters() from other tasks go func() { // reasonably small interval to ensure that users observe clusters as online within 1 minute of adding them. @@ -1394,7 +1447,7 @@ func (a *Server) runPeriodicOperations() { case <-a.closeCtx.Done(): return case <-remoteClustersRefresh.Next(): - a.refreshRemoteClusters(ctx, r) + a.refreshRemoteClusters(a.closeCtx, r) } } }() @@ -1402,62 +1455,120 @@ func (a *Server) runPeriodicOperations() { // cloud auth servers need to periodically sync the upgrade window // from the cloud db. if modules.GetModules().Features().Cloud { - go a.periodicSyncUpgradeWindowStartHour() - } - - // disable periodics that are not required for cloud dashboard tenants - if services.IsDashboard(*modules.GetModules().Features().ToProto()) { - releaseCheck.Stop() - localReleaseCheck.Stop() - heartbeatCheckTicker.Stop() - dynamicLabelsCheck.Stop() + ticker.Push(interval.SubInterval[periodicIntervalKey]{ + Key: upgradeWindowCheckKey, + Duration: 3 * time.Minute, + FirstDuration: utils.FullJitter(30 * time.Second), + Jitter: retryutils.NewSeventhJitter(), + }) } for { select { case <-a.closeCtx.Done(): return - case <-ticker.Chan(): - err := a.AutoRotateCertAuthorities(ctx) - if err != nil { - if trace.IsCompareFailed(err) { - log.Debugf("Cert authority has been updated concurrently: %v.", err) - } else { - log.Errorf("Failed to perform cert rotation check: %v.", err) - } - } - case <-heartbeatCheckTicker.Next(): - nodes, err := a.GetNodes(ctx, apidefaults.Namespace) - if err != nil { - log.Errorf("Failed to load nodes for heartbeat metric calculation: %v", err) - } - for _, node := range nodes { - if services.NodeHasMissedKeepAlives(node) { - missedKeepAliveCount++ - } + case tick := <-ticker.Next(): + switch tick.Key { + case rotationCheckKey: + go func() { + if err := a.AutoRotateCertAuthorities(a.closeCtx); err != nil { + if trace.IsCompareFailed(err) { + log.Debugf("Cert authority has been updated concurrently: %v.", err) + } else { + log.Errorf("Failed to perform cert rotation check: %v.", err) + } + } + }() + case heartbeatCheckKey: + go func() { + req := &proto.ListUnifiedResourcesRequest{Kinds: []string{types.KindNode}, SortBy: types.SortBy{Field: types.ResourceKind}} + + for { + _, next, err := a.UnifiedResourceCache.IterateUnifiedResources(a.closeCtx, + func(rwl types.ResourceWithLabels) (bool, error) { + srv, ok := rwl.(types.Server) + if !ok { + return false, nil + } + if services.NodeHasMissedKeepAlives(srv) { + missedKeepAliveCount++ + } + return false, nil + }, + req, + ) + if err != nil { + log.Errorf("Failed to load nodes for heartbeat metric calculation: %v", err) + return + } + + req.StartKey = next + if req.StartKey == "" { + break + } + } + + // Update prometheus gauge + heartbeatsMissedByAuth.Set(float64(missedKeepAliveCount)) + }() + case metricsKey: + go a.updateAgentMetrics() + case releaseCheckKey: + go a.syncReleaseAlerts(a.closeCtx, true) + case localReleaseCheckKey: + go a.syncReleaseAlerts(a.closeCtx, false) + case instancePeriodicsKey: + go a.doInstancePeriodics(a.closeCtx) + case desktopCheckKey: + go a.syncDesktopsLimitAlert(a.closeCtx) + case dynamicLabelsCheckKey: + go a.syncDynamicLabelsAlert(a.closeCtx) + case notificationsCleanupKey: + go a.CleanupNotifications(a.closeCtx) + case upgradeWindowCheckKey: + go a.periodicSyncUpgradeWindowStartHour() + case roleCountKey: + go a.tallyRoles(a.closeCtx) } - // Update prometheus gauge - heartbeatsMissedByAuth.Set(float64(missedKeepAliveCount)) - case <-promTicker.Next(): - a.updateAgentMetrics() - case <-releaseCheck.Next(): - a.syncReleaseAlerts(ctx, true) - case <-localReleaseCheck.Next(): - a.syncReleaseAlerts(ctx, false) - case <-instancePeriodics.Next(): - // instance periodics are rate-limited and may be time-consuming in large - // clusters, so launch them in the background. - go a.doInstancePeriodics(ctx) - case <-ossDesktopsCheck: - a.syncDesktopsLimitAlert(ctx) - case <-dynamicLabelsCheck.Next(): - a.syncDynamicLabelsAlert(ctx) - case <-notificationsCleanup.Next(): - go a.CleanupNotifications(ctx) } } } +func (a *Server) tallyRoles(ctx context.Context) { + var count = 0 + a.logger.DebugContext(ctx, "tallying roles") + defer func() { + a.logger.DebugContext(ctx, "tallying roles completed", "role_count", count) + }() + + req := &proto.ListRolesRequest{Limit: 20} + + readLimiter := time.NewTicker(20 * time.Millisecond) + defer readLimiter.Stop() + + for { + resp, err := a.Cache.ListRoles(ctx, req) + if err != nil { + return + } + + count += len(resp.Roles) + req.StartKey = resp.NextKey + + if req.StartKey == "" { + break + } + + select { + case <-readLimiter.C: + case <-ctx.Done(): + return + } + } + + roleCount.Set(float64(count)) +} + func (a *Server) doInstancePeriodics(ctx context.Context) { const slowRate = time.Millisecond * 200 // 5 reads per second const fastRate = time.Millisecond * 5 // 200 reads per second diff --git a/lib/auth/authclient/api.go b/lib/auth/authclient/api.go index 80b25dc26d2b..57821e899579 100644 --- a/lib/auth/authclient/api.go +++ b/lib/auth/authclient/api.go @@ -1088,6 +1088,12 @@ type Cache interface { // GetWindowsDesktopService returns a windows desktop host by name. GetWindowsDesktopService(ctx context.Context, name string) (types.WindowsDesktopService, error) + // GetDynamicWindowsDesktop returns registered dynamic Windows desktop by name. + GetDynamicWindowsDesktop(ctx context.Context, name string) (types.DynamicWindowsDesktop, error) + + // ListDynamicWindowsDesktops returns all registered dynamic Windows desktop. + ListDynamicWindowsDesktops(ctx context.Context, pageSize int, pageToken string) ([]types.DynamicWindowsDesktop, string, error) + // GetStaticTokens gets the list of static tokens used to provision nodes. GetStaticTokens() (types.StaticTokens, error) diff --git a/lib/auth/bot.go b/lib/auth/bot.go index 4b6300564278..d2ce2518abb5 100644 --- a/lib/auth/bot.go +++ b/lib/auth/bot.go @@ -288,8 +288,6 @@ func (a *Server) updateBotInstance( } } - // TODO(nklaassen): consider recording both public keys once they are - // actually separated. var publicKeyPEM []byte if req.tlsPublicKey != nil { publicKeyPEM = req.tlsPublicKey diff --git a/lib/auth/crownjewel/crownjewelv1/service.go b/lib/auth/crownjewel/crownjewelv1/service.go index 7655098da228..fa6147860080 100644 --- a/lib/auth/crownjewel/crownjewelv1/service.go +++ b/lib/auth/crownjewel/crownjewelv1/service.go @@ -143,7 +143,7 @@ func (s *Service) emitCreateAuditEvent(ctx context.Context, req *crownjewelv1.Cr }, CrownJewelQuery: req.GetSpec().GetQuery(), }); auditErr != nil { - slog.WarnContext(ctx, "Failed to emit crown jewel create event.", "error", err) + slog.WarnContext(ctx, "Failed to emit crown jewel create event.", "error", auditErr) } } @@ -237,7 +237,7 @@ func (s *Service) emitUpdateAuditEvent(ctx context.Context, old, new *crownjewel CurrentCrownJewelQuery: old.GetSpec().GetQuery(), UpdatedCrownJewelQuery: new.GetSpec().GetQuery(), }); auditErr != nil { - slog.WarnContext(ctx, "Failed to emit crown jewel update event.", "error", err) + slog.WarnContext(ctx, "Failed to emit crown jewel update event.", "error", auditErr) } } @@ -315,7 +315,7 @@ func (s *Service) DeleteCrownJewel(ctx context.Context, req *crownjewelv1.Delete UpdatedBy: authCtx.Identity.GetIdentity().Username, }, }); auditErr != nil { - slog.WarnContext(ctx, "Failed to emit crown jewel delete event.", "error", err) + slog.WarnContext(ctx, "Failed to emit crown jewel delete event.", "error", auditErr) } if err != nil { diff --git a/lib/auth/dynamicwindows/dynamicwindowsv1/service.go b/lib/auth/dynamicwindows/dynamicwindowsv1/service.go new file mode 100644 index 000000000000..98bc2f81e6b5 --- /dev/null +++ b/lib/auth/dynamicwindows/dynamicwindowsv1/service.go @@ -0,0 +1,215 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package dynamicwindowsv1 + +import ( + "context" + "log/slog" + + "github.com/gravitational/trace" + "google.golang.org/protobuf/types/known/emptypb" + + "github.com/gravitational/teleport" + dynamicwindowspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/dynamicwindows/v1" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/authz" +) + +// Service implements the teleport.trust.v1.TrustService RPC service. +type Service struct { + dynamicwindowspb.UnimplementedDynamicWindowsServiceServer + + authorizer authz.Authorizer + backend Backend + cache Cache + logger *slog.Logger +} + +// ServiceConfig holds configuration options for Service +type ServiceConfig struct { + // Authorizer is the authorizer service which checks access to resources. + Authorizer authz.Authorizer + // Backend will be used for writing the dynamic Windows desktop resources. + Backend Backend + // Cache will be used for reading and writing the dynamic Windows desktop resources. + Cache Cache + // Logger is the logger instance to use. + Logger *slog.Logger +} + +// Backend is the interface used for writing dynamic Windows desktops +type Backend interface { + CreateDynamicWindowsDesktop(context.Context, types.DynamicWindowsDesktop) (types.DynamicWindowsDesktop, error) + UpdateDynamicWindowsDesktop(context.Context, types.DynamicWindowsDesktop) (types.DynamicWindowsDesktop, error) + UpsertDynamicWindowsDesktop(context.Context, types.DynamicWindowsDesktop) (types.DynamicWindowsDesktop, error) + DeleteDynamicWindowsDesktop(ctx context.Context, name string) error +} + +// Cache is the interface used for reading dynamic Windows desktops +type Cache interface { + GetDynamicWindowsDesktop(ctx context.Context, name string) (types.DynamicWindowsDesktop, error) + ListDynamicWindowsDesktops(ctx context.Context, pageSize int, pageToken string) ([]types.DynamicWindowsDesktop, string, error) +} + +// NewService creates new dynamic Windows desktop service +func NewService(cfg ServiceConfig) (*Service, error) { + switch { + case cfg.Backend == nil: + return nil, trace.BadParameter("backend service is required") + case cfg.Cache == nil: + return nil, trace.BadParameter("cache service is required") + case cfg.Authorizer == nil: + return nil, trace.BadParameter("authorizer is required") + } + + if cfg.Logger == nil { + cfg.Logger = slog.With(teleport.ComponentKey, "dynamicwindows") + } + + return &Service{ + authorizer: cfg.Authorizer, + backend: cfg.Backend, + cache: cfg.Cache, + logger: cfg.Logger, + }, nil +} + +// GetDynamicWindowsDesktop returns registered dynamic Windows desktop by name. +func (s *Service) GetDynamicWindowsDesktop(ctx context.Context, request *dynamicwindowspb.GetDynamicWindowsDesktopRequest) (*types.DynamicWindowsDesktopV1, error) { + auth, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + if err := auth.CheckAccessToKind(types.KindDynamicWindowsDesktop, types.VerbRead); err != nil { + return nil, trace.Wrap(err) + } + + if request.GetName() == "" { + return nil, trace.BadParameter("dynamic windows desktop name is required") + } + + d, err := s.cache.GetDynamicWindowsDesktop(ctx, request.GetName()) + if err != nil { + return nil, trace.Wrap(err) + } + + desktop, ok := d.(*types.DynamicWindowsDesktopV1) + if !ok { + return nil, trace.BadParameter("unexpected type %T", d) + } + + return desktop, nil +} + +// ListDynamicWindowsDesktops returns list of dynamic Windows desktops. +func (s *Service) ListDynamicWindowsDesktops(ctx context.Context, request *dynamicwindowspb.ListDynamicWindowsDesktopsRequest) (*dynamicwindowspb.ListDynamicWindowsDesktopsResponse, error) { + auth, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + if err := auth.CheckAccessToKind(types.KindDynamicWindowsDesktop, types.VerbRead, types.VerbList); err != nil { + return nil, trace.Wrap(err) + } + + desktops, next, err := s.cache.ListDynamicWindowsDesktops(ctx, int(request.PageSize), request.PageToken) + if err != nil { + return nil, trace.Wrap(err) + } + + response := &dynamicwindowspb.ListDynamicWindowsDesktopsResponse{ + NextPageToken: next, + } + for _, d := range desktops { + desktop, ok := d.(*types.DynamicWindowsDesktopV1) + if !ok { + return nil, trace.BadParameter("unexpected type %T", d) + } + response.Desktops = append(response.Desktops, desktop) + } + + return response, nil +} + +// CreateDynamicWindowsDesktop registers a new dynamic Windows desktop. +func (s *Service) CreateDynamicWindowsDesktop(ctx context.Context, req *dynamicwindowspb.CreateDynamicWindowsDesktopRequest) (*types.DynamicWindowsDesktopV1, error) { + auth, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + if err := auth.AuthorizeAdminAction(); err != nil { + return nil, trace.Wrap(err) + } + if err := auth.CheckAccessToKind(types.KindDynamicWindowsDesktop, types.VerbCreate); err != nil { + return nil, trace.Wrap(err) + } + d, err := s.backend.CreateDynamicWindowsDesktop(ctx, types.DynamicWindowsDesktop(req.Desktop)) + if err != nil { + return nil, trace.Wrap(err) + } + + createdDesktop, ok := d.(*types.DynamicWindowsDesktopV1) + if !ok { + return nil, trace.BadParameter("unexpected type %T", d) + } + + return createdDesktop, nil +} + +// UpdateDynamicWindowsDesktop updates an existing dynamic Windows desktop. +func (s *Service) UpdateDynamicWindowsDesktop(ctx context.Context, req *dynamicwindowspb.UpdateDynamicWindowsDesktopRequest) (*types.DynamicWindowsDesktopV1, error) { + auth, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + if err := auth.AuthorizeAdminAction(); err != nil { + return nil, trace.Wrap(err) + } + if err := auth.CheckAccessToKind(types.KindDynamicWindowsDesktop, types.VerbUpdate); err != nil { + return nil, trace.Wrap(err) + } + d, err := s.backend.UpdateDynamicWindowsDesktop(ctx, req.Desktop) + if err != nil { + return nil, trace.Wrap(err) + } + + updatedDesktop, ok := d.(*types.DynamicWindowsDesktopV1) + if !ok { + return nil, trace.BadParameter("unexpected type %T", d) + } + + return updatedDesktop, nil +} + +// DeleteDynamicWindowsDesktop removes the specified dynamic Windows desktop. +func (s *Service) DeleteDynamicWindowsDesktop(ctx context.Context, req *dynamicwindowspb.DeleteDynamicWindowsDesktopRequest) (*emptypb.Empty, error) { + auth, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + if err := auth.AuthorizeAdminAction(); err != nil { + return nil, trace.Wrap(err) + } + if err := auth.CheckAccessToKind(types.KindDynamicWindowsDesktop, types.VerbDelete); err != nil { + return nil, trace.Wrap(err) + } + if err := s.backend.DeleteDynamicWindowsDesktop(ctx, req.GetName()); err != nil { + return nil, trace.Wrap(err) + } + return &emptypb.Empty{}, nil +} diff --git a/lib/auth/dynamicwindows/dynamicwindowsv1/service_test.go b/lib/auth/dynamicwindows/dynamicwindowsv1/service_test.go new file mode 100644 index 000000000000..8fee09af10df --- /dev/null +++ b/lib/auth/dynamicwindows/dynamicwindowsv1/service_test.go @@ -0,0 +1,220 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package dynamicwindowsv1 + +import ( + "context" + "fmt" + "slices" + "testing" + + "github.com/gravitational/trace" + "github.com/stretchr/testify/require" + + dynamicwindowsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/dynamicwindows/v1" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/authz" + "github.com/gravitational/teleport/lib/backend/memory" + "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/services/local" + "github.com/gravitational/teleport/lib/tlsca" + "github.com/gravitational/teleport/lib/utils" +) + +func TestServiceAccess(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + allowedVerbs []string + allowedStates []authz.AdminActionAuthState + }{ + { + name: "CreateDynamicWindowsDesktop", + allowedStates: []authz.AdminActionAuthState{authz.AdminActionAuthNotRequired, authz.AdminActionAuthMFAVerified}, + allowedVerbs: []string{types.VerbCreate}, + }, + { + name: "UpdateDynamicWindowsDesktop", + allowedStates: []authz.AdminActionAuthState{authz.AdminActionAuthNotRequired, authz.AdminActionAuthMFAVerified}, + allowedVerbs: []string{types.VerbUpdate}, + }, + { + name: "DeleteDynamicWindowsDesktop", + allowedStates: []authz.AdminActionAuthState{authz.AdminActionAuthNotRequired, authz.AdminActionAuthMFAVerified}, + allowedVerbs: []string{types.VerbDelete}, + }, + { + name: "ListDynamicWindowsDesktops", + allowedStates: []authz.AdminActionAuthState{ + authz.AdminActionAuthUnauthorized, authz.AdminActionAuthNotRequired, + authz.AdminActionAuthMFAVerified, authz.AdminActionAuthMFAVerifiedWithReuse, + }, + allowedVerbs: []string{types.VerbRead, types.VerbList}, + }, + { + name: "GetDynamicWindowsDesktop", + allowedStates: []authz.AdminActionAuthState{ + authz.AdminActionAuthUnauthorized, authz.AdminActionAuthNotRequired, + authz.AdminActionAuthMFAVerified, authz.AdminActionAuthMFAVerifiedWithReuse, + }, + allowedVerbs: []string{types.VerbRead}, + }, + } + + for _, tt := range testCases { + for _, state := range tt.allowedStates { + for _, verbs := range utils.Combinations(tt.allowedVerbs) { + t.Run(fmt.Sprintf("%v,allowed:%v,verbs:%v", tt.name, stateToString(state), verbs), func(t *testing.T) { + service := newService(t, state, fakeChecker{allowedVerbs: verbs}) + err := callMethod(service, tt.name) + // expect access denied except with full set of verbs. + if len(verbs) == len(tt.allowedVerbs) { + require.False(t, trace.IsAccessDenied(err)) + } else { + require.Error(t, err) + require.True(t, trace.IsAccessDenied(err), "expected access denied for verbs %v, got err=%v", verbs, err) + } + }) + } + } + + disallowedStates := otherAdminStates(tt.allowedStates) + for _, state := range disallowedStates { + t.Run(fmt.Sprintf("%v,disallowed:%v", tt.name, stateToString(state)), func(t *testing.T) { + // it is enough to test against tt.allowedVerbs, + // this is the only different data point compared to the test cases above. + service := newService(t, state, fakeChecker{allowedVerbs: tt.allowedVerbs}) + err := callMethod(service, tt.name) + require.True(t, trace.IsAccessDenied(err)) + }) + } + } + + // verify that all declared methods have matching test cases + for _, method := range dynamicwindowsv1.DynamicWindowsService_ServiceDesc.Methods { + t.Run(fmt.Sprintf("%v covered", method.MethodName), func(t *testing.T) { + match := false + for _, testCase := range testCases { + match = match || testCase.name == method.MethodName + } + require.True(t, match, "method %v without coverage, no matching tests", method.MethodName) + }) + } +} + +var allAdminStates = map[authz.AdminActionAuthState]string{ + authz.AdminActionAuthUnauthorized: "Unauthorized", + authz.AdminActionAuthNotRequired: "NotRequired", + authz.AdminActionAuthMFAVerified: "MFAVerified", + authz.AdminActionAuthMFAVerifiedWithReuse: "MFAVerifiedWithReuse", +} + +func stateToString(state authz.AdminActionAuthState) string { + str, ok := allAdminStates[state] + if !ok { + return fmt.Sprintf("unknown(%v)", state) + } + return str +} + +// otherAdminStates returns all admin states except for those passed in +func otherAdminStates(states []authz.AdminActionAuthState) []authz.AdminActionAuthState { + var out []authz.AdminActionAuthState + for state := range allAdminStates { + found := slices.Index(states, state) != -1 + if !found { + out = append(out, state) + } + } + return out +} + +// callMethod calls a method with given name in the DynamicWindowsDesktop service +func callMethod(service *Service, method string) error { + for _, desc := range dynamicwindowsv1.DynamicWindowsService_ServiceDesc.Methods { + if desc.MethodName == method { + _, err := desc.Handler(service, context.Background(), func(arg any) error { + switch arg := arg.(type) { + case *dynamicwindowsv1.CreateDynamicWindowsDesktopRequest: + arg.Desktop, _ = types.NewDynamicWindowsDesktopV1("test", nil, types.DynamicWindowsDesktopSpecV1{ + Addr: "test", + }) + case *dynamicwindowsv1.UpdateDynamicWindowsDesktopRequest: + arg.Desktop, _ = types.NewDynamicWindowsDesktopV1("test", nil, types.DynamicWindowsDesktopSpecV1{ + Addr: "test", + }) + } + return nil + }, nil) + return err + } + } + return fmt.Errorf("method %v not found", method) +} + +type fakeChecker struct { + allowedVerbs []string + services.AccessChecker +} + +func (f fakeChecker) CheckAccessToRule(_ services.RuleContext, _ string, resource string, verb string) error { + if resource == types.KindDynamicWindowsDesktop { + if slices.Contains(f.allowedVerbs, verb) { + return nil + } + } + + return trace.AccessDenied("access denied to rule=%v/verb=%v", resource, verb) +} + +func newService(t *testing.T, authState authz.AdminActionAuthState, checker services.AccessChecker) *Service { + t.Helper() + + b, err := memory.New(memory.Config{}) + require.NoError(t, err) + + backendService, err := local.NewDynamicWindowsDesktopService(b) + require.NoError(t, err) + + authorizer := authz.AuthorizerFunc(func(ctx context.Context) (*authz.Context, error) { + user, err := types.NewUser("probakowski") + if err != nil { + return nil, err + } + return &authz.Context{ + User: user, + Checker: checker, + AdminActionAuthState: authState, + Identity: authz.LocalUser{ + Identity: tlsca.Identity{ + Username: user.GetName(), + }, + }, + }, nil + }) + + service, err := NewService(ServiceConfig{ + Authorizer: authorizer, + Backend: backendService, + Cache: backendService, + }) + require.NoError(t, err) + return service +} diff --git a/lib/auth/grpcserver.go b/lib/auth/grpcserver.go index a9f25e84f559..abab08063efa 100644 --- a/lib/auth/grpcserver.go +++ b/lib/auth/grpcserver.go @@ -57,6 +57,7 @@ import ( dbobjectv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/dbobject/v1" dbobjectimportrulev1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/dbobjectimportrule/v1" discoveryconfigv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/discoveryconfig/v1" + dynamicwindowsv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/dynamicwindows/v1" integrationv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1" kubewaitingcontainerv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/kubewaitingcontainer/v1" loginrulev1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/loginrule/v1" @@ -85,6 +86,7 @@ import ( "github.com/gravitational/teleport/lib/auth/dbobject/dbobjectv1" "github.com/gravitational/teleport/lib/auth/dbobjectimportrule/dbobjectimportrulev1" "github.com/gravitational/teleport/lib/auth/discoveryconfig/discoveryconfigv1" + "github.com/gravitational/teleport/lib/auth/dynamicwindows/dynamicwindowsv1" "github.com/gravitational/teleport/lib/auth/integration/integrationv1" "github.com/gravitational/teleport/lib/auth/kubewaitingcontainer/kubewaitingcontainerv1" "github.com/gravitational/teleport/lib/auth/loginrule/loginrulev1" @@ -5141,6 +5143,16 @@ func NewGRPCServer(cfg GRPCServerConfig) (*GRPCServer, error) { collectortracepb.RegisterTraceServiceServer(server, authServer) auditlogpb.RegisterAuditLogServiceServer(server, authServer) + dynamicWindows, err := dynamicwindowsv1.NewService(dynamicwindowsv1.ServiceConfig{ + Authorizer: cfg.Authorizer, + Backend: cfg.AuthServer.Services, + Cache: cfg.AuthServer.Cache, + }) + if err != nil { + return nil, trace.Wrap(err) + } + dynamicwindowsv1pb.RegisterDynamicWindowsServiceServer(server, dynamicWindows) + trust, err := trustv1.NewService(&trustv1.ServiceConfig{ Authorizer: cfg.Authorizer, Cache: cfg.AuthServer.Cache, diff --git a/lib/auth/helpers.go b/lib/auth/helpers.go index 227d9568699d..851cca043ad9 100644 --- a/lib/auth/helpers.go +++ b/lib/auth/helpers.go @@ -342,6 +342,7 @@ func NewTestAuthServer(cfg TestAuthServerConfig) (*TestAuthServer, error) { DiscoveryConfigs: svces.DiscoveryConfigs, DynamicAccess: svces.DynamicAccessExt, Events: svces.Events, + IdentityCenter: svces.IdentityCenter, Integrations: svces.Integrations, KubeWaitingContainers: svces.KubeWaitingContainer, Kubernetes: svces.Kubernetes, diff --git a/lib/auth/init.go b/lib/auth/init.go index 9ac7abc541db..d7c2309cd2b8 100644 --- a/lib/auth/init.go +++ b/lib/auth/init.go @@ -218,9 +218,12 @@ type InitConfig struct { // session related streams Streamer events.Streamer - // WindowsServices is a service that manages Windows desktop resources. + // WindowsDesktops is a service that manages Windows desktop resources. WindowsDesktops services.WindowsDesktops + // DynamicWindowsServices is a service that manages dynamic Windows desktop resources. + DynamicWindowsDesktops services.DynamicWindowsDesktops + // SAMLIdPServiceProviders is a service that manages SAML IdP service providers. SAMLIdPServiceProviders services.SAMLIdPServiceProviders @@ -328,6 +331,10 @@ type InitConfig struct { // Logger is the logger instance for the auth service to use. Logger *slog.Logger + + // IdentityCenter is the Identity Center state storage service to use in + // this node. + IdentityCenter services.IdentityCenter } // Init instantiates and configures an instance of AuthServer @@ -1240,7 +1247,6 @@ func checkResourceConsistency(ctx context.Context, keyStore *keystore.Manager, c // GenerateIdentity generates identity for the auth server func GenerateIdentity(a *Server, id state.IdentityID, additionalPrincipals, dnsNames []string) (*state.Identity, error) { - // TODO(nklaassen): split SSH and TLS keys for host identities. key, err := cryptosuites.GenerateKey(context.Background(), cryptosuites.GetCurrentSuiteFromAuthPreference(a), cryptosuites.HostIdentity) if err != nil { return nil, trace.Wrap(err) diff --git a/lib/authz/permissions.go b/lib/authz/permissions.go index f8b558701860..86375ca1bb5b 100644 --- a/lib/authz/permissions.go +++ b/lib/authz/permissions.go @@ -1175,6 +1175,7 @@ func definitionForBuiltinRole(clusterName string, recConfig readonly.SessionReco types.NewRule(types.KindLock, services.RO()), types.NewRule(types.KindWindowsDesktopService, services.RW()), types.NewRule(types.KindWindowsDesktop, services.RW()), + types.NewRule(types.KindDynamicWindowsDesktop, services.RW()), }, }, }) diff --git a/lib/cache/cache.go b/lib/cache/cache.go index 47678d0b89a4..312705c25819 100644 --- a/lib/cache/cache.go +++ b/lib/cache/cache.go @@ -161,6 +161,7 @@ func ForAuth(cfg Config) Config { {Kind: types.KindLock}, {Kind: types.KindWindowsDesktopService}, {Kind: types.KindWindowsDesktop}, + {Kind: types.KindDynamicWindowsDesktop}, {Kind: types.KindKubeServer}, {Kind: types.KindInstaller}, {Kind: types.KindKubernetesCluster}, @@ -233,6 +234,7 @@ func ForProxy(cfg Config) Config { {Kind: types.KindDatabase}, {Kind: types.KindWindowsDesktopService}, {Kind: types.KindWindowsDesktop}, + {Kind: types.KindDynamicWindowsDesktop}, {Kind: types.KindKubeServer}, {Kind: types.KindInstaller}, {Kind: types.KindKubernetesCluster}, @@ -392,6 +394,7 @@ func ForWindowsDesktop(cfg Config) Config { {Kind: types.KindNamespace, Name: apidefaults.Namespace}, {Kind: types.KindWindowsDesktopService}, {Kind: types.KindWindowsDesktop}, + {Kind: types.KindDynamicWindowsDesktop}, } cfg.QueueSize = defaults.WindowsDesktopQueueSize return cfg @@ -520,6 +523,7 @@ type Cache struct { webSessionCache types.WebSessionInterface webTokenCache types.WebTokenInterface windowsDesktopsCache services.WindowsDesktops + dynamicWindowsDesktopsCache services.DynamicWindowsDesktops samlIdPServiceProvidersCache services.SAMLIdPServiceProviders //nolint:revive // Because we want this to be IdP. userGroupsCache services.UserGroups oktaCache services.Okta @@ -690,6 +694,8 @@ type Config struct { WebToken types.WebTokenInterface // WindowsDesktops is a windows desktop service. WindowsDesktops services.WindowsDesktops + // DynamicWindowsDesktops is a dynamic Windows desktop service. + DynamicWindowsDesktops services.DynamicWindowsDesktops // SAMLIdPServiceProviders is a SAML IdP service providers service. SAMLIdPServiceProviders services.SAMLIdPServiceProviders // UserGroups is a user groups service. @@ -993,6 +999,12 @@ func New(config Config) (*Cache, error) { return nil, trace.Wrap(err) } + dynamicDesktopsService, err := local.NewDynamicWindowsDesktopService(config.Backend) + if err != nil { + cancel() + return nil, trace.Wrap(err) + } + cs := &Cache{ ctx: ctx, cancel: cancel, @@ -1019,6 +1031,7 @@ func New(config Config) (*Cache, error) { webSessionCache: identityService.WebSessions(), webTokenCache: identityService.WebTokens(), windowsDesktopsCache: local.NewWindowsDesktopService(config.Backend), + dynamicWindowsDesktopsCache: dynamicDesktopsService, accessMontoringRuleCache: accessMonitoringRuleCache, samlIdPServiceProvidersCache: samlIdPServiceProvidersCache, userGroupsCache: userGroupsCache, @@ -2822,6 +2835,32 @@ func (c *Cache) ListWindowsDesktopServices(ctx context.Context, req types.ListWi return rg.reader.ListWindowsDesktopServices(ctx, req) } +// GetDynamicWindowsDesktop returns registered dynamic Windows desktop by name. +func (c *Cache) GetDynamicWindowsDesktop(ctx context.Context, name string) (types.DynamicWindowsDesktop, error) { + ctx, span := c.Tracer.Start(ctx, "cache/GetDynamicWindowsDesktop") + defer span.End() + + rg, err := readCollectionCache(c, c.collections.dynamicWindowsDesktops) + if err != nil { + return nil, trace.Wrap(err) + } + defer rg.Release() + return rg.reader.GetDynamicWindowsDesktop(ctx, name) +} + +// ListDynamicWindowsDesktops returns all registered dynamic Windows desktop. +func (c *Cache) ListDynamicWindowsDesktops(ctx context.Context, pageSize int, nextPage string) ([]types.DynamicWindowsDesktop, string, error) { + ctx, span := c.Tracer.Start(ctx, "cache/ListDynamicWindowsDesktops") + defer span.End() + + rg, err := readCollectionCache(c, c.collections.dynamicWindowsDesktops) + if err != nil { + return nil, "", trace.Wrap(err) + } + defer rg.Release() + return rg.reader.ListDynamicWindowsDesktops(ctx, pageSize, nextPage) +} + // ListSAMLIdPServiceProviders returns a paginated list of SAML IdP service provider resources. func (c *Cache) ListSAMLIdPServiceProviders(ctx context.Context, pageSize int, nextKey string) ([]types.SAMLIdPServiceProvider, string, error) { ctx, span := c.Tracer.Start(ctx, "cache/ListSAMLIdPServiceProviders") diff --git a/lib/cache/cache_test.go b/lib/cache/cache_test.go index 6961d1c2e1b1..4176f637fc65 100644 --- a/lib/cache/cache_test.go +++ b/lib/cache/cache_test.go @@ -3424,6 +3424,7 @@ func TestCacheWatchKindExistsInEvents(t *testing.T) { types.KindLock: &types.LockV2{}, types.KindWindowsDesktopService: &types.WindowsDesktopServiceV3{}, types.KindWindowsDesktop: &types.WindowsDesktopV3{}, + types.KindDynamicWindowsDesktop: &types.DynamicWindowsDesktopV1{}, types.KindInstaller: &types.InstallerV1{}, types.KindKubernetesCluster: &types.KubernetesClusterV3{}, types.KindSAMLIdPServiceProvider: &types.SAMLIdPServiceProviderV1{}, diff --git a/lib/cache/collections.go b/lib/cache/collections.go index 093d52b4c374..e072212a4578 100644 --- a/lib/cache/collections.go +++ b/lib/cache/collections.go @@ -45,6 +45,7 @@ import ( "github.com/gravitational/teleport/api/types/discoveryconfig" "github.com/gravitational/teleport/api/types/secreports" "github.com/gravitational/teleport/api/types/userloginstate" + "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/services" ) @@ -258,6 +259,7 @@ type cacheCollections struct { webSessions collectionReader[webSessionGetter] webTokens collectionReader[webTokenGetter] windowsDesktops collectionReader[windowsDesktopsGetter] + dynamicWindowsDesktops collectionReader[dynamicWindowsDesktopsGetter] windowsDesktopServices collectionReader[windowsDesktopServiceGetter] userNotifications collectionReader[notificationGetter] accessGraphSettings collectionReader[accessGraphSettingsGetter] @@ -621,6 +623,15 @@ func setupCollections(c *Cache, watches []types.WatchKind) (*cacheCollections, e watch: watch, } collections.byKind[resourceKind] = collections.windowsDesktops + case types.KindDynamicWindowsDesktop: + if c.WindowsDesktops == nil { + return nil, trace.BadParameter("missing parameter DynamicWindowsDesktops") + } + collections.dynamicWindowsDesktops = &genericCollection[types.DynamicWindowsDesktop, dynamicWindowsDesktopsGetter, dynamicWindowsDesktopsExecutor]{ + cache: c, + watch: watch, + } + collections.byKind[resourceKind] = collections.dynamicWindowsDesktops case types.KindSAMLIdPServiceProvider: if c.SAMLIdPServiceProviders == nil { return nil, trace.BadParameter("missing parameter SAMLIdPServiceProviders") @@ -2318,6 +2329,54 @@ type windowsDesktopsGetter interface { var _ executor[types.WindowsDesktop, windowsDesktopsGetter] = windowsDesktopsExecutor{} +type dynamicWindowsDesktopsExecutor struct{} + +func (dynamicWindowsDesktopsExecutor) getAll(ctx context.Context, cache *Cache, loadSecrets bool) ([]types.DynamicWindowsDesktop, error) { + var desktops []types.DynamicWindowsDesktop + next := "" + for { + d, token, err := cache.dynamicWindowsDesktopsCache.ListDynamicWindowsDesktops(ctx, defaults.MaxIterationLimit, next) + if err != nil { + return nil, err + } + desktops = append(desktops, d...) + if token == "" { + break + } + next = token + } + return desktops, nil +} + +func (dynamicWindowsDesktopsExecutor) upsert(ctx context.Context, cache *Cache, resource types.DynamicWindowsDesktop) error { + _, err := cache.dynamicWindowsDesktopsCache.UpsertDynamicWindowsDesktop(ctx, resource) + return err +} + +func (dynamicWindowsDesktopsExecutor) deleteAll(ctx context.Context, cache *Cache) error { + return cache.dynamicWindowsDesktopsCache.DeleteAllDynamicWindowsDesktops(ctx) +} + +func (dynamicWindowsDesktopsExecutor) delete(ctx context.Context, cache *Cache, resource types.Resource) error { + return cache.dynamicWindowsDesktopsCache.DeleteDynamicWindowsDesktop(ctx, resource.GetName()) +} + +func (dynamicWindowsDesktopsExecutor) isSingleton() bool { return false } + +func (dynamicWindowsDesktopsExecutor) getReader(cache *Cache, cacheOK bool) dynamicWindowsDesktopsGetter { + if cacheOK { + return cache.dynamicWindowsDesktopsCache + } + return cache.Config.DynamicWindowsDesktops +} + +type dynamicWindowsDesktopsGetter interface { + GetDynamicWindowsDesktop(ctx context.Context, name string) (types.DynamicWindowsDesktop, error) + ListDynamicWindowsDesktops(ctx context.Context, pageSize int, nextPage string) ([]types.DynamicWindowsDesktop, string, error) +} + +var _ executor[types.DynamicWindowsDesktop, dynamicWindowsDesktopsGetter] = dynamicWindowsDesktopsExecutor{} + type kubeClusterExecutor struct{} func (kubeClusterExecutor) getAll(ctx context.Context, cache *Cache, loadSecrets bool) ([]types.KubeCluster, error) { diff --git a/lib/cryptosuites/suites.go b/lib/cryptosuites/suites.go index 641c53ce1c8a..d9f1ec8bf7d7 100644 --- a/lib/cryptosuites/suites.go +++ b/lib/cryptosuites/suites.go @@ -100,7 +100,6 @@ const ( // HostSSH represents a host SSH key. HostSSH // HostIdentity represents a key used for a Teleport host identity. - // TODO(nklaassen): split SSH and TLS keys used for host identities. HostIdentity // BotImpersonatedIdentity represents a key used for a general impersonated diff --git a/lib/inventory/controller.go b/lib/inventory/controller.go index e0f17b889a60..92220597b0e9 100644 --- a/lib/inventory/controller.go +++ b/lib/inventory/controller.go @@ -25,6 +25,7 @@ import ( "time" "github.com/gravitational/trace" + "github.com/jonboulle/clockwork" log "github.com/sirupsen/logrus" "github.com/gravitational/teleport/api/client" @@ -288,12 +289,14 @@ func (c *Controller) RegisterControlStream(stream client.UpstreamInventoryContro // as much as possible. this is intended to mitigate load spikes on auth restart, and is reasonably // safe to do since the instance resource is not directly relied upon for use of any particular teleport // service. - ticker := interval.NewMulti(interval.SubInterval[intervalKey]{ - Key: instanceHeartbeatKey, - VariableDuration: c.instanceHBVariableDuration, - FirstDuration: fullJitter(c.instanceHBVariableDuration.Duration()), - Jitter: seventhJitter, - }) + ticker := interval.NewMulti( + clockwork.NewRealClock(), + interval.SubInterval[intervalKey]{ + Key: instanceHeartbeatKey, + VariableDuration: c.instanceHBVariableDuration, + FirstDuration: fullJitter(c.instanceHBVariableDuration.Duration()), + Jitter: seventhJitter, + }) handle := newUpstreamHandle(stream, hello, ticker) c.store.Insert(handle) go c.handleControlStream(handle) diff --git a/lib/reversetunnel/cache.go b/lib/reversetunnel/cache.go index afcbd9355153..3b2e0ab41c55 100644 --- a/lib/reversetunnel/cache.go +++ b/lib/reversetunnel/cache.go @@ -113,8 +113,6 @@ func (c *certificateCache) generateHostCert(ctx context.Context, principals []st return nil, trace.Wrap(err) } - // TODO(nklaassen): request only an SSH cert, we don't need TLS here. - // GenerateHostCert needs support for this. res, err := c.authClient.TrustClient().GenerateHostCert(ctx, &trustpb.GenerateHostCertRequest{ Key: pubBytes, HostId: principals[0], diff --git a/lib/services/dynamic_desktop.go b/lib/services/dynamic_desktop.go new file mode 100644 index 000000000000..76279becb601 --- /dev/null +++ b/lib/services/dynamic_desktop.go @@ -0,0 +1,91 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package services + +import ( + "context" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/utils" +) + +// DynamicWindowsDesktops defines an interface for managing dynamic Windows desktops. +type DynamicWindowsDesktops interface { + GetDynamicWindowsDesktop(ctx context.Context, name string) (types.DynamicWindowsDesktop, error) + CreateDynamicWindowsDesktop(context.Context, types.DynamicWindowsDesktop) (types.DynamicWindowsDesktop, error) + UpdateDynamicWindowsDesktop(context.Context, types.DynamicWindowsDesktop) (types.DynamicWindowsDesktop, error) + UpsertDynamicWindowsDesktop(context.Context, types.DynamicWindowsDesktop) (types.DynamicWindowsDesktop, error) + DeleteDynamicWindowsDesktop(ctx context.Context, name string) error + DeleteAllDynamicWindowsDesktops(ctx context.Context) error + ListDynamicWindowsDesktops(ctx context.Context, pageSize int, pageToken string) ([]types.DynamicWindowsDesktop, string, error) +} + +// MarshalDynamicWindowsDesktop marshals the DynamicWindowsDesktop resource to JSON. +func MarshalDynamicWindowsDesktop(s types.DynamicWindowsDesktop, opts ...MarshalOption) ([]byte, error) { + cfg, err := CollectOptions(opts) + if err != nil { + return nil, trace.Wrap(err) + } + + switch s := s.(type) { + case *types.DynamicWindowsDesktopV1: + if err := s.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + + return utils.FastMarshal(maybeResetProtoRevision(cfg.PreserveRevision, s)) + default: + return nil, trace.BadParameter("unrecognized windows desktop version %T", s) + } +} + +// UnmarshalDynamicWindowsDesktop unmarshals the DynamicWindowsDesktop resource from JSON. +func UnmarshalDynamicWindowsDesktop(data []byte, opts ...MarshalOption) (types.DynamicWindowsDesktop, error) { + if len(data) == 0 { + return nil, trace.BadParameter("missing windows desktop data") + } + cfg, err := CollectOptions(opts) + if err != nil { + return nil, trace.Wrap(err) + } + var h types.ResourceHeader + if err := utils.FastUnmarshal(data, &h); err != nil { + return nil, trace.Wrap(err) + } + switch h.Version { + case types.V1: + var s types.DynamicWindowsDesktopV1 + if err := utils.FastUnmarshal(data, &s); err != nil { + return nil, trace.BadParameter(err.Error()) + } + if err := s.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + if cfg.Revision != "" { + s.SetRevision(cfg.Revision) + } + if !cfg.Expires.IsZero() { + s.SetExpiry(cfg.Expires) + } + return &s, nil + } + return nil, trace.BadParameter("windows desktop resource version %q is not supported", h.Version) +} diff --git a/lib/services/identitycenter.go b/lib/services/identitycenter.go new file mode 100644 index 000000000000..d3fbf6aca975 --- /dev/null +++ b/lib/services/identitycenter.go @@ -0,0 +1,220 @@ +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package services + +import ( + "context" + + "google.golang.org/protobuf/proto" + + identitycenterv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/identitycenter/v1" + "github.com/gravitational/teleport/lib/utils/pagination" +) + +// IdentityCenterAccount wraps a raw identity center record in a new type to +// allow it to implement the interfaces required for use with the Unified +// Resource listing. +// +// IdentityCenterAccount simply wraps a pointer to the underlying +// identitycenterv1.Account record, and can be treated as a reference-like type. +// Copies of an IdentityCenterAccount will point to the same record. +type IdentityCenterAccount struct { + // This wrapper needs to: + // - implement the interfaces required for use with the Unified Resource + // service. + // - expose the existing interfaces & methods on the underlying + // identitycenterv1.Account + // - avoid copying the underlying identitycenterv1.Account due to embedded + // mutexes in the protobuf-generated code + // + // Given those requirements, storing an embedded pointer seems to be the + // least-bad approach. + + *identitycenterv1.Account +} + +// CloneResource creates a deep copy of the underlying account resource +func (a IdentityCenterAccount) CloneResource() IdentityCenterAccount { + return IdentityCenterAccount{ + Account: proto.Clone(a.Account).(*identitycenterv1.Account), + } +} + +// IdentityCenterAccountID is a strongly-typed Identity Center account ID. +type IdentityCenterAccountID string + +// IdentityCenterAccountGetter provides read-only access to Identity Center +// Account records +type IdentityCenterAccountGetter interface { + // ListIdentityCenterAccounts provides a paged list of all known identity + // center accounts + ListIdentityCenterAccounts(context.Context, int, *pagination.PageRequestToken) ([]IdentityCenterAccount, pagination.NextPageToken, error) + + // GetIdentityCenterAccount fetches a specific Identity Center Account + GetIdentityCenterAccount(context.Context, IdentityCenterAccountID) (IdentityCenterAccount, error) +} + +// IdentityCenterAccounts defines read/write access to Identity Center account +// resources +type IdentityCenterAccounts interface { + IdentityCenterAccountGetter + + // CreateIdentityCenterAccount creates a new Identity Center Account record + CreateIdentityCenterAccount(context.Context, IdentityCenterAccount) (IdentityCenterAccount, error) + + // UpdateIdentityCenterAccount performs a conditional update on an Identity + // Center Account record, returning the updated record on success. + UpdateIdentityCenterAccount(context.Context, IdentityCenterAccount) (IdentityCenterAccount, error) + + // UpsertIdentityCenterAccount performs an *unconditional* upsert on an + // Identity Center Account record, returning the updated record on success. + // Be careful when mixing UpsertIdentityCenterAccount() with resources + // protected by optimistic locking + UpsertIdentityCenterAccount(context.Context, IdentityCenterAccount) (IdentityCenterAccount, error) + + // DeleteIdentityCenterAccount deletes an Identity Center Account record + DeleteIdentityCenterAccount(context.Context, IdentityCenterAccountID) error + + // DeleteAllIdentityCenterAccounts deletes all Identity Center Account records + DeleteAllIdentityCenterAccounts(context.Context) error +} + +// PrincipalAssignmentID is a strongly-typed ID for Identity Center Principal +// Assignments +type PrincipalAssignmentID string + +// IdentityCenterPrincipalAssignments defines operations on an Identity Center +// principal assignment database +type IdentityCenterPrincipalAssignments interface { + // ListPrincipalAssignments lists all PrincipalAssignment records in the + // service + ListPrincipalAssignments(context.Context, int, *pagination.PageRequestToken) ([]*identitycenterv1.PrincipalAssignment, pagination.NextPageToken, error) + + // CreatePrincipalAssignment creates a new Principal Assignment record in + // the service from the supplied in-memory representation. Returns the + // created record on success. + CreatePrincipalAssignment(context.Context, *identitycenterv1.PrincipalAssignment) (*identitycenterv1.PrincipalAssignment, error) + + // GetPrincipalAssignment fetches a specific Principal Assignment record. + GetPrincipalAssignment(context.Context, PrincipalAssignmentID) (*identitycenterv1.PrincipalAssignment, error) + + // UpdatePrincipalAssignment performs a conditional update on a Principal + // Assignment record + UpdatePrincipalAssignment(context.Context, *identitycenterv1.PrincipalAssignment) (*identitycenterv1.PrincipalAssignment, error) + + // UpsertPrincipalAssignment performs an unconditional update on a Principal + // Assignment record + UpsertPrincipalAssignment(context.Context, *identitycenterv1.PrincipalAssignment) (*identitycenterv1.PrincipalAssignment, error) + + // DeletePrincipalAssignment deletes a specific principal assignment record + DeletePrincipalAssignment(context.Context, PrincipalAssignmentID) error + + // DeleteAllPrincipalAssignments deletes all assignment record + DeleteAllPrincipalAssignments(context.Context) error +} + +// PermissionSetID is a strongly typed ID for an identitycenterv1.PermissionSet +type PermissionSetID string + +// IdentityCenterPermissionSets defines the operations to create and maintain +// identitycenterv1.PermissionSet records in the service. +type IdentityCenterPermissionSets interface { + // ListPermissionSets list the known Permission Sets + ListPermissionSets(context.Context, int, *pagination.PageRequestToken) ([]*identitycenterv1.PermissionSet, pagination.NextPageToken, error) + + // CreatePermissionSet creates a new PermissionSet record based on the + // supplied in-memory representation, returning the created record on + // success + CreatePermissionSet(context.Context, *identitycenterv1.PermissionSet) (*identitycenterv1.PermissionSet, error) + + // GetPermissionSet fetches a specific PermissionSet record + GetPermissionSet(context.Context, PermissionSetID) (*identitycenterv1.PermissionSet, error) + + // UpdatePermissionSet performs a conditional update on the supplied Identity + // Center Permission Set + UpdatePermissionSet(context.Context, *identitycenterv1.PermissionSet) (*identitycenterv1.PermissionSet, error) + + // DeletePermissionSet deletes a specific Identity Center PermissionSet + DeletePermissionSet(context.Context, PermissionSetID) error +} + +// IdentityCenterAccountAssignment wraps a raw identitycenterv1.AccountAssignment +// record in a new type to allow it to implement the interfaces required for use +// with the Unified Resource listing. IdentityCenterAccountAssignment simply +// wraps a pointer to the underlying account record, and can be treated as a +// reference-like type. +// +// Copies of an IdentityCenterAccountAssignment will point to the same record. +type IdentityCenterAccountAssignment struct { + // This wrapper needs to: + // - implement the interfaces required for use with the Unified Resource + // service. + // - expose the existing interfaces & methods on the underlying + // identitycenterv1.AccountAssignment + // - avoid copying the underlying identitycenterv1.AccountAssignment due to + // embedded mutexes in the protobuf-generated code + // + // Given those requirements, storing an embedded pointer seems to be the + // least-bad approach. + + *identitycenterv1.AccountAssignment +} + +// CloneResource creates a deep copy of the underlying account resource +func (a IdentityCenterAccountAssignment) CloneResource() IdentityCenterAccountAssignment { + return IdentityCenterAccountAssignment{ + AccountAssignment: proto.Clone(a.AccountAssignment).(*identitycenterv1.AccountAssignment), + } +} + +// IdentityCenterAccountAssignmentID is a strongly typed ID for an +// IdentityCenterAccountAssignment +type IdentityCenterAccountAssignmentID string + +// IdentityCenterAccountAssignments defines the operations to create and maintain +// Identity Center account assignment records in the service. +type IdentityCenterAccountAssignments interface { + // ListAccountAssignments lists all IdentityCenterAccountAssignment record + // known to the service + ListAccountAssignments(context.Context, int, *pagination.PageRequestToken) ([]IdentityCenterAccountAssignment, pagination.NextPageToken, error) + + // CreateAccountAssignment creates a new Account Assignment record in + // the service from the supplied in-memory representation. Returns the + // created record on success. + CreateAccountAssignment(context.Context, IdentityCenterAccountAssignment) (IdentityCenterAccountAssignment, error) + + // GetAccountAssignment fetches a specific Account Assignment record. + GetAccountAssignment(context.Context, IdentityCenterAccountAssignmentID) (IdentityCenterAccountAssignment, error) + + // UpdateAccountAssignment performs a conditional update on the supplied + // Account Assignment, returning the updated record on success. + UpdateAccountAssignment(context.Context, IdentityCenterAccountAssignment) (IdentityCenterAccountAssignment, error) + + // DeleteAccountAssignment deletes a specific account assignment + DeleteAccountAssignment(context.Context, IdentityCenterAccountAssignmentID) error + + // DeleteAllAccountAssignments deletes all known account assignments + DeleteAllAccountAssignments(context.Context) error +} + +// IdentityCenter combines all the resource managers used by the Identity Center plugin +type IdentityCenter interface { + IdentityCenterAccounts + IdentityCenterPermissionSets + IdentityCenterPrincipalAssignments + IdentityCenterAccountAssignments +} diff --git a/lib/services/identitycenter_test.go b/lib/services/identitycenter_test.go new file mode 100644 index 000000000000..5cbc87493fee --- /dev/null +++ b/lib/services/identitycenter_test.go @@ -0,0 +1,97 @@ +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package services + +import ( + "testing" + + "github.com/stretchr/testify/require" + + headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" + identitycenterv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/identitycenter/v1" + "github.com/gravitational/teleport/api/types" +) + +func TestIdentityCenterAccountClone(t *testing.T) { + // GIVEN an Account Record + src := IdentityCenterAccount{ + Account: &identitycenterv1.Account{ + Kind: types.KindIdentityCenterAccount, + Version: types.V1, + Metadata: &headerv1.Metadata{Name: "some-account"}, + Spec: &identitycenterv1.AccountSpec{ + Id: "aws-account-id", + Arn: "arn:aws:sso::account-id:", + Description: "Test account", + PermissionSetInfo: []*identitycenterv1.PermissionSetInfo{ + { + Name: "original value", + Arn: "arn:aws:sso:::permissionSet/ic-instance/ps-instance", + }, + }, + }, + }, + } + + // WHEN I clone the resource + dst := src.CloneResource() + + // EXPECT that the resulting clone compares equally + require.Equal(t, src, dst) + + // WHEN I modify the source object in a way that would be shared with a + // shallow copy + src.Spec.PermissionSetInfo[0].Name = "some new value" + + // EXPECT that the cloned object DOES NOT inherit the update + require.NotEqual(t, src, dst) + require.Equal(t, "original value", dst.Spec.PermissionSetInfo[0].Name) +} + +func TestIdentityCenterAccountAssignmentClone(t *testing.T) { + // GIVEN an Account Assignment Record + src := IdentityCenterAccountAssignment{ + AccountAssignment: &identitycenterv1.AccountAssignment{ + Kind: types.KindIdentityCenterAccountAssignment, + Version: types.V1, + Metadata: &headerv1.Metadata{Name: "u-test@example.com"}, + Spec: &identitycenterv1.AccountAssignmentSpec{ + Display: "Some-Permission-set on Some-AWS-account", + PermissionSet: &identitycenterv1.PermissionSetInfo{ + Arn: "arn:aws:sso:::permissionSet/ic-instance/ps-instance", + Name: "original name", + }, + AccountName: "Some Account Name", + AccountId: "some account id", + }, + }, + } + + // WHEN I clone the resource + dst := src.CloneResource() + + // EXPECT that the resulting clone compares equally + require.Equal(t, src, dst) + + // WHEN I modify the source object in a way that would be shared with a + // shallow copy + src.Spec.PermissionSet.Name = "some new name" + + // EXPECT that the cloned object DOES NOT inherit the update + require.NotEqual(t, src, dst) + require.Equal(t, "original name", dst.Spec.PermissionSet.Name) +} diff --git a/lib/services/local/dynamic_desktops.go b/lib/services/local/dynamic_desktops.go new file mode 100644 index 000000000000..b4b482d600de --- /dev/null +++ b/lib/services/local/dynamic_desktops.go @@ -0,0 +1,119 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package local + +import ( + "context" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/backend" + "github.com/gravitational/teleport/lib/defaults" + "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/services/local/generic" +) + +// DynamicWindowsDesktopService manages dynamic Windows desktop resources in the backend. +type DynamicWindowsDesktopService struct { + service *generic.Service[types.DynamicWindowsDesktop] +} + +// NewDynamicWindowsDesktopService creates a new WindowsDesktopsService. +func NewDynamicWindowsDesktopService(b backend.Backend) (*DynamicWindowsDesktopService, error) { + service, err := generic.NewService(&generic.ServiceConfig[types.DynamicWindowsDesktop]{ + Backend: b, + ResourceKind: types.KindDynamicWindowsDesktop, + PageLimit: defaults.MaxIterationLimit, + BackendPrefix: backend.NewKey(dynamicWindowsDesktopsPrefix), + MarshalFunc: services.MarshalDynamicWindowsDesktop, + UnmarshalFunc: services.UnmarshalDynamicWindowsDesktop, + }) + if err != nil { + return nil, trace.Wrap(err) + } + return &DynamicWindowsDesktopService{ + service: service, + }, nil +} + +// GetDynamicWindowsDesktop returns dynamic Windows desktops by name. +func (s *DynamicWindowsDesktopService) GetDynamicWindowsDesktop(ctx context.Context, name string) (types.DynamicWindowsDesktop, error) { + desktop, err := s.service.GetResource(ctx, name) + if err != nil { + return nil, trace.Wrap(err) + } + return desktop, err +} + +// CreateDynamicWindowsDesktop creates a dynamic Windows desktop resource. +func (s *DynamicWindowsDesktopService) CreateDynamicWindowsDesktop(ctx context.Context, desktop types.DynamicWindowsDesktop) (types.DynamicWindowsDesktop, error) { + d, err := s.service.CreateResource(ctx, desktop) + if err != nil { + return nil, trace.Wrap(err) + } + return d, err +} + +// UpdateDynamicWindowsDesktop updates a dynamic Windows desktop resource. +func (s *DynamicWindowsDesktopService) UpdateDynamicWindowsDesktop(ctx context.Context, desktop types.DynamicWindowsDesktop) (types.DynamicWindowsDesktop, error) { + d, err := s.service.UpdateResource(ctx, desktop) + if err != nil { + return nil, trace.Wrap(err) + } + return d, err +} + +// UpsertDynamicWindowsDesktop updates a dynamic Windows desktop resource, creating it if it doesn't exist. +func (s *DynamicWindowsDesktopService) UpsertDynamicWindowsDesktop(ctx context.Context, desktop types.DynamicWindowsDesktop) (types.DynamicWindowsDesktop, error) { + d, err := s.service.UpsertResource(ctx, desktop) + if err != nil { + return nil, trace.Wrap(err) + } + return d, err +} + +// DeleteDynamicWindowsDesktop removes the specified dynamic Windows desktop resource. +func (s *DynamicWindowsDesktopService) DeleteDynamicWindowsDesktop(ctx context.Context, name string) error { + if err := s.service.DeleteResource(ctx, name); err != nil { + return trace.Wrap(err) + } + return nil +} + +// DeleteAllDynamicWindowsDesktops removes all dynamic Windows desktop resources. +func (s *DynamicWindowsDesktopService) DeleteAllDynamicWindowsDesktops(ctx context.Context) error { + if err := s.service.DeleteAllResources(ctx); err != nil { + return trace.Wrap(err) + } + return nil +} + +// ListDynamicWindowsDesktops returns all dynamic Windows desktops matching filter. +func (s *DynamicWindowsDesktopService) ListDynamicWindowsDesktops(ctx context.Context, pageSize int, pageToken string) ([]types.DynamicWindowsDesktop, string, error) { + desktops, next, err := s.service.ListResources(ctx, pageSize, pageToken) + if err != nil { + return nil, "", trace.Wrap(err) + } + return desktops, next, nil +} + +const ( + dynamicWindowsDesktopsPrefix = "dynamicWindowsDesktop" +) diff --git a/lib/services/local/dynamic_desktops_test.go b/lib/services/local/dynamic_desktops_test.go new file mode 100644 index 000000000000..75ed04008064 --- /dev/null +++ b/lib/services/local/dynamic_desktops_test.go @@ -0,0 +1,232 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package local + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/gravitational/trace" + "github.com/jonboulle/clockwork" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/backend" + "github.com/gravitational/teleport/lib/backend/memory" +) + +func newDynamicDesktop(t *testing.T, name string) types.DynamicWindowsDesktop { + desktop, err := types.NewDynamicWindowsDesktopV1(name, nil, types.DynamicWindowsDesktopSpecV1{ + Addr: "xyz", + }) + require.NoError(t, err) + return desktop +} + +func setupDynamicDesktopTest(t *testing.T) (context.Context, *DynamicWindowsDesktopService) { + ctx := context.Background() + clock := clockwork.NewFakeClock() + mem, err := memory.New(memory.Config{ + Context: ctx, + Clock: clock, + }) + require.NoError(t, err) + service, err := NewDynamicWindowsDesktopService(backend.NewSanitizer(mem)) + require.NoError(t, err) + return ctx, service +} + +func TestDynamicWindowsService_CreateDynamicDesktop(t *testing.T) { + t.Parallel() + ctx, service := setupDynamicDesktopTest(t) + t.Run("ok", func(t *testing.T) { + want := newDynamicDesktop(t, "example") + got, err := service.CreateDynamicWindowsDesktop(ctx, want.Copy()) + want.SetRevision(got.GetRevision()) + require.NoError(t, err) + require.NotEmpty(t, got.GetRevision()) + require.Empty(t, cmp.Diff( + want, + got, + protocmp.Transform(), + )) + }) + t.Run("no upsert", func(t *testing.T) { + want := newDynamicDesktop(t, "upsert") + _, err := service.CreateDynamicWindowsDesktop(ctx, want.Copy()) + require.NoError(t, err) + _, err = service.CreateDynamicWindowsDesktop(ctx, want.Copy()) + require.Error(t, err) + require.True(t, trace.IsAlreadyExists(err)) + }) +} + +func TestDynamicWindowsService_UpsertDynamicDesktop(t *testing.T) { + ctx, service := setupDynamicDesktopTest(t) + want := newDynamicDesktop(t, "example") + got, err := service.UpsertDynamicWindowsDesktop(ctx, want.Copy()) + want.SetRevision(got.GetRevision()) + require.NoError(t, err) + require.NotEmpty(t, got.GetRevision()) + require.Empty(t, cmp.Diff( + want, + got, + protocmp.Transform(), + )) + _, err = service.UpsertDynamicWindowsDesktop(ctx, want.Copy()) + require.NoError(t, err) +} + +func TestDynamicWindowsService_GetDynamicDesktop(t *testing.T) { + t.Parallel() + ctx, service := setupDynamicDesktopTest(t) + t.Run("not found", func(t *testing.T) { + _, err := service.GetDynamicWindowsDesktop(ctx, "notfound") + require.Error(t, err) + require.True(t, trace.IsNotFound(err)) + }) + t.Run("ok", func(t *testing.T) { + want := newDynamicDesktop(t, "example") + created, err := service.CreateDynamicWindowsDesktop(ctx, want.Copy()) + require.NoError(t, err) + got, err := service.GetDynamicWindowsDesktop(ctx, "example") + require.NoError(t, err) + require.Empty(t, cmp.Diff( + created, + got, + protocmp.Transform(), + )) + }) +} + +func TestDynamicWindowsService_ListDynamicDesktop(t *testing.T) { + t.Parallel() + t.Run("none", func(t *testing.T) { + ctx, service := setupDynamicDesktopTest(t) + desktops, _, err := service.ListDynamicWindowsDesktops(ctx, 5, "") + require.NoError(t, err) + require.Empty(t, desktops) + }) + t.Run("list all", func(t *testing.T) { + ctx, service := setupDynamicDesktopTest(t) + d1, err := service.CreateDynamicWindowsDesktop(ctx, newDynamicDesktop(t, "d1")) + require.NoError(t, err) + d2, err := service.CreateDynamicWindowsDesktop(ctx, newDynamicDesktop(t, "d2")) + require.NoError(t, err) + desktops, next, err := service.ListDynamicWindowsDesktops(ctx, 5, "") + require.NoError(t, err) + require.Len(t, desktops, 2) + require.Empty(t, next) + require.Empty(t, cmp.Diff( + d1, + desktops[0], + protocmp.Transform(), + )) + require.Empty(t, cmp.Diff( + d2, + desktops[1], + protocmp.Transform(), + )) + }) + t.Run("list paged", func(t *testing.T) { + ctx, service := setupDynamicDesktopTest(t) + d1, err := service.CreateDynamicWindowsDesktop(ctx, newDynamicDesktop(t, "d1")) + require.NoError(t, err) + d2, err := service.CreateDynamicWindowsDesktop(ctx, newDynamicDesktop(t, "d2")) + require.NoError(t, err) + desktops, next, err := service.ListDynamicWindowsDesktops(ctx, 1, "") + require.NoError(t, err) + require.Len(t, desktops, 1) + require.NotEmpty(t, next) + require.Empty(t, cmp.Diff( + d1, + desktops[0], + protocmp.Transform(), + )) + desktops, next, err = service.ListDynamicWindowsDesktops(ctx, 1, next) + require.NoError(t, err) + require.Len(t, desktops, 1) + require.Empty(t, next) + require.Empty(t, cmp.Diff( + d2, + desktops[0], + protocmp.Transform(), + )) + }) +} + +func TestDynamicWindowsService_UpdateDynamicDesktop(t *testing.T) { + t.Parallel() + ctx, service := setupDynamicDesktopTest(t) + t.Run("not found", func(t *testing.T) { + want := newDynamicDesktop(t, "example") + _, err := service.UpdateDynamicWindowsDesktop(ctx, want.Copy()) + require.Error(t, err) + require.True(t, trace.IsNotFound(err)) + }) + t.Run("ok", func(t *testing.T) { + want := newDynamicDesktop(t, "example") + created, err := service.CreateDynamicWindowsDesktop(ctx, want.Copy()) + require.NoError(t, err) + updated, err := service.UpdateDynamicWindowsDesktop(ctx, created.Copy()) + require.NoError(t, err) + require.NotEqual(t, created.GetRevision(), updated.GetRevision()) + }) +} + +func TestDynamicWindowsService_DeleteDynamicDesktop(t *testing.T) { + t.Parallel() + ctx, service := setupDynamicDesktopTest(t) + t.Run("not found", func(t *testing.T) { + err := service.DeleteDynamicWindowsDesktop(ctx, "notfound") + require.Error(t, err) + require.True(t, trace.IsNotFound(err)) + }) + t.Run("ok", func(t *testing.T) { + _, err := service.CreateDynamicWindowsDesktop(ctx, newDynamicDesktop(t, "example")) + require.NoError(t, err) + _, err = service.GetDynamicWindowsDesktop(ctx, "example") + require.NoError(t, err) + err = service.DeleteDynamicWindowsDesktop(ctx, "example") + require.NoError(t, err) + _, err = service.GetDynamicWindowsDesktop(ctx, "example") + require.Error(t, err) + require.True(t, trace.IsNotFound(err)) + }) +} + +func TestDynamicWindowsService_DeleteAllDynamicDesktop(t *testing.T) { + ctx, service := setupDynamicDesktopTest(t) + err := service.DeleteAllDynamicWindowsDesktops(ctx) + require.NoError(t, err) + _, err = service.CreateDynamicWindowsDesktop(ctx, newDynamicDesktop(t, "d1")) + require.NoError(t, err) + _, err = service.CreateDynamicWindowsDesktop(ctx, newDynamicDesktop(t, "d2")) + require.NoError(t, err) + desktops, _, err := service.ListDynamicWindowsDesktops(ctx, 5, "") + require.NoError(t, err) + require.Len(t, desktops, 2) + err = service.DeleteAllDynamicWindowsDesktops(ctx) + require.NoError(t, err) + desktops, _, err = service.ListDynamicWindowsDesktops(ctx, 5, "") + require.NoError(t, err) + require.Empty(t, desktops) +} diff --git a/lib/services/local/events.go b/lib/services/local/events.go index 5123421e9f8d..f0ef42d33648 100644 --- a/lib/services/local/events.go +++ b/lib/services/local/events.go @@ -167,6 +167,8 @@ func (e *EventsService) NewWatcher(ctx context.Context, watch types.Watch) (type parser = newWindowsDesktopServicesParser() case types.KindWindowsDesktop: parser = newWindowsDesktopsParser() + case types.KindDynamicWindowsDesktop: + parser = newDynamicWindowsDesktopsParser() case types.KindInstaller: parser = newInstallerParser() case types.KindKubernetesCluster: @@ -1851,6 +1853,43 @@ func (p *windowsDesktopServicesParser) parse(event backend.Event) (types.Resourc } } +func newDynamicWindowsDesktopsParser() *dynamicWindowsDesktopsParser { + return &dynamicWindowsDesktopsParser{ + baseParser: newBaseParser(backend.NewKey(dynamicWindowsDesktopsPrefix, "")), + } +} + +type dynamicWindowsDesktopsParser struct { + baseParser +} + +func (p *dynamicWindowsDesktopsParser) parse(event backend.Event) (types.Resource, error) { + switch event.Type { + case types.OpDelete: + name := event.Item.Key.TrimPrefix(backend.NewKey(dynamicWindowsDesktopsPrefix, "")).String() + if name == "" { + return nil, trace.NotFound("failed parsing %v", event.Item.Key.String()) + } + + return &types.ResourceHeader{ + Kind: types.KindDynamicWindowsDesktop, + Version: types.V1, + Metadata: types.Metadata{ + Name: strings.TrimPrefix(name, backend.SeparatorString), + Namespace: apidefaults.Namespace, + }, + }, nil + case types.OpPut: + return services.UnmarshalDynamicWindowsDesktop( + event.Item.Value, + services.WithExpires(event.Item.Expires), + services.WithRevision(event.Item.Revision), + ) + default: + return nil, trace.BadParameter("event %v is not supported", event.Type) + } +} + func newWindowsDesktopsParser() *windowsDesktopsParser { return &windowsDesktopsParser{ baseParser: newBaseParser(backend.NewKey(windowsDesktopsPrefix, "")), diff --git a/lib/services/local/events_test.go b/lib/services/local/events_test.go index a57f07aadf2d..c4d7c91ed4e2 100644 --- a/lib/services/local/events_test.go +++ b/lib/services/local/events_test.go @@ -49,7 +49,8 @@ func fetchEvent(t *testing.T, w types.Watcher, timeout time.Duration) types.Even return ev } -func testContext(t *testing.T) context.Context { +func newTestContext(t *testing.T) context.Context { + t.Helper() ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) return ctx @@ -209,7 +210,7 @@ func TestWatchers(t *testing.T) { for _, test := range testCases { t.Run(test.name, func(t *testing.T) { t.Parallel() - ctx := testContext(t) + ctx := newTestContext(t) // GIVEN an empty back-end clock := clockwork.NewFakeClock() diff --git a/lib/services/local/identitycenter.go b/lib/services/local/identitycenter.go new file mode 100644 index 000000000000..a24e433ef3b8 --- /dev/null +++ b/lib/services/local/identitycenter.go @@ -0,0 +1,399 @@ +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package local + +import ( + "context" + "log/slog" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport" + identitycenterv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/identitycenter/v1" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/backend" + "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/services/local/generic" + "github.com/gravitational/teleport/lib/utils/pagination" +) + +const ( + identityCenterPageSize = 100 +) + +const ( + awsResourcePrefix = "identity_center" + awsAccountPrefix = "accounts" + awsPermissionSetPrefix = "permission_sets" + awsPrincipalAssignmentPrefix = "principal_assignments" + awsAccountAssignmentPrefix = "account_assignments" +) + +// IdentityCenterServiceConfig provides configuration parameters for an +// IdentityCenterService +type IdentityCenterServiceConfig struct { + // Backend is the storage backend to use for the service + Backend backend.Backend + + // Logger is the logger for the service to use. A default will be supplied + // if not specified. + Logger *slog.Logger +} + +// CheckAndSetDefaults validates the cfg and supplies defaults where +// appropriate. +func (cfg *IdentityCenterServiceConfig) CheckAndSetDefaults() error { + if cfg.Backend == nil { + return trace.BadParameter("must supply backend") + } + + if cfg.Logger == nil { + cfg.Logger = slog.Default().With(teleport.ComponentKey, "AWS-IC-LOCAL") + } + + return nil +} + +// IdentityCenterService handles low-level CRUD operations for the identity- +// center related resources +type IdentityCenterService struct { + accounts *generic.ServiceWrapper[*identitycenterv1.Account] + permissionSets *generic.ServiceWrapper[*identitycenterv1.PermissionSet] + principalAssignments *generic.ServiceWrapper[*identitycenterv1.PrincipalAssignment] + accountAssignments *generic.ServiceWrapper[*identitycenterv1.AccountAssignment] +} + +// compile-time assertion that the IdentityCenterService implements the +// services.IdentityCenter interface +var _ services.IdentityCenter = (*IdentityCenterService)(nil) + +// NewIdentityCenterService creates a new service for managing identity-center +// related resources +func NewIdentityCenterService(cfg IdentityCenterServiceConfig) (*IdentityCenterService, error) { + if err := cfg.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + + accountsSvc, err := generic.NewServiceWrapper(generic.ServiceWrapperConfig[*identitycenterv1.Account]{ + Backend: cfg.Backend, + ResourceKind: types.KindIdentityCenterAccount, + BackendPrefix: backend.NewKey(awsResourcePrefix, awsAccountPrefix), + MarshalFunc: services.MarshalProtoResource[*identitycenterv1.Account], + UnmarshalFunc: services.UnmarshalProtoResource[*identitycenterv1.Account], + }) + if err != nil { + return nil, trace.Wrap(err, "creating accounts service") + } + + permissionSetSvc, err := generic.NewServiceWrapper(generic.ServiceWrapperConfig[*identitycenterv1.PermissionSet]{ + Backend: cfg.Backend, + ResourceKind: types.KindIdentityCenterPermissionSet, + BackendPrefix: backend.NewKey(awsResourcePrefix, awsPermissionSetPrefix), + MarshalFunc: services.MarshalProtoResource[*identitycenterv1.PermissionSet], + UnmarshalFunc: services.UnmarshalProtoResource[*identitycenterv1.PermissionSet], + }) + if err != nil { + return nil, trace.Wrap(err, "creating permission sets service") + } + + principalsSvc, err := generic.NewServiceWrapper(generic.ServiceWrapperConfig[*identitycenterv1.PrincipalAssignment]{ + Backend: cfg.Backend, + ResourceKind: types.KindIdentityCenterPrincipalAssignment, + BackendPrefix: backend.NewKey(awsResourcePrefix, awsPrincipalAssignmentPrefix), + MarshalFunc: services.MarshalProtoResource[*identitycenterv1.PrincipalAssignment], + UnmarshalFunc: services.UnmarshalProtoResource[*identitycenterv1.PrincipalAssignment], + }) + if err != nil { + return nil, trace.Wrap(err, "creating principal assignments service") + } + + accountAssignmentsSvc, err := generic.NewServiceWrapper(generic.ServiceWrapperConfig[*identitycenterv1.AccountAssignment]{ + Backend: cfg.Backend, + ResourceKind: types.KindIdentityCenterAccountAssignment, + BackendPrefix: backend.NewKey(awsResourcePrefix, awsAccountAssignmentPrefix), + MarshalFunc: services.MarshalProtoResource[*identitycenterv1.AccountAssignment], + UnmarshalFunc: services.UnmarshalProtoResource[*identitycenterv1.AccountAssignment], + }) + if err != nil { + return nil, trace.Wrap(err, "creating account assignments service") + } + + svc := &IdentityCenterService{ + accounts: accountsSvc, + permissionSets: permissionSetSvc, + principalAssignments: principalsSvc, + accountAssignments: accountAssignmentsSvc, + } + + return svc, nil +} + +// ListIdentityCenterAccounts provides a paged list of all AWS accounts known +// to the Identity Center integration +func (svc *IdentityCenterService) ListIdentityCenterAccounts(ctx context.Context, pageSize int, page *pagination.PageRequestToken) ([]services.IdentityCenterAccount, pagination.NextPageToken, error) { + if pageSize == 0 { + pageSize = identityCenterPageSize + } + + pageToken, err := page.Consume() + if err != nil { + return nil, "", trace.Wrap(err, "listing identity center assignment records") + } + + accounts, nextPage, err := svc.accounts.ListResources(ctx, pageSize, pageToken) + if err != nil { + return nil, "", trace.Wrap(err, "listing identity center assignment records") + } + + result := make([]services.IdentityCenterAccount, len(accounts)) + for i, acct := range accounts { + result[i] = services.IdentityCenterAccount{Account: acct} + } + + return result, pagination.NextPageToken(nextPage), nil +} + +// CreateIdentityCenterAccount creates a new Identity Center Account record +func (svc *IdentityCenterService) CreateIdentityCenterAccount(ctx context.Context, acct services.IdentityCenterAccount) (services.IdentityCenterAccount, error) { + created, err := svc.accounts.CreateResource(ctx, acct.Account) + if err != nil { + return services.IdentityCenterAccount{}, trace.Wrap(err, "creating identity center account") + } + return services.IdentityCenterAccount{Account: created}, nil +} + +// GetIdentityCenterAccount fetches a specific Identity Center Account +func (svc *IdentityCenterService) GetIdentityCenterAccount(ctx context.Context, name services.IdentityCenterAccountID) (services.IdentityCenterAccount, error) { + acct, err := svc.accounts.GetResource(ctx, string(name)) + if err != nil { + return services.IdentityCenterAccount{}, trace.Wrap(err, "fetching identity center account") + } + return services.IdentityCenterAccount{Account: acct}, nil +} + +// UpdateIdentityCenterAccount performs a conditional update on an Identity +// Center Account record, returning the updated record on success. +func (svc *IdentityCenterService) UpdateIdentityCenterAccount(ctx context.Context, acct services.IdentityCenterAccount) (services.IdentityCenterAccount, error) { + updated, err := svc.accounts.ConditionalUpdateResource(ctx, acct.Account) + if err != nil { + return services.IdentityCenterAccount{}, trace.Wrap(err, "updating identity center account record") + } + return services.IdentityCenterAccount{Account: updated}, nil +} + +// UpsertIdentityCenterAccount performs an *unconditional* upsert on an +// Identity Center Account record, returning the updated record on success. +// Be careful when mixing UpsertIdentityCenterAccount() with resources +// protected by optimistic locking +func (svc *IdentityCenterService) UpsertIdentityCenterAccount(ctx context.Context, acct services.IdentityCenterAccount) (services.IdentityCenterAccount, error) { + updated, err := svc.accounts.UpsertResource(ctx, acct.Account) + if err != nil { + return services.IdentityCenterAccount{}, trace.Wrap(err, "upserting identity center account record") + } + return services.IdentityCenterAccount{Account: updated}, nil +} + +// DeleteIdentityCenterAccount deletes an Identity Center Account record +func (svc *IdentityCenterService) DeleteIdentityCenterAccount(ctx context.Context, name services.IdentityCenterAccountID) error { + return trace.Wrap(svc.accounts.DeleteResource(ctx, string(name))) +} + +// DeleteAllIdentityCenterAccounts deletes all Identity Center Account records +func (svc *IdentityCenterService) DeleteAllIdentityCenterAccounts(ctx context.Context) error { + return trace.Wrap(svc.accounts.DeleteAllResources(ctx)) +} + +// ListPrincipalAssignments lists all PrincipalAssignment records in the service +func (svc *IdentityCenterService) ListPrincipalAssignments(ctx context.Context, pageSize int, page *pagination.PageRequestToken) ([]*identitycenterv1.PrincipalAssignment, pagination.NextPageToken, error) { + if pageSize == 0 { + pageSize = identityCenterPageSize + } + + pageToken, err := page.Consume() + if err != nil { + return nil, "", trace.Wrap(err, "extracting page token") + } + + resp, nextPage, err := svc.principalAssignments.ListResources(ctx, pageSize, pageToken) + if err != nil { + return nil, "", trace.Wrap(err, "listing identity center assignment records") + } + return resp, pagination.NextPageToken(nextPage), nil +} + +// CreatePrincipalAssignment creates a new Principal Assignment record in the +// service from the supplied in-memory representation. Returns the created +// record on success. +func (svc *IdentityCenterService) CreatePrincipalAssignment(ctx context.Context, asmt *identitycenterv1.PrincipalAssignment) (*identitycenterv1.PrincipalAssignment, error) { + created, err := svc.principalAssignments.CreateResource(ctx, asmt) + if err != nil { + return nil, trace.Wrap(err, "creating principal assignment") + } + return created, nil +} + +// GetPrincipalAssignment fetches a specific Principal Assignment record. +func (svc *IdentityCenterService) GetPrincipalAssignment(ctx context.Context, name services.PrincipalAssignmentID) (*identitycenterv1.PrincipalAssignment, error) { + state, err := svc.principalAssignments.GetResource(ctx, string(name)) + if err != nil { + return nil, trace.Wrap(err, "fetching principal assignment") + } + return state, nil +} + +// UpdatePrincipalAssignment performs a conditional update on a Principal +// Assignment record +func (svc *IdentityCenterService) UpdatePrincipalAssignment(ctx context.Context, asmt *identitycenterv1.PrincipalAssignment) (*identitycenterv1.PrincipalAssignment, error) { + updated, err := svc.principalAssignments.ConditionalUpdateResource(ctx, asmt) + if err != nil { + return nil, trace.Wrap(err, "updating principal assignment record") + } + return updated, nil +} + +// UpsertPrincipalAssignment performs an unconditional update on a Principal +// Assignment record +func (svc *IdentityCenterService) UpsertPrincipalAssignment(ctx context.Context, asmt *identitycenterv1.PrincipalAssignment) (*identitycenterv1.PrincipalAssignment, error) { + updated, err := svc.principalAssignments.UpsertResource(ctx, asmt) + if err != nil { + return nil, trace.Wrap(err, "upserting principal assignment record") + } + return updated, nil +} + +// DeletePrincipalAssignment deletes a specific principal assignment record +func (svc *IdentityCenterService) DeletePrincipalAssignment(ctx context.Context, name services.PrincipalAssignmentID) error { + return trace.Wrap(svc.principalAssignments.DeleteResource(ctx, string(name))) +} + +// DeleteAllPrincipalAssignments deletes all assignment record +func (svc *IdentityCenterService) DeleteAllPrincipalAssignments(ctx context.Context) error { + return trace.Wrap(svc.principalAssignments.DeleteAllResources(ctx)) +} + +// ListPermissionSets list the known Permission Sets in the managed Identity Center +func (svc *IdentityCenterService) ListPermissionSets(ctx context.Context, pageSize int, page *pagination.PageRequestToken) ([]*identitycenterv1.PermissionSet, pagination.NextPageToken, error) { + if pageSize == 0 { + pageSize = identityCenterPageSize + } + pageToken, err := page.Consume() + if err != nil { + return nil, "", trace.Wrap(err, "extracting page token") + } + resp, nextPage, err := svc.permissionSets.ListResources(ctx, pageSize, pageToken) + if err != nil { + return nil, "", trace.Wrap(err, "listing identity center permission set records") + } + return resp, pagination.NextPageToken(nextPage), nil +} + +// CreatePermissionSet creates a new PermissionSet record based on the supplied +// in-memory representation, returning the created record on success. +func (svc *IdentityCenterService) CreatePermissionSet(ctx context.Context, asmt *identitycenterv1.PermissionSet) (*identitycenterv1.PermissionSet, error) { + created, err := svc.permissionSets.CreateResource(ctx, asmt) + if err != nil { + return nil, trace.Wrap(err, "creating identity center permission set") + } + return created, nil +} + +// GetPermissionSet fetches a specific PermissionSet record +func (svc *IdentityCenterService) GetPermissionSet(ctx context.Context, name services.PermissionSetID) (*identitycenterv1.PermissionSet, error) { + state, err := svc.permissionSets.GetResource(ctx, string(name)) + if err != nil { + return nil, trace.Wrap(err, "fetching permission set") + } + return state, nil +} + +// UpdatePermissionSet performs a conditional update on the supplied Identity +// Center Permission Set +func (svc *IdentityCenterService) UpdatePermissionSet(ctx context.Context, asmt *identitycenterv1.PermissionSet) (*identitycenterv1.PermissionSet, error) { + updated, err := svc.permissionSets.ConditionalUpdateResource(ctx, asmt) + if err != nil { + return nil, trace.Wrap(err, "updating permission set record") + } + return updated, nil +} + +// DeletePermissionSet deletes a specific Identity Center PermissionSet +func (svc *IdentityCenterService) DeletePermissionSet(ctx context.Context, name services.PermissionSetID) error { + return trace.Wrap(svc.permissionSets.DeleteResource(ctx, string(name))) +} + +// ListAccountAssignments lists all IdentityCenterAccountAssignment record +// known to the service +func (svc *IdentityCenterService) ListAccountAssignments(ctx context.Context, pageSize int, page *pagination.PageRequestToken) ([]services.IdentityCenterAccountAssignment, pagination.NextPageToken, error) { + if pageSize == 0 { + pageSize = identityCenterPageSize + } + pageToken, err := page.Consume() + if err != nil { + return nil, "", trace.Wrap(err, "extracting page token") + } + assignments, nextPage, err := svc.accountAssignments.ListResources(ctx, pageSize, pageToken) + if err != nil { + return nil, "", trace.Wrap(err, "listing identity center assignment records") + } + + result := make([]services.IdentityCenterAccountAssignment, len(assignments)) + for i, asmt := range assignments { + result[i] = services.IdentityCenterAccountAssignment{AccountAssignment: asmt} + } + + return result, pagination.NextPageToken(nextPage), nil +} + +// CreateAccountAssignment creates a new Account Assignment record in +// the service from the supplied in-memory representation. Returns the +// created record on success. +func (svc *IdentityCenterService) CreateAccountAssignment(ctx context.Context, asmt services.IdentityCenterAccountAssignment) (services.IdentityCenterAccountAssignment, error) { + created, err := svc.accountAssignments.CreateResource(ctx, asmt.AccountAssignment) + if err != nil { + return services.IdentityCenterAccountAssignment{}, trace.Wrap(err, "creating principal assignment") + } + return services.IdentityCenterAccountAssignment{AccountAssignment: created}, nil +} + +// GetAccountAssignment fetches a specific Account Assignment record. +func (svc *IdentityCenterService) GetAccountAssignment(ctx context.Context, name services.IdentityCenterAccountAssignmentID) (services.IdentityCenterAccountAssignment, error) { + asmt, err := svc.accountAssignments.GetResource(ctx, string(name)) + if err != nil { + return services.IdentityCenterAccountAssignment{}, trace.Wrap(err, "fetching principal assignment") + } + return services.IdentityCenterAccountAssignment{AccountAssignment: asmt}, nil +} + +// UpdateAccountAssignment performs a conditional update on the supplied +// Account Assignment, returning the updated record on success. +func (svc *IdentityCenterService) UpdateAccountAssignment(ctx context.Context, asmt services.IdentityCenterAccountAssignment) (services.IdentityCenterAccountAssignment, error) { + updated, err := svc.accountAssignments.ConditionalUpdateResource(ctx, asmt.AccountAssignment) + if err != nil { + return services.IdentityCenterAccountAssignment{}, trace.Wrap(err, "updating principal assignment record") + } + return services.IdentityCenterAccountAssignment{AccountAssignment: updated}, nil +} + +// DeleteAccountAssignment deletes a specific account assignment +func (svc *IdentityCenterService) DeleteAccountAssignment(ctx context.Context, name services.IdentityCenterAccountAssignmentID) error { + return trace.Wrap(svc.accountAssignments.DeleteResource(ctx, string(name))) +} + +// DeleteAllAccountAssignments deletes all known account assignments +func (svc *IdentityCenterService) DeleteAllAccountAssignments(ctx context.Context) error { + return trace.Wrap(svc.accountAssignments.DeleteAllResources(ctx)) +} diff --git a/lib/services/local/identitycenter_test.go b/lib/services/local/identitycenter_test.go new file mode 100644 index 000000000000..a15c2a51f323 --- /dev/null +++ b/lib/services/local/identitycenter_test.go @@ -0,0 +1,298 @@ +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package local + +import ( + "context" + "fmt" + "testing" + + "github.com/gravitational/trace" + "github.com/jonboulle/clockwork" + "github.com/stretchr/testify/require" + + headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" + identitycenterv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/identitycenter/v1" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/backend" + "github.com/gravitational/teleport/lib/backend/lite" + "github.com/gravitational/teleport/lib/services" +) + +func newTestBackend(t *testing.T, ctx context.Context, clock clockwork.Clock) backend.Backend { + t.Helper() + sqliteBackend, err := lite.NewWithConfig(ctx, lite.Config{ + Path: t.TempDir(), + Clock: clock, + }) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, sqliteBackend.Close()) + }) + return sqliteBackend +} + +func TestIdentityCenterResourceCRUD(t *testing.T) { + t.Parallel() + + const resourceID = "alpha" + + testCases := []struct { + name string + createResource func(*testing.T, context.Context, services.IdentityCenter, string) types.Resource153 + getResource func(context.Context, services.IdentityCenter, string) (types.Resource153, error) + updateResource func(context.Context, services.IdentityCenter, types.Resource153) (types.Resource153, error) + upsertResource func(context.Context, services.IdentityCenter, types.Resource153) (types.Resource153, error) + }{ + { + name: "Account", + createResource: func(subtestT *testing.T, subtestCtx context.Context, svc services.IdentityCenter, id string) types.Resource153 { + return makeTestIdentityCenterAccount(subtestT, subtestCtx, svc, id) + }, + getResource: func(subtestCtx context.Context, svc services.IdentityCenter, id string) (types.Resource153, error) { + return svc.GetIdentityCenterAccount(subtestCtx, services.IdentityCenterAccountID(id)) + }, + updateResource: func(subtestCtx context.Context, svc services.IdentityCenter, r types.Resource153) (types.Resource153, error) { + acct := r.(services.IdentityCenterAccount) + return svc.UpdateIdentityCenterAccount(subtestCtx, acct) + }, + upsertResource: func(subtestCtx context.Context, svc services.IdentityCenter, r types.Resource153) (types.Resource153, error) { + acct := r.(services.IdentityCenterAccount) + return svc.UpsertIdentityCenterAccount(subtestCtx, acct) + }, + }, + { + name: "PermissionSet", + createResource: func(subtestT *testing.T, subtestCtx context.Context, svc services.IdentityCenter, id string) types.Resource153 { + return makeTestIdentityCenterPermissionSet(subtestT, subtestCtx, svc, id) + }, + getResource: func(subtestCtx context.Context, svc services.IdentityCenter, id string) (types.Resource153, error) { + return svc.GetPermissionSet(subtestCtx, services.PermissionSetID(id)) + }, + updateResource: func(subtestCtx context.Context, svc services.IdentityCenter, r types.Resource153) (types.Resource153, error) { + ps := r.(*identitycenterv1.PermissionSet) + return svc.UpdatePermissionSet(subtestCtx, ps) + }, + }, + { + name: "AccountAssignment", + createResource: func(subtestT *testing.T, subtestCtx context.Context, svc services.IdentityCenter, id string) types.Resource153 { + return makeTestIdentityCenterAccountAssignment(subtestT, subtestCtx, svc, id) + }, + getResource: func(subtestCtx context.Context, svc services.IdentityCenter, id string) (types.Resource153, error) { + return svc.GetAccountAssignment(subtestCtx, services.IdentityCenterAccountAssignmentID(id)) + }, + updateResource: func(subtestCtx context.Context, svc services.IdentityCenter, r types.Resource153) (types.Resource153, error) { + asmt := r.(services.IdentityCenterAccountAssignment) + return svc.UpdateAccountAssignment(subtestCtx, asmt) + }, + }, + { + name: "PrincipalAssignment", + createResource: func(subtestT *testing.T, subtestCtx context.Context, svc services.IdentityCenter, id string) types.Resource153 { + return makeTestIdentityCenterPrincipalAssignment(subtestT, subtestCtx, svc, id) + }, + getResource: func(subtestCtx context.Context, svc services.IdentityCenter, id string) (types.Resource153, error) { + return svc.GetPrincipalAssignment(subtestCtx, services.PrincipalAssignmentID(id)) + }, + updateResource: func(subtestCtx context.Context, svc services.IdentityCenter, r types.Resource153) (types.Resource153, error) { + asmt := r.(*identitycenterv1.PrincipalAssignment) + return svc.UpdatePrincipalAssignment(subtestCtx, asmt) + }, + upsertResource: func(subtestCtx context.Context, svc services.IdentityCenter, r types.Resource153) (types.Resource153, error) { + asmt := r.(*identitycenterv1.PrincipalAssignment) + return svc.UpsertPrincipalAssignment(subtestCtx, asmt) + }, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + t.Run("OptimisticLocking", func(t *testing.T) { + const resourceID = "alpha" + + ctx := newTestContext(t) + clock := clockwork.NewFakeClock() + backend := newTestBackend(t, ctx, clock) + + // GIVEN an IdentityCenter service populated with a resource + uut, err := NewIdentityCenterService(IdentityCenterServiceConfig{Backend: backend}) + require.NoError(t, err) + createdResource := test.createResource(t, ctx, uut, resourceID) + + // WHEN I modify the backend record for that resource... + tmpResource, err := test.getResource(ctx, uut, resourceID) + require.NoError(t, err) + tmpResource.GetMetadata().Labels = map[string]string{"update": "1"} + updatedResource, err := test.updateResource(ctx, uut, tmpResource) + require.NoError(t, err) + + // EXPECT that any attempt to update backend via the original in-memory + // version of the resource fails with a comparison error + createdResource.GetMetadata().Labels = map[string]string{"update": "2"} + _, err = test.updateResource(ctx, uut, createdResource) + require.True(t, trace.IsCompareFailed(err), "expected a compare failed error, got %T (%s)", err, err) + + // EXPECT that the backend still reflects the first updated revision, + // rather than failed update + r, err := test.getResource(ctx, uut, resourceID) + require.NoError(t, err) + require.Equal(t, "1", r.GetMetadata().Labels["update"]) + + // WHEN I attempt update the backend via the latest revision of the + // record... + updatedResource.GetMetadata().Labels["update"] = "3" + _, err = test.updateResource(ctx, uut, updatedResource) + + // EXPECT the update to succeed, and the backend record to have been + // updated + require.NoError(t, err) + r, err = test.getResource(ctx, uut, resourceID) + require.NoError(t, err) + require.Equal(t, "3", r.GetMetadata().Labels["update"]) + }) + + t.Run("UnconditionalUpsert", func(t *testing.T) { + t.Parallel() + + if test.upsertResource == nil { + t.Skip(test.name + " does not support unconditional upsert") + } + + ctx := newTestContext(t) + clock := clockwork.NewFakeClock() + backend := newTestBackend(t, ctx, clock) + + // GIVEN an IdentityCenter service populated with a resource + uut, err := NewIdentityCenterService(IdentityCenterServiceConfig{Backend: backend}) + require.NoError(t, err) + originalResource := test.createResource(t, ctx, uut, resourceID) + + // GIVEN that the backend record for that resource has been changed + // between us looking up the original resource and us committing + // any changes to it... + tmpResource, err := test.getResource(ctx, uut, resourceID) + require.NoError(t, err) + tmpResource.GetMetadata().Labels = map[string]string{"update": "1"} + _, err = test.updateResource(ctx, uut, tmpResource) + require.NoError(t, err) + + // WHEN I attempt to Update the modified original resource + _, err = test.updateResource(ctx, uut, originalResource) + + // EXPECT the Update to fail due to the changed underlying record + require.True(t, trace.IsCompareFailed(err), "expected a compare failed error, got %T (%s)", err, err) + + // WHEN I attempt to Upsert the modified original resource + originalResource.GetMetadata().Labels = map[string]string{"update": "2"} + _, err = test.upsertResource(ctx, uut, originalResource) + + // EXPECT that an upsert will succeed, even though the underlying + // record has changed + require.NoError(t, err) + + // EXPECT that the backend reflects the updated values from the + // upsert + r, err := test.getResource(ctx, uut, resourceID) + require.NoError(t, err) + require.Equal(t, "2", r.GetMetadata().Labels["update"]) + }) + }) + } +} + +func makeTestIdentityCenterAccount(t *testing.T, ctx context.Context, svc services.IdentityCenter, id string) services.IdentityCenterAccount { + t.Helper() + created, err := svc.CreateIdentityCenterAccount(ctx, services.IdentityCenterAccount{ + Account: &identitycenterv1.Account{ + Kind: types.KindIdentityCenterAccount, + Version: types.V1, + Metadata: &headerv1.Metadata{Name: id}, + Spec: &identitycenterv1.AccountSpec{ + Id: "aws-account-id-" + id, + Arn: fmt.Sprintf("arn:aws:sso::%s:", id), + Description: "Test account " + id, + PermissionSetInfo: []*identitycenterv1.PermissionSetInfo{ + { + Name: "dummy", + Arn: "arn:aws:sso:::permissionSet/ic-instance/ps-instance", + }, + }, + }, + }, + }) + require.NoError(t, err) + return created +} + +func makeTestIdentityCenterPermissionSet(t *testing.T, ctx context.Context, svc services.IdentityCenter, id string) *identitycenterv1.PermissionSet { + t.Helper() + created, err := svc.CreatePermissionSet(ctx, &identitycenterv1.PermissionSet{ + Kind: types.KindIdentityCenterPermissionSet, + Version: types.V1, + Metadata: &headerv1.Metadata{Name: id}, + Spec: &identitycenterv1.PermissionSetSpec{ + Arn: fmt.Sprintf("arn:aws:sso:::permissionSet/ic-instance/%s", id), + Name: "aws-permission-set-" + id, + Description: "Test permission set " + id, + }, + }) + require.NoError(t, err) + return created +} + +func makeTestIdentityCenterAccountAssignment(t *testing.T, ctx context.Context, svc services.IdentityCenter, id string) services.IdentityCenterAccountAssignment { + t.Helper() + created, err := svc.CreateAccountAssignment(ctx, services.IdentityCenterAccountAssignment{ + AccountAssignment: &identitycenterv1.AccountAssignment{ + Kind: types.KindIdentityCenterAccountAssignment, + Version: types.V1, + Metadata: &headerv1.Metadata{Name: id}, + Spec: &identitycenterv1.AccountAssignmentSpec{ + Display: "Some-Permission-set on Some-AWS-account", + PermissionSet: &identitycenterv1.PermissionSetInfo{ + Arn: "arn:aws:sso:::permissionSet/ic-instance/ps-instance", + Name: "some permission set", + }, + AccountName: "Some Account Name", + AccountId: "some account id", + }, + }, + }) + require.NoError(t, err) + return created +} + +func makeTestIdentityCenterPrincipalAssignment(t *testing.T, ctx context.Context, svc services.IdentityCenterPrincipalAssignments, id string) *identitycenterv1.PrincipalAssignment { + t.Helper() + created, err := svc.CreatePrincipalAssignment(ctx, &identitycenterv1.PrincipalAssignment{ + Kind: types.KindIdentityCenterPrincipalAssignment, + Version: types.V1, + Metadata: &headerv1.Metadata{Name: id}, + Spec: &identitycenterv1.PrincipalAssignmentSpec{ + PrincipalType: identitycenterv1.PrincipalType_PRINCIPAL_TYPE_USER, + PrincipalId: id, + ExternalIdSource: "scim", + ExternalId: "some external id", + }, + Status: &identitycenterv1.PrincipalAssignmentStatus{ + ProvisioningState: identitycenterv1.ProvisioningState_PROVISIONING_STATE_STALE, + }, + }) + require.NoError(t, err) + return created +} diff --git a/lib/services/resource.go b/lib/services/resource.go index c336f1b80f1d..819ec724cdc8 100644 --- a/lib/services/resource.go +++ b/lib/services/resource.go @@ -189,6 +189,8 @@ func ParseShortcut(in string) (string, error) { return types.KindWindowsDesktopService, nil case types.KindWindowsDesktop, "win_desktop": return types.KindWindowsDesktop, nil + case types.KindDynamicWindowsDesktop, "dynamic_win_desktop", "dynamic_desktop": + return types.KindDynamicWindowsDesktop, nil case types.KindToken, "tokens": return types.KindToken, nil case types.KindInstaller: diff --git a/lib/services/role.go b/lib/services/role.go index 0d877761bbce..083b277c9b81 100644 --- a/lib/services/role.go +++ b/lib/services/role.go @@ -73,6 +73,7 @@ var DefaultImplicitRules = []types.Rule{ types.NewRule(types.KindApp, RO()), types.NewRule(types.KindWindowsDesktopService, RO()), types.NewRule(types.KindWindowsDesktop, RO()), + types.NewRule(types.KindDynamicWindowsDesktop, RO()), types.NewRule(types.KindKubernetesCluster, RO()), types.NewRule(types.KindUsageEvent, []string{types.VerbCreate}), types.NewRule(types.KindVnetConfig, RO()), diff --git a/lib/services/watcher.go b/lib/services/watcher.go index f8151d491583..93daf0aee5cd 100644 --- a/lib/services/watcher.go +++ b/lib/services/watcher.go @@ -960,6 +960,162 @@ func (p *databaseCollector) processEventsAndUpdateCurrent(ctx context.Context, e func (*databaseCollector) notifyStale() {} +type DynamicWindowsDesktopGetter interface { + ListDynamicWindowsDesktops(ctx context.Context, pageSize int, pageToken string) ([]types.DynamicWindowsDesktop, string, error) +} + +// DynamicWindowsDesktopWatcherConfig is a DynamicWindowsDesktopWatcher configuration. +type DynamicWindowsDesktopWatcherConfig struct { + // ResourceWatcherConfig is the resource watcher configuration. + ResourceWatcherConfig + // DynamicWindowsDesktopGetter is responsible for fetching DynamicWindowsDesktop resources. + DynamicWindowsDesktopGetter + // DynamicWindowsDesktopsC receives up-to-date list of all DynamicWindowsDesktop resources. + DynamicWindowsDesktopsC chan types.DynamicWindowsDesktops +} + +// CheckAndSetDefaults checks parameters and sets default values. +func (cfg *DynamicWindowsDesktopWatcherConfig) CheckAndSetDefaults() error { + if err := cfg.ResourceWatcherConfig.CheckAndSetDefaults(); err != nil { + return trace.Wrap(err) + } + if cfg.DynamicWindowsDesktopGetter == nil { + getter, ok := cfg.Client.(DynamicWindowsDesktopGetter) + if !ok { + return trace.BadParameter("missing parameter DynamicWindowsDesktopGetter and Client %T not usable as DynamicWindowsDesktopGetter", cfg.Client) + } + cfg.DynamicWindowsDesktopGetter = getter + } + if cfg.DynamicWindowsDesktopsC == nil { + cfg.DynamicWindowsDesktopsC = make(chan types.DynamicWindowsDesktops) + } + return nil +} + +// NewDynamicWindowsDesktopWatcher returns a new instance of DynamicWindowsDesktopWatcher. +func NewDynamicWindowsDesktopWatcher(ctx context.Context, cfg DynamicWindowsDesktopWatcherConfig) (*DynamicWindowsDesktopWatcher, error) { + if err := cfg.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + collector := &dynamicWindowsDesktopCollector{ + DynamicWindowsDesktopWatcherConfig: cfg, + initializationC: make(chan struct{}), + } + watcher, err := newResourceWatcher(ctx, collector, cfg.ResourceWatcherConfig) + if err != nil { + return nil, trace.Wrap(err) + } + return &DynamicWindowsDesktopWatcher{watcher, collector}, nil +} + +// DynamicWindowsDesktopWatcher is built on top of resourceWatcher to monitor DynamicWindowsDesktop resources. +type DynamicWindowsDesktopWatcher struct { + *resourceWatcher + *dynamicWindowsDesktopCollector +} + +// dynamicWindowsDesktopCollector accompanies resourceWatcher when monitoring DynamicWindowsDesktop resources. +type dynamicWindowsDesktopCollector struct { + // DynamicWindowsDesktopWatcherConfig is the watcher configuration. + DynamicWindowsDesktopWatcherConfig + // current holds a map of the currently known DynamicWindowsDesktop resources. + current map[string]types.DynamicWindowsDesktop + // lock protects the "current" map. + lock sync.RWMutex + // initializationC is used to check that the + initializationC chan struct{} + once sync.Once +} + +// resourceKinds specifies the resource kind to watch. +func (p *dynamicWindowsDesktopCollector) resourceKinds() []types.WatchKind { + return []types.WatchKind{{Kind: types.KindDynamicWindowsDesktop}} +} + +// isInitialized is used to check that the cache has done its initial +// sync +func (p *dynamicWindowsDesktopCollector) initializationChan() <-chan struct{} { + return p.initializationC +} + +// getResourcesAndUpdateCurrent refreshes the list of current resources. +func (p *dynamicWindowsDesktopCollector) getResourcesAndUpdateCurrent(ctx context.Context) error { + var dynamicWindowsDesktops []types.DynamicWindowsDesktop + next := "" + for { + desktops, token, err := p.DynamicWindowsDesktopGetter.ListDynamicWindowsDesktops(ctx, defaults.MaxIterationLimit, next) + if err != nil { + return trace.Wrap(err) + } + dynamicWindowsDesktops = append(dynamicWindowsDesktops, desktops...) + if token == "" { + break + } + next = token + } + newCurrent := make(map[string]types.DynamicWindowsDesktop, len(dynamicWindowsDesktops)) + for _, dynamicWindowsDesktop := range dynamicWindowsDesktops { + newCurrent[dynamicWindowsDesktop.GetName()] = dynamicWindowsDesktop + } + p.lock.Lock() + defer p.lock.Unlock() + p.current = newCurrent + p.defineCollectorAsInitialized() + + select { + case <-ctx.Done(): + return trace.Wrap(ctx.Err()) + case p.DynamicWindowsDesktopsC <- dynamicWindowsDesktops: + } + + return nil +} + +func (p *dynamicWindowsDesktopCollector) defineCollectorAsInitialized() { + p.once.Do(func() { + // mark watcher as initialized. + close(p.initializationC) + }) +} + +// processEventsAndUpdateCurrent is called when a watcher event is received. +func (p *dynamicWindowsDesktopCollector) processEventsAndUpdateCurrent(ctx context.Context, events []types.Event) { + p.lock.Lock() + defer p.lock.Unlock() + + var updated bool + for _, event := range events { + if event.Resource == nil || event.Resource.GetKind() != types.KindDynamicWindowsDesktop { + p.Logger.WarnContext(ctx, "Received unexpected event", "event", logutils.StringerAttr(event)) + continue + } + switch event.Type { + case types.OpDelete: + delete(p.current, event.Resource.GetName()) + updated = true + case types.OpPut: + dynamicWindowsDesktop, ok := event.Resource.(types.DynamicWindowsDesktop) + if !ok { + p.Logger.WarnContext(ctx, "Received unexpected resource type", "resource", event.Resource.GetKind()) + continue + } + p.current[dynamicWindowsDesktop.GetName()] = dynamicWindowsDesktop + updated = true + default: + p.Logger.WarnContext(ctx, "Received unsupported event type", "event_type", event.Type) + } + } + + if updated { + select { + case <-ctx.Done(): + case p.DynamicWindowsDesktopsC <- resourcesToSlice(p.current): + } + } +} + +func (*dynamicWindowsDesktopCollector) notifyStale() {} + // AppWatcherConfig is an AppWatcher configuration. type AppWatcherConfig struct { // ResourceWatcherConfig is the resource watcher configuration. diff --git a/lib/srv/discovery/discovery.go b/lib/srv/discovery/discovery.go index d1b490bbe083..6e93a8a8eddc 100644 --- a/lib/srv/discovery/discovery.go +++ b/lib/srv/discovery/discovery.go @@ -990,10 +990,10 @@ func (s *Server) handleEC2RemoteInstallation(instances *server.EC2Instances) err installerScript: req.InstallerScriptName(), }, &usertasksv1.DiscoverEC2Instance{ - // TODO(marco): add instance name DiscoveryConfig: instances.DiscoveryConfig, DiscoveryGroup: s.DiscoveryGroup, InstanceId: instance.InstanceID, + Name: instance.InstanceName, SyncTime: timestamppb.New(s.clock.Now()), }, ) diff --git a/lib/srv/discovery/status.go b/lib/srv/discovery/status.go index 4bb70efaa5b6..7fe0b0f39398 100644 --- a/lib/srv/discovery/status.go +++ b/lib/srv/discovery/status.go @@ -310,12 +310,12 @@ func (s *Server) ReportEC2SSMInstallationResult(ctx context.Context, result *ser installerScript: result.InstallerScript, }, &usertasksv1.DiscoverEC2Instance{ - // TODO(marco): add instance name InvocationUrl: result.SSMRunEvent.InvocationURL, DiscoveryConfig: result.DiscoveryConfig, DiscoveryGroup: s.DiscoveryGroup, SyncTime: timestamppb.New(result.SSMRunEvent.Time), InstanceId: result.SSMRunEvent.InstanceID, + Name: result.InstanceName, }, ) diff --git a/lib/srv/server/ec2_watcher.go b/lib/srv/server/ec2_watcher.go index f8390b032c4d..25f12018e017 100644 --- a/lib/srv/server/ec2_watcher.go +++ b/lib/srv/server/ec2_watcher.go @@ -79,6 +79,7 @@ type EC2Instances struct { // discovered. type EC2Instance struct { InstanceID string + InstanceName string Tags map[string]string OriginalInstance ec2.Instance } @@ -92,6 +93,9 @@ func toEC2Instance(originalInst *ec2.Instance) EC2Instance { for _, tag := range originalInst.Tags { if key := aws.StringValue(tag.Key); key != "" { inst.Tags[key] = aws.StringValue(tag.Value) + if key == "Name" { + inst.InstanceName = aws.StringValue(tag.Value) + } } } return inst diff --git a/lib/srv/server/ec2_watcher_test.go b/lib/srv/server/ec2_watcher_test.go index 2647a7b0f527..8279cbbd868f 100644 --- a/lib/srv/server/ec2_watcher_test.go +++ b/lib/srv/server/ec2_watcher_test.go @@ -176,10 +176,16 @@ func TestEC2Watcher(t *testing.T) { present := ec2.Instance{ InstanceId: aws.String("instance-present"), - Tags: []*ec2.Tag{{ - Key: aws.String("teleport"), - Value: aws.String("yes"), - }}, + Tags: []*ec2.Tag{ + { + Key: aws.String("teleport"), + Value: aws.String("yes"), + }, + { + Key: aws.String("Name"), + Value: aws.String("Present"), + }, + }, State: &ec2.InstanceState{ Name: aws.String(ec2.InstanceStateNameRunning), }, @@ -360,3 +366,71 @@ func TestMakeEvents(t *testing.T) { }) } } + +func TestToEC2Instances(t *testing.T) { + sampleInstance := &ec2.Instance{ + InstanceId: aws.String("instance-001"), + Tags: []*ec2.Tag{ + { + Key: aws.String("teleport"), + Value: aws.String("yes"), + }, + { + Key: aws.String("Name"), + Value: aws.String("MyInstanceName"), + }, + }, + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameRunning), + }, + } + + sampleInstanceWithoutName := &ec2.Instance{ + InstanceId: aws.String("instance-001"), + Tags: []*ec2.Tag{ + { + Key: aws.String("teleport"), + Value: aws.String("yes"), + }, + }, + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameRunning), + }, + } + + for _, tt := range []struct { + name string + input []*ec2.Instance + expected []EC2Instance + }{ + { + name: "with name", + input: []*ec2.Instance{sampleInstance}, + expected: []EC2Instance{{ + InstanceID: "instance-001", + Tags: map[string]string{ + "Name": "MyInstanceName", + "teleport": "yes", + }, + InstanceName: "MyInstanceName", + OriginalInstance: *sampleInstance, + }}, + }, + { + name: "without name", + input: []*ec2.Instance{sampleInstanceWithoutName}, + expected: []EC2Instance{{ + InstanceID: "instance-001", + Tags: map[string]string{ + "teleport": "yes", + }, + OriginalInstance: *sampleInstanceWithoutName, + }}, + }, + } { + t.Run(tt.name, func(t *testing.T) { + got := ToEC2Instances(tt.input) + require.Equal(t, tt.expected, got) + }) + } +} diff --git a/lib/srv/server/ssm_install.go b/lib/srv/server/ssm_install.go index 6c0c2f427708..3c23f672884a 100644 --- a/lib/srv/server/ssm_install.go +++ b/lib/srv/server/ssm_install.go @@ -23,6 +23,8 @@ import ( "errors" "fmt" "log/slog" + "maps" + "slices" "strings" "github.com/aws/aws-sdk-go/aws" @@ -67,6 +69,9 @@ type SSMInstallationResult struct { SSMDocumentName string // InstallerScript is the Teleport Installer script name used to install Teleport into the instance. InstallerScript string + // InstanceName is the Instance's name. + // Might be empty. + InstanceName string } // SSMInstaller handles running SSM commands that install Teleport on EC2 instances. @@ -134,9 +139,9 @@ func NewSSMInstaller(cfg SSMInstallerConfig) (*SSMInstaller, error) { // Run executes the SSM document and then blocks until the command has completed. func (si *SSMInstaller) Run(ctx context.Context, req SSMRunRequest) error { - ids := make([]string, 0, len(req.Instances)) + instances := make(map[string]string, len(req.Instances)) for _, inst := range req.Instances { - ids = append(ids, inst.InstanceID) + instances[inst.InstanceID] = inst.InstanceName } params := make(map[string][]*string) @@ -144,8 +149,8 @@ func (si *SSMInstaller) Run(ctx context.Context, req SSMRunRequest) error { params[k] = []*string{aws.String(v)} } - validInstances := ids - instancesState, err := si.describeSSMAgentState(ctx, req, ids) + validInstances := instances + instancesState, err := si.describeSSMAgentState(ctx, req, instances) switch { case trace.IsAccessDenied(err): // describeSSMAgentState uses `ssm:DescribeInstanceInformation` to gather all the instances information. @@ -169,9 +174,10 @@ func (si *SSMInstaller) Run(ctx context.Context, req SSMRunRequest) error { validInstances = instancesState.valid } + validInstanceIDs := instanceIDsFrom(validInstances) output, err := req.SSM.SendCommandWithContext(ctx, &ssm.SendCommandInput{ DocumentName: aws.String(req.DocumentName), - InstanceIds: aws.StringSlice(validInstances), + InstanceIds: aws.StringSlice(validInstanceIDs), Parameters: params, }) if err != nil { @@ -190,7 +196,7 @@ func (si *SSMInstaller) Run(ctx context.Context, req SSMRunRequest) error { delete(params, ParamSSHDConfigPath) output, err = req.SSM.SendCommandWithContext(ctx, &ssm.SendCommandInput{ DocumentName: aws.String(req.DocumentName), - InstanceIds: aws.StringSlice(validInstances), + InstanceIds: aws.StringSlice(validInstanceIDs), Parameters: params, }) if err != nil { @@ -200,16 +206,17 @@ func (si *SSMInstaller) Run(ctx context.Context, req SSMRunRequest) error { g, ctx := errgroup.WithContext(ctx) g.SetLimit(10) - for _, inst := range validInstances { - inst := inst + for instanceID, instanceName := range validInstances { + instanceID := instanceID + instanceName := instanceName g.Go(func() error { - return trace.Wrap(si.checkCommand(ctx, req, output.Command.CommandId, &inst)) + return trace.Wrap(si.checkCommand(ctx, req, output.Command.CommandId, &instanceID, instanceName)) }) } return trace.Wrap(g.Wait()) } -func invalidSSMInstanceInstallationResult(req SSMRunRequest, instanceID, status, issueType string) *SSMInstallationResult { +func invalidSSMInstanceInstallationResult(req SSMRunRequest, instanceID, instanceName, status, issueType string) *SSMInstallationResult { return &SSMInstallationResult{ SSMRunEvent: &apievents.SSMRun{ Metadata: apievents.Metadata{ @@ -228,13 +235,14 @@ func invalidSSMInstanceInstallationResult(req SSMRunRequest, instanceID, status, IssueType: issueType, SSMDocumentName: req.DocumentName, InstallerScript: req.InstallerScriptName(), + InstanceName: instanceName, } } func (si *SSMInstaller) emitInvalidInstanceEvents(ctx context.Context, req SSMRunRequest, instanceIDsState *instanceIDsSSMState) error { var errs []error - for _, instanceID := range instanceIDsState.missing { - installationResult := invalidSSMInstanceInstallationResult(req, instanceID, + for instanceID, instanceName := range instanceIDsState.missing { + installationResult := invalidSSMInstanceInstallationResult(req, instanceID, instanceName, "EC2 Instance is not registered in SSM. Make sure that the instance has AmazonSSMManagedInstanceCore policy assigned.", usertasks.AutoDiscoverEC2IssueSSMInstanceNotRegistered, ) @@ -243,8 +251,8 @@ func (si *SSMInstaller) emitInvalidInstanceEvents(ctx context.Context, req SSMRu } } - for _, instanceID := range instanceIDsState.connectionLost { - installationResult := invalidSSMInstanceInstallationResult(req, instanceID, + for instanceID, instanceName := range instanceIDsState.connectionLost { + installationResult := invalidSSMInstanceInstallationResult(req, instanceID, instanceName, "SSM Agent in EC2 Instance is not connecting to SSM Service. Restart or reinstall the SSM service. See https://docs.aws.amazon.com/systems-manager/latest/userguide/ami-preinstalled-agent.html#verify-ssm-agent-status for more details.", usertasks.AutoDiscoverEC2IssueSSMInstanceConnectionLost, ) @@ -253,8 +261,8 @@ func (si *SSMInstaller) emitInvalidInstanceEvents(ctx context.Context, req SSMRu } } - for _, instanceID := range instanceIDsState.unsupportedOS { - installationResult := invalidSSMInstanceInstallationResult(req, instanceID, + for instanceID, instanceName := range instanceIDsState.unsupportedOS { + installationResult := invalidSSMInstanceInstallationResult(req, instanceID, instanceName, "EC2 instance is running an unsupported Operating System. Only Linux is supported.", usertasks.AutoDiscoverEC2IssueSSMInstanceUnsupportedOS, ) @@ -268,19 +276,29 @@ func (si *SSMInstaller) emitInvalidInstanceEvents(ctx context.Context, req SSMRu // instanceIDsSSMState contains a list of EC2 Instance IDs for a given state. type instanceIDsSSMState struct { - valid []string - missing []string - connectionLost []string - unsupportedOS []string + valid map[string]string + missing map[string]string + connectionLost map[string]string + unsupportedOS map[string]string +} + +func instanceIDsFrom(m map[string]string) []string { + return slices.Collect(maps.Keys(m)) } // describeSSMAgentState returns the instanceIDsSSMState for all the instances. -func (si *SSMInstaller) describeSSMAgentState(ctx context.Context, req SSMRunRequest, allInstanceIDs []string) (*instanceIDsSSMState, error) { - ret := &instanceIDsSSMState{} +func (si *SSMInstaller) describeSSMAgentState(ctx context.Context, req SSMRunRequest, allInstances map[string]string) (*instanceIDsSSMState, error) { + ret := &instanceIDsSSMState{ + valid: make(map[string]string), + missing: make(map[string]string), + connectionLost: make(map[string]string), + unsupportedOS: make(map[string]string), + } + instanceIDs := instanceIDsFrom(allInstances) ssmInstancesInfo, err := req.SSM.DescribeInstanceInformationWithContext(ctx, &ssm.DescribeInstanceInformationInput{ Filters: []*ssm.InstanceInformationStringFilter{ - {Key: aws.String(ssm.InstanceInformationFilterKeyInstanceIds), Values: aws.StringSlice(allInstanceIDs)}, + {Key: aws.String(ssm.InstanceInformationFilterKeyInstanceIds), Values: aws.StringSlice(instanceIDs)}, }, MaxResults: aws.Int64(awsEC2APIChunkSize), }) @@ -294,24 +312,24 @@ func (si *SSMInstaller) describeSSMAgentState(ctx context.Context, req SSMRunReq instanceStateByInstanceID[aws.StringValue(instanceState.InstanceId)] = instanceState } - for _, instanceID := range allInstanceIDs { + for instanceID, instanceName := range allInstances { instanceState, found := instanceStateByInstanceID[instanceID] if !found { - ret.missing = append(ret.missing, instanceID) + ret.missing[instanceID] = instanceName continue } if aws.StringValue(instanceState.PingStatus) == ssm.PingStatusConnectionLost { - ret.connectionLost = append(ret.connectionLost, instanceID) + ret.connectionLost[instanceID] = instanceName continue } if aws.StringValue(instanceState.PlatformType) != ssm.PlatformTypeLinux { - ret.unsupportedOS = append(ret.unsupportedOS, instanceID) + ret.unsupportedOS[instanceID] = instanceName continue } - ret.valid = append(ret.valid, instanceID) + ret.valid[instanceID] = instanceName } return ret, nil @@ -330,7 +348,7 @@ func skipAWSWaitErr(err error) error { return trace.Wrap(err) } -func (si *SSMInstaller) checkCommand(ctx context.Context, req SSMRunRequest, commandID, instanceID *string) error { +func (si *SSMInstaller) checkCommand(ctx context.Context, req SSMRunRequest, commandID, instanceID *string, instanceName string) error { err := req.SSM.WaitUntilCommandExecutedWithContext(ctx, &ssm.GetCommandInvocationInput{ CommandId: commandID, InstanceId: instanceID, @@ -377,6 +395,7 @@ func (si *SSMInstaller) checkCommand(ctx context.Context, req SSMRunRequest, com IssueType: usertasks.AutoDiscoverEC2IssueSSMScriptFailure, SSMDocumentName: req.DocumentName, InstallerScript: req.InstallerScriptName(), + InstanceName: instanceName, })) } @@ -393,6 +412,7 @@ func (si *SSMInstaller) checkCommand(ctx context.Context, req SSMRunRequest, com IssueType: usertasks.AutoDiscoverEC2IssueSSMScriptFailure, SSMDocumentName: req.DocumentName, InstallerScript: req.InstallerScriptName(), + InstanceName: instanceName, })) } } diff --git a/lib/srv/server/ssm_install_test.go b/lib/srv/server/ssm_install_test.go index 2107e91b0e59..c56b28625852 100644 --- a/lib/srv/server/ssm_install_test.go +++ b/lib/srv/server/ssm_install_test.go @@ -102,7 +102,7 @@ func TestSSMInstaller(t *testing.T) { name: "ssm run was successful", req: SSMRunRequest{ Instances: []EC2Instance{ - {InstanceID: "instance-id-1"}, + {InstanceID: "instance-id-1", InstanceName: "my-instance-name"}, }, DocumentName: document, Params: map[string]string{"token": "abcdefg"}, @@ -146,6 +146,7 @@ func TestSSMInstaller(t *testing.T) { }, IssueType: "ec2-ssm-script-failure", SSMDocumentName: "ssmdocument", + InstanceName: "my-instance-name", }}, }, { diff --git a/lib/utils/interval/multi.go b/lib/utils/interval/multi.go index 9932203ea607..f3c1fae80d79 100644 --- a/lib/utils/interval/multi.go +++ b/lib/utils/interval/multi.go @@ -23,6 +23,8 @@ import ( "sync" "time" + "github.com/jonboulle/clockwork" + "github.com/gravitational/teleport/api/utils/retryutils" ) @@ -39,6 +41,7 @@ import ( // but it is still a potential source of bugs/confusion when transitioning to using this type from one // of the single-interval alternatives. type MultiInterval[T comparable] struct { + clock clockwork.Clock subs []subIntervalEntry[T] push chan subIntervalEntry[T] ch chan Tick[T] @@ -125,12 +128,17 @@ func (s *subIntervalEntry[T]) increment() { // NewMulti creates a new multi-interval instance. This function panics on non-positive // interval durations (equivalent to time.NewTicker) or if no sub-intervals are provided. -func NewMulti[T comparable](intervals ...SubInterval[T]) *MultiInterval[T] { +func NewMulti[T comparable](clock clockwork.Clock, intervals ...SubInterval[T]) *MultiInterval[T] { if len(intervals) == 0 { panic(errors.New("empty sub-interval set for interval.NewMulti")) } + if clock == nil { + clock = clockwork.NewRealClock() + } + interval := &MultiInterval[T]{ + clock: clock, subs: make([]subIntervalEntry[T], 0, len(intervals)), push: make(chan subIntervalEntry[T]), ch: make(chan Tick[T], 1), @@ -140,7 +148,7 @@ func NewMulti[T comparable](intervals ...SubInterval[T]) *MultiInterval[T] { } // check and initialize our sub-intervals. - now := time.Now() + now := clock.Now() for _, sub := range intervals { if sub.Duration <= 0 && (sub.VariableDuration == nil || sub.VariableDuration.Duration() <= 0) { panic(errors.New("non-positive sub interval for interval.NewMulti")) @@ -156,7 +164,7 @@ func NewMulti[T comparable](intervals ...SubInterval[T]) *MultiInterval[T] { // start the timer in this goroutine to improve // consistency of first tick. - timer := time.NewTimer(d) + timer := clock.NewTimer(d) go interval.run(timer, key) @@ -173,7 +181,7 @@ func (i *MultiInterval[T]) Push(sub SubInterval[T]) { SubInterval: sub, } // we initialize here in order to improve consistency of start time - entry.init(time.Now()) + entry.init(i.clock.Now()) select { case i.push <- entry: case <-i.done: @@ -257,7 +265,7 @@ func (i *MultiInterval[T]) pushEntry(entry subIntervalEntry[T]) { i.subs = append(i.subs, entry) } -func (i *MultiInterval[T]) run(timer *time.Timer, key T) { +func (i *MultiInterval[T]) run(timer clockwork.Timer, key T) { defer timer.Stop() var pending pendingTicks[T] @@ -276,7 +284,7 @@ func (i *MultiInterval[T]) run(timer *time.Timer, key T) { } select { - case t := <-timer.C: + case t := <-timer.Chan(): // increment the sub-interval for the current key i.increment(key) @@ -292,7 +300,7 @@ func (i *MultiInterval[T]) run(timer *time.Timer, key T) { timer.Reset(d) case resetKey := <-i.reset: - now := time.Now() + now := i.clock.Now() // reset the sub-interval for the target key i.resetEntry(now, resetKey) @@ -307,14 +315,14 @@ func (i *MultiInterval[T]) run(timer *time.Timer, key T) { // stop and drain timer if !timer.Stop() { - <-timer.C + <-timer.Chan() } // apply the new duration timer.Reset(d) case fireKey := <-i.fire: - now := time.Now() + now := i.clock.Now() // reset the sub-interval for the key we are firing i.resetEntry(now, fireKey) @@ -329,13 +337,13 @@ func (i *MultiInterval[T]) run(timer *time.Timer, key T) { // stop and drain timer. if !timer.Stop() { - <-timer.C + <-timer.Chan() } // re-set the timer timer.Reset(d) case entry := <-i.push: - now := time.Now() + now := i.clock.Now() // add the new sub-interval entry i.pushEntry(entry) @@ -351,7 +359,7 @@ func (i *MultiInterval[T]) run(timer *time.Timer, key T) { // stop and drain timer if !timer.Stop() { - <-timer.C + <-timer.Chan() } // apply the new duration diff --git a/lib/utils/interval/multi_test.go b/lib/utils/interval/multi_test.go index 37cf3a10b161..3ef8b1f17ad5 100644 --- a/lib/utils/interval/multi_test.go +++ b/lib/utils/interval/multi_test.go @@ -24,6 +24,7 @@ import ( "testing" "time" + "github.com/jonboulle/clockwork" "github.com/stretchr/testify/require" ) @@ -47,10 +48,12 @@ func TestMultiIntervalReset(t *testing.T) { resetTimer := time.NewTimer(duration / 3) defer resetTimer.Stop() - interval := NewMulti[string](SubInterval[string]{ - Key: "key", - Duration: duration, - }) + interval := NewMulti[string]( + clockwork.NewRealClock(), + SubInterval[string]{ + Key: "key", + Duration: duration, + }) defer interval.Stop() start := time.Now() @@ -92,6 +95,7 @@ func TestMultiIntervalReset(t *testing.T) { func TestMultiIntervalBasics(t *testing.T) { t.Parallel() interval := NewMulti[string]( + clockwork.NewRealClock(), SubInterval[string]{ Key: "fast", Duration: time.Millisecond * 8, @@ -151,6 +155,7 @@ func TestMultiIntervalVariableDuration(t *testing.T) { bar.counter.Store(1) interval := NewMulti[string]( + clockwork.NewRealClock(), SubInterval[string]{ Key: "foo", VariableDuration: foo, @@ -216,6 +221,7 @@ func TestMultiIntervalVariableDuration(t *testing.T) { func TestMultiIntervalPush(t *testing.T) { t.Parallel() interval := NewMulti[string]( + clockwork.NewRealClock(), SubInterval[string]{ Key: "foo", Duration: time.Millisecond * 6, @@ -289,6 +295,7 @@ func TestMultiIntervalFireNow(t *testing.T) { // set up one sub-interval that fires frequently, and another that will never // fire during this test unless we trigger with FireNow. interval := NewMulti[string]( + clockwork.NewRealClock(), SubInterval[string]{ Key: "slow", Duration: time.Hour, diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.story.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.story.tsx index 115f18f84c7b..865ed0a98a02 100644 --- a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.story.tsx +++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.story.tsx @@ -81,7 +81,7 @@ export const Empty = () => { null, createRequest: () => null, - data: [ + pendingAccessRequests: [ { kind: 'app', name: 'app-name', diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.test.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.test.tsx index 89fca74f4ff9..974a2498b92a 100644 --- a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.test.tsx +++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.test.tsx @@ -154,7 +154,7 @@ const props: RequestCheckoutWithSliderProps = { selectedReviewers: [], setSelectedReviewers: () => null, createRequest: () => null, - data: [], + pendingAccessRequests: [], clearAttempt: () => null, onClose: () => null, toggleResource: () => null, diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx index 5ba2b973380f..ee1dc315d360 100644 --- a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx +++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx @@ -150,7 +150,7 @@ export function RequestCheckout({ setPendingRequestTtl, pendingRequestTtlOptions, dryRunResponse, - data, + pendingAccessRequests, showClusterNameColumn, createAttempt, fetchResourceRequestRolesAttempt, @@ -190,12 +190,14 @@ export function RequestCheckout({ selectedResourceRequestRoles.length < 1; const submitBtnDisabled = - data.length === 0 || + pendingAccessRequests.length === 0 || createAttempt.status === 'processing' || isInvalidRoleSelection || fetchResourceRequestRolesAttempt.status === 'failed' || fetchResourceRequestRolesAttempt.status === 'processing'; + const numPendingAccessRequests = pendingAccessRequests.length; + const DefaultHeader = () => { return ( @@ -208,7 +210,8 @@ export function RequestCheckout({ />

- {data.length} {pluralize(data.length, 'Resource')} Selected + {numPendingAccessRequests}{' '} + {pluralize(numPendingAccessRequests, 'Resource')} Selected

@@ -254,7 +257,7 @@ export function RequestCheckout({ )} = isResourceRequest: boolean; requireReason: boolean; selectedReviewers: ReviewerOption[]; - data: T[]; + pendingAccessRequests: T[]; showClusterNameColumn?: boolean; createRequest: (req: CreateRequest) => void; fetchStatus: 'loading' | 'loaded'; diff --git a/web/packages/teleport/src/Discover/Kubernetes/HelmChart/HelmChart.tsx b/web/packages/teleport/src/Discover/Kubernetes/HelmChart/HelmChart.tsx index 53a94e1b17cb..6d383690f48c 100644 --- a/web/packages/teleport/src/Discover/Kubernetes/HelmChart/HelmChart.tsx +++ b/web/packages/teleport/src/Discover/Kubernetes/HelmChart/HelmChart.tsx @@ -365,14 +365,6 @@ export function generateCmd(data: GenerateCmdProps) { // AutomaticUpgradesTargetVersion contains a v, eg, v13.4.2. // However, helm chart expects no 'v', eg, 13.4.2. deployVersion = data.automaticUpgradesTargetVersion.replace(/^v/, ''); - - // TODO(marco): remove when stable/cloud moves to v14 - // For v13 releases of the helm chart, we must remove the App role. - // We get the following error otherwise: - // Error: INSTALLATION FAILED: execution error at (teleport-kube-agent/templates/statefulset.yaml:26:28): at least one of 'apps' and 'appResources' is required in chart values when app role is enabled, see README - if (deployVersion.startsWith('13.')) { - roles = ['Kube']; - } } const yamlRoles = roles.join(',').toLowerCase(); diff --git a/web/packages/teleterm/src/ui/AccessRequestCheckout/AccessRequestCheckout.tsx b/web/packages/teleterm/src/ui/AccessRequestCheckout/AccessRequestCheckout.tsx index 41e18deb0571..44e81b59b382 100644 --- a/web/packages/teleterm/src/ui/AccessRequestCheckout/AccessRequestCheckout.tsx +++ b/web/packages/teleterm/src/ui/AccessRequestCheckout/AccessRequestCheckout.tsx @@ -85,7 +85,7 @@ export function AccessRequestCheckout() { fetchResourceRolesAttempt, setSelectedResourceRequestRoles, clearCreateAttempt, - data, + pendingAccessRequests, shouldShowClusterNameColumn, selectedReviewers, setSelectedReviewers, @@ -104,7 +104,7 @@ export function AccessRequestCheckout() { onStartTimeChange, } = useAccessRequestCheckout(); - const isRoleRequest = data[0]?.kind === 'role'; + const isRoleRequest = pendingAccessRequests[0]?.kind === 'role'; const transitionRef = useRef(); function closeCheckout() { @@ -113,10 +113,15 @@ export function AccessRequestCheckout() { // We should rather detect how much space we have, // but for simplicity we only count items. - const moreToShow = Math.max(data.length - MAX_RESOURCES_IN_BAR_TO_SHOW, 0); + const moreToShow = Math.max( + pendingAccessRequests.length - MAX_RESOURCES_IN_BAR_TO_SHOW, + 0 + ); + const numPendingAccessRequests = pendingAccessRequests.length; + return ( <> - {data.length > 0 && !isCollapsed() && ( + {pendingAccessRequests.length > 0 && !isCollapsed() && ( - {data.length}{' '} - {pluralize(data.length, isRoleRequest ? 'role' : 'resource')}{' '} + {numPendingAccessRequests}{' '} + {pluralize( + numPendingAccessRequests, + isRoleRequest ? 'role' : 'resource' + )}{' '} added to access request: - {data + {pendingAccessRequests .slice(0, MAX_RESOURCES_IN_BAR_TO_SHOW) .map(c => { let resource = { @@ -238,7 +246,7 @@ export function AccessRequestCheckout() { }) } reset={reset} - data={data} + pendingAccessRequests={pendingAccessRequests} showClusterNameColumn={shouldShowClusterNameColumn} createAttempt={createRequestAttempt} resourceRequestRoles={resourceRequestRoles} diff --git a/web/packages/teleterm/src/ui/AccessRequestCheckout/useAccessRequestCheckout.ts b/web/packages/teleterm/src/ui/AccessRequestCheckout/useAccessRequestCheckout.ts index 11f78ea5ebd0..4ad261cf61b3 100644 --- a/web/packages/teleterm/src/ui/AccessRequestCheckout/useAccessRequestCheckout.ts +++ b/web/packages/teleterm/src/ui/AccessRequestCheckout/useAccessRequestCheckout.ts @@ -91,7 +91,7 @@ export default function useAccessRequestCheckout() { workspaceAccessRequest?.getPendingAccessRequest(); useEffect(() => { - // Do a new dry run per changes to pending data + // Do a new dry run per changes to pending access requests // to get the latest time options and latest calculated // suggested reviewers. // Options and reviewers can change depending on the selected @@ -106,12 +106,13 @@ export default function useAccessRequestCheckout() { return; } - const data = getPendingAccessRequestsPerResource(pendingAccessRequest); + const pendingAccessRequests = + getPendingAccessRequestsPerResource(pendingAccessRequest); runFetchResourceRoles(() => retryWithRelogin(ctx, clusterUri, async () => { const { response } = await ctx.tshd.getRequestableRoles({ clusterUri: rootClusterUri, - resourceIds: data + resourceIds: pendingAccessRequests .filter(d => d.kind !== 'role') .map(d => ({ // We have to use id, not name. @@ -148,9 +149,9 @@ export default function useAccessRequestCheckout() { function getPendingAccessRequestsPerResource( pendingRequest: PendingAccessRequest ): PendingListItemWithOriginalItem[] { - const data: PendingListItemWithOriginalItem[] = []; + const pendingAccessRequests: PendingListItemWithOriginalItem[] = []; if (!workspaceAccessRequest) { - return data; + return pendingAccessRequests; } switch (pendingRequest.kind) { @@ -158,7 +159,7 @@ export default function useAccessRequestCheckout() { const clusterName = ctx.clustersService.findCluster(rootClusterUri)?.name; pendingRequest.roles.forEach(role => { - data.push({ + pendingAccessRequests.push({ kind: 'role', id: role, name: role, @@ -171,7 +172,7 @@ export default function useAccessRequestCheckout() { pendingRequest.resources.forEach(resourceRequest => { const { kind, id, name } = extractResourceRequestProperties(resourceRequest); - data.push({ + pendingAccessRequests.push({ kind, id, name, @@ -183,7 +184,7 @@ export default function useAccessRequestCheckout() { }); } } - return data; + return pendingAccessRequests; } function isCollapsed() { @@ -221,13 +222,14 @@ export default function useAccessRequestCheckout() { * Shared logic used both during dry runs and regular access request creation. */ function prepareAndCreateRequest(req: CreateRequest) { - const data = getPendingAccessRequestsPerResource(pendingAccessRequest); + const pendingAccessRequests = + getPendingAccessRequestsPerResource(pendingAccessRequest); const params: CreateAccessRequestRequest = { rootClusterUri, reason: req.reason, suggestedReviewers: req.suggestedReviewers || [], dryRun: req.dryRun, - resourceIds: data + resourceIds: pendingAccessRequests .filter(d => d.kind !== 'role') .map(d => ({ name: d.id, @@ -235,7 +237,9 @@ export default function useAccessRequestCheckout() { kind: d.kind, subResourceName: '', })), - roles: data.filter(d => d.kind === 'role').map(d => d.name), + roles: pendingAccessRequests + .filter(d => d.kind === 'role') + .map(d => d.name), assumeStartTime: req.start && Timestamp.fromDate(req.start), maxDuration: req.maxDuration && Timestamp.fromDate(req.maxDuration), requestTtl: req.requestTTL && Timestamp.fromDate(req.requestTTL), @@ -250,7 +254,10 @@ export default function useAccessRequestCheckout() { return retryWithRelogin(ctx, clusterUri, () => ctx.clustersService.createAccessRequest(params).then(({ response }) => { - return { accessRequest: response.request, requestedCount: data.length }; + return { + accessRequest: response.request, + requestedCount: pendingAccessRequests.length, + }; }) ).catch(e => { setCreateRequestAttempt({ status: 'failed', statusText: e.message }); @@ -337,7 +344,8 @@ export default function useAccessRequestCheckout() { isCollapsed, assumedRequests: getAssumedRequests(), toggleResource, - data: getPendingAccessRequestsPerResource(pendingAccessRequest), + pendingAccessRequests: + getPendingAccessRequestsPerResource(pendingAccessRequest), shouldShowClusterNameColumn, createRequest, reset,