Fix max_duration when session TTL is short (#32356)

* Fix max_duration when session TTL is short

* prettier

* Fix linter issues

* The commit makes minor corrections to the comment, aligning it with the standard comment style for better code readability and to avoid potential linting errors.

The beginning of the multi-line comment has been changed from '/*' to '/**', making it consistent with the standard JavaScript documentation comment style. This ensures our linter recognizes it as a documentation comment, thus preventing any unnecessary linter warnings or errors.

* Update middleValues function in AccessRequests service

The middleValues function has been updated to include 'accessRequest.created' in addition to the 'sessionTTL' and 'maxDuration'.

* prettier

* Address code review comments
Support session TTL < max duration < 1d case

* Round access request duration to 10 minutes
This commit is contained in:
Jakub Nyckowski 2023-09-29 17:52:32 -04:00 committed by GitHub
parent d0e50809da
commit 21895cc241
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 351 additions and 35 deletions

View file

@ -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),
}));
}

View file

@ -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);
});
});

View file

@ -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;
return date;
}
export function middleValues(start: Date, end: Date): TimeDuration[] {
const now = new 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;
}