mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-24 16:17:11 +08:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
+1253
-1015
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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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".
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)',
|
||||
|
||||
@@ -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)',
|
||||
|
||||
+2
-2
@@ -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}`;
|
||||
}
|
||||
Reference in New Issue
Block a user