diff --git a/auth/pom.xml b/auth/pom.xml
index 4804075f8c5..42c6b25c3de 100644
--- a/auth/pom.xml
+++ b/auth/pom.xml
@@ -7,10 +7,21 @@
feast-parent
${revision}
+
feast-auth
Feast Authentication and Authorization
+
+ feast.auth.generated.client
+ 1.8.4
+ 1.5.24
+ 3.14.7
+ 2.8.6
+ 3.10
+ 1.3.2
+ 4.13
+
dev.feast
@@ -32,11 +43,6 @@
spring-security-oauth2-jose
5.3.0.RELEASE
-
- sh.ory.keto
- keto-client
- 0.4.4-alpha.1
-
org.projectlombok
lombok
@@ -46,6 +52,85 @@
hibernate-validator
6.1.2.Final
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ junit
+ junit
+
+
+ io.swagger
+ swagger-annotations
+ ${swagger-core-version}
+
+
+ com.squareup.okhttp3
+ okhttp
+ ${okhttp-version}
+
+
+ com.squareup.okhttp3
+ logging-interceptor
+ ${okhttp-version}
+
+
+ com.google.code.gson
+ gson
+ ${gson-version}
+
+
+ io.gsonfire
+ gson-fire
+ ${gson-fire-version}
+
+
+
+ com.google.code.findbugs
+ jsr305
+ 3.0.2
+
-
+
+
+
+ org.openapitools
+ openapi-generator-maven-plugin
+ 4.3.1
+
+
+
+ generate
+
+
+ ${project.basedir}/src/main/resources/api.yaml
+ java
+ ${external.auth.client.package.name}
+ ${external.auth.client.package.name}.model
+ ${external.auth.client.package.name}.api
+ ${external.auth.client.package.name}.invoker
+
+ ${project.groupId}
+ ${project.artifactId}
+ ${project.version}
+ true
+ java8
+ Apache 2.0
+ https://www.apache.org/licenses/LICENSE-2.0
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+ feast.auth.generated.client.api
+
+
+
+
diff --git a/auth/src/main/java/feast/auth/authorization/AuthorizationProvider.java b/auth/src/main/java/feast/auth/authorization/AuthorizationProvider.java
index 7d4d77b7927..bf0e5797280 100644
--- a/auth/src/main/java/feast/auth/authorization/AuthorizationProvider.java
+++ b/auth/src/main/java/feast/auth/authorization/AuthorizationProvider.java
@@ -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);
}
diff --git a/auth/src/main/java/feast/auth/authorization/HttpAuthorizationProvider.java b/auth/src/main/java/feast/auth/authorization/HttpAuthorizationProvider.java
new file mode 100644
index 00000000000..d3535af6155
--- /dev/null
+++ b/auth/src/main/java/feast/auth/authorization/HttpAuthorizationProvider.java
@@ -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 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);
+ 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 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;
+ }
+}
diff --git a/auth/src/main/java/feast/auth/authorization/Keto/KetoAuthorizationProvider.java b/auth/src/main/java/feast/auth/authorization/Keto/KetoAuthorizationProvider.java
deleted file mode 100644
index 6350bc18488..00000000000
--- a/auth/src/main/java/feast/auth/authorization/Keto/KetoAuthorizationProvider.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * 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.Keto;
-
-import feast.auth.authorization.AuthorizationProvider;
-import feast.auth.authorization.AuthorizationResult;
-import java.util.List;
-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;
-import sh.ory.keto.ApiClient;
-import sh.ory.keto.ApiException;
-import sh.ory.keto.Configuration;
-import sh.ory.keto.api.EnginesApi;
-import sh.ory.keto.model.OryAccessControlPolicyRole;
-
-/** Authorization Provider implementation for Ory Keto */
-public class KetoAuthorizationProvider implements AuthorizationProvider {
-
- private final EnginesApi apiInstance;
-
- /**
- * Initializes the KetoAuthorizationProvider
- *
- * @param options String K/V pair of options to initialize the provider with. Expects at least a
- * "basePath" for the provider URL
- */
- public KetoAuthorizationProvider(Map options) {
- if (options == null) {
- throw new IllegalArgumentException("Cannot pass empty or null options to KetoAuth");
- }
- ApiClient defaultClient = Configuration.getDefaultApiClient();
- defaultClient.setBasePath(options.get("basePath"));
- this.apiInstance = new EnginesApi(defaultClient);
- }
-
- /**
- * Validates whether a user has access to the project
- *
- * @param project Name of the Feast project
- * @param authentication Spring Security Authentication object
- * @return AuthorizationResult result of authorization query
- */
- public AuthorizationResult checkAccess(String project, Authentication authentication) {
- String email = getEmailFromAuth(authentication);
- try {
- // Get all roles from Keto
- List roles =
- this.apiInstance.listOryAccessControlPolicyRoles("glob", 500L, 500L, email);
-
- // Loop through all roles the user has
- for (OryAccessControlPolicyRole role : roles) {
- // If the user has an admin or project specific role, return.
- if (("roles:admin").equals(role.getId())
- || (String.format("roles:feast:%s-member", project)).equals(role.getId())) {
- return AuthorizationResult.success();
- }
- }
- } catch (ApiException e) {
- System.err.println("Exception when calling EnginesApi#doOryAccessControlPoliciesAllow");
- System.err.println("Status code: " + e.getCode());
- System.err.println("Reason: " + e.getResponseBody());
- System.err.println("Response headers: " + e.getResponseHeaders());
- e.printStackTrace();
- }
- // Could not determine project membership, deny access.
- return AuthorizationResult.failed(
- String.format("Access denied to project %s for user %s", project, email));
- }
-
- /**
- * Get user email from their authentication object.
- *
- * @param authentication Spring Security Authentication object, used to extract user details
- * @return String user email
- */
- private String getEmailFromAuth(Authentication authentication) {
- Jwt principle = ((Jwt) authentication.getPrincipal());
- Map claims = principle.getClaims();
- String email = (String) claims.get("email");
-
- if (email.isEmpty()) {
- throw new IllegalStateException("JWT does not have a valid email set.");
- }
- boolean validEmail = (new EmailValidator()).isValid(email, null);
- if (!validEmail) {
- throw new IllegalStateException("JWT contains an invalid email address");
- }
- return email;
- }
-}
diff --git a/auth/src/main/java/feast/auth/config/SecurityConfig.java b/auth/src/main/java/feast/auth/config/SecurityConfig.java
index f5f14ccaef1..f377c76a874 100644
--- a/auth/src/main/java/feast/auth/config/SecurityConfig.java
+++ b/auth/src/main/java/feast/auth/config/SecurityConfig.java
@@ -18,9 +18,10 @@
import feast.auth.authentication.DefaultJwtAuthenticationProvider;
import feast.auth.authorization.AuthorizationProvider;
-import feast.auth.authorization.Keto.KetoAuthorizationProvider;
+import feast.auth.authorization.HttpAuthorizationProvider;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import net.devh.boot.grpc.server.security.authentication.BearerAuthenticationReader;
import net.devh.boot.grpc.server.security.authentication.GrpcAuthenticationReader;
import net.devh.boot.grpc.server.security.check.AccessPredicateVoter;
@@ -107,8 +108,9 @@ AuthorizationProvider authorizationProvider() {
if (securityProperties.getAuthentication().isEnabled()
&& securityProperties.getAuthorization().isEnabled()) {
switch (securityProperties.getAuthorization().getProvider()) {
- case "keto":
- return new KetoAuthorizationProvider(securityProperties.getAuthorization().getOptions());
+ case "http":
+ Map options = securityProperties.getAuthorization().getOptions();
+ return new HttpAuthorizationProvider(options);
default:
throw new IllegalArgumentException(
"Please configure an Authorization Provider if you have enabled authorization.");
diff --git a/auth/src/main/java/feast/auth/config/SecurityProperties.java b/auth/src/main/java/feast/auth/config/SecurityProperties.java
index 4d875caeb58..11eb2f2c13b 100644
--- a/auth/src/main/java/feast/auth/config/SecurityProperties.java
+++ b/auth/src/main/java/feast/auth/config/SecurityProperties.java
@@ -50,7 +50,7 @@ public static class AuthorizationProperties {
private boolean enabled;
// Named authorization provider to use.
- @OneOfStrings({"none", "keto"})
+ @OneOfStrings({"none", "http"})
private String provider;
// K/V options to initialize the provider with
diff --git a/auth/src/main/resources/api.yaml b/auth/src/main/resources/api.yaml
new file mode 100644
index 00000000000..8e6ba5a375b
--- /dev/null
+++ b/auth/src/main/resources/api.yaml
@@ -0,0 +1,111 @@
+openapi: 3.0.1
+info:
+ description: 'Feast Authorization Server'
+ license:
+ name: Apache 2.0
+ url: http://www.apache.org/licenses/LICENSE-2.0.html
+ title: Feast Authorization Server
+ version: 1.0.0
+servers:
+ - url: /
+paths:
+ /healthz:
+ get:
+ responses:
+ "200":
+ description: Online
+ "500":
+ description: Offline
+ /readiness:
+ get:
+ responses:
+ "200":
+ description: Ready
+ "500":
+ description: Not Ready
+ /checkAccess:
+ post:
+ operationId: check_access_post
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/checkAccessRequest'
+ description: Request containing user, resource, and action information. Used to make an authorization decision.
+ required: true
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/authorizationResult'
+ description: Authorization passed response
+ "403":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/authorizationResult'
+ description: Authorization failed response
+ "500":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/inline_response_500'
+ description: The standard error format
+ summary: Check whether request is authorized to access a specific resource
+ x-codegen-request-body-name: body
+components:
+ schemas:
+ checkAccessRequest:
+ example:
+ action: 'read'
+ context: '{}'
+ resource: 'feast:project'
+ subject: 'me@example.com'
+ properties:
+ action:
+ description: Action is the action that is being taken on the requested resource.
+ type: string
+ context:
+ description: Context is the request's environmental context.
+ properties: {}
+ type: object
+ resource:
+ description: Resource is the resource that access is requested to.
+ type: string
+ subject:
+ description: Subject is the subject that is requesting access, typically the user.
+ type: string
+ title: Input for checking if a request is allowed or not.
+ type: object
+ authorizationResult:
+ example:
+ allowed: true
+ properties:
+ allowed:
+ description: Allowed is true if the request should be allowed and false
+ otherwise.
+ type: boolean
+ required:
+ - allowed
+ title: AuthorizationResult is the result of an access control decision. It contains
+ the decision outcome.
+ type: object
+ inline_response_500:
+ properties:
+ code:
+ format: int64
+ type: integer
+ details:
+ items:
+ properties: {}
+ type: object
+ type: array
+ message:
+ type: string
+ reason:
+ type: string
+ request:
+ type: string
+ status:
+ type: string
\ No newline at end of file
diff --git a/core/src/main/java/feast/core/service/AccessManagementService.java b/core/src/main/java/feast/core/service/AccessManagementService.java
index dea284c2308..bd5eeed906b 100644
--- a/core/src/main/java/feast/core/service/AccessManagementService.java
+++ b/core/src/main/java/feast/core/service/AccessManagementService.java
@@ -112,14 +112,15 @@ public List listProjects() {
* Determine whether a user belongs to a Project
*
* @param securityContext User's Spring Security Context. Used to identify user.
- * @param project Name of the project for which membership should be tested.
+ * @param projectId Id (name) of the project for which membership should be tested.
*/
- public void checkIfProjectMember(SecurityContext securityContext, String project) {
+ public void checkIfProjectMember(SecurityContext securityContext, String projectId) {
Authentication authentication = securityContext.getAuthentication();
if (!this.securityProperties.getAuthorization().isEnabled()) {
return;
}
- AuthorizationResult result = this.authorizationProvider.checkAccess(project, authentication);
+ AuthorizationResult result =
+ this.authorizationProvider.checkAccessToProject(projectId, authentication);
if (!result.isAllowed()) {
throw new AccessDeniedException(result.getFailureReason().orElse("AccessDenied"));
}
diff --git a/core/src/main/resources/application.yml b/core/src/main/resources/application.yml
index f03be52d9f1..fbdf6036328 100644
--- a/core/src/main/resources/application.yml
+++ b/core/src/main/resources/application.yml
@@ -91,9 +91,9 @@ feast:
authorization:
enabled: false
- provider: none
+ provider: http
options:
- basePath: http://localhost:3000
+ authorizationUrl: http://localhost:8082
grpc:
server:
diff --git a/core/src/test/java/feast/core/grpc/CoreServiceAuthTest.java b/core/src/test/java/feast/core/grpc/CoreServiceAuthTest.java
index 5da7b1f566d..bd59510c673 100644
--- a/core/src/test/java/feast/core/grpc/CoreServiceAuthTest.java
+++ b/core/src/test/java/feast/core/grpc/CoreServiceAuthTest.java
@@ -96,7 +96,7 @@ void cantApplyFeatureSetIfNotProjectMember() throws InvalidProtocolBufferExcepti
doReturn(AuthorizationResult.failed(null))
.when(authProvider)
- .checkAccess(anyString(), any(Authentication.class));
+ .checkAccessToProject(anyString(), any(Authentication.class));
StreamRecorder responseObserver = StreamRecorder.create();
FeatureSetProto.FeatureSet incomingFeatureSet = newDummyFeatureSet("f2", 1, project).toProto();
@@ -121,7 +121,7 @@ void canApplyFeatureSetIfProjectMember() throws InvalidProtocolBufferException {
when(context.getAuthentication()).thenReturn(auth);
doReturn(AuthorizationResult.success())
.when(authProvider)
- .checkAccess(anyString(), any(Authentication.class));
+ .checkAccessToProject(anyString(), any(Authentication.class));
StreamRecorder responseObserver = StreamRecorder.create();
FeatureSetProto.FeatureSet incomingFeatureSet = newDummyFeatureSet("f2", 1, project).toProto();