Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add caching to authorization #884

Merged
merged 3 commits into from
Jul 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions auth/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
<artifactId>feast-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-starter</artifactId>
Expand Down Expand Up @@ -91,6 +95,17 @@
<artifactId>jsr305</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down Expand Up @@ -131,6 +146,10 @@
<excludePackageNames>feast.auth.generated.client.api</excludePackageNames>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@
*/
package feast.auth.authorization;

import feast.auth.config.CacheConfiguration;
import feast.auth.generated.client.api.DefaultApi;
import feast.auth.generated.client.invoker.ApiClient;
import feast.auth.generated.client.invoker.ApiException;
import feast.auth.generated.client.model.CheckAccessRequest;
import feast.auth.utils.AuthUtils;
import java.util.Map;
import org.hibernate.validator.internal.constraintvalidators.bv.EmailValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;

Expand All @@ -41,7 +43,7 @@ public class HttpAuthorizationProvider implements AuthorizationProvider {
* The default subject claim is the key within the Authentication object where the user's identity
* can be found
*/
private final String DEFAULT_SUBJECT_CLAIM = "email";
private final String subjectClaim;

/**
* Initializes the HTTPAuthorizationProvider
Expand All @@ -58,26 +60,29 @@ public HttpAuthorizationProvider(Map<String, String> options) {
ApiClient apiClient = new ApiClient();
apiClient.setBasePath(options.get("authorizationUrl"));
this.defaultApiClient = new DefaultApi(apiClient);
subjectClaim = options.get("subjectClaim");
}

/**
* Validates whether a user has access to a project
* Validates whether a user has access to a project. @Cacheable is using {@link
* CacheConfiguration} settings to cache output of the method {@link AuthorizationResult} for a
* specified duration set in cache settings.
*
* @param projectId Name of the Feast project
* @param authentication Spring Security Authentication object
* @return AuthorizationResult result of authorization query
*/
@Cacheable(value = CacheConfiguration.AUTHORIZATION_CACHE, keyGenerator = "authKeyGenerator")
public AuthorizationResult checkAccessToProject(String projectId, Authentication authentication) {

CheckAccessRequest checkAccessRequest = new CheckAccessRequest();
Object context = getContext(authentication);
String subject = getSubjectFromAuth(authentication, DEFAULT_SUBJECT_CLAIM);
String subject = AuthUtils.getSubjectFromAuth(authentication, subjectClaim);
String resource = "projects:" + projectId;
checkAccessRequest.setAction("ALL");
checkAccessRequest.setContext(context);
checkAccessRequest.setResource(resource);
checkAccessRequest.setSubject(subject);

try {
Jwt credentials = ((Jwt) authentication.getCredentials());
// Make authorization request to external service
Expand Down Expand Up @@ -114,31 +119,4 @@ private Object getContext(Authentication authentication) {
// Not implemented yet, left empty
return new Object();
}

/**
* Get user email from their authentication object.
*
* @param authentication Spring Security Authentication object, used to extract user details
* @param subjectClaim Indicates the claim where the subject can be found
* @return String user email
*/
private String getSubjectFromAuth(Authentication authentication, String subjectClaim) {
Jwt principle = ((Jwt) authentication.getPrincipal());
Map<String, Object> claims = principle.getClaims();
String subjectValue = (String) claims.get(subjectClaim);

if (subjectValue.isEmpty()) {
throw new IllegalStateException(
String.format("JWT does not have a valid claim %s.", subjectClaim));
}

if (subjectClaim.equals("email")) {
boolean validEmail = (new EmailValidator()).isValid(subjectValue, null);
if (!validEmail) {
throw new IllegalStateException("JWT contains an invalid email address");
}
}

return subjectValue;
}
}
107 changes: 107 additions & 0 deletions auth/src/main/java/feast/auth/config/CacheConfiguration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright 2018-2020 The Feast 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
*
* https://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 feast.auth.config;

import com.google.common.cache.CacheBuilder;
import feast.auth.utils.AuthUtils;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.cache.interceptor.CacheResolver;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;

/** CacheConfiguration class defines Cache settings for HttpAuthorizationProvider class. */
@Configuration
@EnableCaching
@Setter
@Getter
public class CacheConfiguration implements CachingConfigurer {

private static final int CACHE_SIZE = 10000;

public static int TTL = 60;

public static final String AUTHORIZATION_CACHE = "authorization";

@Autowired SecurityProperties secutiryProps;

@Bean
public CacheManager cacheManager() {
ConcurrentMapCacheManager cacheManager =
new ConcurrentMapCacheManager(AUTHORIZATION_CACHE) {

@Override
protected Cache createConcurrentMapCache(final String name) {
return new ConcurrentMapCache(
name,
CacheBuilder.newBuilder()
.expireAfterWrite(TTL, TimeUnit.SECONDS)
.maximumSize(CACHE_SIZE)
.build()
.asMap(),
false);
}
};

return cacheManager;
}

/*
* KeyGenerator used by {@link Cacheable} for caching authorization requests.
* Key format : checkAccessToProject-<projectId>-<subjectClaim>
*/
@Bean
public KeyGenerator authKeyGenerator() {
return (Object target, Method method, Object... params) -> {
String projectId = (String) params[0];
jmelinav marked this conversation as resolved.
Show resolved Hide resolved
Authentication authentication = (Authentication) params[1];
String subject =
AuthUtils.getSubjectFromAuth(
authentication, secutiryProps.getAuthorization().getOptions().get("subjectClaim"));
return String.format("%s-%s-%s", method.getName(), projectId, subject);
};
}

@Override
public CacheResolver cacheResolver() {
// TODO Auto-generated method stub
return null;
}

@Override
public KeyGenerator keyGenerator() {
return null;
}

@Override
public CacheErrorHandler errorHandler() {
// TODO Auto-generated method stub
return null;
}
}
54 changes: 54 additions & 0 deletions auth/src/main/java/feast/auth/utils/AuthUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright 2018-2020 The Feast 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
*
* https://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 feast.auth.utils;

import java.util.Map;
import org.hibernate.validator.internal.constraintvalidators.bv.EmailValidator;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;

public class AuthUtils {

// Suppresses default constructor, ensuring non-instantiability.
private AuthUtils() {}

/**
* Get user email from their authentication object.
*
* @param authentication Spring Security Authentication object, used to extract user details
* @param subjectClaim Indicates the claim where the subject can be found
* @return String user email
*/
public static String getSubjectFromAuth(Authentication authentication, String subjectClaim) {
Jwt principle = ((Jwt) authentication.getPrincipal());
Map<String, Object> claims = principle.getClaims();
String subjectValue = (String) claims.getOrDefault(subjectClaim, "");

if (subjectValue.isEmpty()) {
throw new IllegalStateException(
String.format("JWT does not have a valid claim %s.", subjectClaim));
}

if (subjectClaim.equals("email")) {
boolean validEmail = (new EmailValidator()).isValid(subjectValue, null);
if (!validEmail) {
throw new IllegalStateException("JWT contains an invalid email address");
}
}
return subjectValue;
}
}
Loading