Skip to content

Commit

Permalink
Add Authentication and Authorization for feast serving (#865)
Browse files Browse the repository at this point in the history
* Authentication and authorization for feast serving, squashed on 07/21

* fix e2e, add metadata plugin in jobs, merge labels, auth failure test, removed unwanted expire time validation from gauth.

* fix rebase adaption.

* Fix core integration test.

* Authentication integration test.

* Add authorization test and minor refactoring.

* fix failing integration test.

* fix lint error.
  • Loading branch information
jmelinav authored Jul 29, 2020
1 parent f41c372 commit 4ed5cda
Show file tree
Hide file tree
Showing 46 changed files with 1,974 additions and 255 deletions.
6 changes: 6 additions & 0 deletions auth/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<javax-annotation-version>1.3.2</javax-annotation-version>
<junit-version>4.13</junit-version>
<springfox-version>2.8.0</springfox-version>
<google-auth-library-oauth2-http-version>0.20.0</google-auth-library-oauth2-http-version>
</properties>
<dependencies>
<dependency>
Expand Down Expand Up @@ -153,6 +154,11 @@
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
<version>${google-auth-library-oauth2-http-version}</version>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
6 changes: 3 additions & 3 deletions auth/src/main/java/feast/auth/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,13 @@ GrpcAuthenticationReader authenticationReader() {
}

/**
* Creates an AccessDecisionManager if authorization is enabled. This object determines the policy
* used to make authorization decisions.
* Creates an AccessDecisionManager if authentication is enabled. This object determines the
* policy used to make authentication decisions.
*
* @return AccessDecisionManager
*/
@Bean
@ConditionalOnProperty(prefix = "feast.security.authorization", name = "enabled")
@ConditionalOnProperty(prefix = "feast.security.authentication", name = "enabled")
AccessDecisionManager accessDecisionManager() {
final List<AccessDecisionVoter<?>> voters = new ArrayList<>();
voters.add(new AccessPredicateVoter());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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.credentials;

import feast.common.validators.OneOfStrings;
import java.util.Map;

public class CoreAuthenticationProperties {
// needs to be set to true if authentication is enabled on core
private boolean enabled;

// authentication provider to use
@OneOfStrings({"google", "oauth"})
private String provider;

// K/V options to initialize the provider.
Map<String, String> options;

public boolean isEnabled() {
return enabled;
}

public void setEnabled(boolean enabled) {
this.enabled = enabled;
}

public String getProvider() {
return provider;
}

public void setProvider(String provider) {
this.provider = provider;
}

public Map<String, String> getOptions() {
return options;
}

public void setOptions(Map<String, String> options) {
this.options = options;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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.credentials;

import static io.grpc.Metadata.ASCII_STRING_MARSHALLER;

import com.google.auth.oauth2.IdTokenCredentials;
import com.google.auth.oauth2.ServiceAccountCredentials;
import io.grpc.CallCredentials;
import io.grpc.Metadata;
import io.grpc.Status;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.Executor;

/*
* Google auth provider's callCredentials Implementation for serving.
* Used by CoreSpecService to connect to core.
*/
public class GoogleAuthCredentials extends CallCredentials {
private final IdTokenCredentials credentials;
private static final String BEARER_TYPE = "Bearer";
private static final Metadata.Key<String> AUTHORIZATION_METADATA_KEY =
Metadata.Key.of("Authorization", ASCII_STRING_MARSHALLER);

public GoogleAuthCredentials(Map<String, String> options) throws IOException {

String targetAudience = options.getOrDefault("audience", "https://localhost");
ServiceAccountCredentials serviceCreds =
(ServiceAccountCredentials)
ServiceAccountCredentials.getApplicationDefault()
.createScoped(Arrays.asList("openid", "email"));

credentials =
IdTokenCredentials.newBuilder()
.setIdTokenProvider(serviceCreds)
.setTargetAudience(targetAudience)
.build();
}

@Override
public void applyRequestMetadata(
RequestInfo requestInfo, Executor appExecutor, MetadataApplier applier) {
appExecutor.execute(
() -> {
try {
credentials.refreshIfExpired();
Metadata headers = new Metadata();
headers.put(
AUTHORIZATION_METADATA_KEY,
String.format("%s %s", BEARER_TYPE, credentials.getIdToken().getTokenValue()));
applier.apply(headers);
} catch (Throwable e) {
applier.fail(Status.UNAUTHENTICATED.withCause(e));
}
});
}

@Override
public void thisUsesUnstableApi() {
// TODO Auto-generated method stub

}
}
120 changes: 120 additions & 0 deletions auth/src/main/java/feast/auth/credentials/OAuthCredentials.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* 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.credentials;

import static io.grpc.Metadata.ASCII_STRING_MARSHALLER;

import com.nimbusds.jose.util.JSONObjectUtils;
import io.grpc.CallCredentials;
import io.grpc.Metadata;
import io.grpc.Status;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.Executor;
import javax.security.sasl.AuthenticationException;
import net.minidev.json.JSONObject;
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;

/*
* Oauth Credentials Implementation for serving.
* Used by CoreSpecService to connect to core.
*/
public class OAuthCredentials extends CallCredentials {

private static final String JWK_ENDPOINT_URI = "jwkEndpointURI";
static final String APPLICATION_JSON = "application/json";
static final String CONTENT_TYPE = "content-type";
static final String BEARER_TYPE = "Bearer";
static final String GRANT_TYPE = "grant_type";
static final String CLIENT_ID = "client_id";
static final String CLIENT_SECRET = "client_secret";
static final String AUDIENCE = "audience";
static final String OAUTH_URL = "oauth_url";
static final Metadata.Key<String> AUTHORIZATION_METADATA_KEY =
Metadata.Key.of("Authorization", ASCII_STRING_MARSHALLER);

private OkHttpClient httpClient;
private Request request;
private String accessToken;
private Instant tokenExpiryTime;
private NimbusJwtDecoder jwtDecoder;

public OAuthCredentials(Map<String, String> options) {
this.httpClient = new OkHttpClient();
if (!(options.containsKey(GRANT_TYPE)
&& options.containsKey(CLIENT_ID)
&& options.containsKey(AUDIENCE)
&& options.containsKey(CLIENT_SECRET)
&& options.containsKey(OAUTH_URL)
&& options.containsKey(JWK_ENDPOINT_URI))) {
throw new AssertionError(
"please configure the properties:"
+ " grant_type, client_id, client_secret, audience, oauth_url, jwkEndpointURI");
}
RequestBody requestBody =
new FormBody.Builder()
.add(GRANT_TYPE, options.get(GRANT_TYPE))
.add(CLIENT_ID, options.get(CLIENT_ID))
.add(CLIENT_SECRET, options.get(CLIENT_SECRET))
.add(AUDIENCE, options.get(AUDIENCE))
.build();
this.request =
new Request.Builder()
.url(options.get(OAUTH_URL))
.addHeader(CONTENT_TYPE, APPLICATION_JSON)
.post(requestBody)
.build();
this.jwtDecoder = NimbusJwtDecoder.withJwkSetUri(options.get(JWK_ENDPOINT_URI)).build();
}

@Override
public void thisUsesUnstableApi() {
// TODO Auto-generated method stub

}

@Override
public void applyRequestMetadata(
RequestInfo requestInfo, Executor appExecutor, MetadataApplier applier) {
appExecutor.execute(
() -> {
try {
// Fetches new token if it is not available or if token has expired.
if (this.accessToken == null || Instant.now().isAfter(this.tokenExpiryTime)) {
Response response = httpClient.newCall(request).execute();
if (!response.isSuccessful()) {
throw new AuthenticationException(response.message());
}
JSONObject json = JSONObjectUtils.parse(response.body().string());
this.accessToken = json.getAsString("access_token");
this.tokenExpiryTime = jwtDecoder.decode(this.accessToken).getExpiresAt();
}
Metadata headers = new Metadata();
headers.put(
AUTHORIZATION_METADATA_KEY, String.format("%s %s", BEARER_TYPE, this.accessToken));
applier.apply(headers);
} catch (Throwable e) {
applier.fail(Status.UNAUTHENTICATED.withCause(e));
}
});
}
}
63 changes: 63 additions & 0 deletions auth/src/main/java/feast/auth/service/AuthorizationService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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.service;

import feast.auth.authorization.AuthorizationProvider;
import feast.auth.authorization.AuthorizationResult;
import feast.auth.config.SecurityProperties;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.stereotype.Service;

@AllArgsConstructor
@Service
public class AuthorizationService {

private final SecurityProperties securityProperties;
private final AuthorizationProvider authorizationProvider;

@Autowired
public AuthorizationService(
SecurityProperties securityProperties,
ObjectProvider<AuthorizationProvider> authorizationProvider) {
this.securityProperties = securityProperties;
this.authorizationProvider = authorizationProvider.getIfAvailable();
}

/**
* Determine whether a user has access to a project.
*
* @param securityContext Spring Security Context used to identify a user or service.
* @param project Name of the project for which membership should be tested.
*/
public void authorizeRequest(SecurityContext securityContext, String project) {
Authentication authentication = securityContext.getAuthentication();
if (!this.securityProperties.getAuthorization().isEnabled()) {
return;
}

AuthorizationResult result =
this.authorizationProvider.checkAccessToProject(project, authentication);
if (!result.isAllowed()) {
throw new AccessDeniedException(result.getFailureReason().orElse("Access Denied"));
}
}
}
2 changes: 1 addition & 1 deletion core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.5.0.RELEASE</version>
<version>${spring.security.oauth2.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

@Configuration
@Slf4j
@ComponentScan("feast.auth.config")
@ComponentScan(basePackages = {"feast.auth.config", "feast.auth.service"})
public class CoreSecurityConfig {

/**
Expand Down
Loading

0 comments on commit 4ed5cda

Please sign in to comment.