diff --git a/app/account.go b/app/account.go index 82d797af..418c7977 100644 --- a/app/account.go +++ b/app/account.go @@ -7,7 +7,9 @@ import ( "sort" "strconv" "strings" + "time" + "github.com/gogo/protobuf/types" "github.com/temporalio/tcld/protogen/api/account/v1" "github.com/temporalio/tcld/protogen/api/accountservice/v1" cloudaccount "github.com/temporalio/tcld/protogen/api/cloud/account/v1" @@ -25,6 +27,8 @@ const ( kinesisAuditLogSinkType = "kinesis" pubsubAuditLogSinkType = "pubsub" sinkServiceAccountEmailFlagName = "service-account-email" + startTimeInclusiveFlagName = "start-time" + endTimeExclusiveFlagName = "end-time" ) var ( @@ -86,6 +90,19 @@ var ( Usage: "The topic name to write to the sink", Aliases: []string{"tn"}, } + + startTimeInclusiveTimeFlag = &cli.TimestampFlag{ + Name: startTimeInclusiveFlagName, + Usage: "The start time (inclusive) in RFC3339 format (e.g., '2006-01-02T15:04:05Z' or '2006-01-02T15:04:05+07:00')", + Aliases: []string{"st"}, + Layout: time.RFC3339, + } + endTimeExclusiveTimeFlag = &cli.TimestampFlag{ + Name: endTimeExclusiveFlagName, + Usage: "The end time (exclusive) in RFC3339 format (e.g., '2006-01-02T15:04:05Z' or '2006-01-02T15:04:05+07:00')", + Aliases: []string{"et"}, + Layout: time.RFC3339, + } ) type AccountClient struct { @@ -867,6 +884,51 @@ func NewAccountCommand(getAccountClientFn GetAccountClientFn) (CommandOut, error auditLogCommands.Subcommands = []*cli.Command{ kinesisAuditLogCommands, pubsubAuditLogCommands, + { + Name: "list", + Usage: "List audit logs", + Aliases: []string{"l"}, + Flags: []cli.Flag{ + startTimeInclusiveTimeFlag, + endTimeExclusiveTimeFlag, + pageSizeFlag, + pageTokenFlag, + }, + Action: func(ctx *cli.Context) error { + req := &cloudservice.GetAuditLogsRequest{ + PageSize: int32(ctx.Int(pageSizeFlag.Name)), + PageToken: ctx.String(pageTokenFlag.Name), + } + + if ctx.IsSet(startTimeInclusiveTimeFlag.Name) { + startTime := ctx.Timestamp(startTimeInclusiveTimeFlag.Name) + if startTime != nil && !startTime.IsZero() { + startTimeProto, err := types.TimestampProto(*startTime) + if err != nil { + return fmt.Errorf("invalid start time: %w", err) + } + req.StartTimeInclusive = startTimeProto + } + } + + if ctx.IsSet(endTimeExclusiveTimeFlag.Name) { + endTime := ctx.Timestamp(endTimeExclusiveTimeFlag.Name) + if endTime != nil && !endTime.IsZero() { + endTimeProto, err := types.TimestampProto(*endTime) + if err != nil { + return fmt.Errorf("invalid end time: %w", err) + } + req.EndTimeExclusive = endTimeProto + } + } + + resp, err := c.cloudAPIClient.GetAuditLogs(c.ctx, req) + if err != nil { + return err + } + return PrintProto(resp) + }, + }, } commandOut.Command.Subcommands = append(commandOut.Command.Subcommands, auditLogCommands) return commandOut, nil diff --git a/app/account_test.go b/app/account_test.go index 5a056898..685dc6ec 100644 --- a/app/account_test.go +++ b/app/account_test.go @@ -9,7 +9,9 @@ import ( "os" "strings" "testing" + "time" + "github.com/gogo/protobuf/types" "github.com/golang/mock/gomock" "github.com/stretchr/testify/suite" "github.com/temporalio/tcld/protogen/api/account/v1" @@ -1200,3 +1202,145 @@ func (s *AccountTestSuite) TestGetAuditLogSink() { }) } } + +func (s *AccountTestSuite) TestListAuditLogs() { + startTime := time.Date(2024, 1, 13, 0, 0, 0, 0, time.UTC) + endTime := time.Date(2024, 1, 14, 0, 0, 0, 0, time.UTC) + startTimeProto, _ := types.TimestampProto(startTime) + endTimeProto, _ := types.TimestampProto(endTime) + + tests := []struct { + name string + args []string + expectErr bool + expectRequest func() *cloudservice.GetAuditLogsRequest + queryError error + }{ + { + name: "query with both start and end time succeeds", + args: []string{"a", "al", "list", "--start-time", "2024-01-13T00:00:00Z", "--end-time", "2024-01-14T00:00:00Z"}, + expectErr: false, + expectRequest: func() *cloudservice.GetAuditLogsRequest { + return &cloudservice.GetAuditLogsRequest{ + StartTimeInclusive: startTimeProto, + EndTimeExclusive: endTimeProto, + PageSize: DefaultPageSize, + PageToken: "", + } + }, + }, + { + name: "query with aliases st and et succeeds", + args: []string{"a", "al", "list", "--st", "2024-01-13T00:00:00Z", "--et", "2024-01-14T00:00:00Z"}, + expectErr: false, + expectRequest: func() *cloudservice.GetAuditLogsRequest { + return &cloudservice.GetAuditLogsRequest{ + StartTimeInclusive: startTimeProto, + EndTimeExclusive: endTimeProto, + PageSize: DefaultPageSize, + PageToken: "", + } + }, + }, + { + name: "query with only start time succeeds", + args: []string{"a", "al", "list", "--st", "2024-01-13T00:00:00Z"}, + expectErr: false, + expectRequest: func() *cloudservice.GetAuditLogsRequest { + return &cloudservice.GetAuditLogsRequest{ + StartTimeInclusive: startTimeProto, + PageSize: DefaultPageSize, + PageToken: "", + } + }, + }, + { + name: "query with only end time succeeds", + args: []string{"a", "al", "list", "--et", "2024-01-14T00:00:00Z"}, + expectErr: false, + expectRequest: func() *cloudservice.GetAuditLogsRequest { + return &cloudservice.GetAuditLogsRequest{ + EndTimeExclusive: endTimeProto, + PageSize: DefaultPageSize, + PageToken: "", + } + }, + }, + { + name: "query with page size and token succeeds", + args: []string{"a", "al", "list", "--st", "2024-01-13T00:00:00Z", "--page-size", "50", "--page-token", "token123"}, + expectErr: false, + expectRequest: func() *cloudservice.GetAuditLogsRequest { + return &cloudservice.GetAuditLogsRequest{ + StartTimeInclusive: startTimeProto, + PageSize: 50, + PageToken: "token123", + } + }, + }, + { + name: "query without time filters succeeds", + args: []string{"a", "al", "list"}, + expectErr: false, + expectRequest: func() *cloudservice.GetAuditLogsRequest { + return &cloudservice.GetAuditLogsRequest{ + PageSize: DefaultPageSize, + PageToken: "", + } + }, + }, + { + name: "query with invalid timestamp format fails", + args: []string{"a", "al", "list", "--st", "invalid-date"}, + expectErr: true, + }, + { + name: "query with API error fails", + args: []string{"a", "al", "list", "--st", "2024-01-13T00:00:00Z"}, + expectErr: true, + expectRequest: func() *cloudservice.GetAuditLogsRequest { + return &cloudservice.GetAuditLogsRequest{ + StartTimeInclusive: startTimeProto, + PageSize: DefaultPageSize, + PageToken: "", + } + }, + queryError: fmt.Errorf("API error"), + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + if tc.expectRequest != nil { + expectedReq := tc.expectRequest() + s.mockCloudApiClient.EXPECT(). + GetAuditLogs(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, req *cloudservice.GetAuditLogsRequest, opts ...interface{}) (*cloudservice.GetAuditLogsResponse, error) { + s.Equal(expectedReq.PageSize, req.PageSize) + s.Equal(expectedReq.PageToken, req.PageToken) + if expectedReq.StartTimeInclusive != nil { + s.NotNil(req.StartTimeInclusive) + s.Equal(expectedReq.StartTimeInclusive.Seconds, req.StartTimeInclusive.Seconds) + } else { + s.Nil(req.StartTimeInclusive) + } + if expectedReq.EndTimeExclusive != nil { + s.NotNil(req.EndTimeExclusive) + s.Equal(expectedReq.EndTimeExclusive.Seconds, req.EndTimeExclusive.Seconds) + } else { + s.Nil(req.EndTimeExclusive) + } + return &cloudservice.GetAuditLogsResponse{}, tc.queryError + }). + Times(1) + } + + err := s.RunCmd(tc.args...) + if tc.expectErr { + s.Error(err) + } else { + s.NoError(err) + } + }) + } +}