Skip to content

Commit 4ed5cda

Browse files
authored
Add Authentication and Authorization for feast serving (#865)
* 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.
1 parent f41c372 commit 4ed5cda

46 files changed

Lines changed: 1974 additions & 255 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

auth/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<javax-annotation-version>1.3.2</javax-annotation-version>
2424
<junit-version>4.13</junit-version>
2525
<springfox-version>2.8.0</springfox-version>
26+
<google-auth-library-oauth2-http-version>0.20.0</google-auth-library-oauth2-http-version>
2627
</properties>
2728
<dependencies>
2829
<dependency>
@@ -153,6 +154,11 @@
153154
<artifactId>junit</artifactId>
154155
<version>4.12</version>
155156
</dependency>
157+
<dependency>
158+
<groupId>com.google.auth</groupId>
159+
<artifactId>google-auth-library-oauth2-http</artifactId>
160+
<version>${google-auth-library-oauth2-http-version}</version>
161+
</dependency>
156162
</dependencies>
157163
<build>
158164
<plugins>

auth/src/main/java/feast/auth/config/SecurityConfig.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,13 @@ GrpcAuthenticationReader authenticationReader() {
8383
}
8484

8585
/**
86-
* Creates an AccessDecisionManager if authorization is enabled. This object determines the policy
87-
* used to make authorization decisions.
86+
* Creates an AccessDecisionManager if authentication is enabled. This object determines the
87+
* policy used to make authentication decisions.
8888
*
8989
* @return AccessDecisionManager
9090
*/
9191
@Bean
92-
@ConditionalOnProperty(prefix = "feast.security.authorization", name = "enabled")
92+
@ConditionalOnProperty(prefix = "feast.security.authentication", name = "enabled")
9393
AccessDecisionManager accessDecisionManager() {
9494
final List<AccessDecisionVoter<?>> voters = new ArrayList<>();
9595
voters.add(new AccessPredicateVoter());
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright 2018-2020 The Feast Authors
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package feast.auth.credentials;
18+
19+
import feast.common.validators.OneOfStrings;
20+
import java.util.Map;
21+
22+
public class CoreAuthenticationProperties {
23+
// needs to be set to true if authentication is enabled on core
24+
private boolean enabled;
25+
26+
// authentication provider to use
27+
@OneOfStrings({"google", "oauth"})
28+
private String provider;
29+
30+
// K/V options to initialize the provider.
31+
Map<String, String> options;
32+
33+
public boolean isEnabled() {
34+
return enabled;
35+
}
36+
37+
public void setEnabled(boolean enabled) {
38+
this.enabled = enabled;
39+
}
40+
41+
public String getProvider() {
42+
return provider;
43+
}
44+
45+
public void setProvider(String provider) {
46+
this.provider = provider;
47+
}
48+
49+
public Map<String, String> getOptions() {
50+
return options;
51+
}
52+
53+
public void setOptions(Map<String, String> options) {
54+
this.options = options;
55+
}
56+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright 2018-2020 The Feast Authors
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package feast.auth.credentials;
18+
19+
import static io.grpc.Metadata.ASCII_STRING_MARSHALLER;
20+
21+
import com.google.auth.oauth2.IdTokenCredentials;
22+
import com.google.auth.oauth2.ServiceAccountCredentials;
23+
import io.grpc.CallCredentials;
24+
import io.grpc.Metadata;
25+
import io.grpc.Status;
26+
import java.io.IOException;
27+
import java.util.Arrays;
28+
import java.util.Map;
29+
import java.util.concurrent.Executor;
30+
31+
/*
32+
* Google auth provider's callCredentials Implementation for serving.
33+
* Used by CoreSpecService to connect to core.
34+
*/
35+
public class GoogleAuthCredentials extends CallCredentials {
36+
private final IdTokenCredentials credentials;
37+
private static final String BEARER_TYPE = "Bearer";
38+
private static final Metadata.Key<String> AUTHORIZATION_METADATA_KEY =
39+
Metadata.Key.of("Authorization", ASCII_STRING_MARSHALLER);
40+
41+
public GoogleAuthCredentials(Map<String, String> options) throws IOException {
42+
43+
String targetAudience = options.getOrDefault("audience", "https://localhost");
44+
ServiceAccountCredentials serviceCreds =
45+
(ServiceAccountCredentials)
46+
ServiceAccountCredentials.getApplicationDefault()
47+
.createScoped(Arrays.asList("openid", "email"));
48+
49+
credentials =
50+
IdTokenCredentials.newBuilder()
51+
.setIdTokenProvider(serviceCreds)
52+
.setTargetAudience(targetAudience)
53+
.build();
54+
}
55+
56+
@Override
57+
public void applyRequestMetadata(
58+
RequestInfo requestInfo, Executor appExecutor, MetadataApplier applier) {
59+
appExecutor.execute(
60+
() -> {
61+
try {
62+
credentials.refreshIfExpired();
63+
Metadata headers = new Metadata();
64+
headers.put(
65+
AUTHORIZATION_METADATA_KEY,
66+
String.format("%s %s", BEARER_TYPE, credentials.getIdToken().getTokenValue()));
67+
applier.apply(headers);
68+
} catch (Throwable e) {
69+
applier.fail(Status.UNAUTHENTICATED.withCause(e));
70+
}
71+
});
72+
}
73+
74+
@Override
75+
public void thisUsesUnstableApi() {
76+
// TODO Auto-generated method stub
77+
78+
}
79+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright 2018-2020 The Feast Authors
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package feast.auth.credentials;
18+
19+
import static io.grpc.Metadata.ASCII_STRING_MARSHALLER;
20+
21+
import com.nimbusds.jose.util.JSONObjectUtils;
22+
import io.grpc.CallCredentials;
23+
import io.grpc.Metadata;
24+
import io.grpc.Status;
25+
import java.time.Instant;
26+
import java.util.Map;
27+
import java.util.concurrent.Executor;
28+
import javax.security.sasl.AuthenticationException;
29+
import net.minidev.json.JSONObject;
30+
import okhttp3.FormBody;
31+
import okhttp3.OkHttpClient;
32+
import okhttp3.Request;
33+
import okhttp3.RequestBody;
34+
import okhttp3.Response;
35+
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
36+
37+
/*
38+
* Oauth Credentials Implementation for serving.
39+
* Used by CoreSpecService to connect to core.
40+
*/
41+
public class OAuthCredentials extends CallCredentials {
42+
43+
private static final String JWK_ENDPOINT_URI = "jwkEndpointURI";
44+
static final String APPLICATION_JSON = "application/json";
45+
static final String CONTENT_TYPE = "content-type";
46+
static final String BEARER_TYPE = "Bearer";
47+
static final String GRANT_TYPE = "grant_type";
48+
static final String CLIENT_ID = "client_id";
49+
static final String CLIENT_SECRET = "client_secret";
50+
static final String AUDIENCE = "audience";
51+
static final String OAUTH_URL = "oauth_url";
52+
static final Metadata.Key<String> AUTHORIZATION_METADATA_KEY =
53+
Metadata.Key.of("Authorization", ASCII_STRING_MARSHALLER);
54+
55+
private OkHttpClient httpClient;
56+
private Request request;
57+
private String accessToken;
58+
private Instant tokenExpiryTime;
59+
private NimbusJwtDecoder jwtDecoder;
60+
61+
public OAuthCredentials(Map<String, String> options) {
62+
this.httpClient = new OkHttpClient();
63+
if (!(options.containsKey(GRANT_TYPE)
64+
&& options.containsKey(CLIENT_ID)
65+
&& options.containsKey(AUDIENCE)
66+
&& options.containsKey(CLIENT_SECRET)
67+
&& options.containsKey(OAUTH_URL)
68+
&& options.containsKey(JWK_ENDPOINT_URI))) {
69+
throw new AssertionError(
70+
"please configure the properties:"
71+
+ " grant_type, client_id, client_secret, audience, oauth_url, jwkEndpointURI");
72+
}
73+
RequestBody requestBody =
74+
new FormBody.Builder()
75+
.add(GRANT_TYPE, options.get(GRANT_TYPE))
76+
.add(CLIENT_ID, options.get(CLIENT_ID))
77+
.add(CLIENT_SECRET, options.get(CLIENT_SECRET))
78+
.add(AUDIENCE, options.get(AUDIENCE))
79+
.build();
80+
this.request =
81+
new Request.Builder()
82+
.url(options.get(OAUTH_URL))
83+
.addHeader(CONTENT_TYPE, APPLICATION_JSON)
84+
.post(requestBody)
85+
.build();
86+
this.jwtDecoder = NimbusJwtDecoder.withJwkSetUri(options.get(JWK_ENDPOINT_URI)).build();
87+
}
88+
89+
@Override
90+
public void thisUsesUnstableApi() {
91+
// TODO Auto-generated method stub
92+
93+
}
94+
95+
@Override
96+
public void applyRequestMetadata(
97+
RequestInfo requestInfo, Executor appExecutor, MetadataApplier applier) {
98+
appExecutor.execute(
99+
() -> {
100+
try {
101+
// Fetches new token if it is not available or if token has expired.
102+
if (this.accessToken == null || Instant.now().isAfter(this.tokenExpiryTime)) {
103+
Response response = httpClient.newCall(request).execute();
104+
if (!response.isSuccessful()) {
105+
throw new AuthenticationException(response.message());
106+
}
107+
JSONObject json = JSONObjectUtils.parse(response.body().string());
108+
this.accessToken = json.getAsString("access_token");
109+
this.tokenExpiryTime = jwtDecoder.decode(this.accessToken).getExpiresAt();
110+
}
111+
Metadata headers = new Metadata();
112+
headers.put(
113+
AUTHORIZATION_METADATA_KEY, String.format("%s %s", BEARER_TYPE, this.accessToken));
114+
applier.apply(headers);
115+
} catch (Throwable e) {
116+
applier.fail(Status.UNAUTHENTICATED.withCause(e));
117+
}
118+
});
119+
}
120+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright 2018-2020 The Feast Authors
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package feast.auth.service;
18+
19+
import feast.auth.authorization.AuthorizationProvider;
20+
import feast.auth.authorization.AuthorizationResult;
21+
import feast.auth.config.SecurityProperties;
22+
import lombok.AllArgsConstructor;
23+
import org.springframework.beans.factory.ObjectProvider;
24+
import org.springframework.beans.factory.annotation.Autowired;
25+
import org.springframework.security.access.AccessDeniedException;
26+
import org.springframework.security.core.Authentication;
27+
import org.springframework.security.core.context.SecurityContext;
28+
import org.springframework.stereotype.Service;
29+
30+
@AllArgsConstructor
31+
@Service
32+
public class AuthorizationService {
33+
34+
private final SecurityProperties securityProperties;
35+
private final AuthorizationProvider authorizationProvider;
36+
37+
@Autowired
38+
public AuthorizationService(
39+
SecurityProperties securityProperties,
40+
ObjectProvider<AuthorizationProvider> authorizationProvider) {
41+
this.securityProperties = securityProperties;
42+
this.authorizationProvider = authorizationProvider.getIfAvailable();
43+
}
44+
45+
/**
46+
* Determine whether a user has access to a project.
47+
*
48+
* @param securityContext Spring Security Context used to identify a user or service.
49+
* @param project Name of the project for which membership should be tested.
50+
*/
51+
public void authorizeRequest(SecurityContext securityContext, String project) {
52+
Authentication authentication = securityContext.getAuthentication();
53+
if (!this.securityProperties.getAuthorization().isEnabled()) {
54+
return;
55+
}
56+
57+
AuthorizationResult result =
58+
this.authorizationProvider.checkAccessToProject(project, authentication);
59+
if (!result.isAllowed()) {
60+
throw new AccessDeniedException(result.getFailureReason().orElse("Access Denied"));
61+
}
62+
}
63+
}

core/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@
126126
<dependency>
127127
<groupId>org.springframework.security.oauth</groupId>
128128
<artifactId>spring-security-oauth2</artifactId>
129-
<version>2.5.0.RELEASE</version>
129+
<version>${spring.security.oauth2.version}</version>
130130
</dependency>
131131
<dependency>
132132
<groupId>org.springframework.security</groupId>

core/src/main/java/feast/core/config/CoreSecurityConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
@Configuration
3030
@Slf4j
31-
@ComponentScan("feast.auth.config")
31+
@ComponentScan(basePackages = {"feast.auth.config", "feast.auth.service"})
3232
public class CoreSecurityConfig {
3333

3434
/**

0 commit comments

Comments
 (0)