diff --git a/web/packages/teleport/src/AccessRequests/service.ts b/web/packages/teleport/src/AccessRequests/service.ts index 4d45da487ce30..97ff4e2810527 100644 --- a/web/packages/teleport/src/AccessRequests/service.ts +++ b/web/packages/teleport/src/AccessRequests/service.ts @@ -83,10 +83,12 @@ export async function getDurationOptions( return []; } - return middleValues(accessRequest.sessionTTL, accessRequest.maxDuration).map( - duration => ({ - value: duration.timestamp, - label: formatDuration(duration.duration), - }) - ); + return middleValues( + accessRequest.created, + accessRequest.sessionTTL, + accessRequest.maxDuration + ).map(duration => ({ + value: duration.timestamp, + label: formatDuration(duration.duration), + })); } diff --git a/web/packages/teleport/src/AccessRequests/utils.test.ts b/web/packages/teleport/src/AccessRequests/utils.test.ts new file mode 100644 index 0000000000000..98da520f28339 --- /dev/null +++ b/web/packages/teleport/src/AccessRequests/utils.test.ts @@ -0,0 +1,283 @@ +/* + * 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 { Duration } from 'date-fns'; + +import { + middleValues, + roundToNearestTenMinutes, +} from 'teleport/AccessRequests/utils'; + +// Generate testing response +function generateResponse( + currentDate: Date, + values: Array<{ + days: number; + hours: number; + minutes: number; + }> +) { + const defaultValues = { + years: 0, + months: 0, + minutes: 0, + seconds: 0, + }; + const result = []; + for (let i = 0; i < values.length; i++) { + const { days, hours, minutes } = values[i]; + const duration = { + ...defaultValues, + days, + hours, + minutes, + }; + let d = new Date(currentDate); + d.setDate(currentDate.getDate() + days); + d.setHours(currentDate.getHours() + hours); + d.setMinutes(currentDate.getMinutes() + minutes); + const timestamp = d.getTime(); + result.push({ + timestamp, + duration, + }); + } + return result; +} + +describe('generate middle times', () => { + const cases: { + name: string; + created: string; + sessionTTL: string; + maxDuration: string; + expected: Array<{ + days: number; + hours: number; + minutes: number; + }>; + }[] = [ + { + name: '3 days max', + created: '2021-09-01T00:00:00.000Z', + sessionTTL: '2021-09-01T01:00:00.000Z', + maxDuration: '2021-09-04T00:00:00.000Z', + expected: [ + { + days: 0, + hours: 1, + minutes: 0, + }, + { + days: 1, + hours: 0, + minutes: 0, + }, + { + days: 2, + hours: 0, + minutes: 0, + }, + { + days: 3, + hours: 0, + minutes: 0, + }, + ], + }, + { + name: '1 day max', + created: '2021-09-01T00:00:00.000Z', + sessionTTL: '2021-09-01T01:00:00.000Z', + maxDuration: '2021-09-02T00:00:00.000Z', + expected: [ + { + days: 0, + hours: 1, + minutes: 0, + }, + { + days: 1, + hours: 0, + minutes: 0, + }, + ], + }, + { + name: 'session ttl is 10 min', + created: '2021-09-01T00:00:00.000Z', + sessionTTL: '2021-09-01T00:10:00.000Z', + maxDuration: '2021-09-03T00:00:00.000Z', + expected: [ + { + days: 0, + hours: 0, + minutes: 10, + }, + { + days: 1, + hours: 0, + minutes: 0, + }, + { + days: 2, + hours: 0, + minutes: 0, + }, + ], + }, + { + name: '10 minutes min - real values', + created: '2023-09-21T20:50:52.669012121Z', + sessionTTL: '2023-09-21T21:00:52.669081473Z', + maxDuration: '2023-09-27T20:50:52.669081473Z', + expected: [ + { + days: 0, + hours: 0, + minutes: 10, + }, + { + days: 1, + hours: 0, + minutes: 0, + }, + { + days: 2, + hours: 0, + minutes: 0, + }, + { + days: 3, + hours: 0, + minutes: 0, + }, + { + days: 4, + hours: 0, + minutes: 0, + }, + { + days: 5, + hours: 0, + minutes: 0, + }, + { + days: 6, + hours: 0, + minutes: 0, + }, + ], + }, + { + name: 'only one option generated', + created: '2023-09-21T10:00:52.669012121Z', + sessionTTL: '2023-09-21T15:00:52.669081473Z', + maxDuration: '2023-09-21T15:00:52.669081473Z', + expected: [ + { + days: 0, + hours: 5, + minutes: 0, + }, + ], + }, + { + name: 'generate all options if max duration is grater than session ttl but less than 1d', + created: '2023-09-21T10:00:52.669012121Z', + sessionTTL: '2023-09-21T15:00:52.669081473Z', + maxDuration: '2023-09-21T17:00:52.669081473Z', + expected: [ + { + days: 0, + hours: 5, + minutes: 0, + }, + { + days: 0, + hours: 7, + minutes: 0, + }, + ], + }, + ]; + + test.each(cases)( + '$name', + ({ sessionTTL, maxDuration, created, expected }) => { + const result = middleValues( + new Date(created), + new Date(sessionTTL), + new Date(maxDuration) + ); + expect(result).toEqual(generateResponse(new Date(created), expected)); + } + ); +}); + +describe('round to nearest 10 minutes', () => { + const cases: { + name: string; + input: Duration; + expected: Duration; + }[] = [ + { + name: 'round up', + input: { minutes: 9, seconds: 0 }, + expected: { minutes: 10, seconds: 0 }, + }, + { + name: 'round down', + input: { minutes: 11, seconds: 0 }, + expected: { minutes: 10, seconds: 0 }, + }, + { + name: 'round to 10', + input: { minutes: 15, seconds: 0 }, + expected: { minutes: 20, seconds: 0 }, + }, + { + name: 'do not round to 0', + input: { minutes: 1, seconds: 0 }, + expected: { minutes: 10, seconds: 0 }, + }, + { + name: 'round minutes to 0 when days or hours are present', + input: { hours: 3, minutes: 1, seconds: 0 }, + expected: { hours: 3, minutes: 0, seconds: 0 }, + }, + { + name: 'do not round to 0', + input: { minutes: 0, seconds: 0 }, + expected: { minutes: 10, seconds: 0 }, + }, + { + name: 'seconds are removed', + input: { minutes: 9, seconds: 10 }, + expected: { minutes: 10, seconds: 0 }, + }, + { + name: "duration doesn't change when days are present", + input: { days: 1, minutes: 9, seconds: 10 }, + expected: { days: 1, minutes: 10, seconds: 0 }, + }, + ]; + + test.each(cases)('$name', ({ input, expected }) => { + const result = roundToNearestTenMinutes(input); + expect(result).toEqual(expected); + }); +}); diff --git a/web/packages/teleport/src/AccessRequests/utils.ts b/web/packages/teleport/src/AccessRequests/utils.ts index 34a77edec0de5..81d67f8e33216 100644 --- a/web/packages/teleport/src/AccessRequests/utils.ts +++ b/web/packages/teleport/src/AccessRequests/utils.ts @@ -20,57 +20,88 @@ import { Duration, intervalToDuration, isAfter, + isBefore, } from 'date-fns'; -interface TimeDuration { +type TimeDuration = { timestamp: number; duration: Duration; -} +}; + +// Round the duration to the nearest 10 minutes +// Example: +// 9m -> 10m +// 10m -> 10m +// 11m -> 10m +// 15m -> 20m +// 1d -> 1d +// 1d 1h -> 1d 1h +// The only exception is 0m, which is rounded to 10m +export function roundToNearestTenMinutes(date: Duration): Duration { + let minutes = date.minutes; + let roundedMinutes = Math.round(minutes / 10) * 10; // Round to the nearest 10 + if (roundedMinutes === 0 && !date.days && !date.hours) { + // Do not round down to 0. This + roundedMinutes = 10; + } + date.minutes = roundedMinutes; + date.seconds = 0; -export function middleValues(start: Date, end: Date): TimeDuration[] { - const now = new Date(); + return date; +} - const roundDuration = (d: Date) => - roundToNearestHour( +// Generate a list of middle values between start and end. The first value is the +// session TTL that is rounded to the nearest hour. The rest of the values are +// rounded to the nearest day. Example: +// +// created: 2021-09-01T00:00:00.000Z +// start: 2021-09-01T01:00:00.000Z +// end: 2021-09-03T00:00:00.000Z +// now: 2021-09-01T00:00:00.000Z +// +// returns: [1h, 1d, 2d, 3d] +export function middleValues( + created: Date, + start: Date, + end: Date +): TimeDuration[] { + const getInterval = (d: Date) => + roundToNearestTenMinutes( intervalToDuration({ - start: now, + start: created, end: d, }) ); const points: Date[] = [start]; - if (isAfter(addDays(start, 1), end)) { + if (isAfter(addDays(created, 1), end)) { + // Add all possible options to the list. This covers the case when the + // max duration is less than 24 hours. + if (isBefore(addHours(points[points.length - 1], 1), end)) { + points.push(end); + } + return points.map(d => ({ timestamp: d.getTime(), - duration: roundDuration(d), + duration: getInterval(d), })); } - points.push(addDays(now, 1)); + points.push(addDays(created, 1)); - while (points[points.length - 1] <= end) { - points.push(addHours(points[points.length - 1], 24)); + // I also prefer while(true), but our linter doesn't + for (;;) { + const next = addHours(points[points.length - 1], 24); + // Allow next == end + if (next > end) { + break; + } + points.push(next); } return points.map(d => ({ timestamp: d.getTime(), - duration: roundDuration(d), + duration: getInterval(d), })); } - -export function roundToNearestHour(duration: Duration): Duration { - if (duration.minutes > 30) { - duration.hours += 1; - } - - if (duration.hours >= 24) { - duration.days += 1; - duration.hours -= 24; - } - - duration.minutes = 0; - duration.seconds = 0; - - return duration; -}