diff --git a/charts/ephemeral/templates/ephemeral.yaml b/charts/ephemeral/templates/ephemeral.yaml index 6acd4f8a..de134269 100644 --- a/charts/ephemeral/templates/ephemeral.yaml +++ b/charts/ephemeral/templates/ephemeral.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2021-2023 - for information on the respective copyright owner +# Copyright (c) 2021-2024 - for information on the respective copyright owner # see the NOTICE file and/or the repository https://github.com/carbynestack/ephemeral. # # SPDX-License-Identifier: Apache-2.0 @@ -35,6 +35,11 @@ spec: containerPort: 8080 - name: tcp containerPort: 5000 + env: + - name: EPHEMERAL_PROGRAM_IDENTIFIER + value: {{ .Values.ephemeral.programIdentifier }} + - name: EPHEMERAL_OPA_POLICY_PACKAGE + value: {{ .Values.ephemeral.opa.policyPackage }} volumeMounts: - name: config-volume mountPath: /etc/config @@ -72,6 +77,7 @@ metadata: data: config.json: |- { + "authUserIdField": "{{ .Values.ephemeral.authUserIdField }}", "retrySleep": "50ms", "networkEstablishTimeout": "{{ .Values.ephemeral.networkEstablishTimeout }}", "prime": "{{ .Values.ephemeral.spdz.prime }}", @@ -81,6 +87,9 @@ data: "gf2nBitLength": {{ .Values.ephemeral.spdz.gf2nBitLength }}, "gf2nStorageSize": {{ .Values.ephemeral.spdz.gf2nStorageSize }}, "prepFolder": "{{ .Values.ephemeral.spdz.prepFolder }}", + "opaConfig": { + "endpoint": "{{ .Values.ephemeral.opa.endpoint }}" + }, "amphoraConfig": { "host": "{{ .Values.ephemeral.amphora.host }}", "scheme": "{{ .Values.ephemeral.amphora.scheme }}", diff --git a/charts/ephemeral/values.yaml b/charts/ephemeral/values.yaml index e36aa185..e5f4a285 100644 --- a/charts/ephemeral/values.yaml +++ b/charts/ephemeral/values.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2021-2023 - for information on the respective copyright owner +# Copyright (c) 2021-2024 - for information on the respective copyright owner # see the NOTICE file and/or the repository https://github.com/carbynestack/ephemeral. # # SPDX-License-Identifier: Apache-2.0 @@ -46,6 +46,11 @@ ephemeral: memory: cpu: minScale: 1 + programIdentifier: "ephemeral-generic" + authUserIdField: "sub" + opa: + endpoint: "http://opa.default.svc.cluster.local:8081/" + policyPackage: "carbynestack.def" amphora: host: "amphora" scheme: "http" diff --git a/cmd/ephemeral/main.go b/cmd/ephemeral/main.go index 603d8140..af3883f3 100644 --- a/cmd/ephemeral/main.go +++ b/cmd/ephemeral/main.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021-2023 - for information on the respective copyright owner +// Copyright (c) 2021-2024 - for information on the respective copyright owner // see the NOTICE file and/or the repository https://github.com/carbynestack/ephemeral. // // SPDX-License-Identifier: Apache-2.0 @@ -11,7 +11,9 @@ import ( "github.com/carbynestack/ephemeral/pkg/castor" . "github.com/carbynestack/ephemeral/pkg/ephemeral" l "github.com/carbynestack/ephemeral/pkg/logger" + "github.com/carbynestack/ephemeral/pkg/opa" "github.com/carbynestack/ephemeral/pkg/utils" + "os" . "github.com/carbynestack/ephemeral/pkg/types" "math/big" @@ -54,7 +56,7 @@ func main() { // GetHandlerChain returns a chain of handlers that are used to process HTTP requests. func GetHandlerChain(conf *SPDZEngineConfig, logger *zap.SugaredLogger) (http.Handler, error) { - typedConfig, err := InitTypedConfig(conf) + typedConfig, err := InitTypedConfig(conf, logger) if err != nil { return nil, err } @@ -62,14 +64,14 @@ func GetHandlerChain(conf *SPDZEngineConfig, logger *zap.SugaredLogger) (http.Ha if err != nil { return nil, err } - server := NewServer(spdzClient.Compile, spdzClient.Activate, logger, typedConfig) + server := NewServer(conf.AuthUserIdField, spdzClient.Compile, spdzClient.Activate, logger, typedConfig) activationHandler := http.HandlerFunc(server.ActivationHandler) // Apply in Order: // 1) MethodFilter: Check that only POST Requests can go through - // 2) BodyFilter: Check that Request Body is set properly and Sets the CtxConfig to the request + // 2) RequestFilter: Check that Request Body is set properly and Sets the CtxConfig to the request // 3) CompilationHandler: Compiles the script if ?compile=true // 4) ActivationHandler: Runs the script - filterChain := server.MethodFilter(server.BodyFilter(server.CompilationHandler(activationHandler))) + filterChain := server.MethodFilter(server.RequestFilter(server.CompilationHandler(activationHandler))) return filterChain, nil } @@ -89,7 +91,7 @@ func ParseConfig(path string) (*SPDZEngineConfig, error) { // InitTypedConfig converts the string parameters that were parsed by standard json parser to // the parameters which are used internally, e.g. string -> time.Duration. -func InitTypedConfig(conf *SPDZEngineConfig) (*SPDZEngineTypedConfig, error) { +func InitTypedConfig(conf *SPDZEngineConfig, logger *zap.SugaredLogger) (*SPDZEngineTypedConfig, error) { retrySleep, err := time.ParseDuration(conf.RetrySleep) if err != nil { return nil, err @@ -123,6 +125,19 @@ func InitTypedConfig(conf *SPDZEngineConfig) (*SPDZEngineTypedConfig, error) { if err != nil { return nil, err } + programIdentifier, ok := os.LookupEnv("EPHEMERAL_PROGRAM_IDENTIFIER") + if !ok { + programIdentifier = conf.ProgramIdentifier + } + + policyPackage, ok := os.LookupEnv("EPHEMERAL_OPA_POLICY_PACKAGE") + if !ok { + policyPackage = conf.OpaConfig.PolicyPackage + } + opaClient, err := opa.NewClient(logger, conf.OpaConfig.Endpoint, policyPackage) + if err != nil { + return nil, err + } amphoraURL := url.URL{ Host: conf.AmphoraConfig.Host, @@ -145,6 +160,7 @@ func InitTypedConfig(conf *SPDZEngineConfig) (*SPDZEngineTypedConfig, error) { } return &SPDZEngineTypedConfig{ + ProgramIdentifier: programIdentifier, NetworkEstablishTimeout: networkEstablishTimeout, RetrySleep: retrySleep, Prime: p, @@ -154,6 +170,7 @@ func InitTypedConfig(conf *SPDZEngineConfig) (*SPDZEngineTypedConfig, error) { Gf2nBitLength: conf.Gf2nBitLength, Gf2nStorageSize: conf.Gf2nStorageSize, PrepFolder: conf.PrepFolder, + OpaClient: opaClient, AmphoraClient: amphoraClient, CastorClient: castorClient, TupleStock: conf.CastorConfig.TupleStock, diff --git a/cmd/ephemeral/main_test.go b/cmd/ephemeral/main_test.go index ace22e12..e16dc7c1 100644 --- a/cmd/ephemeral/main_test.go +++ b/cmd/ephemeral/main_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021-2023 - for information on the respective copyright owner +// Copyright (c) 2021-2024 - for information on the respective copyright owner // see the NOTICE file and/or the repository https://github.com/carbynestack/ephemeral. // // SPDX-License-Identifier: Apache-2.0 @@ -22,7 +22,7 @@ import ( ) var _ = Describe("Main", func() { - + logger := zap.NewNop().Sugar() Context("when manipulating ephemeral configuration", func() { Context("when working with real config file", func() { @@ -49,6 +49,7 @@ var _ = Describe("Main", func() { It("initializes the config", func() { data := []byte( `{ + "programIdentifier":"ephemeral-generic", "retrySleep":"50ms", "networkEstablishTimeout":"1m", "prime":"p", @@ -58,6 +59,10 @@ var _ = Describe("Main", func() { "gf2nBitLength":40, "gf2nStorageSize":8, "prepFolder":"Player-Data", + "opaConfig": { + "endpoint": "http://opa.carbynestack.io", + "policePackage": "carbynestack.def" + }, "amphoraConfig": { "host":"mock-server:1080", "scheme":"http","path":"/amphora1" @@ -107,6 +112,7 @@ var _ = Describe("Main", func() { Context("when initializing typed config", func() { It("succeeds when all parameters are specified", func() { conf := &SPDZEngineConfig{ + ProgramIdentifier: "ephemeral-generic", NetworkEstablishTimeout: "2s", RetrySleep: "1s", Prime: "198766463529478683931867765928436695041", @@ -114,6 +120,10 @@ var _ = Describe("Main", func() { GfpMacKey: "1113507028231509545156335486838233835", Gf2nBitLength: 40, Gf2nStorageSize: 8, + OpaConfig: OpaConfig{ + Endpoint: "http://opa.carbynestack.io", + PolicyPackage: "carbynestack.def", + }, AmphoraConfig: AmphoraConfig{ Host: "localhost", Scheme: "http", @@ -132,7 +142,7 @@ var _ = Describe("Main", func() { StateTimeout: "5s", ComputationTimeout: "10s", } - typedConf, err := InitTypedConfig(conf) + typedConf, err := InitTypedConfig(conf, logger) Expect(err).NotTo(HaveOccurred()) Expect(typedConf.NetworkEstablishTimeout).To(Equal(2 * time.Second)) Expect(typedConf.RetrySleep).To(Equal(1 * time.Second)) @@ -143,9 +153,10 @@ var _ = Describe("Main", func() { Context("retry timeout format is corrupt", func() { It("returns an error", func() { conf := &SPDZEngineConfig{ + ProgramIdentifier: "ephemeral-generic", NetworkEstablishTimeout: "2", } - typedConf, err := InitTypedConfig(conf) + typedConf, err := InitTypedConfig(conf, logger) Expect(err).To(HaveOccurred()) Expect(typedConf).To(BeNil()) }) @@ -153,10 +164,11 @@ var _ = Describe("Main", func() { Context("retry sleep format is corrupt", func() { It("returns an error", func() { conf := &SPDZEngineConfig{ + ProgramIdentifier: "ephemeral-generic", NetworkEstablishTimeout: "2s", RetrySleep: "1", } - typedConf, err := InitTypedConfig(conf) + typedConf, err := InitTypedConfig(conf, logger) Expect(err).To(HaveOccurred()) Expect(typedConf).To(BeNil()) }) @@ -164,11 +176,12 @@ var _ = Describe("Main", func() { Context("prime number is not specified", func() { It("returns an error", func() { conf := &SPDZEngineConfig{ + ProgramIdentifier: "ephemeral-generic", NetworkEstablishTimeout: "2s", RetrySleep: "1s", Prime: "", } - typedConf, err := InitTypedConfig(conf) + typedConf, err := InitTypedConfig(conf, logger) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("wrong prime number format")) Expect(typedConf).To(BeNil()) @@ -177,12 +190,13 @@ var _ = Describe("Main", func() { Context("inverse R is not specified", func() { It("returns an error", func() { conf := &SPDZEngineConfig{ + ProgramIdentifier: "ephemeral-generic", NetworkEstablishTimeout: "2s", RetrySleep: "1s", Prime: "123", RInv: "", } - typedConf, err := InitTypedConfig(conf) + typedConf, err := InitTypedConfig(conf, logger) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("wrong rInv format")) Expect(typedConf).To(BeNil()) @@ -191,13 +205,14 @@ var _ = Describe("Main", func() { Context("gfpMacKey is not specified", func() { It("returns an error", func() { conf := &SPDZEngineConfig{ + ProgramIdentifier: "ephemeral-generic", NetworkEstablishTimeout: "2s", RetrySleep: "1s", Prime: "123", RInv: "123", GfpMacKey: "", } - typedConf, err := InitTypedConfig(conf) + typedConf, err := InitTypedConfig(conf, logger) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("wrong gfpMacKey format")) Expect(typedConf).To(BeNil()) @@ -206,6 +221,7 @@ var _ = Describe("Main", func() { Context("amphora URL is not specified", func() { It("returns an error", func() { conf := &SPDZEngineConfig{ + ProgramIdentifier: "ephemeral-generic", NetworkEstablishTimeout: "2s", RetrySleep: "1s", Prime: "123", @@ -213,6 +229,10 @@ var _ = Describe("Main", func() { GfpMacKey: "123", Gf2nBitLength: 40, Gf2nStorageSize: 8, + OpaConfig: OpaConfig{ + Endpoint: "http://opa.carbynestack.io", + PolicyPackage: "carbynestack.def", + }, AmphoraConfig: AmphoraConfig{ Host: "", }, @@ -230,7 +250,7 @@ var _ = Describe("Main", func() { StateTimeout: "0s", ComputationTimeout: "0s", } - typedConf, err := InitTypedConfig(conf) + typedConf, err := InitTypedConfig(conf, logger) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("invalid Url")) Expect(typedConf).To(BeNil()) @@ -239,6 +259,7 @@ var _ = Describe("Main", func() { Context("amphora scheme is not specified", func() { It("returns an error", func() { conf := &SPDZEngineConfig{ + ProgramIdentifier: "ephemeral-generic", NetworkEstablishTimeout: "2s", RetrySleep: "1s", Prime: "123", @@ -246,6 +267,10 @@ var _ = Describe("Main", func() { GfpMacKey: "123", Gf2nBitLength: 40, Gf2nStorageSize: 8, + OpaConfig: OpaConfig{ + Endpoint: "http://opa.carbynestack.io", + PolicyPackage: "carbynestack.def", + }, AmphoraConfig: AmphoraConfig{ Host: "localhost", Scheme: "", @@ -264,7 +289,7 @@ var _ = Describe("Main", func() { StateTimeout: "0s", ComputationTimeout: "0s", } - typedConf, err := InitTypedConfig(conf) + typedConf, err := InitTypedConfig(conf, logger) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("invalid Url")) Expect(typedConf).To(BeNil()) @@ -273,6 +298,7 @@ var _ = Describe("Main", func() { Context("castor URL is not specified", func() { It("returns an error", func() { conf := &SPDZEngineConfig{ + ProgramIdentifier: "ephemeral-generic", NetworkEstablishTimeout: "2s", RetrySleep: "1s", Prime: "123", @@ -280,6 +306,10 @@ var _ = Describe("Main", func() { GfpMacKey: "123", Gf2nBitLength: 40, Gf2nStorageSize: 8, + OpaConfig: OpaConfig{ + Endpoint: "http://opa.carbynestack.io", + PolicyPackage: "carbynestack.def", + }, AmphoraConfig: AmphoraConfig{ Host: "localhost", Scheme: "http", @@ -296,7 +326,7 @@ var _ = Describe("Main", func() { StateTimeout: "0s", ComputationTimeout: "0s", } - typedConf, err := InitTypedConfig(conf) + typedConf, err := InitTypedConfig(conf, logger) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("invalid Url")) Expect(typedConf).To(BeNil()) @@ -305,6 +335,7 @@ var _ = Describe("Main", func() { Context("castor scheme is not specified", func() { It("returns an error", func() { conf := &SPDZEngineConfig{ + ProgramIdentifier: "ephemeral-generic", NetworkEstablishTimeout: "2s", RetrySleep: "1s", Prime: "123", @@ -312,6 +343,10 @@ var _ = Describe("Main", func() { GfpMacKey: "123", Gf2nBitLength: 40, Gf2nStorageSize: 8, + OpaConfig: OpaConfig{ + Endpoint: "http://opa.carbynestack.io", + PolicyPackage: "carbynestack.def", + }, AmphoraConfig: AmphoraConfig{ Host: "localhost", Scheme: "http", @@ -329,7 +364,7 @@ var _ = Describe("Main", func() { StateTimeout: "0s", ComputationTimeout: "0s", } - typedConf, err := InitTypedConfig(conf) + typedConf, err := InitTypedConfig(conf, logger) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("invalid Url")) Expect(typedConf).To(BeNil()) @@ -338,6 +373,7 @@ var _ = Describe("Main", func() { Context("stateTimeout format is corrupt", func() { It("returns an error", func() { conf := &SPDZEngineConfig{ + ProgramIdentifier: "ephemeral-generic", NetworkEstablishTimeout: "2s", RetrySleep: "1s", Prime: "198766463529478683931867765928436695041", @@ -345,6 +381,10 @@ var _ = Describe("Main", func() { GfpMacKey: "1113507028231509545156335486838233835", Gf2nBitLength: 40, Gf2nStorageSize: 8, + OpaConfig: OpaConfig{ + Endpoint: "http://opa.carbynestack.io", + PolicyPackage: "carbynestack.def", + }, AmphoraConfig: AmphoraConfig{ Host: "localhost", Scheme: "http", @@ -362,7 +402,7 @@ var _ = Describe("Main", func() { }, StateTimeout: "corrupt", } - typedConf, err := InitTypedConfig(conf) + typedConf, err := InitTypedConfig(conf, logger) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("time: invalid duration corrupt")) Expect(typedConf).To(BeNil()) @@ -371,6 +411,7 @@ var _ = Describe("Main", func() { Context("discovery config's connect timeout format is corrupt", func() { It("returns an error", func() { conf := &SPDZEngineConfig{ + ProgramIdentifier: "ephemeral-generic", NetworkEstablishTimeout: "2s", RetrySleep: "1s", Prime: "198766463529478683931867765928436695041", @@ -378,6 +419,10 @@ var _ = Describe("Main", func() { GfpMacKey: "1113507028231509545156335486838233835", Gf2nBitLength: 40, Gf2nStorageSize: 8, + OpaConfig: OpaConfig{ + Endpoint: "http://opa.carbynestack.io", + PolicyPackage: "carbynestack.def", + }, AmphoraConfig: AmphoraConfig{ Host: "localhost", Scheme: "http", @@ -396,7 +441,7 @@ var _ = Describe("Main", func() { StateTimeout: "0s", ComputationTimeout: "0s", } - typedConf, err := InitTypedConfig(conf) + typedConf, err := InitTypedConfig(conf, logger) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("time: invalid duration corrupt")) Expect(typedConf).To(BeNil()) @@ -405,6 +450,7 @@ var _ = Describe("Main", func() { Context("computationTimeout format is corrupt", func() { It("returns an error", func() { conf := &SPDZEngineConfig{ + ProgramIdentifier: "ephemeral-generic", NetworkEstablishTimeout: "2s", RetrySleep: "1s", Prime: "198766463529478683931867765928436695041", @@ -412,6 +458,10 @@ var _ = Describe("Main", func() { GfpMacKey: "1113507028231509545156335486838233835", Gf2nBitLength: 40, Gf2nStorageSize: 8, + OpaConfig: OpaConfig{ + Endpoint: "http://opa.carbynestack.io", + PolicyPackage: "carbynestack.def", + }, AmphoraConfig: AmphoraConfig{ Host: "localhost", Scheme: "http", @@ -430,7 +480,7 @@ var _ = Describe("Main", func() { StateTimeout: "0s", ComputationTimeout: "corrupt", } - typedConf, err := InitTypedConfig(conf) + typedConf, err := InitTypedConfig(conf, logger) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("time: invalid duration corrupt")) Expect(typedConf).To(BeNil()) @@ -439,6 +489,7 @@ var _ = Describe("Main", func() { Context("networkEstablishTimeout format is corrupt", func() { It("returns an error", func() { conf := &SPDZEngineConfig{ + ProgramIdentifier: "ephemeral-generic", NetworkEstablishTimeout: "corrupt", RetrySleep: "1s", Prime: "198766463529478683931867765928436695041", @@ -446,6 +497,10 @@ var _ = Describe("Main", func() { GfpMacKey: "1113507028231509545156335486838233835", Gf2nBitLength: 40, Gf2nStorageSize: 8, + OpaConfig: OpaConfig{ + Endpoint: "http://opa.carbynestack.io", + PolicyPackage: "carbynestack.def", + }, AmphoraConfig: AmphoraConfig{ Host: "localhost", Scheme: "http", @@ -464,7 +519,7 @@ var _ = Describe("Main", func() { StateTimeout: "0s", ComputationTimeout: "0s", } - typedConf, err := InitTypedConfig(conf) + typedConf, err := InitTypedConfig(conf, logger) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("time: invalid duration corrupt")) Expect(typedConf).To(BeNil()) @@ -480,6 +535,7 @@ var _ = Describe("Main", func() { defer os.RemoveAll(tmpPrepDir) logger := zap.NewNop().Sugar() conf := &SPDZEngineConfig{ + ProgramIdentifier: "ephemeral-generic", NetworkEstablishTimeout: "2s", RetrySleep: "1s", Prime: "198766463529478683931867765928436695041", @@ -490,6 +546,10 @@ var _ = Describe("Main", func() { Gf2nStorageSize: 8, PlayerCount: 2, PrepFolder: tmpPrepDir, + OpaConfig: OpaConfig{ + Endpoint: "http://opa.carbynestack.io", + PolicyPackage: "carbynestack.def", + }, AmphoraConfig: AmphoraConfig{ Host: "localhost", Scheme: "http", @@ -518,6 +578,7 @@ var _ = Describe("Main", func() { It("is returned", func() { logger := zap.NewNop().Sugar() conf := &SPDZEngineConfig{ + ProgramIdentifier: "ephemeral-generic", NetworkEstablishTimeout: "2s", RetrySleep: "1s", Prime: "198766463529478683931867765928436695041", @@ -525,6 +586,10 @@ var _ = Describe("Main", func() { GfpMacKey: "1113507028231509545156335486838233835", Gf2nBitLength: 40, Gf2nStorageSize: 8, + OpaConfig: OpaConfig{ + Endpoint: "http://opa.carbynestack.io", + PolicyPackage: "carbynestack.def", + }, // an empty amphora config is given to provoke an error. AmphoraConfig: AmphoraConfig{}, CastorConfig: CastorConfig{ diff --git a/pkg/amphora/amphora_demo_test.go b/pkg/amphora/amphora_demo_test.go index 393459ee..f96ba7c3 100644 --- a/pkg/amphora/amphora_demo_test.go +++ b/pkg/amphora/amphora_demo_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021 - for information on the respective copyright owner +// Copyright (c) 2021-2024 - for information on the respective copyright owner // see the NOTICE file and/or the repository https://github.com/carbynestack/ephemeral. // // SPDX-License-Identifier: Apache-2.0 @@ -35,6 +35,7 @@ func TestPostAndGet(t *testing.T) { } secretID := uuid.New().String() + programID := "ephemeral-generic" fmt.Printf("Operating on shared secret with UUID: %s\n", secretID) os := SecretShare{ SecretID: secretID, @@ -47,7 +48,7 @@ func TestPostAndGet(t *testing.T) { t.Error(err) } - retrieved, err := client.GetSecretShare(secretID) + retrieved, err := client.GetSecretShare(secretID, programID) if retrieved.Data != os.Data { t.Error("Retrieved object data is not equal to expected.") } diff --git a/pkg/amphora/amphora_test.go b/pkg/amphora/amphora_test.go index 10032ac5..92d781db 100644 --- a/pkg/amphora/amphora_test.go +++ b/pkg/amphora/amphora_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021 - for information on the respective copyright owner +// Copyright (c) 2021-2024 - for information on the respective copyright owner // see the NOTICE file and/or the repository https://github.com/carbynestack/ephemeral. // // SPDX-License-Identifier: Apache-2.0 @@ -29,20 +29,25 @@ var _ = Describe("Amphora", func() { }) Context("when retrieving a shared secret", func() { It("returns a shared object when it exists in amphora", func() { - rt := MockedRoundTripper{ExpectedPath: "/intra-vcp/secret-shares/xyz", ReturnJSON: js, ExpectedResponseCode: http.StatusOK} + rt := MockedRoundTripper{ExpectedPath: "/intra-vcp/secret-shares/xyz", + ExpectedRawQuery: "programId=ephemeral-generic", + ReturnJSON: js, + ExpectedResponseCode: http.StatusOK} HTTPClient := http.Client{Transport: &rt} client := Client{HTTPClient: HTTPClient, URL: url.URL{Host: "test", Scheme: "http"}} - secret, err := client.GetSecretShare("xyz") + secret, err := client.GetSecretShare("xyz", "ephemeral-generic") Expect(secret.SecretID).To(Equal("xyz")) Expect(err).NotTo(HaveOccurred()) }) It("returns an error when shared secret does not exist", func() { - rt := MockedRoundTripper{ExpectedPath: "/intra-vcp/secret-shares/xxx", ReturnJSON: js, ExpectedResponseCode: http.StatusOK} + rt := MockedRoundTripper{ExpectedPath: "/intra-vcp/secret-shares/xxx", + ExpectedRawQuery: "programId=ephemeral-generic", + ReturnJSON: js, ExpectedResponseCode: http.StatusOK} HTTPClient := http.Client{Transport: &rt} client := Client{HTTPClient: HTTPClient, URL: url.URL{Host: "test", Scheme: "http"}} - _, err := client.GetSecretShare("xyz") + _, err := client.GetSecretShare("xyz", "ephemeral-generic") Expect(err).To(HaveOccurred()) }) }) diff --git a/pkg/amphora/client.go b/pkg/amphora/client.go index ae25d6e3..88f0f59e 100644 --- a/pkg/amphora/client.go +++ b/pkg/amphora/client.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021 - for information on the respective copyright owner +// Copyright (c) 2021-2024 - for information on the respective copyright owner // see the NOTICE file and/or the repository https://github.com/carbynestack/ephemeral. // // SPDX-License-Identifier: Apache-2.0 @@ -33,7 +33,7 @@ type Tag struct { // AbstractClient is an interface for object storage client. type AbstractClient interface { - GetSecretShare(string) (SecretShare, error) + GetSecretShare(string, string) (SecretShare, error) CreateSecretShare(*SecretShare) error } @@ -56,12 +56,15 @@ type Client struct { const secretShareURI = "/intra-vcp/secret-shares" // GetSecretShare creates a new secret share by sending a POST request against Amphora. -func (c *Client) GetSecretShare(id string) (SecretShare, error) { +func (c *Client) GetSecretShare(id string, programIdentifier string) (SecretShare, error) { var os SecretShare req, err := http.NewRequest(http.MethodGet, c.URL.String()+fmt.Sprintf("%s/%s", secretShareURI, id), nil) if err != nil { return os, err } + query := req.URL.Query() + query.Add("programId", programIdentifier) + req.URL.RawQuery = query.Encode() body, err := c.doRequest(req, http.StatusOK) if err != nil { return os, err diff --git a/pkg/discovery/game.go b/pkg/discovery/game.go index 625d3d4d..14bcd495 100644 --- a/pkg/discovery/game.go +++ b/pkg/discovery/game.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021-2023 - for information on the respective copyright owner +// Copyright (c) 2021-2024 - for information on the respective copyright owner // see the NOTICE file and/or the repository https://github.com/carbynestack/ephemeral. // // SPDX-License-Identifier: Apache-2.0 @@ -75,6 +75,7 @@ func NewGame(ctx context.Context, id string, bus mb.MessageBus, stateTimeout tim fsm.WhenIn(WaitTCPCheck).GotEvent(TCPCheckSuccess).Stay(), fsm.WhenIn(WaitTCPCheck).GotEvent(TCPCheckSuccessAll).GoTo(Playing).WithTimeout(computationTimeout), fsm.WhenIn(WaitTCPCheck).GotEvent(TCPCheckFailure).GoTo(GameError), + fsm.WhenIn(WaitTCPCheck).GotEvent(GameFinishedWithError).GoTo(GameError), fsm.WhenIn(Playing).GotEvent(GameFinishedWithSuccess).Stay(), fsm.WhenIn(Playing).GotEvent(GameFinishedWithError).GoTo(GameError), fsm.WhenIn(Playing).GotEvent(GameSuccess).GoTo(GameDone), diff --git a/pkg/ephemeral/io/feeder.go b/pkg/ephemeral/io/feeder.go index f30ee2b5..c2125ec2 100644 --- a/pkg/ephemeral/io/feeder.go +++ b/pkg/ephemeral/io/feeder.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021-2023 - for information on the respective copyright owner +// Copyright (c) 2021-2024 - for information on the respective copyright owner // see the NOTICE file and/or the repository https://github.com/carbynestack/ephemeral. // // SPDX-License-Identifier: Apache-2.0 @@ -11,13 +11,18 @@ import ( "github.com/carbynestack/ephemeral/pkg/ephemeral/network" . "github.com/carbynestack/ephemeral/pkg/types" "strings" + "time" "go.uber.org/zap" ) // Feeder is an interface. type Feeder interface { + // LoadFromSecretStoreAndFeed loads input parameters from Amphora. LoadFromSecretStoreAndFeed(act *Activation, feedPort string, ctx *CtxConfig) ([]byte, error) + // LoadFromRequestAndFeed loads input parameters from the request body. + // + // Deprecated: providing secrets in the request body is not recommended and will be removed in the future. LoadFromRequestAndFeed(act *Activation, feedPort string, ctx *CtxConfig) ([]byte, error) Close() error } @@ -50,21 +55,48 @@ type AmphoraFeeder struct { // LoadFromSecretStoreAndFeed loads input parameters from Amphora. func (f *AmphoraFeeder) LoadFromSecretStoreAndFeed(act *Activation, feedPort string, ctx *CtxConfig) ([]byte, error) { var data []string + inputs := []ActivationInput{} client := f.conf.AmphoraClient for i := range act.AmphoraParams { - osh, err := client.GetSecretShare(act.AmphoraParams[i]) + osh, err := client.GetSecretShare(act.AmphoraParams[i], ctx.Spdz.ProgramIdentifier) if err != nil { return nil, err } + policy := DefaultPolicy + owner, _ := findValueForKeyInTags(osh.Tags, "owner") + policy, _ = findValueForKeyInTags(osh.Tags, "accessPolicy") + inputs = append(inputs, ActivationInput{ + SecretId: osh.SecretID, + Owner: owner, + AccessPolicy: policy, + }) data = append(data, osh.Data) } + t := time.Now() + opaInput := map[string]interface{}{ + "subject": ctx.Spdz.ProgramIdentifier, + "executor": ctx.AuthorizedUser, + "inputs": inputs, + "time": map[string]interface{}{ + "formatted": t.String(), + "nano": t.UnixNano(), + }, + "playerCount": ctx.Spdz.PlayerCount, + } + canExecute, err := f.conf.OpaClient.CanExecute(opaInput) + if err != nil { + return nil, fmt.Errorf("failed to check if program can be executed: %w", err) + } + if !canExecute { + return nil, fmt.Errorf("unauthorized: program cannot be executed") + } resp, err := f.feedAndRead(data, feedPort, ctx) if err != nil { return nil, err } // Write to amphora if required and return amphora secret ids. if act.Output.Type == AmphoraSecret { - ids, err := f.writeToAmphora(act, *resp) + ids, err := f.writeToAmphora(act, opaInput, *resp) if err != nil { return nil, err } @@ -74,6 +106,8 @@ func (f *AmphoraFeeder) LoadFromSecretStoreAndFeed(act *Activation, feedPort str } // LoadFromRequestAndFeed loads input parameteters from the request body. +// +// Deprecated: providing secrets in the request body is not recommended and will be removed in the future. func (f *AmphoraFeeder) LoadFromRequestAndFeed(act *Activation, feedPort string, ctx *CtxConfig) ([]byte, error) { resp, err := f.feedAndRead(act.SecretParams, feedPort, ctx) if err != nil { @@ -81,7 +115,7 @@ func (f *AmphoraFeeder) LoadFromRequestAndFeed(act *Activation, feedPort string, } // Write to amphora if required and return amphora secret ids. if act.Output.Type == AmphoraSecret { - ids, err := f.writeToAmphora(act, *resp) + ids, err := f.writeToAmphora(act, map[string]interface{}{}, *resp) if err != nil { return nil, err } @@ -96,7 +130,9 @@ func (f *AmphoraFeeder) Close() error { return f.carrier.Close() } -// feedAndRead takes a slice of base64 encoded secret shared parameters along with the port where SPDZ runtime is listening for the input. The base64 input params are converted into a form digestable by SPDZ and sent to the socket. The runtime must send back a response for this function to finish without an error. +// feedAndRead takes a slice of base64 encoded secret shared parameters along with the port where SPDZ runtime is +// listening for the input. The base64 input params are converted into a form digestable by SPDZ and sent to the socket. +// The runtime must send back a response for this function to finish without an error. func (f *AmphoraFeeder) feedAndRead(params []string, feedPort string, ctx *CtxConfig) (*Result, error) { var conv ResponseConverter f.logger.Debugw(fmt.Sprintf("Received secret shared parameters \"%.10s...\" (len: %d)", params, len(params)), GameID, ctx.Act.GameID) @@ -137,24 +173,44 @@ func (f *AmphoraFeeder) feedAndRead(params []string, feedPort string, ctx *CtxCo return f.carrier.Read(conv, isBulk) } -func (f *AmphoraFeeder) writeToAmphora(act *Activation, resp Result) ([]string, error) { +func (f *AmphoraFeeder) writeToAmphora(act *Activation, opaInput map[string]interface{}, resp Result) ([]string, error) { client := f.conf.AmphoraClient + generatedTags, err := f.conf.OpaClient.GenerateTags(opaInput) + if err != nil { + return nil, fmt.Errorf("failed to generate tags for program output: %w", err) + } + for i := range generatedTags { + if generatedTags[i].ValueType == "" { + generatedTags[i].ValueType = "STRING" + } + } + tags := []amphora.Tag{ + amphora.Tag{ + ValueType: "STRING", + Key: "gameID", + Value: act.GameID, + }, + } + tags = append(tags, generatedTags...) os := amphora.SecretShare{ SecretID: act.GameID, // When writing to Amphora, the slice has exactly 1 element. Data: resp.Response[0], - Tags: []amphora.Tag{ - amphora.Tag{ - ValueType: "STRING", - Key: "gameID", - Value: act.GameID, - }, - }, + Tags: tags, } - err := client.CreateSecretShare(&os) + err = client.CreateSecretShare(&os) f.logger.Infow(fmt.Sprintf("Created secret share with id %s", os.SecretID), GameID, act.GameID) if err != nil { return nil, err } return []string{act.GameID}, nil } + +func findValueForKeyInTags(tags []amphora.Tag, key string) (string, bool) { + for _, tag := range tags { + if tag.Key == key { + return tag.Value, true + } + } + return "", false +} diff --git a/pkg/ephemeral/io/feeder_test.go b/pkg/ephemeral/io/feeder_test.go index 38c13e9f..19202c87 100644 --- a/pkg/ephemeral/io/feeder_test.go +++ b/pkg/ephemeral/io/feeder_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021-2023 - for information on the respective copyright owner +// Copyright (c) 2021-2024 - for information on the respective copyright owner // see the NOTICE file and/or the repository https://github.com/carbynestack/ephemeral. // // SPDX-License-Identifier: Apache-2.0 @@ -34,6 +34,7 @@ var _ = Describe("Feeder", func() { f = AmphoraFeeder{ conf: &SPDZEngineTypedConfig{ AmphoraClient: &FakeAmphoraClient{}, + OpaClient: &FakeOpaClient{}, }, carrier: carrier, logger: zap.NewNop().Sugar(), @@ -175,10 +176,21 @@ var _ = Describe("Feeder", func() { }) }) +type FakeOpaClient struct { +} + +func (f *FakeOpaClient) GenerateTags(interface{}) ([]amphora.Tag, error) { + return []amphora.Tag{}, nil +} + +func (f *FakeOpaClient) CanExecute(interface{}) (bool, error) { + return true, nil +} + type FakeAmphoraClient struct { } -func (f *FakeAmphoraClient) GetSecretShare(string) (amphora.SecretShare, error) { +func (f *FakeAmphoraClient) GetSecretShare(string, string) (amphora.SecretShare, error) { return amphora.SecretShare{}, nil } func (f *FakeAmphoraClient) CreateSecretShare(*amphora.SecretShare) error { @@ -188,7 +200,7 @@ func (f *FakeAmphoraClient) CreateSecretShare(*amphora.SecretShare) error { type BrokenReadFakeAmphoraClient struct { } -func (f *BrokenReadFakeAmphoraClient) GetSecretShare(string) (amphora.SecretShare, error) { +func (f *BrokenReadFakeAmphoraClient) GetSecretShare(string, string) (amphora.SecretShare, error) { return amphora.SecretShare{}, errors.New("amphora read error") } func (f *BrokenReadFakeAmphoraClient) CreateSecretShare(*amphora.SecretShare) error { @@ -198,7 +210,7 @@ func (f *BrokenReadFakeAmphoraClient) CreateSecretShare(*amphora.SecretShare) er type BrokenWriteFakeAmphoraClient struct { } -func (f *BrokenWriteFakeAmphoraClient) GetSecretShare(string) (amphora.SecretShare, error) { +func (f *BrokenWriteFakeAmphoraClient) GetSecretShare(string, string) (amphora.SecretShare, error) { return amphora.SecretShare{}, nil } func (f *BrokenWriteFakeAmphoraClient) CreateSecretShare(*amphora.SecretShare) error { diff --git a/pkg/ephemeral/network/proxy.go b/pkg/ephemeral/network/proxy.go index 8d8fb5b6..5d5118cf 100644 --- a/pkg/ephemeral/network/proxy.go +++ b/pkg/ephemeral/network/proxy.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021-2023 - for information on the respective copyright owner +// Copyright (c) 2021-2024 - for information on the respective copyright owner // see the NOTICE file and/or the repository https://github.com/carbynestack/ephemeral. // // SPDX-License-Identifier: Apache-2.0 @@ -110,7 +110,7 @@ func (p *Proxy) checkConnectionToPeers() error { proxyEntry := proxyEntry waitGroup.Add(1) go func() { - err := p.checkTCPConnectionToPeer(proxyEntry) + err := p.checkTCPConnectionToPeer(p.ctx.Context, proxyEntry) defer waitGroup.Done() if err != nil { errorsCheckingConnection = append(errorsCheckingConnection, err) @@ -142,9 +142,9 @@ func (p *Proxy) addProxyEntry(config *ProxyConfig) *PingAwareTarget { return pat } -func (p *Proxy) checkTCPConnectionToPeer(config *ProxyConfig) error { +func (p *Proxy) checkTCPConnectionToPeer(ctx context.Context, config *ProxyConfig) error { p.logger.Info(fmt.Sprintf("Checking if connection to peer works for config: %s", config)) - err := p.tcpChecker.Verify(config.Host, config.Port) + err := p.tcpChecker.Verify(ctx, config.Host, config.Port) if err != nil { return fmt.Errorf("error checking connection to the peer '%s:%s': %s", config.Host, config.Port, err) } diff --git a/pkg/ephemeral/network/tcpchecker.go b/pkg/ephemeral/network/tcpchecker.go index 0a30e29e..6cb012c6 100644 --- a/pkg/ephemeral/network/tcpchecker.go +++ b/pkg/ephemeral/network/tcpchecker.go @@ -1,10 +1,11 @@ -// Copyright (c) 2021 - for information on the respective copyright owner +// Copyright (c) 2021-2024 - for information on the respective copyright owner // see the NOTICE file and/or the repository https://github.com/carbynestack/ephemeral. // // SPDX-License-Identifier: Apache-2.0 package network import ( + "context" "fmt" "io" "net" @@ -15,7 +16,7 @@ import ( // NetworkChecker verifies the network connectivity between the players before starting the computation. type NetworkChecker interface { - Verify(string, string) error + Verify(context.Context, string, string) error } // NoopChecker verifies the network for all MPC players is in place. @@ -23,7 +24,7 @@ type NoopChecker struct { } // Verify checks network connectivity between the players and communicates its results to discovery and players FSM. -func (t *NoopChecker) Verify(host, port string) error { +func (t *NoopChecker) Verify(context.Context, string, string) error { return nil } @@ -48,10 +49,12 @@ type TCPChecker struct { } // Verify checks network connectivity between the players and communicates its results to discovery and players FSM. -func (t *TCPChecker) Verify(host, port string) error { +func (t *TCPChecker) Verify(ctx context.Context, host, port string) error { done := time.After(t.conf.RetryTimeout) for { select { + case <-ctx.Done(): + return fmt.Errorf("TCPCheck for '%s:%s' aborted after %d attempts", host, port, t.retries) case <-done: return fmt.Errorf("TCPCheck for '%s:%s' failed after %s and %d attempts", host, port, t.conf.RetryTimeout.String(), t.retries) default: diff --git a/pkg/ephemeral/network/tcpchecker_test.go b/pkg/ephemeral/network/tcpchecker_test.go index 60743695..40eb8641 100644 --- a/pkg/ephemeral/network/tcpchecker_test.go +++ b/pkg/ephemeral/network/tcpchecker_test.go @@ -1,10 +1,11 @@ -// Copyright (c) 2021 - for information on the respective copyright owner +// Copyright (c) 2021-2024 - for information on the respective copyright owner // see the NOTICE file and/or the repository https://github.com/carbynestack/ephemeral. // // SPDX-License-Identifier: Apache-2.0 package network import ( + "context" "io" "net" "sync" @@ -48,7 +49,7 @@ var _ = Describe("TcpChecker", func() { Logger: zap.NewNop().Sugar(), } checker := NewTCPChecker(conf) - err := checker.Verify(host, port) + err := checker.Verify(context.TODO(), host, port) Expect(err).NotTo(HaveOccurred()) wg.Wait() }) @@ -59,7 +60,7 @@ var _ = Describe("TcpChecker", func() { Logger: zap.NewNop().Sugar(), } checker := NewTCPChecker(conf) - err := checker.Verify(host, port) + err := checker.Verify(context.TODO(), host, port) Expect(err).To(HaveOccurred()) }) It("returns an error if dialing succeeds but the connection is closed down shortly", func() { @@ -87,7 +88,7 @@ var _ = Describe("TcpChecker", func() { Logger: zap.NewNop().Sugar(), } checker := NewTCPChecker(conf) - err := checker.Verify(host, port) + err := checker.Verify(context.TODO(), host, port) Expect(err).To(HaveOccurred()) Expect(checker.retries > 1).To(BeTrue()) wg.Wait() @@ -100,8 +101,22 @@ var _ = Describe("TcpChecker", func() { Logger: zap.NewNop().Sugar(), } checker := NewTCPChecker(conf) - err := checker.Verify(host, port) + err := checker.Verify(context.TODO(), host, port) Expect(err).To(HaveOccurred()) Expect(checker.retries > 1).To(BeTrue()) }) + It("aborts if context is closed", func() { + ctx, cancel := context.WithCancel(context.TODO()) + cancel() + conf := &TCPCheckerConf{ + DialTimeout: 50 * time.Millisecond, + RetryTimeout: 100 * time.Millisecond, + Logger: zap.NewNop().Sugar(), + } + checker := NewTCPChecker(conf) + err := checker.Verify(ctx, host, port) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("TCPCheck for 'localhost:9999' aborted after 0 attempts")) + Expect(checker.retries == 0).To(BeTrue()) + }) }) diff --git a/pkg/ephemeral/server.go b/pkg/ephemeral/server.go index 2f7bf0a8..acce692a 100644 --- a/pkg/ephemeral/server.go +++ b/pkg/ephemeral/server.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021-2023 - for information on the respective copyright owner +// Copyright (c) 2021-2024 - for information on the respective copyright owner // see the NOTICE file and/or the repository https://github.com/carbynestack/ephemeral. // // SPDX-License-Identifier: Apache-2.0 @@ -42,28 +42,32 @@ var ( ) // NewServer returns a new server. -func NewServer(compile func(*CtxConfig) error, activate func(*CtxConfig) ([]byte, error), logger *zap.SugaredLogger, config *SPDZEngineTypedConfig) *Server { +func NewServer(authUserIdField string, + compile func(*CtxConfig) error, + activate func(*CtxConfig) ([]byte, error), logger *zap.SugaredLogger, config *SPDZEngineTypedConfig) *Server { return &Server{ - player: &PlayerWithIO{}, - compile: compile, - activate: activate, - logger: logger, - config: config, - executor: NewCommander(), + authUserIdField: authUserIdField, + player: &PlayerWithIO{}, + compile: compile, + activate: activate, + logger: logger, + config: config, + executor: NewCommander(), } } // Server is a HTTP server which wraps the handling of incoming requests that trigger the MPC computation. type Server struct { - player AbstractPlayerWithIO - compile func(*CtxConfig) error - activate func(*CtxConfig) ([]byte, error) - logger *zap.SugaredLogger - config *SPDZEngineTypedConfig - respCh chan []byte - errCh chan error - execErrCh chan error - executor Executor + authUserIdField string + player AbstractPlayerWithIO + compile func(*CtxConfig) error + activate func(*CtxConfig) ([]byte, error) + logger *zap.SugaredLogger + config *SPDZEngineTypedConfig + respCh chan []byte + errCh chan error + execErrCh chan error + executor Executor } // MethodFilter assures that only HTTP POST requests are able to get through. @@ -89,11 +93,19 @@ func (s *Server) MethodFilter(next http.Handler) http.Handler { }) } -// BodyFilter verifies all necessary parameters are set in the request body. +// RequestFilter verifies all necessary headers and parameters in the request body are set. // Also sets the CtxConfig to the request -func (s *Server) BodyFilter(next http.Handler) http.Handler { +func (s *Server) RequestFilter(next http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { var act Activation + authorizedUser, err := GetUserFromAuthHeader(req.Header.Get("Authorization"), s.authUserIdField) + if err != nil { + msg := "unauthorized request" + writer.WriteHeader(http.StatusUnauthorized) + writer.Write([]byte(msg)) + s.logger.Errorw(msg, "Error", err) + return + } if req.Body == nil { msg := "request body is nil" writer.WriteHeader(http.StatusBadRequest) @@ -104,7 +116,7 @@ func (s *Server) BodyFilter(next http.Handler) http.Handler { bodyBytes, _ := ioutil.ReadAll(req.Body) req.Body.Close() req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) - err := json.Unmarshal(bodyBytes, &act) + err = json.Unmarshal(bodyBytes, &act) if err != nil { msg := "error decoding the request body" writer.WriteHeader(http.StatusBadRequest) @@ -147,8 +159,9 @@ func (s *Server) BodyFilter(next http.Handler) http.Handler { } con := context.Background() ctx := &CtxConfig{ - Act: &act, - Spdz: s.config, + AuthorizedUser: authorizedUser, + Act: &act, + Spdz: s.config, } con = context.WithValue(con, ctxConf, ctx) r := req.Clone(con) @@ -157,6 +170,43 @@ func (s *Server) BodyFilter(next http.Handler) http.Handler { }) } +func GetUserFromAuthHeader(header string, idField string) (string, error) { + token := strings.TrimPrefix(header, "Bearer ") + if token == "" { + return "", fmt.Errorf("no token provided") + } + return GetUserIDFromToken(token, idField) +} + +func GetUserIDFromToken(token string, field string) (string, error) { + jwtParts := strings.Split(token, ".") + if len(jwtParts) != 3 { + return "", fmt.Errorf("invalid JWT format") + } + jwt, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(jwtParts[1]) + if err != nil { + return "", fmt.Errorf("error decoding JWT claims: %w", err) + } + var claimsMap map[string]interface{} + err = json.Unmarshal(jwt, &claimsMap) + if err != nil { + return "", fmt.Errorf("error unmarshalling JWT claims: %w", err) + } + var ok bool + path := strings.Split(field, ".") + for _, part := range path[:len(path)-1] { + claimsMap, ok = claimsMap[part].(map[string]interface{}) + if !ok { + return "", fmt.Errorf("field %s not found in JWT claims or invalid", part) + } + } + id, ok := claimsMap[path[len(path)-1]].(string) + if !ok { + return "", fmt.Errorf("field %s is not a string", field) + } + return id, nil +} + // CompilationHandler parses the JSON payload and adds it to the request context. func (s *Server) CompilationHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { diff --git a/pkg/ephemeral/server_test.go b/pkg/ephemeral/server_test.go index 578ca6b6..e6c32c1c 100644 --- a/pkg/ephemeral/server_test.go +++ b/pkg/ephemeral/server_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021-2023 - for information on the respective copyright owner +// Copyright (c) 2021-2024 - for information on the respective copyright owner // see the NOTICE file and/or the repository https://github.com/carbynestack/ephemeral. // // SPDX-License-Identifier: Apache-2.0 @@ -7,8 +7,10 @@ package ephemeral import ( "bytes" "context" + "encoding/base64" "encoding/json" "errors" + "fmt" "github.com/carbynestack/ephemeral/pkg/discovery/fsm" "time" @@ -33,6 +35,7 @@ var _ = Describe("Server", func() { ) const gameID = "71b2a100-f3f6-11e9-81b4-2a2ae2dbcce4" + authHeader := fmt.Sprintf("Bearer header.%s.signature", base64.StdEncoding.WithPadding(base64.NoPadding).EncodeToString([]byte(`{"sub":"someID"}`))) Context("when sending http requests", func() { BeforeEach(func() { act = &Activation{ @@ -49,7 +52,7 @@ var _ = Describe("Server", func() { StateTimeout: 10 * time.Second, NetworkEstablishTimeout: 10 * time.Second, } - s = NewServer(func(*CtxConfig) error { return nil }, func(*CtxConfig) ([]byte, error) { return nil, nil }, l, config) + s = NewServer("sub", func(*CtxConfig) error { return nil }, func(*CtxConfig) ([]byte, error) { return nil, nil }, l, config) }) Context("when going through body filter", func() { @@ -63,14 +66,16 @@ var _ = Describe("Server", func() { }) body, _ := json.Marshal(&act) req, _ := http.NewRequest("POST", "/", bytes.NewReader(body)) - s.BodyFilter(handler200).ServeHTTP(rr, req) + req.Header.Add("Authorization", authHeader) + s.RequestFilter(handler200).ServeHTTP(rr, req) }) Context("when the game id is not a valid UUID", func() { It("responds with 400 http code", func() { act.GameID = "123" body, _ := json.Marshal(act) req, _ := http.NewRequest("POST", "/", bytes.NewReader(body)) - s.BodyFilter(handler200).ServeHTTP(rr, req) + req.Header.Add("Authorization", authHeader) + s.RequestFilter(handler200).ServeHTTP(rr, req) respCode := rr.Code respBody := rr.Body.String() Expect(respCode).To(Equal(http.StatusBadRequest)) @@ -82,7 +87,8 @@ var _ = Describe("Server", func() { act.GameID = gameID body, _ := json.Marshal(&act) req, _ := http.NewRequest("POST", "/", bytes.NewReader(body)) - s.BodyFilter(handler200).ServeHTTP(rr, req) + req.Header.Add("Authorization", authHeader) + s.RequestFilter(handler200).ServeHTTP(rr, req) respCode := rr.Code Expect(respCode).To(Equal(http.StatusOK)) }) @@ -90,7 +96,8 @@ var _ = Describe("Server", func() { Context("when the body is empty", func() { It("returns a 400 response code", func() { req, _ := http.NewRequest("POST", "/", nil) - s.BodyFilter(handler200).ServeHTTP(rr, req) + req.Header.Add("Authorization", authHeader) + s.RequestFilter(handler200).ServeHTTP(rr, req) respCode := rr.Code respBody := rr.Body.String() Expect(respCode).To(Equal(http.StatusBadRequest)) @@ -102,7 +109,8 @@ var _ = Describe("Server", func() { body := []byte("a") checker := http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) {}) req, _ := http.NewRequest("POST", "/", bytes.NewReader(body)) - s.BodyFilter(checker).ServeHTTP(rr, req) + req.Header.Add("Authorization", authHeader) + s.RequestFilter(checker).ServeHTTP(rr, req) respCode := rr.Code respBody := rr.Body.String() Expect(respCode).To(Equal(http.StatusBadRequest)) @@ -115,6 +123,7 @@ var _ = Describe("Server", func() { Context("when a get request is being sent", func() { It("returns a 405 response code", func() { req, _ := http.NewRequest("GET", "/", nil) + req.Header.Add("Authorization", authHeader) s.MethodFilter(handler200).ServeHTTP(rr, req) respCode := rr.Code respBody := rr.Body.String() @@ -425,6 +434,44 @@ var _ = Describe("Server", func() { }) }) }) + + Context("when extracting authorization data form request", func() { + It("fails when bearer token is not provided", func() { + _, err := GetUserFromAuthHeader("", "sub") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("no token provided")) + }) + It("fails when bearer token is not valid", func() { + _, err := GetUserFromAuthHeader("Bearer invalid.token", "sub") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("invalid JWT format")) + }) + It("fails when jwt data field is invalid", func() { + _, err := GetUserFromAuthHeader( + fmt.Sprintf( + "Bearer header.%s.signature", + base64.StdEncoding.WithPadding(base64.NoPadding).EncodeToString([]byte("{"))), "sub") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(HavePrefix("error unmarshalling JWT claims")) + }) + It("returns the user id when the token is valid", func() { + id, err := GetUserFromAuthHeader( + fmt.Sprintf( + "Bearer header.%s.signature", + base64.StdEncoding.WithPadding(base64.NoPadding).EncodeToString([]byte(`{"sub":"someID"}`))), "sub") + Expect(err).NotTo(HaveOccurred()) + Expect(id).To(Equal("someID")) + }) + It("returns the user id when field is nested", func() { + id, err := GetUserFromAuthHeader( + fmt.Sprintf( + "Bearer header.%s.signature", + base64.StdEncoding.WithPadding(base64.NoPadding).EncodeToString([]byte(`{"traits": {"email": "someMail"}}`))), "traits.email") + Expect(err).NotTo(HaveOccurred()) + Expect(id).To(Equal("someMail")) + }) + + }) }) var _ = Describe("PlayerWithIO", func() { diff --git a/pkg/opa/client.go b/pkg/opa/client.go new file mode 100644 index 00000000..23b84720 --- /dev/null +++ b/pkg/opa/client.go @@ -0,0 +1,123 @@ +// Copyright (c) 2024 - for information on the respective copyright owner +// see the NOTICE file and/or the repository https://github.com/carbynestack/ephemeral. +// +// SPDX-License-Identifier: Apache-2.0 + +package opa + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/asaskevich/govalidator" + "github.com/carbynestack/ephemeral/pkg/amphora" + "go.uber.org/zap" + "io/ioutil" + "net/http" + "net/url" + "strings" +) + +const ( + TagsAction = "tags" + ExecuteAction = "execute" +) + +type OpaRequest struct { + Input interface{} `json:"input"` +} + +type TagResponse struct { + Tags []amphora.Tag `json:"result"` +} + +type ExecuteResponse struct { + IsAllowed bool `json:"result"` +} + +// AbstractClient is an interface that defines the methods that an OPA client must implement. +type AbstractClient interface { + GenerateTags(input interface{}) ([]amphora.Tag, error) + CanExecute(input interface{}) (bool, error) +} + +// NewClient creates a new OPA client with the given endpoint and policy package. It returns an error if the endpoint is +// invalid or the policy package is empty. +func NewClient(logger *zap.SugaredLogger, endpoint string, policyPackage string) (*Client, error) { + u, err := url.Parse(endpoint) + if err != nil || !govalidator.IsURL(u.String()) { + return nil, fmt.Errorf("invalid URL: %w", err) + } + if strings.TrimSpace(policyPackage) == "" { + return nil, fmt.Errorf("invalid policy package") + } + return &Client{Logger: logger, HttpClient: http.Client{}, URL: *u, PolicyPackage: strings.TrimSpace(policyPackage)}, nil +} + +// Client represents an OPA client that can be used to interact with an OPA server. +type Client struct { + URL url.URL + PolicyPackage string + HttpClient http.Client + Logger *zap.SugaredLogger +} + +// GenerateTags generates tags for the data described by the data provided according to the policy package. It returns +// the tags if the request was successful. An error is returned if the request fails. +func (c *Client) GenerateTags(data interface{}) ([]amphora.Tag, error) { + result := TagResponse{} + err := c.makeOpaRequest(TagsAction, data, &result) + if err != nil { + return nil, err + } + return result.Tags, nil + +} + +// CanExecute checks if the program can be executed with the input described by the data provided according to the +// policy package. It returns true if the program can be executed, false otherwise. An error is returned if the request +// fails. +func (c *Client) CanExecute(data interface{}) (bool, error) { + result := ExecuteResponse{} + err := c.makeOpaRequest(ExecuteAction, data, &result) + if err != nil { + return false, err + } + return result.IsAllowed, nil +} + +func (c *Client) makeOpaRequest(action string, data interface{}, v interface{}) error { + payload, err := json.Marshal(OpaRequest{Input: data}) + if err != nil { + return fmt.Errorf("invalid opa input: %w", err) + } + bytes.NewBuffer(payload) + req, err := http.NewRequest( + "POST", + fmt.Sprintf("%s/v1/data/%s/%s", + strings.Trim(c.URL.String(), "/"), + strings.ReplaceAll(c.PolicyPackage, ".", "/"), + action), + bytes.NewBuffer(payload)) + if err != nil { + return fmt.Errorf("failed to create opa \"%s\" request: %w", action, err) + } + c.Logger.Debugw("Sending OPA request", "url", req.URL.String(), "payload", string(payload)) + resp, err := c.HttpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to check \"%s\" access: %w", action, err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to check \"%s\" access (Code %d)", action, resp.StatusCode) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read opa response body: %w", err) + } + err = json.Unmarshal(body, &v) + if err != nil { + return fmt.Errorf("failed to unmarshal opa response body: %w", err) + } + return nil +} diff --git a/pkg/opa/opa_suite_test.go b/pkg/opa/opa_suite_test.go new file mode 100644 index 00000000..1aceda5e --- /dev/null +++ b/pkg/opa/opa_suite_test.go @@ -0,0 +1,17 @@ +// Copyright (c) 2024 - for information on the respective copyright owner +// see the NOTICE file and/or the repository https://github.com/carbynestack/ephemeral. +// +// SPDX-License-Identifier: Apache-2.0 +package opa_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestOpa(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Opa Suite") +} diff --git a/pkg/opa/opa_test.go b/pkg/opa/opa_test.go new file mode 100644 index 00000000..c689dde3 --- /dev/null +++ b/pkg/opa/opa_test.go @@ -0,0 +1,154 @@ +// Copyright (c) 2024 - for information on the respective copyright owner +// see the NOTICE file and/or the repository https://github.com/carbynestack/ephemeral. +// +// SPDX-License-Identifier: Apache-2.0 + +package opa_test + +import ( + "github.com/carbynestack/ephemeral/pkg/amphora" + . "github.com/carbynestack/ephemeral/pkg/opa" + "go.uber.org/zap" + "net/http" + "net/http/httptest" + "net/url" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("OpaClient", func() { + logger := zap.NewNop().Sugar() + Context("when creating a new client", func() { + It("returns an error when the endpoint is invalid", func() { + client, err := NewClient(logger, "invalid-url", "valid.policy.package") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(HavePrefix("invalid URL")) + Expect(client).To(BeNil()) + }) + It("returns an error when the policy package is empty", func() { + client, err := NewClient(logger, "http://valid-url.com", "") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(HavePrefix("invalid policy package")) + Expect(client).To(BeNil()) + }) + It("returns a new client when the endpoint and policy package are valid", func() { + client, err := NewClient(logger, "http://valid-url.com", "valid.policy.package") + Expect(err).NotTo(HaveOccurred()) + Expect(client.URL.String()).To(Equal("http://valid-url.com")) + Expect(client.PolicyPackage).To(Equal("valid.policy.package")) + }) + }) + + Context("when generating tags", func() { + It("returns tags when the response is valid", func() { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"result": [{"key": "tag1", "value": "value1", "valueType": "STRING"}, {"key": "tag2", "value": "1", "valueType": "LONG"}]}`)) + })) + defer server.Close() + + u, _ := url.Parse(server.URL) + client := &Client{ + URL: *u, + HttpClient: http.Client{}, + Logger: logger, + } + + tags, err := client.GenerateTags(map[string]interface{}{"key": "value"}) + Expect(err).NotTo(HaveOccurred()) + Expect(tags).To(Equal([]amphora.Tag{ + {Key: "tag1", Value: "value1", ValueType: "STRING"}, + {Key: "tag2", Value: "1", ValueType: "LONG"}})) + }) + It("returns an error when the response code is not 200", func() { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"result": []}`)) + })) + defer server.Close() + + u, _ := url.Parse(server.URL) + client := &Client{ + URL: *u, + HttpClient: http.Client{}, + Logger: logger, + } + + _, err := client.GenerateTags(map[string]interface{}{"key": "value"}) + Expect(err).To(HaveOccurred()) + }) + It("returns an error when the response body is invalid", func() { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`invalid json`)) + })) + defer server.Close() + + u, _ := url.Parse(server.URL) + client := &Client{ + URL: *u, + HttpClient: http.Client{}, + Logger: logger, + } + + _, err := client.GenerateTags(map[string]interface{}{"key": "value"}) + Expect(err).To(HaveOccurred()) + }) + }) + + Context("when checking if program can be executed", func() { + It("returns true when the response is valid", func() { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"result": true}`)) + })) + defer server.Close() + + u, _ := url.Parse(server.URL) + client := &Client{ + URL: *u, + HttpClient: http.Client{}, + Logger: logger, + } + + result, err := client.CanExecute(map[string]interface{}{"key": "value"}) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeTrue()) + }) + It("returns an error when the response code is not 200", func() { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"result": true}`)) + })) + defer server.Close() + + u, _ := url.Parse(server.URL) + client := &Client{ + URL: *u, + HttpClient: http.Client{}, + Logger: logger, + } + + _, err := client.CanExecute(map[string]interface{}{"key": "value"}) + Expect(err).To(HaveOccurred()) + }) + It("returns an error when the response body is invalid", func() { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`invalid json`)) + })) + defer server.Close() + + u, _ := url.Parse(server.URL) + client := &Client{ + URL: *u, + HttpClient: http.Client{}, + Logger: logger, + } + + _, err := client.CanExecute(map[string]interface{}{"key": "value"}) + Expect(err).To(HaveOccurred()) + }) + }) +}) diff --git a/pkg/types/consts.go b/pkg/types/consts.go index 5f04d43f..702ce44f 100644 --- a/pkg/types/consts.go +++ b/pkg/types/consts.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021-2023 - for information on the respective copyright owner +// Copyright (c) 2021-2024 - for information on the respective copyright owner // see the NOTICE file and/or the repository https://github.com/carbynestack/ephemeral. // // SPDX-License-Identifier: Apache-2.0 @@ -52,4 +52,6 @@ const ( EventScope = "EventScope" EventScopeAll = "EventScopeAll" EventScopeSelf = "EventScropeSelf" + + DefaultPolicy = "carbynestack.def" ) diff --git a/pkg/types/types.go b/pkg/types/types.go index 03af0659..428cf776 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021-2023 - for information on the respective copyright owner +// Copyright (c) 2021-2024 - for information on the respective copyright owner // see the NOTICE file and/or the repository https://github.com/carbynestack/ephemeral. // // SPDX-License-Identifier: Apache-2.0 @@ -9,6 +9,7 @@ import ( "github.com/carbynestack/ephemeral/pkg/amphora" "github.com/carbynestack/ephemeral/pkg/castor" pb "github.com/carbynestack/ephemeral/pkg/discovery/transport/proto" + "github.com/carbynestack/ephemeral/pkg/opa" "math/big" "time" @@ -68,6 +69,12 @@ type Activation struct { Output OutputConfig `json:"output"` } +type ActivationInput struct { + SecretId string `json:"secretId"` + Owner string `json:"owner"` + AccessPolicy string `json:"accessPolicy"` +} + // ProxyConfig is the configuration used by the proxy when the connection between players is established. type ProxyConfig struct { Host string `json:"host"` @@ -77,15 +84,18 @@ type ProxyConfig struct { // CtxConfig contains both execution and platform specific parameters. type CtxConfig struct { - Act *Activation - Spdz *SPDZEngineTypedConfig - ProxyEntries []*ProxyConfig - ErrCh chan error - Context context.Context + AuthorizedUser string + Act *Activation + Spdz *SPDZEngineTypedConfig + ProxyEntries []*ProxyConfig + ErrCh chan error + Context context.Context } // SPDZEngineConfig is the VPC specific configuration. type SPDZEngineConfig struct { + ProgramIdentifier string `json:"programIdentifier"` + AuthUserIdField string `json:"authUserIdField"` RetrySleep string `json:"retrySleep"` NetworkEstablishTimeout string `json:"networkEstablishTimeout"` Prime string `json:"prime"` @@ -97,6 +107,7 @@ type SPDZEngineConfig struct { // being set when compiling SPDZ where storage size is 16 for USE_GF2N_LONG=1, or 8 if set to 0 Gf2nStorageSize int32 `json:"gf2nStorageSize"` PrepFolder string `json:"prepFolder"` + OpaConfig OpaConfig `json:"opaConfig"` AmphoraConfig AmphoraConfig `json:"amphoraConfig"` CastorConfig CastorConfig `json:"castorConfig"` FrontendURL string `json:"frontendURL"` @@ -108,6 +119,11 @@ type SPDZEngineConfig struct { ComputationTimeout string `json:"computationTimeout"` } +type OpaConfig struct { + Endpoint string `json:"endpoint"` + PolicyPackage string `json:"policyPackage"` +} + // AmphoraConfig specifies the amphora host parameters. type AmphoraConfig struct { Host string `json:"host"` @@ -145,6 +161,8 @@ type OutputConfig struct { // SPDZEngineTypedConfig reflects SPDZEngineConfig, but it contains the real property types. // We need this type, since the default json decoder doesn't know how to deserialize big.Int. type SPDZEngineTypedConfig struct { + ProgramIdentifier string + AuthUserIdField string RetrySleep time.Duration NetworkEstablishTimeout time.Duration Prime big.Int @@ -154,6 +172,7 @@ type SPDZEngineTypedConfig struct { Gf2nBitLength int32 Gf2nStorageSize int32 PrepFolder string + OpaClient opa.AbstractClient AmphoraClient amphora.AbstractClient CastorClient castor.AbstractClient TupleStock int32 diff --git a/pkg/utils/mocks.go b/pkg/utils/mocks.go index 6eb8971f..ef3425f4 100644 --- a/pkg/utils/mocks.go +++ b/pkg/utils/mocks.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 - for information on the respective copyright owner +// Copyright (c) 2021-2024 - for information on the respective copyright owner // see the NOTICE file and/or the repository https://github.com/carbynestack/ephemeral. // // SPDX-License-Identifier: Apache-2.0 @@ -18,6 +18,7 @@ import ( // MockedRoundTripper mocks http.RoundTripper for testing which always returns successful type MockedRoundTripper struct { ExpectedPath string + ExpectedRawQuery string ReturnJSON []byte ExpectedResponseCode int } @@ -29,6 +30,9 @@ func (m *MockedRoundTripper) RoundTrip(req *http.Request) (*http.Response, error if p != m.ExpectedPath { statusCode = http.StatusNotFound } + if m.ExpectedRawQuery != "" && req.URL.RawQuery != m.ExpectedRawQuery { + statusCode = http.StatusNotFound + } b := bytes.NewBuffer(m.ReturnJSON) resp := &http.Response{