diff --git a/clients/nodejs/zts/package.json b/clients/nodejs/zts/package.json index db452ccca63..cc07595d805 100644 --- a/clients/nodejs/zts/package.json +++ b/clients/nodejs/zts/package.json @@ -32,7 +32,7 @@ "debug": "^2.6.8", "lodash.clone": "^4.5.0", "memory-cache": "^0.2.0", - "axios": "1.6.0", + "axios": "1.7.4", "winston": "^3.7.2" }, "devDependencies": { diff --git a/libs/java/server_common/pom.xml b/libs/java/server_common/pom.xml index 947691a4ac4..2895c4723e2 100644 --- a/libs/java/server_common/pom.xml +++ b/libs/java/server_common/pom.xml @@ -37,9 +37,50 @@ 2.0.2 5.1.0 0.3 + 1.40.0 + + + + io.opentelemetry + opentelemetry-bom + ${opentelemetry.version} + pom + import + + + + + + io.opentelemetry + opentelemetry-sdk-extension-autoconfigure + + + io.opentelemetry + opentelemetry-exporter-otlp + + + io.opentelemetry + opentelemetry-sdk + + + io.opentelemetry + opentelemetry-sdk-common + + + io.opentelemetry + opentelemetry-api + + + io.opentelemetry + opentelemetry-sdk-metrics + + + io.opentelemetry + opentelemetry-exporter-logging + org.slf4j slf4j-api @@ -94,7 +135,7 @@ com.fasterxml.uuid java-uuid-generator ${uuid.version} - + ${project.groupId} athenz-auth-core @@ -283,11 +324,11 @@ athenz-zms-core ${project.parent.version} - - com.yahoo.athenz - athenz-zms-java-client - ${project.parent.version} - + + com.yahoo.athenz + athenz-zms-java-client + ${project.parent.version} + org.eclipse.jetty jetty-server diff --git a/libs/java/server_common/src/main/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetric.java b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetric.java new file mode 100644 index 00000000000..e26a562f765 --- /dev/null +++ b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetric.java @@ -0,0 +1,152 @@ +/* + * Copyright The Athenz Authors + * + * 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 com.yahoo.athenz.common.metrics.impl; + +import com.yahoo.athenz.common.metrics.Metric; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; + +public class OpenTelemetryMetric implements Metric { + final Meter meter; + final Tracer tracer; + + private static final String REQUEST_DOMAIN_NAME = "requestDomainName"; + private static final String PRINCIPAL_DOMAIN_NAME = "principalDomainName"; + private static final String HTTP_METHOD_NAME = "httpMethodName"; + private static final String HTTP_STATUS = "httpStatus"; + private static final String API_NAME = "apiName"; + + public OpenTelemetryMetric(OpenTelemetry openTelemetry) { + meter = openTelemetry.getMeter("meter"); + tracer = openTelemetry.getTracer("tracer"); + } + + @Override + public void increment(String metric) { + LongCounter counter = meter.counterBuilder(metric).build(); + counter.add(1); + } + + @Override + public void increment(String metric, String requestDomainName) { + increment(metric, requestDomainName, 1); + } + + @Override + public void increment(String metric, String requestDomainName, int count) { + LongCounter counter = meter.counterBuilder(metric).build(); + Attributes attributes = Attributes.builder() + .put(REQUEST_DOMAIN_NAME, requestDomainName) + .build(); + counter.add(count, attributes); + } + + @Override + public void increment(String metric, String requestDomainName, String principalDomainName) { + increment(metric, requestDomainName, principalDomainName, 1); + } + + @Override + public void increment(String metric, String requestDomainName, String principalDomainName, String httpMethod, int httpStatus, String apiName) { + LongCounter counter = meter.counterBuilder(metric).build(); + Attributes attributes = Attributes.builder() + .put(REQUEST_DOMAIN_NAME, requestDomainName) + .put(PRINCIPAL_DOMAIN_NAME, principalDomainName) + .put(HTTP_METHOD_NAME, httpMethod) + .put(HTTP_STATUS, Integer.toString(httpStatus)) + .put(API_NAME, apiName) + .build(); + counter.add(1, attributes); + } + + @Override + public void increment(String metric, String requestDomainName, String principalDomainName, int count) { + LongCounter counter = meter.counterBuilder(metric).build(); + Attributes attributes = Attributes.builder() + .put(REQUEST_DOMAIN_NAME, requestDomainName) + .put(PRINCIPAL_DOMAIN_NAME, principalDomainName) + .build(); + counter.add(count, attributes); + } + + @Override + public Object startTiming(String metric, String requestDomainName) { + Span span = tracer.spanBuilder(metric).startSpan(); + Context context = Context.current().with(span); + return new Timer(context, System.currentTimeMillis(), span); + } + + @Override + public void stopTiming(Object timerMetric) { + //not necessary method + } + + @Override + public void stopTiming(Object timerMetric, String requestDomainName, String principalDomainName) { + stopTiming(timerMetric, requestDomainName, principalDomainName, null, -1, null); + } + + @Override + public void stopTiming(Object timerMetric, String requestDomainName, String principalDomainName, + String httpMethod, int httpStatus, String apiName) { + Timer timer = (Timer) timerMetric; + long duration = System.currentTimeMillis() - timer.start; + Span span = timer.getSpan(); + span.setAttribute("duration", duration); + span.setAttribute(REQUEST_DOMAIN_NAME, requestDomainName); + span.setAttribute(PRINCIPAL_DOMAIN_NAME, principalDomainName); + + if (httpMethod != null) { + span.setAttribute(HTTP_METHOD_NAME, httpMethod); + } + if (httpStatus != -1) { + span.setAttribute(HTTP_STATUS, Integer.toString(httpStatus)); + } + if (apiName != null) { + span.setAttribute(API_NAME, apiName); + } + span.end(); + } + + @Override + public void flush() { + //doesn't require flushing + } + + @Override + public void quit() { + //don't need to quit anything + } + + static class Timer { + private final Context context; + private final long start; + private final Span span; + public Timer(Context context, long start, Span span) { + this.context = context; + this.start = start; + this.span = span; + } + public Span getSpan() { + return span; + } + } +} diff --git a/libs/java/server_common/src/main/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetricFactory.java b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetricFactory.java new file mode 100644 index 00000000000..e9137f529bc --- /dev/null +++ b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetricFactory.java @@ -0,0 +1,42 @@ +/* + * Copyright The Athenz Authors + * + * 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 com.yahoo.athenz.common.metrics.impl; + +import com.yahoo.athenz.common.metrics.Metric; +import com.yahoo.athenz.common.metrics.MetricFactory; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; + +/* + In order to use the otlp exporters you need to configure the environment variables. + You need to set the endpoint (OTEL_EXPORTER_OTLP_ENDPOINT) which is defaulted to + "http:://localhost:4317" and the attributes (OTEL_RESOURCE_ATTRIBUTES) which is defaulted + to "service.name=my-service." AutoConfiguredOpenTelemetrySdk automatically reads the + configuration and sets up the exporter. +*/ + +public class OpenTelemetryMetricFactory implements MetricFactory { + @Override + public Metric create() { + OpenTelemetry openTelemetry = initialize(); + return new OpenTelemetryMetric(openTelemetry); + } + + public OpenTelemetry initialize() { + return AutoConfiguredOpenTelemetrySdk.initialize().getOpenTelemetrySdk(); + } +} diff --git a/libs/java/server_common/src/test/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetricFactoryTest.java b/libs/java/server_common/src/test/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetricFactoryTest.java new file mode 100644 index 00000000000..c08a3e311e8 --- /dev/null +++ b/libs/java/server_common/src/test/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetricFactoryTest.java @@ -0,0 +1,36 @@ +/* + * Copyright The Athenz Authors + * + * 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 com.yahoo.athenz.common.metrics.impl; + +import static org.testng.Assert.*; +import com.yahoo.athenz.common.metrics.Metric; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +public class OpenTelemetryMetricFactoryTest { + private OpenTelemetryMetricFactory factory; + + @BeforeMethod + public void setUp() { + factory = new OpenTelemetryMetricFactory(); + } + + @Test + public void testCreate() { + Metric metric = factory.create(); + assertNotNull(metric); + assertTrue(metric instanceof OpenTelemetryMetric); + } +} diff --git a/libs/java/server_common/src/test/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetricsTest.java b/libs/java/server_common/src/test/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetricsTest.java new file mode 100644 index 00000000000..da544a444a7 --- /dev/null +++ b/libs/java/server_common/src/test/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetricsTest.java @@ -0,0 +1,179 @@ +/* + * Copyright The Athenz Authors + * + * 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 com.yahoo.athenz.common.metrics.impl; + +import static org.mockito.Mockito.*; +import static org.testng.Assert.*; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongCounterBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.mockito.ArgumentCaptor; + +public class OpenTelemetryMetricsTest { + private Meter meter; + private Tracer tracer; + private LongCounter counter; + private Span span; + private OpenTelemetryMetric metric; + + @BeforeMethod + public void setUp() { + meter = mock(Meter.class); + tracer = mock(Tracer.class); + counter = mock(LongCounter.class); + span = mock(Span.class); + OpenTelemetry openTelemetry = mock(OpenTelemetry.class); + + LongCounterBuilder counterBuilder = mock(LongCounterBuilder.class); + when(meter.counterBuilder(anyString())).thenReturn(counterBuilder); + when(counterBuilder.build()).thenReturn(counter); + + SpanBuilder spanBuilder = mock(SpanBuilder.class); + when(tracer.spanBuilder(anyString())).thenReturn(spanBuilder); + when(spanBuilder.startSpan()).thenReturn(span); + + when(openTelemetry.getMeter("meter")).thenReturn(meter); + when(openTelemetry.getTracer("tracer")).thenReturn(tracer); + + metric = new OpenTelemetryMetric(openTelemetry); + } + + @Test + public void testIncrementMetric() { + metric.increment("testIncrement"); + verify(counter).add(1L); + } + + @Test + public void testIncrementMetricRequest() { + metric.increment("testMetric", "testRequestDomain"); + ArgumentCaptor captor = ArgumentCaptor.forClass(Attributes.class); + verify(counter).add(eq(1L), captor.capture()); + Attributes attributes = captor.getValue(); + assertEquals(attributes.get(AttributeKey.stringKey("requestDomainName")), "testRequestDomain"); + } + + @Test + public void testIncrementMetricRequestCount() { + metric.increment("testMetric", "testRequestDomain", 3); + ArgumentCaptor captor = ArgumentCaptor.forClass(Attributes.class); + verify(counter).add(eq(3L), captor.capture()); + Attributes attributes = captor.getValue(); + assertEquals(attributes.get(AttributeKey.stringKey("requestDomainName")), "testRequestDomain"); + } + + @Test + public void testIncrementMetricRequestPrincipal() { + metric.increment("testMetric", "testRequestDomain", "testPrincipalDomain"); + ArgumentCaptor captor = ArgumentCaptor.forClass(Attributes.class); + verify(counter).add(eq(1L), captor.capture()); + Attributes attributes = captor.getValue(); + assertEquals(attributes.get(AttributeKey.stringKey("requestDomainName")), "testRequestDomain"); + assertEquals(attributes.get(AttributeKey.stringKey("principalDomainName")), "testPrincipalDomain"); + } + + @Test + public void testIncrementMetricRequestPrincipalCount() { + metric.increment("testMetric", "testRequestDomain", + "testPrincipalDomain", 5); + ArgumentCaptor captor = ArgumentCaptor.forClass(Attributes.class); + verify(counter).add(eq(5L), captor.capture()); + Attributes attributes = captor.getValue(); + assertEquals(attributes.get(AttributeKey.stringKey("requestDomainName")), "testRequestDomain"); + assertEquals(attributes.get(AttributeKey.stringKey("principalDomainName")), "testPrincipalDomain"); + } + + @Test + public void testIncrementAllAttributes() { + metric.increment("testMetric", "testRequestDomain", + "testPrincipalDomain", "GET", 200, "testAPI"); + ArgumentCaptor captor = ArgumentCaptor.forClass(Attributes.class); + verify(counter).add(eq(1L), captor.capture()); + Attributes attributes = captor.getValue(); + assertEquals(attributes.get(AttributeKey.stringKey("requestDomainName")), "testRequestDomain"); + assertEquals(attributes.get(AttributeKey.stringKey("principalDomainName")), "testPrincipalDomain"); + assertEquals(attributes.get(AttributeKey.stringKey("httpMethodName")), "GET"); + assertEquals(attributes.get(AttributeKey.stringKey("httpStatus")), "200"); + assertEquals(attributes.get(AttributeKey.stringKey("apiName")), "testAPI"); + } + + @Test + public void testStartTiming() { + Object timerMetric = metric.startTiming("testMetric", "testRequestDomain"); + assertNotNull(timerMetric); + assertTrue(timerMetric instanceof OpenTelemetryMetric.Timer); + OpenTelemetryMetric.Timer timer = (OpenTelemetryMetric.Timer) timerMetric; + assertEquals(span, timer.getSpan()); + } + + @Test + public void testStopTimingTimer() { + OpenTelemetryMetric.Timer timer = new OpenTelemetryMetric.Timer(Context.current(), + System.currentTimeMillis(), span); + metric.stopTiming(timer); + verifyNoInteractions(meter, tracer, counter, span); + } + + @Test + public void testStopTimingTimerRequestPrincipal() { + OpenTelemetryMetric.Timer timer = new OpenTelemetryMetric.Timer(Context.current(), + System.currentTimeMillis(), span); + metric.stopTiming(timer, "testRequestDomain", "testPrincipalDomain"); + verify(span).setAttribute("requestDomainName", "testRequestDomain"); + verify(span).setAttribute("principalDomainName", "testPrincipalDomain"); + verify(span).setAttribute(eq("duration"), anyLong()); + verify(span).end(); + } + + @Test + public void testStopTimingAllAttributes() { + OpenTelemetryMetric.Timer timer = new OpenTelemetryMetric.Timer(Context.current(), + System.currentTimeMillis(), span); + metric.stopTiming(timer, "testRequestDomain", + "testPrincipalDomain", "GET", 200, "testAPI"); + verify(span).setAttribute("requestDomainName", "testRequestDomain"); + verify(span).setAttribute("principalDomainName", "testPrincipalDomain"); + verify(span).setAttribute("httpMethodName", "GET"); + verify(span).setAttribute("httpStatus", "200"); + verify(span).setAttribute("apiName", "testAPI"); + verify(span).setAttribute(eq("duration"), anyLong()); + verify(span).end(); + } + + + @Test + public void testFlush() { + metric.flush(); + verifyNoInteractions(meter, tracer, counter, span); + } + + @Test + public void testQuit() { + metric.quit(); + verifyNoInteractions(meter, tracer, counter, span); + } +} diff --git a/ui/src/__tests__/components/role/RoleRow.test.js b/ui/src/__tests__/components/role/RoleRow.test.js index 66b746379c5..dc8b5476912 100644 --- a/ui/src/__tests__/components/role/RoleRow.test.js +++ b/ui/src/__tests__/components/role/RoleRow.test.js @@ -14,15 +14,19 @@ * limitations under the License. */ import React from 'react'; -import RoleRow from '../../../components/role/RoleRow'; +import RoleRow, { getSmallestExpiryOrReview, isReviewRequired } from '../../../components/role/RoleRow'; +import { _ } from 'lodash'; import { colors } from '../../../components/denali/styles'; import { renderWithRedux } from '../../../tests_utils/ComponentsTestUtils'; -import { fireEvent, screen, waitFor } from '@testing-library/react'; -import { configure } from '@testing-library/dom'; -import { act } from 'react-dom/test-utils'; -import { USER_DOMAIN } from '../../../components/constants/constants'; +import { fireEvent, screen } from '@testing-library/react'; +import moment from 'moment'; + +describe('RoleRow', (object, method) => { + + afterAll(() => { + jest.clearAllMocks(); + }); -describe('RoleRow', () => { it('should render', () => { const details = { name: 'athens:role.ztssia_cert_rotate', @@ -79,4 +83,106 @@ describe('RoleRow', () => { fireEvent.mouseEnter(descriptionIcon); await screen.findByText('test description'); }); + + it('getSmallestExpiryOrReview check memberExpiryDays value is picked up', async () => { + const role = {memberExpiryDays: 5, lastReviewedDate: '2024-07-24T10:58:07.533Z'}; + let actualDays = getSmallestExpiryOrReview(role); + + expect(_.isEqual(role.memberExpiryDays, actualDays)).toBeTruthy(); + }); + + it('getSmallestExpiryOrReview check serviceExpiryDays value is picked up', async () => { + const role = {serviceExpiryDays: 9, lastReviewedDate: '2024-07-24T10:58:07.533Z'}; + let actualDays = getSmallestExpiryOrReview(role); + + expect(_.isEqual(role.serviceExpiryDays, actualDays)).toBeTruthy(); + }); + + it('getSmallestExpiryOrReview check memberReviewDays value is picked up', async () => { + const role = {memberReviewDays: 6, lastReviewedDate: '2024-07-24T10:58:07.533Z'}; + let actualDays = getSmallestExpiryOrReview(role); + + expect(_.isEqual(role.memberReviewDays, actualDays)).toBeTruthy(); + }); + + it('getSmallestExpiryOrReview check serviceReviewDays value is picked up', async () => { + const role = {serviceReviewDays: 10, lastReviewedDate: '2024-07-24T10:58:07.533Z'}; + let actualDays = getSmallestExpiryOrReview(role); + + expect(_.isEqual(role.serviceReviewDays, actualDays)).toBeTruthy(); + }); + + it('getSmallestExpiryOrReview check groupReviewDays value is picked up', async () => { + const role = {groupReviewDays: 8, lastReviewedDate: '2024-07-24T10:58:07.533Z'}; + let actualDays = getSmallestExpiryOrReview(role); + + expect(_.isEqual(role.groupReviewDays, actualDays)).toBeTruthy(); + }); + + it('getSmallestExpiryOrReview check groupExpiryDays value is picked up', async () => { + const role = {groupExpiryDays: 7, lastReviewedDate: '2024-07-24T10:58:07.533Z'}; + let actualDays = getSmallestExpiryOrReview(role); + + expect(_.isEqual(role.groupExpiryDays, actualDays)).toBeTruthy(); + }); + + it("getSmallestExpiryOrReview check smallest value picked, non 0, null doesn't produce error", async () => { + const role = { + memberExpiryDays: 0, + serviceExpiryDays: null, + memberReviewDays: 6, + serviceReviewDays: 10, + groupReviewDays: 8, + groupExpiryDays: 7, + lastReviewedDate: '2024-07-24T10:58:07.533Z' + }; + let actualDays = getSmallestExpiryOrReview(role); + + expect(_.isEqual(role.memberReviewDays, actualDays)).toBeTruthy(); + }); + + it("getSmallestExpiryOrReview when no expiry or review days assigned to the role, return 0", async () => { + const role = { + lastReviewedDate: '2024-07-24T10:58:07.533Z' + }; + let actualDays = getSmallestExpiryOrReview(role); + + expect(_.isEqual(0, actualDays)).toBeTruthy(); + }); + + it("isReviewRequired when no expiry or review days assigned to the role, return false", async () => { + const role = { + }; + let reviewRequired = isReviewRequired(role); + + expect(_.isEqual(false, reviewRequired)).toBeTruthy(); + }); + + it("isReviewRequired when lastReviewDate is more than 80% of memberExpiryDate ago - return true", async () => { + const role = { + lastReviewedDate: '2024-07-01T11:59:59.000Z', + memberExpiryDays: '10' + }; + // mock current date + jest.spyOn(moment.prototype, 'utc') + .mockReturnValue(moment('2024-07-09T12:00:00.000Z').utc()); + + let reviewRequired = isReviewRequired(role); + + expect(_.isEqual(true, reviewRequired)).toBeTruthy(); + }); + + it("isReviewRequired when lastReviewDate is less than 80% of memberExpiryDate ago - return false", async () => { + const role = { + lastReviewedDate: '2024-07-01T12:00:01.000Z', + memberExpiryDays: '10' + }; + // mock current date + // jest.spyOn(moment.prototype, 'utc') + // .mockReturnValue(moment('2024-07-09T12:00:00.000Z').utc()); + + let reviewRequired = isReviewRequired(role); + + expect(_.isEqual(false, reviewRequired)).toBeTruthy(); + }); }); diff --git a/ui/src/__tests__/components/role/__snapshots__/RoleRow.test.js.snap b/ui/src/__tests__/components/role/__snapshots__/RoleRow.test.js.snap index 732b88bbb14..6f53681b80f 100644 --- a/ui/src/__tests__/components/role/__snapshots__/RoleRow.test.js.snap +++ b/ui/src/__tests__/components/role/__snapshots__/RoleRow.test.js.snap @@ -117,9 +117,6 @@ exports[`RoleRow should render 1`] = ` viewBox="0 0 1024 1024" width="1.25em" > - - assignment-priority - diff --git a/ui/src/__tests__/components/role/__snapshots__/RoleTable.test.js.snap b/ui/src/__tests__/components/role/__snapshots__/RoleTable.test.js.snap index 98dfbc634c2..ae00fd9e4c5 100644 --- a/ui/src/__tests__/components/role/__snapshots__/RoleTable.test.js.snap +++ b/ui/src/__tests__/components/role/__snapshots__/RoleTable.test.js.snap @@ -200,9 +200,6 @@ exports[`RoleTable should render 1`] = ` viewBox="0 0 1024 1024" width="1.25em" > - - assignment-priority - @@ -396,9 +393,6 @@ exports[`RoleTable should render 1`] = ` viewBox="0 0 1024 1024" width="1.25em" > - - assignment-priority - diff --git a/ui/src/components/constants/constants.js b/ui/src/components/constants/constants.js index a12e916c032..3aff9dc7fa2 100644 --- a/ui/src/components/constants/constants.js +++ b/ui/src/components/constants/constants.js @@ -277,3 +277,5 @@ export const ENVIRONMENT_DROPDOWN_OPTIONS = [ export const MEMBER_AUTHORITY_FILTER_DISABLED = 1; export const MEMBER_AUTHORITY_SYSTEM_SUSPENDED = 2; export const MEMBER_ATHENZ_SYSTEM_DISABLED = 4; + +export const ROLE_PERCENTAGE_OF_DAYS_TILL_NEXT_REVIEW = 0.2; diff --git a/ui/src/components/role/RoleRow.js b/ui/src/components/role/RoleRow.js index ae15d7415c9..1bce9cfd758 100644 --- a/ui/src/components/role/RoleRow.js +++ b/ui/src/components/role/RoleRow.js @@ -27,6 +27,8 @@ import { css, keyframes } from '@emotion/react'; import { deleteRole } from '../../redux/thunks/roles'; import { connect } from 'react-redux'; import { selectDomainAuditEnabled } from '../../redux/selectors/domainData'; +import moment from 'moment-timezone'; +import { ROLE_PERCENTAGE_OF_DAYS_TILL_NEXT_REVIEW } from '../constants/constants'; const TDStyledName = styled.div` background-color: ${(props) => props.color}; @@ -93,6 +95,46 @@ const LeftSpan = styled.span` padding-left: 20px; `; +export function isReviewRequired(role) { + // determine last review or last modified date + const reviewData = role.lastReviewedDate ? {lastReviewedDate: role.lastReviewedDate} + : {lastReviewedDate: role.modified}; + // get smallest expiry or review days value for the role + const smallestExpiryOrReview = getSmallestExpiryOrReview(role); + + if (smallestExpiryOrReview === 0) { + // review or expiry days were not set in settings - no review required + return false; + } + + // get 20% of the smallest review period + reviewData.pct20 = Math.ceil(smallestExpiryOrReview * ROLE_PERCENTAGE_OF_DAYS_TILL_NEXT_REVIEW); + + const lastReviewedDate = moment(reviewData.lastReviewedDate, 'YYYY-MM-DDTHH:mm:ss.SSSZ'); + const now = moment().utc(); + + // check if expiry/review is coming up within 20% of the smallest review/expiry period + return now.subtract(smallestExpiryOrReview, 'days').add(reviewData.pct20, 'days').isAfter(lastReviewedDate); +} + +export function getSmallestExpiryOrReview(role){ + const values = [ + role.memberExpiryDays, + role.memberReviewDays, + role.groupExpiryDays, + role.groupReviewDays, + role.serviceExpiryDays, + role.serviceReviewDays + ].filter(obj => obj > 0); // pick only those that have days set and days > 0 + + if (values.length > 0) { + // pick the one with the smallest days value + return values.reduce((obj1, obj2) => obj1 < obj2 ? + obj1 : obj2); + } + return 0; +} + class RoleRow extends React.Component { constructor(props) { super(props); @@ -277,8 +319,7 @@ class RoleRow extends React.Component { ); - let reviewRequired = - role.reviewEnabled && (role.memberExpiryDays || role.serviceExpiry); + let reviewRequired = isReviewRequired(role); let roleTypeIcon = role.trust ? iconDelegated : ''; let roleDescriptionIcon = role.description ? iconDescription : ''; @@ -354,11 +395,12 @@ class RoleRow extends React.Component { isLink size={'1.25em'} verticalAlign={'text-bottom'} + enableTitle={false} /> } > - Review Members + {reviewRequired ? 'Role Review is required' : 'Review Members'}