This is an automated email from the ASF dual-hosted git repository.
avikg pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract-cn-anubis.git
The following commit(s) were added to refs/heads/develop by this push:
new ef40781 keycloak-authorization
new 9c4c2c0 Merge pull request #15 from fynmanoj/keycloak-develop
ef40781 is described below
commit ef40781a0936f62d5e94d20a6aaf4634c4e67202
Author: Manoj <[email protected]>
AuthorDate: Wed Aug 4 11:03:48 2021 +0530
keycloak-authorization
---
.../cn/anubis/api/v1/domain/AccountAccess.java | 53 ++++--
.../api/v1/domain/AccountAccessTokenContent.java | 37 ++--
library/build.gradle | 4 +
.../cn/anubis/config/AnubisImportSelector.java | 11 +-
.../config/AnubisSecurityConfigurerAdapter.java | 2 +
.../fineract/cn/anubis/config/EnableAnubis.java | 3 +-
.../FinKeycloakSecurityConfigurerAdapter.java | 159 ++++++++++++++++
.../anubis/provider/FinKeycloakRsaKeyProvider.java | 45 +++++
.../security/AccountLevelAccessVerifierCustom.java | 55 ++++++
.../cn/anubis/security/ApplicationPermission.java | 29 +++
.../FinKeycloakAuthenticationProvider.java | 202 +++++++++++++++++++++
.../security/FinKeycloakTenantAuthenticator.java | 141 ++++++++++++++
12 files changed, 701 insertions(+), 40 deletions(-)
diff --git
a/library/src/main/java/org/apache/fineract/cn/anubis/config/EnableAnubis.java
b/api/src/main/java/org/apache/fineract/cn/anubis/api/v1/domain/AccountAccess.java
similarity index 55%
copy from
library/src/main/java/org/apache/fineract/cn/anubis/config/EnableAnubis.java
copy to
api/src/main/java/org/apache/fineract/cn/anubis/api/v1/domain/AccountAccess.java
index e333306..427bdd4 100644
---
a/library/src/main/java/org/apache/fineract/cn/anubis/config/EnableAnubis.java
+++
b/api/src/main/java/org/apache/fineract/cn/anubis/api/v1/domain/AccountAccess.java
@@ -16,23 +16,38 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.cn.anubis.config;
-
-import org.springframework.context.annotation.Import;
-
-import java.lang.annotation.*;
-
-@SuppressWarnings({"unused", "WeakerAccess"})
-@Target({ElementType.TYPE})
-@Retention(RetentionPolicy.RUNTIME)
-@Inherited
-@Import({
- AnubisConfiguration.class,
- AnubisImportSelector.class,
- AnubisSecurityConfigurerAdapter.class
-})
-public @interface EnableAnubis {
- boolean provideSignatureRestController() default true;
- boolean provideSignatureStorage() default true;
- boolean generateEmptyInitializeEndpoint() default false;
+package org.apache.fineract.cn.anubis.api.v1.domain;
+
+import java.util.Set;
+
+/**
+ * @author manoj
+ */
+public class AccountAccess {
+ private String number;
+ private Set<String> access;
+
+ public AccountAccess() {
+ }
+
+ public AccountAccess(String number, Set<String> access) {
+ this.number = number;
+ this.access = access;
+ }
+
+ public String getNumber() {
+ return number;
+ }
+
+ public void setNumber(String number) {
+ this.number = number;
+ }
+
+ public Set<String> getAccess() {
+ return access;
+ }
+
+ public void setAccess(Set<String> access) {
+ this.access = access;
+ }
}
diff --git
a/library/src/main/java/org/apache/fineract/cn/anubis/config/EnableAnubis.java
b/api/src/main/java/org/apache/fineract/cn/anubis/api/v1/domain/AccountAccessTokenContent.java
similarity index 58%
copy from
library/src/main/java/org/apache/fineract/cn/anubis/config/EnableAnubis.java
copy to
api/src/main/java/org/apache/fineract/cn/anubis/api/v1/domain/AccountAccessTokenContent.java
index e333306..03de5e8 100644
---
a/library/src/main/java/org/apache/fineract/cn/anubis/config/EnableAnubis.java
+++
b/api/src/main/java/org/apache/fineract/cn/anubis/api/v1/domain/AccountAccessTokenContent.java
@@ -16,23 +16,28 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.cn.anubis.config;
+package org.apache.fineract.cn.anubis.api.v1.domain;
-import org.springframework.context.annotation.Import;
+import java.util.List;
-import java.lang.annotation.*;
+/**
+ * @author manoj
+ */
+public class AccountAccessTokenContent {
+ private List<AccountAccess> accounts;
+
+ public AccountAccessTokenContent() {
+ }
+
+ public AccountAccessTokenContent(List<AccountAccess> accounts) {
+ this.accounts = accounts;
+ }
+
+ public List<AccountAccess> getAccounts() {
+ return accounts;
+ }
-@SuppressWarnings({"unused", "WeakerAccess"})
-@Target({ElementType.TYPE})
-@Retention(RetentionPolicy.RUNTIME)
-@Inherited
-@Import({
- AnubisConfiguration.class,
- AnubisImportSelector.class,
- AnubisSecurityConfigurerAdapter.class
-})
-public @interface EnableAnubis {
- boolean provideSignatureRestController() default true;
- boolean provideSignatureStorage() default true;
- boolean generateEmptyInitializeEndpoint() default false;
+ public void setAccounts(List<AccountAccess> accounts) {
+ this.accounts = accounts;
+ }
}
diff --git a/library/build.gradle b/library/build.gradle
index 5e18c95..7ffeac5 100644
--- a/library/build.gradle
+++ b/library/build.gradle
@@ -39,6 +39,9 @@ dependencyManagement {
imports {
mavenBom 'org.springframework.cloud:spring-cloud-netflix:1.2.0.RELEASE'
}
+ imports {
+ mavenBom 'org.keycloak.bom:keycloak-adapter-bom:4.0.0.Final'
+ }
}
dependencies {
@@ -46,6 +49,7 @@ dependencies {
[group: 'org.springframework.cloud', name:
'spring-cloud-starter-feign'],
[group: 'org.springframework.cloud', name:
'spring-cloud-starter-eureka'],
[group: 'org.springframework.cloud', name:
'spring-cloud-starter-security'],
+ [group: 'org.keycloak', name: 'keycloak-spring-boot-starter',
version: '4.0.0.Final'],
[group: 'org.hibernate', name: 'hibernate-validator', version:
versions.hibernatevalidator],
[group: 'io.jsonwebtoken', name: 'jjwt', version: versions.jjwt],
[group: 'org.apache.fineract.cn', name: 'lang', version:
versions.frameworklang],
diff --git
a/library/src/main/java/org/apache/fineract/cn/anubis/config/AnubisImportSelector.java
b/library/src/main/java/org/apache/fineract/cn/anubis/config/AnubisImportSelector.java
index a214d24..260cff8 100644
---
a/library/src/main/java/org/apache/fineract/cn/anubis/config/AnubisImportSelector.java
+++
b/library/src/main/java/org/apache/fineract/cn/anubis/config/AnubisImportSelector.java
@@ -22,13 +22,11 @@ import
org.apache.fineract.cn.anubis.controller.EmptyInitializeResourcesRestCont
import org.apache.fineract.cn.anubis.controller.PermittableRestController;
import org.apache.fineract.cn.anubis.controller.SignatureCreatorRestController;
import org.apache.fineract.cn.anubis.controller.SignatureRestController;
+import org.apache.fineract.cn.anubis.provider.FinKeycloakRsaKeyProvider;
import org.apache.fineract.cn.anubis.provider.SystemRsaKeyProvider;
import org.apache.fineract.cn.anubis.provider.TenantRsaKeyProvider;
import
org.apache.fineract.cn.anubis.repository.TenantAuthorizationDataRepository;
-import org.apache.fineract.cn.anubis.security.GuestAuthenticator;
-import
org.apache.fineract.cn.anubis.security.IsisAuthenticatedAuthenticationProvider;
-import org.apache.fineract.cn.anubis.security.SystemAuthenticator;
-import org.apache.fineract.cn.anubis.security.TenantAuthenticator;
+import org.apache.fineract.cn.anubis.security.*;
import org.apache.fineract.cn.anubis.service.PermittableService;
import org.apache.fineract.cn.anubis.token.SystemAccessTokenSerializer;
import org.apache.fineract.cn.anubis.token.TenantAccessTokenSerializer;
@@ -49,6 +47,7 @@ class AnubisImportSelector implements ImportSelector {
final Set<Class> classesToImport = new HashSet<>();
classesToImport.add(TenantRsaKeyProvider.class);
classesToImport.add(SystemRsaKeyProvider.class);
+ classesToImport.add(FinKeycloakRsaKeyProvider.class);
classesToImport.add(SystemAccessTokenSerializer.class);
classesToImport.add(TenantAccessTokenSerializer.class);
@@ -62,6 +61,10 @@ class AnubisImportSelector implements ImportSelector {
classesToImport.add(PermittableRestController.class);
classesToImport.add(PermittableService.class);
+ classesToImport.add(FinKeycloakAuthenticationProvider.class);
+ classesToImport.add(FinKeycloakTenantAuthenticator.class);
+ classesToImport.add(AccountLevelAccessVerifierCustom.class);
+
final boolean provideSignatureRestController =
(boolean)importingClassMetadata
.getAnnotationAttributes(EnableAnubis.class.getTypeName())
.get("provideSignatureRestController");
diff --git
a/library/src/main/java/org/apache/fineract/cn/anubis/config/AnubisSecurityConfigurerAdapter.java
b/library/src/main/java/org/apache/fineract/cn/anubis/config/AnubisSecurityConfigurerAdapter.java
index 91da9de..3a45c3d 100644
---
a/library/src/main/java/org/apache/fineract/cn/anubis/config/AnubisSecurityConfigurerAdapter.java
+++
b/library/src/main/java/org/apache/fineract/cn/anubis/config/AnubisSecurityConfigurerAdapter.java
@@ -28,6 +28,7 @@ import org.apache.http.HttpStatus;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -55,6 +56,7 @@ import java.util.List;
@SuppressWarnings("WeakerAccess")
@Configuration
@EnableWebSecurity
+@ConditionalOnProperty("authentication.service.anubis")
public class AnubisSecurityConfigurerAdapter extends
WebSecurityConfigurerAdapter {
final private Logger logger;
final private ApplicationName applicationName;
diff --git
a/library/src/main/java/org/apache/fineract/cn/anubis/config/EnableAnubis.java
b/library/src/main/java/org/apache/fineract/cn/anubis/config/EnableAnubis.java
index e333306..a23471c 100644
---
a/library/src/main/java/org/apache/fineract/cn/anubis/config/EnableAnubis.java
+++
b/library/src/main/java/org/apache/fineract/cn/anubis/config/EnableAnubis.java
@@ -29,7 +29,8 @@ import java.lang.annotation.*;
@Import({
AnubisConfiguration.class,
AnubisImportSelector.class,
- AnubisSecurityConfigurerAdapter.class
+ AnubisSecurityConfigurerAdapter.class,
+ FinKeycloakSecurityConfigurerAdapter.class
})
public @interface EnableAnubis {
boolean provideSignatureRestController() default true;
diff --git
a/library/src/main/java/org/apache/fineract/cn/anubis/config/FinKeycloakSecurityConfigurerAdapter.java
b/library/src/main/java/org/apache/fineract/cn/anubis/config/FinKeycloakSecurityConfigurerAdapter.java
new file mode 100644
index 0000000..7860409
--- /dev/null
+++
b/library/src/main/java/org/apache/fineract/cn/anubis/config/FinKeycloakSecurityConfigurerAdapter.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.fineract.cn.anubis.config;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.fineract.cn.anubis.filter.IsisAuthenticatedProcessingFilter;
+import
org.apache.fineract.cn.anubis.security.FinKeycloakAuthenticationProvider;
+import org.apache.fineract.cn.anubis.security.UrlPermissionChecker;
+import org.apache.fineract.cn.lang.ApplicationName;
+import org.keycloak.KeycloakPrincipal;
+import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
+import org.keycloak.adapters.springsecurity.KeycloakSecurityComponents;
+import org.keycloak.adapters.springsecurity.account.KeycloakRole;
+import
org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
+import
org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
+import
org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter;
+import
org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter;
+import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
+import org.keycloak.representations.AccessToken;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.access.AccessDecisionManager;
+import org.springframework.security.access.AccessDecisionVoter;
+import org.springframework.security.access.vote.UnanimousBased;
+import
org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import
org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import
org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import
org.springframework.security.config.annotation.web.configurers.UrlAuthorizationConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
+import
org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
+import
org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
+import
org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
+
+import javax.servlet.Filter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author manoj
+ */
+@Configuration
+@EnableWebSecurity
+@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
+@ConditionalOnProperty({"authentication.service.keycloak"})
+public class FinKeycloakSecurityConfigurerAdapter extends
KeycloakWebSecurityConfigurerAdapter {
+ final private Logger logger;
+ final private ApplicationName applicationName;
+
+ public FinKeycloakSecurityConfigurerAdapter(final
@Qualifier(AnubisConstants.LOGGER_NAME) Logger logger,
+ final ApplicationName
applicationName) {
+ this.logger = logger;
+ this.applicationName = applicationName;
+ }
+
+ static class CustomKeycloakAccessToken extends AccessToken {
+ @JsonProperty("roles")
+ protected Set<String> roles;
+
+ public Set<String> getRoles() {
+ return roles;
+ }
+
+ public void setRoles(Set<String> roles) {
+ this.roles = roles;
+ }
+ }
+
+ @Override
+ protected KeycloakAuthenticationProvider keycloakAuthenticationProvider() {
+ return new KeycloakAuthenticationProvider() {
+
+ @Override
+ public Authentication authenticate(Authentication authentication) throws
AuthenticationException {
+ KeycloakAuthenticationToken token = (KeycloakAuthenticationToken)
authentication;
+ List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
+
+ for (String role :
((CustomKeycloakAccessToken)((KeycloakPrincipal<KeycloakSecurityContext>)token.getPrincipal()).getKeycloakSecurityContext().getToken()).getRoles())
{
+ grantedAuthorities.add(new KeycloakRole(role));
+ }
+
+ return new KeycloakAuthenticationToken(token.getAccount(),
token.isInteractive(), new
SimpleAuthorityMapper().mapAuthorities(grantedAuthorities));
+ }
+
+ };
+ }
+
+ @Autowired
+ public void configureGlobal(
+ final AuthenticationManagerBuilder auth,
+ @SuppressWarnings("SpringJavaAutowiringInspection") final
FinKeycloakAuthenticationProvider provider)
+ throws Exception {
+ auth.authenticationProvider(provider);
+ }
+
+ @Bean
+ @Override
+ protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
+ return new NullAuthenticatedSessionStrategy();
+ }
+ @Bean
+ public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {
+ return new KeycloakSpringBootConfigResolver();
+ }
+
+ @Bean
+ public FilterRegistrationBean
keycloakAuthenticationProcessingFilterRegistrationBean(
+ KeycloakAuthenticationProcessingFilter filter) {
+ FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
+ registrationBean.setEnabled(false);
+ return registrationBean;
+ }
+
+ @Bean
+ public FilterRegistrationBean
keycloakPreAuthActionsFilterRegistrationBean(KeycloakPreAuthActionsFilter
filter) {
+ FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
+ registrationBean.setEnabled(false);
+ return registrationBean;
+ }
+
+ private AccessDecisionManager defaultAccessDecisionManager() {
+ final List<AccessDecisionVoter<?>> voters = new ArrayList<>();
+ voters.add(new UrlPermissionChecker(logger, applicationName));return new
UnanimousBased(voters);
+ }
+
+ protected void configure(HttpSecurity http) throws Exception {
+ Filter filter = new
IsisAuthenticatedProcessingFilter(super.authenticationManager());
+
((HttpSecurity)((HttpSecurity)((HttpSecurity)((HttpSecurity)((UrlAuthorizationConfigurer.StandardInterceptUrlRegistry)((UrlAuthorizationConfigurer.AuthorizedUrl)((UrlAuthorizationConfigurer)((HttpSecurity)((HttpSecurity)http.httpBasic().disable()).csrf().disable()).apply(new
UrlAuthorizationConfigurer(this.getApplicationContext()))).getRegistry().anyRequest()).hasAuthority("maats_feather").accessDecisionManager(this.defaultAccessDecisionManager())).and()).formLogin().disable()).logout(
[...]
+ response.setStatus(404);
+ });
+ }
+
+}
\ No newline at end of file
diff --git
a/library/src/main/java/org/apache/fineract/cn/anubis/provider/FinKeycloakRsaKeyProvider.java
b/library/src/main/java/org/apache/fineract/cn/anubis/provider/FinKeycloakRsaKeyProvider.java
new file mode 100644
index 0000000..4c1d071
--- /dev/null
+++
b/library/src/main/java/org/apache/fineract/cn/anubis/provider/FinKeycloakRsaKeyProvider.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.fineract.cn.anubis.provider;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+
+/**
+ * @author manoj
+ */
+@Component
+public class FinKeycloakRsaKeyProvider {
+ @Value("${fin.keycloak.realm.publicKey}")
+ private String rsaPublicKey;
+
+ public PublicKey getPublicKey() throws InvalidKeySpecException,
NoSuchAlgorithmException {
+
+ X509EncodedKeySpec keySpec = new
X509EncodedKeySpec(Base64.getDecoder().decode(rsaPublicKey));
+ KeyFactory kf = KeyFactory.getInstance("RSA");
+ return kf.generatePublic(keySpec);
+ }
+}
diff --git
a/library/src/main/java/org/apache/fineract/cn/anubis/security/AccountLevelAccessVerifierCustom.java
b/library/src/main/java/org/apache/fineract/cn/anubis/security/AccountLevelAccessVerifierCustom.java
new file mode 100644
index 0000000..5e66118
--- /dev/null
+++
b/library/src/main/java/org/apache/fineract/cn/anubis/security/AccountLevelAccessVerifierCustom.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.fineract.cn.anubis.security;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Service;
+
+import java.util.Collection;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author manoj
+ */
+@Service
+public class AccountLevelAccessVerifierCustom {
+ private final static String OWNER = "OWNER";
+
+ @Value("${conf.enableAccountLevelAccessVerification}")
+ private String isAccountLevelAccessVerificationEnabled;
+
+ public void validate(String accountNo, String operation){
+ if(!"true".equals(isAccountLevelAccessVerificationEnabled)) return;
+ AnubisAuthentication authentication =
(AnubisAuthentication)SecurityContextHolder.getContext().getAuthentication();
+ String acctPermission = "ACCT_ACCESS_" + accountNo;
+ final Collection<? extends GrantedAuthority> authorities =
authentication.getAuthorities();
+ final Set<String> accountOperation = authorities.stream()
+ .map(x -> (ApplicationPermission) x)
+ .filter(x -> x.matches(acctPermission, "get",
authentication.getPrincipal().getForApplicationName(),
authentication.getPrincipal()))
+ .map(ApplicationPermission::getAccountOperation)
+ .collect(Collectors.toSet());
+
+ if(accountOperation.size() == 0 || !(accountOperation.contains(OWNER)
|| accountOperation.contains(operation))) {
+ throw AmitAuthenticationException.internalError("Access Denied, "
+ operation + " on " + accountNo);
+ }
+ }
+}
diff --git
a/library/src/main/java/org/apache/fineract/cn/anubis/security/ApplicationPermission.java
b/library/src/main/java/org/apache/fineract/cn/anubis/security/ApplicationPermission.java
index c24a7c5..987f3e9 100644
---
a/library/src/main/java/org/apache/fineract/cn/anubis/security/ApplicationPermission.java
+++
b/library/src/main/java/org/apache/fineract/cn/anubis/security/ApplicationPermission.java
@@ -43,6 +43,8 @@ public class ApplicationPermission implements
GrantedAuthority {
private final List<PermissionSegmentMatcher> servletPathSegmentMatchers;
private final AllowedOperation allowedOperation;
+ private final String accountOperation;
+
private final boolean acceptTokenIntendedForForeignApplication;
@@ -51,6 +53,17 @@ public class ApplicationPermission implements
GrantedAuthority {
final AllowedOperation allowedOperation,
final boolean acceptTokenIntendedForForeignApplication) {
this.allowedOperation = allowedOperation;
+ this.accountOperation = null;
+ servletPathSegmentMatchers =
PermissionSegmentMatcher.getServletPathSegmentMatchers(servletPath);
+ this.acceptTokenIntendedForForeignApplication =
acceptTokenIntendedForForeignApplication;
+ }
+
+ public ApplicationPermission(
+ final String servletPath,
+ final String accountOperation,
+ final boolean acceptTokenIntendedForForeignApplication) {
+ this.allowedOperation = AllowedOperation.READ;
+ this.accountOperation = accountOperation;
servletPathSegmentMatchers =
PermissionSegmentMatcher.getServletPathSegmentMatchers(servletPath);
this.acceptTokenIntendedForForeignApplication =
acceptTokenIntendedForForeignApplication;
}
@@ -64,6 +77,10 @@ public class ApplicationPermission implements
GrantedAuthority {
return URL_AUTHORITY;
}
+ public String getAccountOperation() {
+ return accountOperation;
+ }
+
boolean matches(final FilterInvocation filterInvocation,
final ApplicationName applicationName,
final AnubisPrincipal principal) {
@@ -82,6 +99,18 @@ public class ApplicationPermission implements
GrantedAuthority {
(matcher, segment) -> matcher.matches(segment, principal,
acceptTokenIntendedForForeignApplication, isSu));
}
+ boolean matches(final String path, String method,
+ final String applicationName,
+ final AnubisPrincipal principal) {
+ if (!acceptTokenIntendedForForeignApplication &&
!applicationName.equals(principal.getForApplicationName()))
+ return false;
+ boolean isSu = principal.getUser().equals(ApiConstants.SYSTEM_SU);
+ return matchesHelper(
+ path,
+ method,
+ (matcher, segment) -> matcher.matches(segment, principal,
acceptTokenIntendedForForeignApplication, isSu));
+ }
+
private boolean matchesHelper(final String servletPath, final String method,
@Nonnull final
BiPredicate<PermissionSegmentMatcher, String> segmentMatcher) {
final boolean opMatches = allowedOperation.containsHttpMethod(method);
diff --git
a/library/src/main/java/org/apache/fineract/cn/anubis/security/FinKeycloakAuthenticationProvider.java
b/library/src/main/java/org/apache/fineract/cn/anubis/security/FinKeycloakAuthenticationProvider.java
new file mode 100644
index 0000000..c236f0b
--- /dev/null
+++
b/library/src/main/java/org/apache/fineract/cn/anubis/security/FinKeycloakAuthenticationProvider.java
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.fineract.cn.anubis.security;
+
+import io.jsonwebtoken.*;
+import org.apache.fineract.cn.anubis.api.v1.TokenConstants;
+import org.apache.fineract.cn.anubis.provider.FinKeycloakRsaKeyProvider;
+import org.apache.fineract.cn.anubis.provider.SystemRsaKeyProvider;
+import org.apache.fineract.cn.anubis.provider.TenantRsaKeyProvider;
+import org.apache.fineract.cn.anubis.token.TokenType;
+import
org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import
org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
+import org.springframework.stereotype.Component;
+import org.springframework.util.Assert;
+
+import javax.annotation.Nonnull;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Optional;
+
+import static org.apache.fineract.cn.anubis.config.AnubisConstants.LOGGER_NAME;
+
+@Component
+public class FinKeycloakAuthenticationProvider extends
KeycloakAuthenticationProvider {
+ private final SystemRsaKeyProvider systemRsaKeyProvider;
+ private final TenantRsaKeyProvider tenantRsaKeyProvider;
+ private final SystemAuthenticator systemAuthenticator;
+ private final FinKeycloakRsaKeyProvider keycloakRsaKeyProvider;
+ private final FinKeycloakTenantAuthenticator tenantAuthenticator;
+ private final GuestAuthenticator guestAuthenticator;
+ private final Logger logger;
+
+ @Autowired
+ public FinKeycloakAuthenticationProvider(
+ final SystemRsaKeyProvider systemRsaKeyProvider,
+ final TenantRsaKeyProvider tenantRsaKeyProvider,
+ final SystemAuthenticator systemAuthenticator,
+ final FinKeycloakRsaKeyProvider keycloakRsaKeyProvider,
+ final FinKeycloakTenantAuthenticator tenantAuthenticator,
+ final GuestAuthenticator guestAuthenticator,
+ final @Qualifier(LOGGER_NAME) Logger logger) {
+ this.systemRsaKeyProvider = systemRsaKeyProvider;
+ this.tenantRsaKeyProvider = tenantRsaKeyProvider;
+ this.systemAuthenticator = systemAuthenticator;
+ this.keycloakRsaKeyProvider = keycloakRsaKeyProvider;
+ this.tenantAuthenticator = tenantAuthenticator;
+ this.guestAuthenticator = guestAuthenticator;
+ this.logger = logger;
+ }
+
+ @Override public boolean supports(final Class<?> clazz) {
+ return
PreAuthenticatedAuthenticationToken.class.isAssignableFrom(clazz);
+ }
+
+ @Override public Authentication authenticate(Authentication authentication)
+ throws AuthenticationException {
+ if
(!PreAuthenticatedAuthenticationToken.class.isAssignableFrom(authentication.getClass()))
+ {
+ throw AmitAuthenticationException.internalError(
+ "authentication called with unexpected authentication
object.");
+ }
+
+ final PreAuthenticatedAuthenticationToken preAuthentication =
(PreAuthenticatedAuthenticationToken) authentication;
+
+ final String user = (String) preAuthentication.getPrincipal();
+ Assert.hasText(user, "user cannot be empty. This should have been
assured in preauthentication");
+
+ return convert(user, (String)preAuthentication.getCredentials());
+ }
+
+ private Authentication convert(final @Nonnull String user, final String
authenticationHeader) {
+ final Optional<String> token = getJwtTokenString(authenticationHeader);
+ return (Authentication)token.map(x -> {
+
+ final TokenInfo tokenInfo = getTokenInfo(x);//new
TokenInfo(TokenType.TENANT, getKeyTimestamp());//getTokenInfo(x);//;//
+
+ switch (tokenInfo.getType()) {
+ case TENANT:
+ case SYSTEM:
+ return tenantAuthenticator.authenticate(user, x,
tokenInfo.getKeyTimestamp());
+ default:
+ logger.debug("Authentication failed for a token with a
token type other than tenant or system.");
+ throw
AmitAuthenticationException.invalidTokenIssuer(tokenInfo.getType().getIssuer());
+ }
+ }).orElseGet(() -> guestAuthenticator.authenticate(user));
+ }
+
+ private Optional<String> getJwtTokenString(final String
authenticationHeader) {
+ if ((authenticationHeader == null) || authenticationHeader.equals(
+ TokenConstants.NO_AUTHENTICATION)){
+ return Optional.empty();
+ }
+
+ if (!authenticationHeader.startsWith(TokenConstants.PREFIX)) {
+ logger.debug("Authentication failed for a token which does not
begin with the token prefix.");
+ throw AmitAuthenticationException.invalidHeader();
+ }
+ return
Optional.of(authenticationHeader.substring(TokenConstants.PREFIX.length()).trim());
+ }
+
+ @Nonnull private TokenInfo getTokenInfo(final String token)
+ {
+ try {
+ @SuppressWarnings("unchecked")
+ final Jwt<Header, Claims> jwt =
Jwts.parser().setSigningKeyResolver(new SigningKeyResolver() {
+ @Override public Key resolveSigningKey(final JwsHeader header,
final Claims claims) {
+ final TokenType tokenType =
getTokenTypeFromClaims(claims);//
TokenType.TENANT;//getTokenTypeFromClaims(claims);
+ final String keyTimestamp =
getKeyTimestampFromClaims(claims);
+
+ try {
+ switch (tokenType) {
+ case TENANT:
+ case SYSTEM:
+ return keycloakRsaKeyProvider.getPublicKey();
+ default:
+ logger.debug("Authentication failed in token
type discovery for a token with a token type other than tenant or system.");
+ throw
AmitAuthenticationException.invalidTokenIssuer(tokenType.getIssuer());
+ }
+ }
+ catch (final IllegalArgumentException e)
+ {
+ logger.debug("Authentication failed because no tenant
was provided.");
+ throw AmitAuthenticationException.missingTenant();
+ }
+ catch (final InvalidKeySpecException |
NoSuchAlgorithmException e)
+ {
+ logger.debug("Authentication failed because the
provided rsa public key.");
+ throw
AmitAuthenticationException.invalidTokenKeyTimestamp(tokenType.getIssuer(),
keyTimestamp);
+ }
+ }
+
+ @Override public Key resolveSigningKey(final JwsHeader header,
final String plaintext) {
+ return null;
+ }
+ }).parse(token);
+
+ final String alg = jwt.getHeader().get("alg").toString();
+ final SignatureAlgorithm signatureAlgorithm =
SignatureAlgorithm.forName(alg);
+ if (!signatureAlgorithm.isRsa()) {
+ logger.debug("Authentication failed because the token is
signed with an algorithm other than RSA.");
+ throw AmitAuthenticationException.invalidTokenAlgorithm(alg);
+ }
+
+ final String keyTimestamp =
getKeyTimestampFromClaims(jwt.getBody());
+ final TokenType tokenType = getTokenTypeFromClaims(jwt.getBody());
+
+ return new TokenInfo(tokenType, keyTimestamp);
+ }
+ catch (final JwtException e)
+ {
+ logger.debug("Authentication failed because token parsing
failed.");
+ throw AmitAuthenticationException.invalidToken();
+ }
+ }
+
+ private @Nonnull String getKeyTimestampFromClaims(final Claims claims) {
+ SimpleDateFormat formatter = new
SimpleDateFormat("yyyy-MM-dd'T'HH_mm_ss");
+ //Integer millis = claims.get("iat", Integer.class);
+ Date date = claims.getIssuedAt();
+ return formatter.format(date);
+ }
+
+ private @Nonnull String getKeyTimestamp() {
+ SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd'T'HH_mm_ss");
+ Date date = new Date();
+ return sdf1.format(date);
+ }
+
+ private @Nonnull TokenType getTokenTypeFromClaims(final Claims claims) {
+ final String issuer = claims.get("authType", String.class);
+ final Optional<TokenType> tokenType = TokenType.valueOfIssuer(issuer);
+ if (!tokenType.isPresent()) {
+ logger.debug("Authentication failed for a token with a missing or
invalid token type.");
+ throw AmitAuthenticationException.invalidTokenIssuer(issuer);
+ }
+ return tokenType.get();
+ }
+}
diff --git
a/library/src/main/java/org/apache/fineract/cn/anubis/security/FinKeycloakTenantAuthenticator.java
b/library/src/main/java/org/apache/fineract/cn/anubis/security/FinKeycloakTenantAuthenticator.java
new file mode 100644
index 0000000..75b3d16
--- /dev/null
+++
b/library/src/main/java/org/apache/fineract/cn/anubis/security/FinKeycloakTenantAuthenticator.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.fineract.cn.anubis.security;
+
+import com.google.gson.Gson;
+import io.jsonwebtoken.*;
+import org.apache.fineract.cn.anubis.annotation.AcceptedTokenType;
+import org.apache.fineract.cn.anubis.api.v1.TokenConstants;
+import org.apache.fineract.cn.anubis.api.v1.domain.AccountAccess;
+import org.apache.fineract.cn.anubis.api.v1.domain.AccountAccessTokenContent;
+import org.apache.fineract.cn.anubis.api.v1.domain.TokenContent;
+import org.apache.fineract.cn.anubis.api.v1.domain.TokenPermission;
+import org.apache.fineract.cn.anubis.provider.FinKeycloakRsaKeyProvider;
+import org.apache.fineract.cn.anubis.service.PermittableService;
+import org.apache.fineract.cn.lang.ApplicationName;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Nonnull;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.apache.fineract.cn.anubis.config.AnubisConstants.LOGGER_NAME;
+
+/**
+ * @author manoj
+ */
+@Component
+public class FinKeycloakTenantAuthenticator {
+ private final FinKeycloakRsaKeyProvider keycloakRsaKeyProvider;
+ private final String applicationNameWithVersion;
+ private final Gson gson;
+ private final Set<ApplicationPermission> guestPermissions;
+ private final Logger logger;
+
+ @Autowired
+ public FinKeycloakTenantAuthenticator(
+ final FinKeycloakRsaKeyProvider keycloakRsaKeyProvider,
+ final ApplicationName applicationName,
+ final PermittableService permittableService,
+ final @Qualifier("anubisGson") Gson gson,
+ final @Qualifier(LOGGER_NAME) Logger logger) {
+ this.keycloakRsaKeyProvider = keycloakRsaKeyProvider;
+ this.applicationNameWithVersion = applicationName.toString();
+ this.gson = gson;
+ this.guestPermissions
+ =
permittableService.getPermittableEndpointsAsPermissions(AcceptedTokenType.GUEST);
+ this.logger = logger;
+ }
+
+ AnubisAuthentication authenticate(
+ final @Nonnull String user,
+ final @Nonnull String token,
+ final @Nonnull String keyTimestamp) {
+ try {
+ final JwtParser parser = Jwts.parser()
+ .setSigningKey(keycloakRsaKeyProvider.getPublicKey());
+
+ @SuppressWarnings("unchecked") Jwt<Header, Claims> jwt =
parser.parse(token);
+
+ final String serializedTokenContent = jwt.getBody().get("tokenPermissions",
String.class);
+
+
+ final String sourceApplication = "Keycloak";
+ final TokenContent tokenContent = gson.fromJson(serializedTokenContent,
TokenContent.class);
+ if (tokenContent == null)
+ throw AmitAuthenticationException.missingTokenContent();
+
+ final Set<ApplicationPermission> permissions =
translatePermissions(tokenContent.getTokenPermissions());
+ permissions.addAll(guestPermissions);
+
+
+ if(jwt.getBody().get("fin") != null){
+ final String serializedAccountAccess = jwt.getBody().get("fin",
String.class);
+ final AccountAccessTokenContent accountAccess =
gson.fromJson(serializedAccountAccess, AccountAccessTokenContent.class);
+ final Set<ApplicationPermission> acctPermissions =
translateAccountPermissions(accountAccess.getAccounts());
+ permissions.addAll(acctPermissions);
+ }
+
+
+ logger.info("Tenant token for user {}, with key timestamp {} authenticated
successfully.", user, keyTimestamp);
+
+ return new AnubisAuthentication(TokenConstants.PREFIX + token,
+ jwt.getBody().get("preferred_username", String.class),
applicationNameWithVersion, sourceApplication, permissions
+ );
+ }
+ catch (final JwtException | InvalidKeySpecException |
NoSuchAlgorithmException e) {
+ logger.info("Tenant token for user {}, with key timestamp {} failed to
authenticate. Exception was {}", user, keyTimestamp, e);
+ throw AmitAuthenticationException.invalidToken();
+ }
+ }
+
+ private Set<ApplicationPermission> translatePermissions(
+ @Nonnull final List<TokenPermission> tokenPermissions)
+ {
+ return tokenPermissions.stream()
+ .filter(x -> x.getPath().startsWith(applicationNameWithVersion))
+ .flatMap(this::getAppPermissionFromTokenPermission)
+ .collect(Collectors.toSet());
+ }
+
+ private Set<ApplicationPermission> translateAccountPermissions(
+ @Nonnull final List<AccountAccess> tokenPermissions)
+ {
+ return tokenPermissions.stream()
+ .flatMap(this::getAppPermissionFromAcctPermission)
+ .collect(Collectors.toSet());
+ }
+
+ private Stream<ApplicationPermission>
getAppPermissionFromTokenPermission(final TokenPermission tokenPermission) {
+ final String servletPath =
tokenPermission.getPath().substring(applicationNameWithVersion.length());
+ return tokenPermission.getAllowedOperations().stream().map(x -> new
ApplicationPermission(servletPath, x, false));
+ }
+
+ private Stream<ApplicationPermission>
getAppPermissionFromAcctPermission(final AccountAccess tokenPermission) {
+ final String servletPath = "ACCT_ACCESS_"+ tokenPermission.getNumber();
+ return tokenPermission.getAccess().stream().map(x -> new
ApplicationPermission(servletPath, x, false));
+ }
+}