Enable limited Access Requests feature for the Team plan (#29866)

* Add access request monthly limit to Features

* Add a method to retrieve usage of Access Requests

* Verify access request limit when creating

* Remove AccessRequestsFeature.Enabled

Feature is always enabled now, the flag serves no purpose

* Expose usage of access requests in auth server

* Reimplement access request usage using audit log

* Disallow unauthenticated access to resource usage

* Tidy proto structures

* Start enforcing access request limit in auth

* Run GCI

* Add a license header to usage_test.go

* Add missing godoc

* Restructure usage proto per PR comments

* Construct proto.AccessRequestUsage in getARMU

* Reduce log level when encountering an unexpected event

* Simplify return

* Add godoc to GetResourcesUsage in client

* Fix merge mess-ups

* Count all requests, no matter their review state

* Factor out GetAccessRequestMonthlyUsage

* Move out resource usage to its own service

* Validate the limit after dry run is done

Unbreak UI when the limit is up

* Fix imports

* Remove Limit

SearchEvents will default to defaults.EventsIterationLimit

* Rename the service proto

* Add godocs to the gRPC service

* Move resource usage to a dedicated package

* Define single GetUsage rpc for all resource types

* Additions for limited access requests UI rework

* Add a test for GetAccessRequestMonthlyUsage

* Address lint

* Run GCI

* Add licenses where missing

* Expect CTA_UNSPEICIFED rather than `undefined`

* Expect CTA_UNSPECIFIED instead of undefined
This commit is contained in:
Justinas Stankevičius
2023-08-16 17:20:34 +00:00
committed by GitHub
parent ce642266c6
commit 96f3df96b1
19 changed files with 2182 additions and 1040 deletions
+10
View File
@@ -58,6 +58,7 @@ import (
loginrulepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/loginrule/v1"
oktapb "github.com/gravitational/teleport/api/gen/proto/go/teleport/okta/v1"
pluginspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/plugins/v1"
resourceusagepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/resourceusage/v1"
samlidppb "github.com/gravitational/teleport/api/gen/proto/go/teleport/samlidp/v1"
trustpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/trust/v1"
userloginstatev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/userloginstate/v1"
@@ -4139,3 +4140,12 @@ func (c *Client) UpsertUserPreferences(ctx context.Context, in *userpreferencesp
}
return nil
}
// ResourceUsageClient returns an unadorned Resource Usage service client,
// using the underlying Auth gRPC connection.
// Clients connecting to non-Enterprise clusters, or older Teleport versions,
// still get a plugins client when calling this method, but all RPCs will return
// "not implemented" errors (as per the default gRPC behavior).
func (c *Client) ResourceUsageClient() resourceusagepb.ResourceUsageServiceClient {
return resourceusagepb.NewResourceUsageServiceClient(c.conn)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,312 @@
// Copyright 2023 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.31.0
// protoc (unknown)
// source: teleport/resourceusage/v1/resourceusage_service.proto
package resourceusagev1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// GetUsageRequest is the request for GetUsage
type GetUsageRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *GetUsageRequest) Reset() {
*x = GetUsageRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_teleport_resourceusage_v1_resourceusage_service_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetUsageRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetUsageRequest) ProtoMessage() {}
func (x *GetUsageRequest) ProtoReflect() protoreflect.Message {
mi := &file_teleport_resourceusage_v1_resourceusage_service_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetUsageRequest.ProtoReflect.Descriptor instead.
func (*GetUsageRequest) Descriptor() ([]byte, []int) {
return file_teleport_resourceusage_v1_resourceusage_service_proto_rawDescGZIP(), []int{0}
}
// GetUsageResponse is the response for GetUsage
type GetUsageResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
AccessRequests *AccessRequestsUsage `protobuf:"bytes,1,opt,name=access_requests,json=accessRequests,proto3" json:"access_requests,omitempty"`
}
func (x *GetUsageResponse) Reset() {
*x = GetUsageResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_teleport_resourceusage_v1_resourceusage_service_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetUsageResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetUsageResponse) ProtoMessage() {}
func (x *GetUsageResponse) ProtoReflect() protoreflect.Message {
mi := &file_teleport_resourceusage_v1_resourceusage_service_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetUsageResponse.ProtoReflect.Descriptor instead.
func (*GetUsageResponse) Descriptor() ([]byte, []int) {
return file_teleport_resourceusage_v1_resourceusage_service_proto_rawDescGZIP(), []int{1}
}
func (x *GetUsageResponse) GetAccessRequests() *AccessRequestsUsage {
if x != nil {
return x.AccessRequests
}
return nil
}
// AccessRequestsUsage defines the usage limits for access requests.
// Currently this is limited on the basis of access requests used per calendar month.
type AccessRequestsUsage struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// MonthlyLimit is the amount of requests that are allowed per month
MonthlyLimit int32 `protobuf:"varint,1,opt,name=monthly_limit,json=monthlyLimit,proto3" json:"monthly_limit,omitempty"`
// MonthlyUsed is the amount of requests that have been used this month
MonthlyUsed int32 `protobuf:"varint,2,opt,name=monthly_used,json=monthlyUsed,proto3" json:"monthly_used,omitempty"`
}
func (x *AccessRequestsUsage) Reset() {
*x = AccessRequestsUsage{}
if protoimpl.UnsafeEnabled {
mi := &file_teleport_resourceusage_v1_resourceusage_service_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *AccessRequestsUsage) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AccessRequestsUsage) ProtoMessage() {}
func (x *AccessRequestsUsage) ProtoReflect() protoreflect.Message {
mi := &file_teleport_resourceusage_v1_resourceusage_service_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AccessRequestsUsage.ProtoReflect.Descriptor instead.
func (*AccessRequestsUsage) Descriptor() ([]byte, []int) {
return file_teleport_resourceusage_v1_resourceusage_service_proto_rawDescGZIP(), []int{2}
}
func (x *AccessRequestsUsage) GetMonthlyLimit() int32 {
if x != nil {
return x.MonthlyLimit
}
return 0
}
func (x *AccessRequestsUsage) GetMonthlyUsed() int32 {
if x != nil {
return x.MonthlyUsed
}
return 0
}
var File_teleport_resourceusage_v1_resourceusage_service_proto protoreflect.FileDescriptor
var file_teleport_resourceusage_v1_resourceusage_service_proto_rawDesc = []byte{
0x0a, 0x35, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x75, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x19, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72,
0x74, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e,
0x76, 0x31, 0x22, 0x11, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x6b, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x55, 0x73, 0x61, 0x67,
0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0f, 0x61, 0x63, 0x63,
0x65, 0x73, 0x73, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x72, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41,
0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x55, 0x73, 0x61,
0x67, 0x65, 0x52, 0x0e, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x73, 0x22, 0x5d, 0x0a, 0x13, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x73, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x6f, 0x6e,
0x74, 0x68, 0x6c, 0x79, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05,
0x52, 0x0c, 0x6d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x21,
0x0a, 0x0c, 0x6d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x64, 0x18, 0x02,
0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x6d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x55, 0x73, 0x65,
0x64, 0x32, 0x7b, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x55, 0x73, 0x61,
0x67, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x63, 0x0a, 0x08, 0x47, 0x65, 0x74,
0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2a, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74,
0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76,
0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x2b, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x72, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65,
0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x5e,
0x5a, 0x5c, 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, 0x72, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x72,
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x75, 0x73, 0x61, 0x67, 0x65, 0x76, 0x31, 0x62, 0x06,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_teleport_resourceusage_v1_resourceusage_service_proto_rawDescOnce sync.Once
file_teleport_resourceusage_v1_resourceusage_service_proto_rawDescData = file_teleport_resourceusage_v1_resourceusage_service_proto_rawDesc
)
func file_teleport_resourceusage_v1_resourceusage_service_proto_rawDescGZIP() []byte {
file_teleport_resourceusage_v1_resourceusage_service_proto_rawDescOnce.Do(func() {
file_teleport_resourceusage_v1_resourceusage_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_teleport_resourceusage_v1_resourceusage_service_proto_rawDescData)
})
return file_teleport_resourceusage_v1_resourceusage_service_proto_rawDescData
}
var file_teleport_resourceusage_v1_resourceusage_service_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_teleport_resourceusage_v1_resourceusage_service_proto_goTypes = []interface{}{
(*GetUsageRequest)(nil), // 0: teleport.resourceusage.v1.GetUsageRequest
(*GetUsageResponse)(nil), // 1: teleport.resourceusage.v1.GetUsageResponse
(*AccessRequestsUsage)(nil), // 2: teleport.resourceusage.v1.AccessRequestsUsage
}
var file_teleport_resourceusage_v1_resourceusage_service_proto_depIdxs = []int32{
2, // 0: teleport.resourceusage.v1.GetUsageResponse.access_requests:type_name -> teleport.resourceusage.v1.AccessRequestsUsage
0, // 1: teleport.resourceusage.v1.ResourceUsageService.GetUsage:input_type -> teleport.resourceusage.v1.GetUsageRequest
1, // 2: teleport.resourceusage.v1.ResourceUsageService.GetUsage:output_type -> teleport.resourceusage.v1.GetUsageResponse
2, // [2:3] is the sub-list for method output_type
1, // [1:2] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_teleport_resourceusage_v1_resourceusage_service_proto_init() }
func file_teleport_resourceusage_v1_resourceusage_service_proto_init() {
if File_teleport_resourceusage_v1_resourceusage_service_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_teleport_resourceusage_v1_resourceusage_service_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetUsageRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_teleport_resourceusage_v1_resourceusage_service_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetUsageResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_teleport_resourceusage_v1_resourceusage_service_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*AccessRequestsUsage); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_teleport_resourceusage_v1_resourceusage_service_proto_rawDesc,
NumEnums: 0,
NumMessages: 3,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_teleport_resourceusage_v1_resourceusage_service_proto_goTypes,
DependencyIndexes: file_teleport_resourceusage_v1_resourceusage_service_proto_depIdxs,
MessageInfos: file_teleport_resourceusage_v1_resourceusage_service_proto_msgTypes,
}.Build()
File_teleport_resourceusage_v1_resourceusage_service_proto = out.File
file_teleport_resourceusage_v1_resourceusage_service_proto_rawDesc = nil
file_teleport_resourceusage_v1_resourceusage_service_proto_goTypes = nil
file_teleport_resourceusage_v1_resourceusage_service_proto_depIdxs = nil
}
@@ -0,0 +1,125 @@
// Copyright 2023 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.3.0
// - protoc (unknown)
// source: teleport/resourceusage/v1/resourceusage_service.proto
package resourceusagev1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
const (
ResourceUsageService_GetUsage_FullMethodName = "/teleport.resourceusage.v1.ResourceUsageService/GetUsage"
)
// ResourceUsageServiceClient is the client API for ResourceUsageService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type ResourceUsageServiceClient interface {
// GetUsage returns usage information for all limited resources
GetUsage(ctx context.Context, in *GetUsageRequest, opts ...grpc.CallOption) (*GetUsageResponse, error)
}
type resourceUsageServiceClient struct {
cc grpc.ClientConnInterface
}
func NewResourceUsageServiceClient(cc grpc.ClientConnInterface) ResourceUsageServiceClient {
return &resourceUsageServiceClient{cc}
}
func (c *resourceUsageServiceClient) GetUsage(ctx context.Context, in *GetUsageRequest, opts ...grpc.CallOption) (*GetUsageResponse, error) {
out := new(GetUsageResponse)
err := c.cc.Invoke(ctx, ResourceUsageService_GetUsage_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// ResourceUsageServiceServer is the server API for ResourceUsageService service.
// All implementations must embed UnimplementedResourceUsageServiceServer
// for forward compatibility
type ResourceUsageServiceServer interface {
// GetUsage returns usage information for all limited resources
GetUsage(context.Context, *GetUsageRequest) (*GetUsageResponse, error)
mustEmbedUnimplementedResourceUsageServiceServer()
}
// UnimplementedResourceUsageServiceServer must be embedded to have forward compatible implementations.
type UnimplementedResourceUsageServiceServer struct {
}
func (UnimplementedResourceUsageServiceServer) GetUsage(context.Context, *GetUsageRequest) (*GetUsageResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetUsage not implemented")
}
func (UnimplementedResourceUsageServiceServer) mustEmbedUnimplementedResourceUsageServiceServer() {}
// UnsafeResourceUsageServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ResourceUsageServiceServer will
// result in compilation errors.
type UnsafeResourceUsageServiceServer interface {
mustEmbedUnimplementedResourceUsageServiceServer()
}
func RegisterResourceUsageServiceServer(s grpc.ServiceRegistrar, srv ResourceUsageServiceServer) {
s.RegisterService(&ResourceUsageService_ServiceDesc, srv)
}
func _ResourceUsageService_GetUsage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetUsageRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ResourceUsageServiceServer).GetUsage(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ResourceUsageService_GetUsage_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ResourceUsageServiceServer).GetUsage(ctx, req.(*GetUsageRequest))
}
return interceptor(ctx, in, info, handler)
}
// ResourceUsageService_ServiceDesc is the grpc.ServiceDesc for ResourceUsageService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var ResourceUsageService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "teleport.resourceusage.v1.ResourceUsageService",
HandlerType: (*ResourceUsageServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetUsage",
Handler: _ResourceUsageService_GetUsage_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "teleport/resourceusage/v1/resourceusage_service.proto",
}
@@ -499,6 +499,8 @@ message Features {
DeviceTrustFeature DeviceTrust = 19 [(gogoproto.jsontag) = "device_trust,omitempty"];
// FeatureHiding enables hiding features from being discoverable for users who don't have the necessary permissions.
bool FeatureHiding = 20 [(gogoproto.jsontag) = "feature_hiding,omitempty"];
// AccessRequests holds its namesake feature settings.
AccessRequestsFeature AccessRequests = 21 [(gogoproto.jsontag) = "access_requests,omitempty"];
}
// DeviceTrustFeature holds the Device Trust feature general and usage-based
@@ -514,6 +516,17 @@ message DeviceTrustFeature {
int32 devices_usage_limit = 2 [(gogoproto.jsontag) = "devices_usage_limit,omitempty"];
}
// AccessRequestsFeature holds the AccessRequest feature general and usage-based
// settings.
// Requires Teleport Enterprise.
message AccessRequestsFeature {
// Usage-based limit for the number of limit for the number of
// access requests created in a calendar month.
// Meant for usage-based accounts, like Teleport Team. Has no effect if
// [Features.IsUsageBased] is `false`.
int32 monthly_request_limit = 1 [(gogoproto.jsontag) = "monthly_request_limit"];
}
// DeleteUserRequest is the input value for the DeleteUser method.
message DeleteUserRequest {
// Name is the user name to delete.
@@ -0,0 +1,42 @@
// Copyright 2023 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package teleport.resourceusage.v1;
option go_package = "github.com/gravitational/teleport/api/gen/proto/go/teleport/resourceusage/v1;resourceusagev1";
// ResourceUsageService is a service to fetch information about the usage of limited resources on usage-billed plans.
service ResourceUsageService {
// GetUsage returns usage information for all limited resources
rpc GetUsage(GetUsageRequest) returns (GetUsageResponse);
}
// GetUsageRequest is the request for GetUsage
message GetUsageRequest {}
// GetUsageResponse is the response for GetUsage
message GetUsageResponse {
AccessRequestsUsage access_requests = 1;
}
// AccessRequestsUsage defines the usage limits for access requests.
// Currently this is limited on the basis of access requests used per calendar month.
message AccessRequestsUsage {
// MonthlyLimit is the amount of requests that are allowed per month
int32 monthly_limit = 1;
// MonthlyUsed is the amount of requests that have been used this month
int32 monthly_used = 2;
}
+32
View File
@@ -92,6 +92,7 @@ import (
"github.com/gravitational/teleport/lib/observability/metrics"
"github.com/gravitational/teleport/lib/observability/tracing"
"github.com/gravitational/teleport/lib/release"
"github.com/gravitational/teleport/lib/resourceusage"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/services/local"
"github.com/gravitational/teleport/lib/srv/db/common/role"
@@ -4073,6 +4074,10 @@ func (a *Server) CreateAccessRequestV2(ctx context.Context, req types.AccessRequ
return req, nil
}
if err := a.verifyAccessRequestMonthlyLimit(ctx); err != nil {
return nil, trace.Wrap(err)
}
log.Debugf("Creating Access Request %v with expiry %v.", req.GetName(), req.Expiry())
if _, err := a.Services.CreateAccessRequestV2(ctx, req); err != nil {
@@ -5532,6 +5537,33 @@ func (a *Server) CompareAndSwapHeadlessAuthentication(ctx context.Context, old,
return headlessAuthn, trace.Wrap(err)
}
// getAccessRequestMonthlyUsage returns the number of access requests that have been created this month.
func (a *Server) getAccessRequestMonthlyUsage(ctx context.Context) (int, error) {
return resourceusage.GetAccessRequestMonthlyUsage(ctx, a.Services.AuditLogSessionStreamer, a.clock.Now().UTC())
}
// verifyAccessRequestMonthlyLimit checks whether the cluster has exceeded the monthly access request limit.
// If so, it returns an error. This is only applicable on usage-based billing plans.
func (a *Server) verifyAccessRequestMonthlyLimit(ctx context.Context) error {
f := modules.GetModules().Features()
if !f.IsUsageBasedBilling {
return nil // unlimited
}
monthlyLimit := f.AccessRequests.MonthlyRequestLimit
const limitReachedMessage = "cluster has reached its monthly access request limit, please contact the cluster administrator"
usage, err := a.getAccessRequestMonthlyUsage(ctx)
if err != nil {
return trace.Wrap(err)
}
if usage >= monthlyLimit {
return trace.AccessDenied(limitReachedMessage)
}
return nil
}
// getProxyPublicAddr returns the first valid, non-empty proxy public address it
// finds, or empty otherwise.
func (a *Server) getProxyPublicAddr() string {
+10
View File
@@ -47,6 +47,7 @@ import (
loginrulepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/loginrule/v1"
oktapb "github.com/gravitational/teleport/api/gen/proto/go/teleport/okta/v1"
pluginspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/plugins/v1"
resourceusagepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/resourceusage/v1"
samlidppb "github.com/gravitational/teleport/api/gen/proto/go/teleport/samlidp/v1"
trustpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/trust/v1"
userloginstatev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/userloginstate/v1"
@@ -345,6 +346,15 @@ func (a *ServerWithRoles) AccessListClient() services.AccessLists {
utils.NewGRPCDummyClientConnection("AccessListClient() should not be called on ServerWithRoles")))
}
// ResourceUsageClient allows ServerWithRoles to implement ClientI.
// It should not be called through ServerWithRoles,
// as it returns a dummy client that will always respond with "not implemented".
func (a *ServerWithRoles) ResourceUsageClient() resourceusagepb.ResourceUsageServiceClient {
return resourceusagepb.NewResourceUsageServiceClient(
utils.NewGRPCDummyClientConnection("ResourceUsageClient() should not be called on ServerWithRoles"),
)
}
// UserLoginStateClient allows ServerWithRoles to implement ClientI.
// It should not be called through ServerWithRoles,
// as it returns a dummy client that will always respond with "not implemented".
+7
View File
@@ -32,6 +32,7 @@ import (
devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1"
loginrulepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/loginrule/v1"
pluginspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/plugins/v1"
resourceusagepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/resourceusage/v1"
samlidppb "github.com/gravitational/teleport/api/gen/proto/go/teleport/samlidp/v1"
userpreferencesv1 "github.com/gravitational/teleport/api/gen/proto/go/userpreferences/v1"
"github.com/gravitational/teleport/api/types"
@@ -843,6 +844,12 @@ type ClientI interface {
// (as per the default gRPC behavior).
UserLoginStateClient() services.UserLoginStates
// ResourceUsageClient returns a resource usage service client.
// Clients connecting to non-Enterprise clusters, or older Teleport versions,
// still get a client when calling this method, but all RPCs will return
// "not implemented" errors (as per the default gRPC behavior).
ResourceUsageClient() resourceusagepb.ResourceUsageServiceClient
// CloneHTTPClient creates a new HTTP client with the same configuration.
CloneHTTPClient(params ...roundtrip.ClientParam) (*HTTPClient, error)
+120
View File
@@ -0,0 +1,120 @@
// Copyright 2023 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"context"
"testing"
"time"
"github.com/google/uuid"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slices"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/lib/events"
eventstest "github.com/gravitational/teleport/lib/events/test"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/tlsca"
)
func TestAccessRequestLimit(t *testing.T) {
const monthlyLimit = 3
makeEvent := func(eventType string, id string, timestamp time.Time) apievents.AuditEvent {
return &apievents.AccessRequestCreate{
Metadata: apievents.Metadata{
Type: eventType,
Time: timestamp,
},
RequestID: id,
}
}
features := modules.GetModules().Features()
features.IsUsageBasedBilling = true
features.AccessRequests.MonthlyRequestLimit = monthlyLimit
modules.SetTestModules(t, &modules.TestModules{
TestFeatures: features,
})
ctx := context.Background()
p, err := newTestPack(ctx, t.TempDir())
require.NoError(t, err)
// Set up RBAC
access, err := types.NewRole("access", types.RoleSpecV6{})
require.NoError(t, err)
p.a.CreateRole(ctx, access)
require.NoError(t, err)
requestor, err := types.NewRole("requestor", types.RoleSpecV6{
Allow: types.RoleConditions{
Request: &types.AccessRequestConditions{
Roles: []string{"access"},
},
},
})
require.NoError(t, err)
p.a.CreateRole(ctx, requestor)
require.NoError(t, err)
alice, err := types.NewUser("alice")
alice.SetRoles([]string{"requestor"})
require.NoError(t, err)
err = p.a.CreateUser(ctx, alice)
require.NoError(t, err)
// Mock audit log
// Create a clock in the middle of the month for easy manipulation
clock := clockwork.NewFakeClockAt(
time.Date(2023, 07, 15, 1, 2, 3, 0, time.UTC))
p.a.SetClock(clock)
july := clock.Now()
august := clock.Now().AddDate(0, 1, 0)
mockEvents := []apievents.AuditEvent{
// 3 created requests in July: can not create any more
makeEvent(events.AccessRequestCreateEvent, "aaa", july.AddDate(0, 0, -3)),
makeEvent(events.AccessRequestCreateEvent, "bbb", july.AddDate(0, 0, -2)),
makeEvent(events.AccessRequestCreateEvent, "ccc", july.AddDate(0, 0, -1)),
// 2 access requests created in August: can create one more
makeEvent(events.AccessRequestCreateEvent, "ddd", august.AddDate(0, 0, -2)),
makeEvent(events.AccessRequestCreateEvent, "eee", august.AddDate(0, 0, -1)),
}
al := eventstest.NewMockAuditLogSessionStreamer(mockEvents, func(req events.SearchEventsRequest) error {
if !slices.Equal([]string{events.AccessRequestCreateEvent}, req.EventTypes) {
return trace.BadParameter("expected AccessRequestCreateEvent only, got %v", req.EventTypes)
}
return nil
})
p.a.SetAuditLog(al)
// Check July
req, err := types.NewAccessRequest(uuid.New().String(), "alice", "access")
require.NoError(t, err)
err = p.a.CreateAccessRequest(ctx, req, tlsca.Identity{})
require.Error(t, err, "expected access request creation to fail due to the monthly limit")
// Check August
clock.Advance(31 * 24 * time.Hour)
req, err = types.NewAccessRequest(uuid.New().String(), "alice", "access")
require.NoError(t, err)
err = p.a.CreateAccessRequest(ctx, req, tlsca.Identity{})
require.NoError(t, err)
}
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2023 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package test
import (
"context"
"github.com/gravitational/trace"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/lib/events"
)
type MockAuditLogSessionStreamer struct {
events.DiscardAuditLog
events []apievents.AuditEvent
verifyRequest func(events.SearchEventsRequest) error
}
func NewMockAuditLogSessionStreamer(events []apievents.AuditEvent, verifyRequest func(events.SearchEventsRequest) error) *MockAuditLogSessionStreamer {
return &MockAuditLogSessionStreamer{
events: events,
verifyRequest: verifyRequest,
}
}
// SearchEvents implements events.AuditLogSessionStreamer
func (m *MockAuditLogSessionStreamer) SearchEvents(ctx context.Context, req events.SearchEventsRequest) ([]apievents.AuditEvent, string, error) {
if m.verifyRequest != nil {
if err := m.verifyRequest(req); err != nil {
return nil, "", trace.Wrap(err)
}
}
var results []apievents.AuditEvent
for _, ev := range m.events {
if !req.From.IsZero() && ev.GetTime().Before(req.From) {
continue
}
if !req.To.IsZero() && ev.GetTime().After(req.To) {
continue
}
results = append(results, ev)
}
return results, "", nil
}
+14
View File
@@ -74,6 +74,8 @@ type Features struct {
DeviceTrust DeviceTrustFeature
// FeatureHiding enables hiding features from being discoverable for users who don't have the necessary permissions.
FeatureHiding bool
// AccessRequests holds its namesake feature settings.
AccessRequests AccessRequestsFeature
}
// DeviceTrustFeature holds the Device Trust feature general and usage-based
@@ -89,6 +91,15 @@ type DeviceTrustFeature struct {
DevicesUsageLimit int
}
// AccessRequestsFeature holds the Access Requests feature general and usage-based settings.
type AccessRequestsFeature struct {
// MonthlyRequestLimit is the usage-based limit for the number of
// access requests created in a calendar month.
// Meant for usage-based accounts, like Teleport Team. Has no effect if
// [Features.IsUsageBasedBilling] is `false`.
MonthlyRequestLimit int
}
// ToProto converts Features into proto.Features
func (f Features) ToProto() *proto.Features {
return &proto.Features{
@@ -112,6 +123,9 @@ func (f Features) ToProto() *proto.Features {
Enabled: f.DeviceTrust.Enabled,
DevicesUsageLimit: int32(f.DeviceTrust.DevicesUsageLimit),
},
AccessRequests: &proto.AccessRequestsFeature{
MonthlyRequestLimit: int32(f.AccessRequests.MonthlyRequestLimit),
},
}
}
+68
View File
@@ -0,0 +1,68 @@
// Copyright 2023 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package resourceusage
import (
"context"
"time"
"github.com/gravitational/trace"
log "github.com/sirupsen/logrus"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/lib/events"
)
// GetAccessRequestMonthlyUsage returns the number of access requests that have been created this month.
func GetAccessRequestMonthlyUsage(ctx context.Context, alog events.AuditLogger, now time.Time) (int, error) {
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
created := make(map[string]struct{})
var results []apievents.AuditEvent
var startKey string
var err error
for {
results, startKey, err = alog.SearchEvents(ctx, events.SearchEventsRequest{
From: monthStart,
To: now,
Order: types.EventOrderAscending,
EventTypes: []string{events.AccessRequestCreateEvent},
StartKey: startKey,
})
if err != nil {
return 0, trace.Wrap(err)
}
for _, ev := range results {
ev, ok := ev.(*apievents.AccessRequestCreate)
if !ok {
return 0, trace.BadParameter("expected *AccessRequestCreate, but got %T", ev)
}
id := ev.RequestID
switch ev.GetType() {
case events.AccessRequestCreateEvent:
created[id] = struct{}{}
default:
log.Warnf("Expected event type %q, got %q", events.AccessRequestCreateEvent, ev.GetType())
}
}
if startKey == "" {
break
}
}
return len(created), nil
}
+63
View File
@@ -0,0 +1,63 @@
// Copyright 2023 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package resourceusage
import (
"context"
"testing"
"time"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slices"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/lib/events"
eventstest "github.com/gravitational/teleport/lib/events/test"
)
func TestGetAccessRequestMonthlyUsage(t *testing.T) {
makeEvent := func(eventType string, id string, timestamp time.Time) apievents.AuditEvent {
return &apievents.AccessRequestCreate{
Metadata: apievents.Metadata{
Type: eventType,
Time: timestamp,
},
RequestID: id,
}
}
// Mock audit log
clock := clockwork.NewFakeClockAt(time.Date(2023, 07, 15, 1, 2, 3, 0, time.UTC))
now := clock.Now()
mockEvents := []apievents.AuditEvent{
makeEvent(events.AccessRequestCreateEvent, "aaa", now.AddDate(0, 0, -4)),
makeEvent(events.AccessRequestCreateEvent, "bbb", now.AddDate(0, 0, -3)),
makeEvent(events.AccessRequestCreateEvent, "ccc", now.AddDate(0, 0, -2)),
makeEvent(events.AccessRequestCreateEvent, "ddd", now.AddDate(0, 0, -1)),
}
al := eventstest.NewMockAuditLogSessionStreamer(mockEvents, func(req events.SearchEventsRequest) error {
if !slices.Equal([]string{events.AccessRequestCreateEvent}, req.EventTypes) {
return trace.BadParameter("expected AccessRequestCreateEvent only, got %v", req.EventTypes)
}
return nil
})
result, err := GetAccessRequestMonthlyUsage(context.Background(), al, now)
require.NoError(t, err)
require.Equal(t, len(mockEvents), result)
}
@@ -169,6 +169,10 @@ const colors = {
active: '#FFCD66',
},
notice: {
background: '#344179', // elevated
},
action: {
active: '#FFFFFF',
hover: 'rgba(255, 255, 255, 0.1)',
+5 -1
View File
@@ -16,7 +16,7 @@ limitations under the License.
import { fonts } from './fonts';
import { darken, getContrastRatio } from './utils/colorManipulator';
import { lightBlue, blueGrey, yellow } from './palette';
import { blue, lightBlue, blueGrey, yellow } from './palette';
import typography, { fontSizes, fontWeights } from './typography';
import { sharedStyles } from './sharedStyles';
@@ -165,6 +165,10 @@ const colors = {
active: '#996700',
},
notice: {
background: blue[50],
},
action: {
active: '#FFFFFF',
hover: 'rgba(255, 255, 255, 0.1)',
@@ -56,7 +56,7 @@ describe('buttonLockedFeature', () => {
);
expect(screen.getByText('text').closest('a')).toHaveAttribute(
'href',
`https://goteleport.com/r/upgrade-team?${version}&utm_campaign=undefined`
`https://goteleport.com/r/upgrade-team?${version}&utm_campaign=CTA_UNSPECIFIED`
);
renderWithContext(
@@ -80,7 +80,7 @@ describe('buttonLockedFeature', () => {
);
expect(screen.getByText('text').closest('a')).toHaveAttribute(
'href',
`https://goteleport.com/r/upgrade-community?${version}&utm_campaign=undefined`
`https://goteleport.com/r/upgrade-community?${version}&utm_campaign=CTA_UNSPECIFIED`
);
renderWithContext(
@@ -20,6 +20,8 @@ import { ButtonPrimary } from 'design/Button';
import { Unlock } from 'design/Icon';
import Flex from 'design/Flex';
import { getSalesURL } from 'teleport/services/sales';
import { CtaEvent, userEventService } from 'teleport/services/userEvent';
import useTeleport from 'teleport/useTeleport';
@@ -32,9 +34,6 @@ export type Props = {
[index: string]: any;
};
const UPGRADE_TEAM_URL = 'https://goteleport.com/r/upgrade-team';
const UPGRADE_COMMUNITY_URL = 'https://goteleport.com/r/upgrade-community';
export function ButtonLockedFeature({
children,
noIcon = false,
@@ -45,14 +44,7 @@ export function ButtonLockedFeature({
const version = ctx.storeUser.state.cluster.authVersion;
const isEnterprise = ctx.isEnterprise;
const upgradeURL = cfg.isUsageBasedBilling
? UPGRADE_TEAM_URL
: UPGRADE_COMMUNITY_URL;
const upgradeURLWithParams = `${upgradeURL}?${getParams(
version,
isEnterprise,
event
)}`;
const isUsageBased = cfg.isUsageBasedBilling;
function handleClick() {
userEventService.captureCtaEvent(event);
@@ -62,7 +54,7 @@ export function ButtonLockedFeature({
<ButtonPrimary
as="a"
target="blank"
href={`${upgradeURLWithParams}`}
href={getSalesURL(version, isEnterprise, isUsageBased, event)}
onClick={handleClick}
py="12px"
width="100%"
@@ -78,16 +70,6 @@ export function ButtonLockedFeature({
);
}
function getParams(
version: string,
isEnterprise: boolean,
event: CtaEvent
): string {
return `${isEnterprise ? 'e_' : ''}${version}&utm_campaign=${
CtaEvent[event]
}`;
}
const UnlockIcon = styled(Unlock)`
color: inherit;
font-weight: 500;
@@ -0,0 +1,41 @@
/**
* Copyright 2023 Gravitational, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CtaEvent } from 'teleport/services/userEvent';
const UPGRADE_TEAM_URL = 'https://goteleport.com/r/upgrade-team';
const UPGRADE_COMMUNITY_URL = 'https://goteleport.com/r/upgrade-community';
function getParams(
version: string,
isEnterprise: boolean,
event?: CtaEvent
): string {
return `${isEnterprise ? 'e_' : ''}${version}&utm_campaign=${
CtaEvent[event ?? CtaEvent.CTA_UNSPECIFIED]
}`;
}
export function getSalesURL(
version: string,
isEnterprise: boolean,
isUsageBased: boolean,
event?: CtaEvent
) {
const url = isUsageBased ? UPGRADE_TEAM_URL : UPGRADE_COMMUNITY_URL;
const params = getParams(version, isEnterprise, event);
return `${url}?${params}`;
}