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

Replace Keto Authorization with External HTTP Authorization #864

Merged
merged 11 commits into from
Jul 10, 2020
97 changes: 91 additions & 6 deletions auth/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,21 @@
<artifactId>feast-parent</artifactId>
<version>${revision}</version>
</parent>

<artifactId>feast-auth</artifactId>

<name>Feast Authentication and Authorization</name>

<properties>
<external.auth.client.package.name>feast.auth.generated.client</external.auth.client.package.name>
<gson-fire-version>1.8.4</gson-fire-version>
<swagger-core-version>1.5.24</swagger-core-version>
<okhttp-version>3.14.7</okhttp-version>
<gson-version>2.8.6</gson-version>
<commons-lang3-version>3.10</commons-lang3-version>
<javax-annotation-version>1.3.2</javax-annotation-version>
<junit-version>4.13</junit-version>
</properties>
<dependencies>
<dependency>
<groupId>dev.feast</groupId>
Expand All @@ -32,11 +43,6 @@
<artifactId>spring-security-oauth2-jose</artifactId>
<version>5.3.0.RELEASE</version>
</dependency>
<dependency>
<groupId>sh.ory.keto</groupId>
<artifactId>keto-client</artifactId>
<version>0.4.4-alpha.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
Expand All @@ -46,6 +52,85 @@
<artifactId>hibernate-validator</artifactId>
<version>6.1.2.Final</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>${swagger-core-version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp-version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>logging-interceptor</artifactId>
<version>${okhttp-version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson-version}</version>
</dependency>
<dependency>
<groupId>io.gsonfire</groupId>
<artifactId>gson-fire</artifactId>
<version>${gson-fire-version}</version>
</dependency>
<!-- @Nullable annotation -->
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>3.0.2</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>4.3.1</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/api.yaml</inputSpec>
<generatorName>java</generatorName>
<packageName>${external.auth.client.package.name}</packageName>
<modelPackage>${external.auth.client.package.name}.model</modelPackage>
<apiPackage>${external.auth.client.package.name}.api</apiPackage>
<invokerPackage>${external.auth.client.package.name}.invoker</invokerPackage>
<configOptions>
<groupId>${project.groupId}</groupId>
<artifactId>${project.artifactId}</artifactId>
<artifactVersion>${project.version}</artifactVersion>
<java8>true</java8>
<dateLibrary>java8</dateLibrary>
<licenseName>Apache 2.0</licenseName>
<licenseUrl>https://www.apache.org/licenses/LICENSE-2.0</licenseUrl>
<output>${project.build.directory}/generated-sources</output>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<excludePackageNames>feast.auth.generated.client.api</excludePackageNames>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@
public interface AuthorizationProvider {

/**
* Validates whether a user is allowed access to the project
* Validates whether a user is allowed access to a project
*
* @param project Name of the Feast project
* @param projectId Id of the Feast project
* @param authentication Spring Security Authentication object
* @return AuthorizationResult result of authorization query
*/
AuthorizationResult checkAccess(String project, Authentication authentication);
AuthorizationResult checkAccessToProject(String projectId, Authentication authentication);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* 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.authorization;

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 java.util.Map;
import org.hibernate.validator.internal.constraintvalidators.bv.EmailValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;

/**
* HTTPAuthorizationProvider uses an external HTTP service for authorizing requests. Please see
* auth/src/main/resources/api.yaml for the API specification of this external service.
*/
public class HttpAuthorizationProvider implements AuthorizationProvider {

private static final Logger log = LoggerFactory.getLogger(HttpAuthorizationProvider.class);

private final DefaultApi defaultApiClient;

/**
* 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";

/**
* Initializes the HTTPAuthorizationProvider
*
* @param options String K/V pair of options to initialize the provider with. Expects at least a
* "basePath" for the provider URL
*/
public HttpAuthorizationProvider(Map<String, String> options) {
if (options == null) {
throw new IllegalArgumentException(
"Cannot pass empty or null options to HTTPAuthorizationProvider");
}

ApiClient apiClient = new ApiClient();
apiClient.setBasePath(options.get("authorizationUrl"));
this.defaultApiClient = new DefaultApi(apiClient);
}

/**
* Validates whether a user has access to a project
*
* @param projectId Name of the Feast project
* @param authentication Spring Security Authentication object
* @return AuthorizationResult result of authorization query
*/
public AuthorizationResult checkAccessToProject(String projectId, Authentication authentication) {

CheckAccessRequest checkAccessRequest = new CheckAccessRequest();
Object context = getContext(authentication);
String subject = getSubjectFromAuth(authentication, DEFAULT_SUBJECT_CLAIM);
checkAccessRequest.setAction("ALL");
checkAccessRequest.setContext(context);
checkAccessRequest.setResource(projectId);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we probably need to change this to include the resourcetype like org.feast.project:{projectId} or something

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, I fixed it but havent pushed yet.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the do-not-merge on for protection ;)

But no worries, this at least allows your team to move ahead. I'll submit a patch tomorrow. I am going to bed now.

checkAccessRequest.setSubject(subject);

try {
// Make authorization request to external service
feast.auth.generated.client.model.AuthorizationResult authResult =
defaultApiClient.checkAccessPost(checkAccessRequest);
if (authResult == null) {
throw new RuntimeException(
String.format(
"Empty response returned for access to project %s for subject %s",
projectId, subject));
}
if (authResult.getAllowed()) {
// Successfully authenticated
return AuthorizationResult.success();
}
} catch (ApiException e) {
log.error("API exception has occurred during authorization: {}", e.getMessage(), e);
}

// Could not determine project membership, deny access.
return AuthorizationResult.failed(
String.format("Access denied to project %s for subject %s", projectId, subject));
}

/**
* Extract a context object to send as metadata to the authorization service
*
* @param authentication Spring Security Authentication object
* @return Returns a context object that will be serialized and sent as metadata to the
* authorization service
*/
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;
}
}

This file was deleted.

Loading