From 59bf780ef34add2d80fec48a2865d619ab0a2d9c Mon Sep 17 00:00:00 2001 From: e10112844 Date: Tue, 28 Jul 2020 12:45:46 -0400 Subject: [PATCH] Add authorization test and minor refactoring. --- serving/pom.xml | 6 + .../ServingServiceGRpcController.java | 34 ++- serving/src/main/resources/application.yml | 2 +- .../java/feast/serving/it/AuthTestUtils.java | 72 +++++++ .../java/feast/serving/it/BaseAuthIT.java | 6 +- .../ServingServiceOauthAuthenticationIT.java | 3 + .../ServingServiceOauthAuthroizationIT.java | 204 ++++++++++++++++++ .../{core-it.yml => core/application-it.yml} | 0 .../docker-compose/docker-compose-it-core.yml | 2 +- .../docker-compose-it-hydra.yml | 6 +- .../docker-compose/docker-compose-it-keto.yml | 44 ++++ 11 files changed, 358 insertions(+), 21 deletions(-) create mode 100644 serving/src/test/java/feast/serving/it/ServingServiceOauthAuthroizationIT.java rename serving/src/test/resources/docker-compose/{core-it.yml => core/application-it.yml} (100%) create mode 100644 serving/src/test/resources/docker-compose/docker-compose-it-keto.yml diff --git a/serving/pom.xml b/serving/pom.xml index 47006e6e14..70da0e3b04 100644 --- a/serving/pom.xml +++ b/serving/pom.xml @@ -334,6 +334,12 @@ 3.0.0 test + + sh.ory.keto + keto-client + 0.4.4-alpha.1 + test + diff --git a/serving/src/main/java/feast/serving/controller/ServingServiceGRpcController.java b/serving/src/main/java/feast/serving/controller/ServingServiceGRpcController.java index be0bd411f2..ad97747ab1 100644 --- a/serving/src/main/java/feast/serving/controller/ServingServiceGRpcController.java +++ b/serving/src/main/java/feast/serving/controller/ServingServiceGRpcController.java @@ -38,6 +38,8 @@ import io.opentracing.Span; import io.opentracing.Tracer; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import net.devh.boot.grpc.server.service.GrpcService; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; @@ -83,11 +85,15 @@ public void getOnlineFeatures( Span span = tracer.buildSpan("getOnlineFeatures").start(); try (Scope scope = tracer.scopeManager().activate(span, false)) { // authorize for the project in request object. - this.authorizationService.authorizeRequest( - SecurityContextHolder.getContext(), request.getProject()); - // authorize for projects set in feature list, backward compatibility for - // <=v0.5.X - this.checkProjectAccess(request.getFeaturesList()); + if (request.getProject() != null && !request.getProject().isEmpty()) { + // project set at root level overrides the project set at feature set level + this.authorizationService.authorizeRequest( + SecurityContextHolder.getContext(), request.getProject()); + } else { + // authorize for projects set in feature list, backward compatibility for + // <=v0.5.X + this.checkProjectAccess(request.getFeaturesList()); + } RequestHelper.validateOnlineRequest(request); GetOnlineFeaturesResponse onlineFeatures = servingService.getOnlineFeatures(request); responseObserver.onNext(onlineFeatures); @@ -149,11 +155,17 @@ public void getJob(GetJobRequest request, StreamObserver respons } private void checkProjectAccess(List featureList) { - featureList.stream() - .forEach( - featureRef -> { - this.authorizationService.authorizeRequest( - SecurityContextHolder.getContext(), featureRef.getProject()); - }); + Set projectList = + featureList.stream().map(FeatureReference::getProject).collect(Collectors.toSet()); + if (projectList.isEmpty()) { + authorizationService.authorizeRequest(SecurityContextHolder.getContext(), "default"); + } else { + projectList.stream() + .forEach( + project -> { + this.authorizationService.authorizeRequest( + SecurityContextHolder.getContext(), project); + }); + } } } diff --git a/serving/src/main/resources/application.yml b/serving/src/main/resources/application.yml index 3fef07eb1a..08fcdf2874 100644 --- a/serving/src/main/resources/application.yml +++ b/serving/src/main/resources/application.yml @@ -30,7 +30,7 @@ feast: jwkEndpointURI: "https://www.googleapis.com/oauth2/v3/certs" authorization: enabled: false - provider: none + provider: http options: basePath: http://localhost:3000 diff --git a/serving/src/test/java/feast/serving/it/AuthTestUtils.java b/serving/src/test/java/feast/serving/it/AuthTestUtils.java index 31ca0ff661..5ec7298e98 100644 --- a/serving/src/test/java/feast/serving/it/AuthTestUtils.java +++ b/serving/src/test/java/feast/serving/it/AuthTestUtils.java @@ -40,6 +40,8 @@ import io.grpc.ManagedChannelBuilder; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -51,9 +53,17 @@ import okhttp3.Response; import org.apache.commons.lang3.tuple.Pair; import org.junit.runners.model.InitializationError; +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.OryAccessControlPolicy; +import sh.ory.keto.model.OryAccessControlPolicyRole; public class AuthTestUtils { + private static final String DEFAULT_FLAVOR = "glob"; + static SourceProto.Source defaultSource = createSource("kafka:9092,localhost:9094", "feast-features"); @@ -208,4 +218,66 @@ public static void seedHydra( throw new InitializationError(response.message()); } } + + public static void seedKeto(String url, String project, String subjectInProject, String admin) + throws ApiException { + ApiClient ketoClient = Configuration.getDefaultApiClient(); + ketoClient.setBasePath(url); + EnginesApi enginesApi = new EnginesApi(ketoClient); + + // Add policies + OryAccessControlPolicy adminPolicy = getAdminPolicy(); + enginesApi.upsertOryAccessControlPolicy(DEFAULT_FLAVOR, adminPolicy); + + OryAccessControlPolicy projectPolicy = getMyProjectMemberPolicy(project); + enginesApi.upsertOryAccessControlPolicy(DEFAULT_FLAVOR, projectPolicy); + + // Add policy roles + OryAccessControlPolicyRole adminPolicyRole = getAdminPolicyRole(admin); + enginesApi.upsertOryAccessControlPolicyRole(DEFAULT_FLAVOR, adminPolicyRole); + + OryAccessControlPolicyRole myProjectMemberPolicyRole = + getMyProjectMemberPolicyRole(project, subjectInProject); + enginesApi.upsertOryAccessControlPolicyRole(DEFAULT_FLAVOR, myProjectMemberPolicyRole); + } + + private static OryAccessControlPolicyRole getMyProjectMemberPolicyRole( + String project, String subjectInProject) { + OryAccessControlPolicyRole role = new OryAccessControlPolicyRole(); + role.setId(String.format("roles:%s-project-members", project)); + role.setMembers(Collections.singletonList("users:" + subjectInProject)); + return role; + } + + private static OryAccessControlPolicyRole getAdminPolicyRole(String subjectIsAdmin) { + OryAccessControlPolicyRole role = new OryAccessControlPolicyRole(); + role.setId("roles:admin"); + role.setMembers(Collections.singletonList("users:" + subjectIsAdmin)); + return role; + } + + private static OryAccessControlPolicy getAdminPolicy() { + OryAccessControlPolicy policy = new OryAccessControlPolicy(); + policy.setId("policies:admin"); + policy.subjects(Collections.singletonList("roles:admin")); + policy.resources(Collections.singletonList("resources:**")); + policy.actions(Collections.singletonList("actions:**")); + policy.effect("allow"); + policy.conditions(null); + return policy; + } + + private static OryAccessControlPolicy getMyProjectMemberPolicy(String project) { + OryAccessControlPolicy policy = new OryAccessControlPolicy(); + policy.setId(String.format("policies:%s-project-members-policy", project)); + policy.subjects(Collections.singletonList(String.format("roles:%s-project-members", project))); + policy.resources( + Arrays.asList( + String.format("resources:projects:%s", project), + String.format("resources:projects:%s:**", project))); + policy.actions(Collections.singletonList("actions:**")); + policy.effect("allow"); + policy.conditions(null); + return policy; + } } diff --git a/serving/src/test/java/feast/serving/it/BaseAuthIT.java b/serving/src/test/java/feast/serving/it/BaseAuthIT.java index 023ee67388..2d8701cd04 100644 --- a/serving/src/test/java/feast/serving/it/BaseAuthIT.java +++ b/serving/src/test/java/feast/serving/it/BaseAuthIT.java @@ -18,8 +18,6 @@ import java.net.InetAddress; import java.net.UnknownHostException; -import java.util.HashMap; -import java.util.Map; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.DynamicPropertyRegistry; @@ -45,7 +43,6 @@ public class BaseAuthIT { static final String CORE = "core_1"; static final String HYDRA = "hydra_1"; - static final Map options = new HashMap<>(); static final int HYDRA_PORT = 4445; static CoreSimpleAPIClient insecureApiClient; @@ -56,7 +53,7 @@ public class BaseAuthIT { static final int FEAST_SERVING_PORT = 6566; @DynamicPropertySource - static void initialize(DynamicPropertyRegistry registry) throws UnknownHostException { + static void properties(DynamicPropertyRegistry registry) { registry.add("feast.stores[0].name", () -> "online"); registry.add("feast.stores[0].type", () -> "REDIS"); // Redis needs to accessible by both core and serving, hence using host address @@ -66,7 +63,6 @@ static void initialize(DynamicPropertyRegistry registry) throws UnknownHostExcep try { return InetAddress.getLocalHost().getHostAddress(); } catch (UnknownHostException e) { - // TODO Auto-generated catch block e.printStackTrace(); return ""; } diff --git a/serving/src/test/java/feast/serving/it/ServingServiceOauthAuthenticationIT.java b/serving/src/test/java/feast/serving/it/ServingServiceOauthAuthenticationIT.java index e120550530..b4a4c4b76e 100644 --- a/serving/src/test/java/feast/serving/it/ServingServiceOauthAuthenticationIT.java +++ b/serving/src/test/java/feast/serving/it/ServingServiceOauthAuthenticationIT.java @@ -29,6 +29,7 @@ import java.io.File; import java.io.IOException; import java.time.Duration; +import java.util.HashMap; import java.util.Map; import org.junit.ClassRule; import org.junit.jupiter.api.BeforeAll; @@ -52,6 +53,8 @@ @Testcontainers public class ServingServiceOauthAuthenticationIT extends BaseAuthIT { + static final Map options = new HashMap<>(); + @ClassRule @Container public static DockerComposeContainer environment = new DockerComposeContainer( diff --git a/serving/src/test/java/feast/serving/it/ServingServiceOauthAuthroizationIT.java b/serving/src/test/java/feast/serving/it/ServingServiceOauthAuthroizationIT.java new file mode 100644 index 0000000000..5ec7767307 --- /dev/null +++ b/serving/src/test/java/feast/serving/it/ServingServiceOauthAuthroizationIT.java @@ -0,0 +1,204 @@ +/* + * 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.serving.it; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.testcontainers.containers.wait.strategy.Wait.forHttp; + +import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequest; +import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesResponse; +import feast.proto.serving.ServingServiceGrpc.ServingServiceBlockingStub; +import feast.proto.types.ValueProto.Value; +import io.grpc.StatusRuntimeException; +import java.io.File; +import java.io.IOException; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import org.junit.ClassRule; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.runners.model.InitializationError; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import sh.ory.keto.ApiException; + +@ActiveProfiles("it") +@SpringBootTest( + properties = { + "feast.core-authentication.enabled=true", + "feast.core-authentication.provider=oauth", + "feast.security.authentication.enabled=true", + "feast.security.authorization.enabled=true" + }) +@Testcontainers +public class ServingServiceOauthAuthroizationIT extends BaseAuthIT { + + static final Map adminCredentials = new HashMap<>(); + static final Map memberCredentials = new HashMap<>(); + static final String PROJECT_MEMBER_CLIENT_ID = "client_id_1"; + static final String NOT_PROJECT_MEMBER_CLIENT_ID = "client_id_2"; + private static int KETO_PORT = 4466; + private static int KETO_ADAPTOR_PORT = 8080; + static String subjectClaim = "sub"; + static CoreSimpleAPIClient coreClient; + + @ClassRule @Container + public static DockerComposeContainer environment = + new DockerComposeContainer( + new File("src/test/resources/docker-compose/docker-compose-it-hydra.yml"), + new File("src/test/resources/docker-compose/docker-compose-it-core.yml"), + new File("src/test/resources/docker-compose/docker-compose-it-keto.yml")) + .withExposedService(HYDRA, HYDRA_PORT, forHttp("/health/alive").forStatusCode(200)) + .withExposedService( + CORE, + 6565, + Wait.forLogMessage(".*gRPC Server started.*\\n", 1) + .withStartupTimeout(Duration.ofMinutes(CORE_START_MAX_WAIT_TIME_IN_MINUTES))) + .withExposedService("adaptor_1", KETO_ADAPTOR_PORT) + .withExposedService("keto_1", KETO_PORT, forHttp("/health/ready").forStatusCode(200));; + + @DynamicPropertySource + static void initialize(DynamicPropertyRegistry registry) { + + // Seed Keto with data + String ketoExternalHost = environment.getServiceHost("keto_1", KETO_PORT); + Integer ketoExternalPort = environment.getServicePort("keto_1", KETO_PORT); + String ketoExternalUrl = String.format("http://%s:%s", ketoExternalHost, ketoExternalPort); + try { + AuthTestUtils.seedKeto(ketoExternalUrl, PROJECT_NAME, PROJECT_MEMBER_CLIENT_ID, CLIENT_ID); + } catch (ApiException e) { + throw new RuntimeException(String.format("Could not seed Keto store %s", ketoExternalUrl)); + } + + // Get Keto Authorization Server (Adaptor) url + String ketoAdaptorHost = environment.getServiceHost("adaptor_1", KETO_ADAPTOR_PORT); + Integer ketoAdaptorPort = environment.getServicePort("adaptor_1", KETO_ADAPTOR_PORT); + String ketoAdaptorUrl = String.format("http://%s:%s", ketoAdaptorHost, ketoAdaptorPort); + + // Initialize dynamic properties + registry.add("feast.security.authorization.options.subjectClaim", () -> subjectClaim); + registry.add("feast.security.authentication.options.jwkEndpointURI", () -> JWK_URI); + registry.add("feast.security.authorization.options.authorizationUrl", () -> ketoAdaptorUrl); + } + + @BeforeAll + static void globalSetup() throws IOException, InitializationError, InterruptedException { + String hydraExternalHost = environment.getServiceHost(HYDRA, HYDRA_PORT); + Integer hydraExternalPort = environment.getServicePort(HYDRA, HYDRA_PORT); + String hydraExternalUrl = String.format("http://%s:%s", hydraExternalHost, hydraExternalPort); + AuthTestUtils.seedHydra(hydraExternalUrl, CLIENT_ID, CLIENT_SECRET, AUDIENCE, GRANT_TYPE); + AuthTestUtils.seedHydra( + hydraExternalUrl, PROJECT_MEMBER_CLIENT_ID, CLIENT_SECRET, AUDIENCE, GRANT_TYPE); + AuthTestUtils.seedHydra( + hydraExternalUrl, NOT_PROJECT_MEMBER_CLIENT_ID, CLIENT_SECRET, AUDIENCE, GRANT_TYPE); + // set up options for call credentials + adminCredentials.put("oauth_url", TOKEN_URL); + adminCredentials.put(CLIENT_ID, CLIENT_ID); + adminCredentials.put(CLIENT_SECRET, CLIENT_SECRET); + adminCredentials.put("jwkEndpointURI", JWK_URI); + adminCredentials.put("audience", AUDIENCE); + adminCredentials.put("grant_type", GRANT_TYPE); + + coreClient = AuthTestUtils.getSecureApiClientForCore(FEAST_CORE_PORT, adminCredentials); + } + + @BeforeEach + public void setUp() { + // seed core + AuthTestUtils.applyFeatureSet(coreClient, PROJECT_NAME, ENTITY_ID, FEATURE_NAME); + } + + @Test + public void shouldNotAllowUnauthenticatedGetOnlineFeatures() { + ServingServiceBlockingStub servingStub = + AuthTestUtils.getServingServiceStub(false, FEAST_SERVING_PORT, null); + GetOnlineFeaturesRequest onlineFeatureRequest = + AuthTestUtils.createOnlineFeatureRequest(PROJECT_NAME, FEATURE_NAME, ENTITY_ID, 1); + Exception exception = + assertThrows( + StatusRuntimeException.class, + () -> { + servingStub.getOnlineFeatures(onlineFeatureRequest); + }); + + String expectedMessage = "UNAUTHENTICATED: Authentication failed"; + String actualMessage = exception.getMessage(); + assertEquals(actualMessage, expectedMessage); + } + + @Test + void canGetOnlineFeaturesIfAdmin() { + // apply feature set + ServingServiceBlockingStub servingStub = + AuthTestUtils.getServingServiceStub(true, FEAST_SERVING_PORT, adminCredentials); + GetOnlineFeaturesRequest onlineFeatureRequest = + AuthTestUtils.createOnlineFeatureRequest(PROJECT_NAME, FEATURE_NAME, ENTITY_ID, 1); + GetOnlineFeaturesResponse featureResponse = servingStub.getOnlineFeatures(onlineFeatureRequest); + assertEquals(1, featureResponse.getFieldValuesCount()); + Map fieldsMap = featureResponse.getFieldValues(0).getFieldsMap(); + assertTrue(fieldsMap.containsKey(ENTITY_ID)); + assertTrue(fieldsMap.containsKey(FEATURE_NAME)); + } + + @Test + void canGetOnlineFeaturesIfProjectMember() { + Map memberCredsOptions = new HashMap<>(); + memberCredsOptions.putAll(adminCredentials); + memberCredsOptions.put(CLIENT_ID, PROJECT_MEMBER_CLIENT_ID); + ServingServiceBlockingStub servingStub = + AuthTestUtils.getServingServiceStub(true, FEAST_SERVING_PORT, memberCredsOptions); + GetOnlineFeaturesRequest onlineFeatureRequest = + AuthTestUtils.createOnlineFeatureRequest(PROJECT_NAME, FEATURE_NAME, ENTITY_ID, 1); + GetOnlineFeaturesResponse featureResponse = servingStub.getOnlineFeatures(onlineFeatureRequest); + assertEquals(1, featureResponse.getFieldValuesCount()); + Map fieldsMap = featureResponse.getFieldValues(0).getFieldsMap(); + assertTrue(fieldsMap.containsKey(ENTITY_ID)); + assertTrue(fieldsMap.containsKey(FEATURE_NAME)); + } + + @Test + void cantGetOnlineFeaturesIfNotProjectMember() { + Map notMemberCredsOptions = new HashMap<>(); + notMemberCredsOptions.putAll(adminCredentials); + notMemberCredsOptions.put(CLIENT_ID, NOT_PROJECT_MEMBER_CLIENT_ID); + ServingServiceBlockingStub servingStub = + AuthTestUtils.getServingServiceStub(true, FEAST_SERVING_PORT, notMemberCredsOptions); + GetOnlineFeaturesRequest onlineFeatureRequest = + AuthTestUtils.createOnlineFeatureRequest(PROJECT_NAME, FEATURE_NAME, ENTITY_ID, 1); + StatusRuntimeException exception = + assertThrows( + StatusRuntimeException.class, + () -> servingStub.getOnlineFeatures(onlineFeatureRequest)); + + String expectedMessage = + String.format( + "PERMISSION_DENIED: Access denied to project %s for subject %s", + PROJECT_NAME, NOT_PROJECT_MEMBER_CLIENT_ID); + String actualMessage = exception.getMessage(); + assertEquals(actualMessage, expectedMessage); + } +} diff --git a/serving/src/test/resources/docker-compose/core-it.yml b/serving/src/test/resources/docker-compose/core/application-it.yml similarity index 100% rename from serving/src/test/resources/docker-compose/core-it.yml rename to serving/src/test/resources/docker-compose/core/application-it.yml diff --git a/serving/src/test/resources/docker-compose/docker-compose-it-core.yml b/serving/src/test/resources/docker-compose/docker-compose-it-core.yml index 0a686ffaa4..bb7cdce8ab 100644 --- a/serving/src/test/resources/docker-compose/docker-compose-it-core.yml +++ b/serving/src/test/resources/docker-compose/docker-compose-it-core.yml @@ -4,7 +4,7 @@ services: core: image: gcr.io/kf-feast/feast-core:latest volumes: - - ./core-it.yml:/etc/feast/application.yml + - ./core/application-it.yml:/etc/feast/application.yml environment: DB_HOST: db restart: on-failure diff --git a/serving/src/test/resources/docker-compose/docker-compose-it-hydra.yml b/serving/src/test/resources/docker-compose/docker-compose-it-hydra.yml index 42b7c346f2..1c20610cc7 100644 --- a/serving/src/test/resources/docker-compose/docker-compose-it-hydra.yml +++ b/serving/src/test/resources/docker-compose/docker-compose-it-hydra.yml @@ -4,7 +4,7 @@ services: hydra-migrate: image: oryd/hydra:v1.6.0 environment: - - DSN=postgres://hydra:secret@postgresd:5433/hydra?sslmode=disable&max_conns=20&max_idle_conns=4 + - DSN=postgres://hydra:secret@postgresd:5432/hydra?sslmode=disable&max_conns=20&max_idle_conns=4 command: migrate sql -e --yes restart: on-failure @@ -13,12 +13,12 @@ services: depends_on: - hydra-migrate environment: - - DSN=postgres://hydra:secret@postgresd:5433/hydra?sslmode=disable&max_conns=20&max_idle_conns=4 + - DSN=postgres://hydra:secret@postgresd:5432/hydra?sslmode=disable&max_conns=20&max_idle_conns=4 postgresd: image: postgres:9.6 ports: - - "5433:5433" + - "54320:5432" environment: - POSTGRES_USER=hydra - POSTGRES_PASSWORD=secret diff --git a/serving/src/test/resources/docker-compose/docker-compose-it-keto.yml b/serving/src/test/resources/docker-compose/docker-compose-it-keto.yml new file mode 100644 index 0000000000..8ebf7f225e --- /dev/null +++ b/serving/src/test/resources/docker-compose/docker-compose-it-keto.yml @@ -0,0 +1,44 @@ +version: '3' +services: + keto: + depends_on: + - ketodb + - migrations + image: oryd/keto:v0.4.3-alpha.2 + environment: + - DSN=postgres://keto:keto@ketodb:5432/keto?sslmode=disable + command: + - serve + ports: + - 4466 + + ketodb: + image: bitnami/postgresql:9.6 + environment: + - POSTGRESQL_USERNAME=keto + - POSTGRESQL_PASSWORD=keto + - POSTGRESQL_DATABASE=keto + ports: + - "54340:5432" + + migrations: + depends_on: + - ketodb + image: oryd/keto:v0.4.3-alpha.2 + environment: + - DSN=postgres://keto:keto@ketodb:5432/keto?sslmode=disable + command: + - migrate + - sql + - -e + + adaptor: + depends_on: + - keto + image: gcr.io/kf-feast/feast-keto-auth-server:latest + environment: + SERVER_PORT: 8080 + KETO_URL: http://keto:4466 + ports: + - 8080 + restart: on-failure \ No newline at end of file