diff --git a/notification-core/src/main/java/org/opengroup/osdu/notification/auth/AuthorizationFilter.java b/notification-core/src/main/java/org/opengroup/osdu/notification/auth/AuthorizationFilter.java index 31ac34da9bbbfb90e1a74aaa15968721525939cd..1c8336f3c361b1eef5cd7a6e649984bb6e605c2e 100644 --- a/notification-core/src/main/java/org/opengroup/osdu/notification/auth/AuthorizationFilter.java +++ b/notification-core/src/main/java/org/opengroup/osdu/notification/auth/AuthorizationFilter.java @@ -17,10 +17,13 @@ package org.opengroup.osdu.notification.auth; import org.apache.commons.lang3.StringUtils; +import org.opengroup.osdu.core.common.cache.ICache; import org.opengroup.osdu.core.common.model.entitlements.AuthorizationResponse; +import org.opengroup.osdu.core.common.model.entitlements.Groups; import org.opengroup.osdu.core.common.model.http.AppException; import org.opengroup.osdu.core.common.model.http.DpsHeaders; import org.opengroup.osdu.core.common.provider.interfaces.IAuthorizationService; +import org.opengroup.osdu.core.common.util.Crc32c; import org.opengroup.osdu.notification.di.RequestInfoExt; import org.opengroup.osdu.notification.utils.Config; import org.opengroup.osdu.notification.provider.interfaces.IServiceAccountValidator; @@ -44,6 +47,8 @@ public class AuthorizationFilter { private RequestInfoExt requestInfoExt; @Autowired private IServiceAccountValidator validator; + @Autowired + private ICache<String, Groups> cache; public boolean hasAnyPermission(String... requiredRoles) { DpsHeaders dpsHeaders = requestInfoExt.getHeaders(); @@ -66,20 +71,45 @@ public class AuthorizationFilter { } else if (Arrays.asList(requiredRoles).contains(Config.PUBSUB)) { String jwt = dpsHeaders.getAuthorization().substring(BEARER_PREFIX.length()); if (!this.validator.isValidPublisherServiceAccount(jwt)) { - this.authorizeWithEntitlements(requiredRoles); + return this.authorizeWithCacheOrEntitlements(requiredRoles, dpsHeaders); } else { return false; } } else { - authorizeWithEntitlements(requiredRoles); + return this.authorizeWithCacheOrEntitlements(requiredRoles, dpsHeaders); } return true; } - private void authorizeWithEntitlements(String... requiredRoles) { - DpsHeaders dpsHeaders = requestInfoExt.getHeaders(); + private boolean authorizeWithCacheOrEntitlements(String[] requiredRoles, DpsHeaders dpsHeaders) { + String cacheKey = getGroupCacheKey(dpsHeaders); + Groups groups = cache.get(cacheKey); + if(groups == null) { + AuthorizationResponse authorizationResponse = this.authorizeWithEntitlements(requiredRoles, dpsHeaders); + cache.put(cacheKey, authorizationResponse.getGroups()); + return true; + } else { + return authorizeWithCache(requiredRoles, groups); + } + } + + private boolean authorizeWithCache(String[] requiredRoles, Groups groups) { + if(groups.any(requiredRoles)) { + return true; + } + return false; + } + + private String getGroupCacheKey(DpsHeaders dpsHeaders) { + String key = String.format("notification-entitlement-groups:%s:%s", dpsHeaders.getPartitionIdWithFallbackToAccountId(), + dpsHeaders.getAuthorization()); + return Crc32c.hashToBase64EncodedString(key); + } + + private AuthorizationResponse authorizeWithEntitlements(String[] requiredRoles, DpsHeaders dpsHeaders) { AuthorizationResponse authorizationResponse = authService.authorizeAny(dpsHeaders, requiredRoles); dpsHeaders.put(DpsHeaders.USER_EMAIL, authorizationResponse.getUser()); requestInfoExt.setHeaders(dpsHeaders); + return authorizationResponse; } } diff --git a/notification-core/src/main/java/org/opengroup/osdu/notification/cache/GroupVmCache.java b/notification-core/src/main/java/org/opengroup/osdu/notification/cache/GroupVmCache.java new file mode 100644 index 0000000000000000000000000000000000000000..972d2023b99506c4b3f3fe89645806b4fa9006c8 --- /dev/null +++ b/notification-core/src/main/java/org/opengroup/osdu/notification/cache/GroupVmCache.java @@ -0,0 +1,32 @@ +// Copyright 2017-2025, SLB +// +// 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 org.opengroup.osdu.notification.cache; + +import org.opengroup.osdu.core.common.cache.VmCache; +import org.opengroup.osdu.core.common.model.entitlements.Groups; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnProperty(value = "cache.provider", havingValue = "vm", matchIfMissing = true) +public class GroupVmCache { + @Bean + public VmCache<String, Groups> groupCache(@Value("${group.cache.expiration:30}") final int expiration, + @Value("${group.cache.maxSize:1000}") final int maxSize) { + return new VmCache<>(expiration, maxSize); + } +} \ No newline at end of file diff --git a/notification-core/src/test/java/org/opengroup/osdu/notification/auth/AuthorizationFilterTest.java b/notification-core/src/test/java/org/opengroup/osdu/notification/auth/AuthorizationFilterTest.java index 01f4493443aeb68e7fed11d7b205fe691464b67e..53e2cfdbbd0adc4b99ea08d597c40f3173342e23 100644 --- a/notification-core/src/test/java/org/opengroup/osdu/notification/auth/AuthorizationFilterTest.java +++ b/notification-core/src/test/java/org/opengroup/osdu/notification/auth/AuthorizationFilterTest.java @@ -21,7 +21,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.opengroup.osdu.core.common.cache.ICache; import org.opengroup.osdu.core.common.model.entitlements.AuthorizationResponse; +import org.opengroup.osdu.core.common.model.entitlements.GroupInfo; +import org.opengroup.osdu.core.common.model.entitlements.Groups; import org.opengroup.osdu.core.common.model.http.AppException; import org.opengroup.osdu.core.common.model.http.DpsHeaders; import org.opengroup.osdu.core.common.provider.interfaces.IAuthorizationService; @@ -32,13 +35,17 @@ import org.opengroup.osdu.notification.utils.Config; import org.powermock.modules.junit4.PowerMockRunner; import jakarta.servlet.http.HttpServletRequest; + +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -69,6 +76,9 @@ public class AuthorizationFilterTest { @Mock private IPubsubRequestBodyExtractor extractor; + @Mock + private ICache<String, Groups> cache; + @InjectMocks private AuthorizationFilter sut; @@ -142,4 +152,21 @@ public class AuthorizationFilterTest { this.sut.hasAnyPermission(ROLE4, ROLE2); verify(headers).put(DpsHeaders.USER_EMAIL, USER_EMAIL); } + + @Test + public void should_authenticateRequest_when_groupsAreAvailableInCache() { + Groups groups = new Groups(); + List<GroupInfo> groupInfos = new ArrayList<>(); + GroupInfo groupInfo = new GroupInfo(); + groupInfo.setEmail("groupInfoEmail"); + groupInfo.setDescription("description"); + groupInfo.setName("role1"); + groupInfos.add(groupInfo); + groups.setGroups(groupInfos); + groups.setDesId("desid"); + groups.setMemberEmail("memberEmail"); + when(this.cache.get("8Z2MjQ==")).thenReturn(groups); + assertTrue(this.sut.hasAnyPermission(ROLE1, ROLE2)); + verify(authorizationService, never()).authorizeAny(any(DpsHeaders.class), any(String[].class)); + } } diff --git a/provider/notification-azure/src/main/java/org/opengroup/osdu/notification/provider/azure/cache/GroupRedisCache.java b/provider/notification-azure/src/main/java/org/opengroup/osdu/notification/provider/azure/cache/GroupRedisCache.java new file mode 100644 index 0000000000000000000000000000000000000000..ca9e06d97bb6f0d4b6cfcdf1fff96c6c7b301f2b --- /dev/null +++ b/provider/notification-azure/src/main/java/org/opengroup/osdu/notification/provider/azure/cache/GroupRedisCache.java @@ -0,0 +1,47 @@ +// Copyright 2017-2025, SLB +// +// 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 org.opengroup.osdu.notification.provider.azure.cache; + +import org.opengroup.osdu.azure.cache.RedisAzureCache; +import org.opengroup.osdu.azure.di.RedisAzureConfiguration; +import org.opengroup.osdu.core.common.model.entitlements.Groups; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnProperty(value = "cache.provider", havingValue = "redis") +public class GroupRedisCache { + @Value("${redis.port:6380}") + private int port; + + @Value("${group.cache.expiration:30}") + public int groupRedisTtl; + + @Value("${redis.database}") + private int database; + + @Value("${redis.connection.timeout:15}") + private int timeout; + + @Value("${redis.command.timeout:5}") + private int commandTimeout; + + @Bean + public RedisAzureCache<String, Groups> groupCache() { + return new RedisAzureCache<>(String.class, Groups.class, new RedisAzureConfiguration(database, groupRedisTtl, port, timeout, commandTimeout)); + } +}