From 10095ebfb9485ac196fe28b2766ea4f85a1ed6f1 Mon Sep 17 00:00:00 2001 From: Bernard Kim Date: Wed, 15 Oct 2025 10:41:02 -0700 Subject: [PATCH] access_monitoring_rules: Add timezone to schedules spec (#60067) * Add timezone to AMR schedules * Update documentation with accepted timezone values * Address feedback - Allow empty timezone value - Reference IANA in error message - Add additional timeonze test cases * Fix test case --- .../v1/access_monitoring_rules.pb.go | 22 ++++- .../v1/access_monitoring_rules.proto | 9 +- .../data-sources/access_monitoring_rule.mdx | 3 +- .../resources/access_monitoring_rule.mdx | 3 +- .../v1/access_monitoring_rules_terraform.go | 88 +++++++++++++----- lib/services/access_monitoring_rules.go | 5 + lib/services/access_monitoring_rules_test.go | 92 +++++++++++++++++++ 7 files changed, 194 insertions(+), 28 deletions(-) diff --git a/api/gen/proto/go/teleport/accessmonitoringrules/v1/access_monitoring_rules.pb.go b/api/gen/proto/go/teleport/accessmonitoringrules/v1/access_monitoring_rules.pb.go index 68613018c47..a38388e950d 100644 --- a/api/gen/proto/go/teleport/accessmonitoringrules/v1/access_monitoring_rules.pb.go +++ b/api/gen/proto/go/teleport/accessmonitoringrules/v1/access_monitoring_rules.pb.go @@ -394,8 +394,14 @@ func (x *Schedule) GetTime() *TimeSchedule { type TimeSchedule struct { state protoimpl.MessageState `protogen:"open.v1"` // Shifts contains a set of shifts that make up the schedule. - // Shifts are configured in UTC. - Shifts []*TimeSchedule_Shift `protobuf:"bytes,1,rep,name=shifts,proto3" json:"shifts,omitempty"` + Shifts []*TimeSchedule_Shift `protobuf:"bytes,1,rep,name=shifts,proto3" json:"shifts,omitempty"` + // Timezone specifies the schedule timezone. This field is optional and defaults + // to "UTC". Accepted values use timezone locations as defined in the IANA + // Time Zone Database, such as "America/Los_Angeles", "Europe/Lisbon", or + // "Asia/Singapore". + // + // See https://data.iana.org/time-zones/tzdb/zone1970.tab for a list of supported values. + Timezone string `protobuf:"bytes,2,opt,name=timezone,proto3" json:"timezone,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -437,6 +443,13 @@ func (x *TimeSchedule) GetShifts() []*TimeSchedule_Shift { return nil } +func (x *TimeSchedule) GetTimezone() string { + if x != nil { + return x.Timezone + } + return "" +} + // CreateAccessMonitoringRuleRequest is the request for CreateAccessMonitoringRule. type CreateAccessMonitoringRuleRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1014,9 +1027,10 @@ const file_teleport_accessmonitoringrules_v1_access_monitoring_rules_proto_rawDe "\vintegration\x18\x01 \x01(\tR\vintegration\x12\x1a\n" + "\bdecision\x18\x02 \x01(\tR\bdecision\"O\n" + "\bSchedule\x12C\n" + - "\x04time\x18\x01 \x01(\v2/.teleport.accessmonitoringrules.v1.TimeScheduleR\x04time\"\xa8\x01\n" + + "\x04time\x18\x01 \x01(\v2/.teleport.accessmonitoringrules.v1.TimeScheduleR\x04time\"\xc4\x01\n" + "\fTimeSchedule\x12M\n" + - "\x06shifts\x18\x01 \x03(\v25.teleport.accessmonitoringrules.v1.TimeSchedule.ShiftR\x06shifts\x1aI\n" + + "\x06shifts\x18\x01 \x03(\v25.teleport.accessmonitoringrules.v1.TimeSchedule.ShiftR\x06shifts\x12\x1a\n" + + "\btimezone\x18\x02 \x01(\tR\btimezone\x1aI\n" + "\x05Shift\x12\x18\n" + "\aweekday\x18\x01 \x01(\tR\aweekday\x12\x14\n" + "\x05start\x18\x02 \x01(\tR\x05start\x12\x10\n" + diff --git a/api/proto/teleport/accessmonitoringrules/v1/access_monitoring_rules.proto b/api/proto/teleport/accessmonitoringrules/v1/access_monitoring_rules.proto index 13ebe953a10..3d9e098b04b 100644 --- a/api/proto/teleport/accessmonitoringrules/v1/access_monitoring_rules.proto +++ b/api/proto/teleport/accessmonitoringrules/v1/access_monitoring_rules.proto @@ -100,9 +100,16 @@ message Schedule { // TimeSchedule specifies an in-line schedule. message TimeSchedule { // Shifts contains a set of shifts that make up the schedule. - // Shifts are configured in UTC. repeated Shift shifts = 1; + // Timezone specifies the schedule timezone. This field is optional and defaults + // to "UTC". Accepted values use timezone locations as defined in the IANA + // Time Zone Database, such as "America/Los_Angeles", "Europe/Lisbon", or + // "Asia/Singapore". + // + // See https://data.iana.org/time-zones/tzdb/zone1970.tab for a list of supported values. + string timezone = 2; + // Shift contains the weekday, start time, and end time of a shift. message Shift { // Weekday specifies the day of the week, e.g., "Sunday", "Monday", "Tuesday". diff --git a/docs/pages/reference/infrastructure-as-code/terraform-provider/data-sources/access_monitoring_rule.mdx b/docs/pages/reference/infrastructure-as-code/terraform-provider/data-sources/access_monitoring_rule.mdx index 796b235167d..c16015769c0 100644 --- a/docs/pages/reference/infrastructure-as-code/terraform-provider/data-sources/access_monitoring_rule.mdx +++ b/docs/pages/reference/infrastructure-as-code/terraform-provider/data-sources/access_monitoring_rule.mdx @@ -76,7 +76,8 @@ Optional: Optional: -- `shifts` (Attributes List) Shifts contains a set of shifts that make up the schedule. Shifts are configured in UTC. (see [below for nested schema](#nested-schema-for-specschedulestimeshifts)) +- `shifts` (Attributes List) Shifts contains a set of shifts that make up the schedule. (see [below for nested schema](#nested-schema-for-specschedulestimeshifts)) +- `timezone` (String) Timezone specifies the schedule timezone. This field is optional and defaults to "UTC". Accepted values use timezone locations as defined in the IANA Time Zone Database, such as "America/Los_Angeles", "Europe/Lisbon", or "Asia/Singapore". See https://data.iana.org/time-zones/tzdb/zone1970.tab for a list of supported values. ### Nested Schema for `spec.schedules.time.shifts` diff --git a/docs/pages/reference/infrastructure-as-code/terraform-provider/resources/access_monitoring_rule.mdx b/docs/pages/reference/infrastructure-as-code/terraform-provider/resources/access_monitoring_rule.mdx index 487ca4f1710..5a826751fb7 100644 --- a/docs/pages/reference/infrastructure-as-code/terraform-provider/resources/access_monitoring_rule.mdx +++ b/docs/pages/reference/infrastructure-as-code/terraform-provider/resources/access_monitoring_rule.mdx @@ -98,7 +98,8 @@ Optional: Optional: -- `shifts` (Attributes List) Shifts contains a set of shifts that make up the schedule. Shifts are configured in UTC. (see [below for nested schema](#nested-schema-for-specschedulestimeshifts)) +- `shifts` (Attributes List) Shifts contains a set of shifts that make up the schedule. (see [below for nested schema](#nested-schema-for-specschedulestimeshifts)) +- `timezone` (String) Timezone specifies the schedule timezone. This field is optional and defaults to "UTC". Accepted values use timezone locations as defined in the IANA Time Zone Database, such as "America/Los_Angeles", "Europe/Lisbon", or "Asia/Singapore". See https://data.iana.org/time-zones/tzdb/zone1970.tab for a list of supported values. ### Nested Schema for `spec.schedules.time.shifts` diff --git a/integrations/terraform/tfschema/accessmonitoringrules/v1/access_monitoring_rules_terraform.go b/integrations/terraform/tfschema/accessmonitoringrules/v1/access_monitoring_rules_terraform.go index 0fa3920979e..8be88376ac8 100644 --- a/integrations/terraform/tfschema/accessmonitoringrules/v1/access_monitoring_rules_terraform.go +++ b/integrations/terraform/tfschema/accessmonitoringrules/v1/access_monitoring_rules_terraform.go @@ -142,27 +142,34 @@ func GenSchemaAccessMonitoringRule(ctx context.Context) (github_com_hashicorp_te }, "schedules": { Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.MapNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{"time": { - Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.SingleNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{"shifts": { - Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.ListNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{ - "end": { - Description: "End specifies the end time in the format HH:MM, e.g., \"12:30\".", - Optional: true, - Type: github_com_hashicorp_terraform_plugin_framework_types.StringType, - }, - "start": { - Description: "Start specifies the start time in the format HH:MM, e.g., \"12:30\".", - Optional: true, - Type: github_com_hashicorp_terraform_plugin_framework_types.StringType, - }, - "weekday": { - Description: "Weekday specifies the day of the week, e.g., \"Sunday\", \"Monday\", \"Tuesday\".", - Optional: true, - Type: github_com_hashicorp_terraform_plugin_framework_types.StringType, - }, - }), - Description: "Shifts contains a set of shifts that make up the schedule. Shifts are configured in UTC.", - Optional: true, - }}), + Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.SingleNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{ + "shifts": { + Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.ListNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{ + "end": { + Description: "End specifies the end time in the format HH:MM, e.g., \"12:30\".", + Optional: true, + Type: github_com_hashicorp_terraform_plugin_framework_types.StringType, + }, + "start": { + Description: "Start specifies the start time in the format HH:MM, e.g., \"12:30\".", + Optional: true, + Type: github_com_hashicorp_terraform_plugin_framework_types.StringType, + }, + "weekday": { + Description: "Weekday specifies the day of the week, e.g., \"Sunday\", \"Monday\", \"Tuesday\".", + Optional: true, + Type: github_com_hashicorp_terraform_plugin_framework_types.StringType, + }, + }), + Description: "Shifts contains a set of shifts that make up the schedule.", + Optional: true, + }, + "timezone": { + Description: "Timezone specifies the schedule timezone. This field is optional and defaults to \"UTC\". Accepted values use timezone locations as defined in the IANA Time Zone Database, such as \"America/Los_Angeles\", \"Europe/Lisbon\", or \"Asia/Singapore\". See https://data.iana.org/time-zones/tzdb/zone1970.tab for a list of supported values.", + Optional: true, + Type: github_com_hashicorp_terraform_plugin_framework_types.StringType, + }, + }), Description: "TimeSchedule specifies an in-line schedule.", Optional: true, }}), @@ -701,6 +708,23 @@ func CopyAccessMonitoringRuleFromTerraform(_ context.Context, tf github_com_hash } } } + { + a, ok := tf.Attrs["timezone"] + if !ok { + diags.Append(attrReadMissingDiag{"AccessMonitoringRule.spec.schedules.time.timezone"}) + } else { + v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrReadConversionFailureDiag{"AccessMonitoringRule.spec.schedules.time.timezone", "github.com/hashicorp/terraform-plugin-framework/types.String"}) + } else { + var t string + if !v.Null && !v.Unknown { + t = string(v.Value) + } + obj.Timezone = t + } + } + } } } } @@ -1524,6 +1548,28 @@ func CopyAccessMonitoringRuleToTerraform(ctx context.Context, obj *github_com_gr } } } + { + t, ok := tf.AttrTypes["timezone"] + if !ok { + diags.Append(attrWriteMissingDiag{"AccessMonitoringRule.spec.schedules.time.timezone"}) + } else { + v, ok := tf.Attrs["timezone"].(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + i, err := t.ValueFromTerraform(ctx, github_com_hashicorp_terraform_plugin_go_tftypes.NewValue(t.TerraformType(ctx), nil)) + if err != nil { + diags.Append(attrWriteGeneralError{"AccessMonitoringRule.spec.schedules.time.timezone", err}) + } + v, ok = i.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrWriteConversionFailureDiag{"AccessMonitoringRule.spec.schedules.time.timezone", "github.com/hashicorp/terraform-plugin-framework/types.String"}) + } + v.Null = string(obj.Timezone) == "" + } + v.Value = string(obj.Timezone) + v.Unknown = false + tf.Attrs["timezone"] = v + } + } } v.Unknown = false tf.Attrs["time"] = v diff --git a/lib/services/access_monitoring_rules.go b/lib/services/access_monitoring_rules.go index 8fbdcf4e37f..6ff8c60b9e1 100644 --- a/lib/services/access_monitoring_rules.go +++ b/lib/services/access_monitoring_rules.go @@ -22,6 +22,7 @@ import ( "context" "slices" "time" + _ "time/tzdata" "github.com/gravitational/trace" @@ -156,6 +157,10 @@ func validateSchedules(schedules map[string]*accessmonitoringrulesv1.Schedule) e } func validateTimeSchedule(schedule *accessmonitoringrulesv1.TimeSchedule) error { + if _, err := time.LoadLocation(schedule.GetTimezone()); err != nil { + return trace.Wrap(err, "invalid timezone: refer to the IANA Time Zone Database for valid options") + } + if len(schedule.GetShifts()) == 0 { return trace.BadParameter("at least one shift is required") } diff --git a/lib/services/access_monitoring_rules_test.go b/lib/services/access_monitoring_rules_test.go index 0620aea4504..06037353543 100644 --- a/lib/services/access_monitoring_rules_test.go +++ b/lib/services/access_monitoring_rules_test.go @@ -249,6 +249,98 @@ func TestValidateSchedules(t *testing.T) { require.ErrorContains(t, err, "at least one shift is require") }, }, + { + description: "valid timezone (UTC)", + schedules: map[string]*accessmonitoringrulesv1.Schedule{ + "default": { + Time: &accessmonitoringrulesv1.TimeSchedule{ + Timezone: "UTC", + Shifts: []*accessmonitoringrulesv1.TimeSchedule_Shift{ + { + Weekday: time.Monday.String(), + Start: "00:00", + End: "23:59", + }, + }, + }, + }, + }, + assertErr: require.NoError, + }, + { + description: "valid timezone (America/Los_Angeles)", + schedules: map[string]*accessmonitoringrulesv1.Schedule{ + "default": { + Time: &accessmonitoringrulesv1.TimeSchedule{ + Timezone: "America/Los_Angeles", + Shifts: []*accessmonitoringrulesv1.TimeSchedule_Shift{ + { + Weekday: time.Monday.String(), + Start: "00:00", + End: "23:59", + }, + }, + }, + }, + }, + assertErr: require.NoError, + }, + { + description: "valid timezone (Europe/Lisbon)", + schedules: map[string]*accessmonitoringrulesv1.Schedule{ + "default": { + Time: &accessmonitoringrulesv1.TimeSchedule{ + Timezone: "Europe/Lisbon", + Shifts: []*accessmonitoringrulesv1.TimeSchedule_Shift{ + { + Weekday: time.Monday.String(), + Start: "00:00", + End: "23:59", + }, + }, + }, + }, + }, + assertErr: require.NoError, + }, + { + description: "valid timezone (Asia/Singapore)", + schedules: map[string]*accessmonitoringrulesv1.Schedule{ + "default": { + Time: &accessmonitoringrulesv1.TimeSchedule{ + Timezone: "Asia/Singapore", + Shifts: []*accessmonitoringrulesv1.TimeSchedule_Shift{ + { + Weekday: time.Monday.String(), + Start: "00:00", + End: "23:59", + }, + }, + }, + }, + }, + assertErr: require.NoError, + }, + { + description: "invalid timezone", + schedules: map[string]*accessmonitoringrulesv1.Schedule{ + "default": { + Time: &accessmonitoringrulesv1.TimeSchedule{ + Timezone: "invalid", + Shifts: []*accessmonitoringrulesv1.TimeSchedule_Shift{ + { + Weekday: time.Monday.String(), + Start: "00:00", + End: "23:59", + }, + }, + }, + }, + }, + assertErr: func(t require.TestingT, err error, _ ...interface{}) { + require.ErrorContains(t, err, "invalid timezone") + }, + }, { description: "start time is not before end time", schedules: map[string]*accessmonitoringrulesv1.Schedule{