diff --git a/api/kms/v1/routing.proto b/api/kms/v1/routing.proto index 9de83a1c8ea6fa538ad5f834f2dfbfb465a8e7f9..36a21b948ccd84dc7cfb49bad00002e01eece6cd 100644 --- a/api/kms/v1/routing.proto +++ b/api/kms/v1/routing.proto @@ -4,7 +4,7 @@ package kms.v1; import "shared/v1/util.proto"; // KmsRoutingService contains all calls that are used by the controller -// to interact with a KMS so configure routes. +// to interact with a KMS to configure routes. service KmsRoutingService { rpc AddRoute(AddRouteRequest) returns (AddRouteResponse); rpc GetRoute(GetRouteRequest) returns (GetRouteResponse); diff --git a/ctrl/cmd/main.go b/ctrl/cmd/main.go index 0f3a7905c8e6e5ae6301c5358ccb0033fcf50e81..2e9a2a80fc20ffc8720c9aaf288e613301d20924 100644 --- a/ctrl/cmd/main.go +++ b/ctrl/cmd/main.go @@ -26,7 +26,7 @@ func main() { flag.StringVar( &bindAddr, "bindaddr", - "127.0.0.1:1337", + "127.0.0.1:1338", "default address and port to bind the controller interface to", ) diff --git a/docs/Tracing.md b/docs/Tracing.md index 4f20d4313fe08a646a2fcf5dbfd53679600632d5..23cd612dee0a680ab5f344b8f3ec6f4ffb220e71 100644 --- a/docs/Tracing.md +++ b/docs/Tracing.md @@ -34,11 +34,10 @@ differentiate spans in a trace. ## Testing -The KMS is instrumented with tracing support on its `HealthKms/Health` service. +The KMS is instrumented with tracing support on its `kms.v1.HealthKmsService/Health` service. For now the traces are just emitted on stdout until we decided for a common trace backend. ```sh -grpcurl -plaintext -import-path api/ -proto api/kms/v1/health.proto localhost:1337 kms.v1.HealthKms/Health - + grpcurl -plaintext -import-path api/ -proto api/kms/v1/health.proto -d '{"message": "hello world"}' localhost:1337 kms.v1.HealthKmsService/Health ``` diff --git a/docs/kms.md b/docs/kms.md new file mode 100644 index 0000000000000000000000000000000000000000..8ba13eb8b129eee01ac95e246c9149426a8bb610 --- /dev/null +++ b/docs/kms.md @@ -0,0 +1,19 @@ +# KMS + +## Manual interactions + +### Health + +```sh + grpcurl -plaintext -import-path api/ -proto api/kms/v1/health.proto localhost:1337 kms.v1.HealthKmsService/Health +``` + +### Routing + +```sh + grpcurl -plaintext -import-path api/ -proto api/kms/v1/routing.proto -d '{"route": {"id": "afeda267-edce-4ab6-af51-a14cf7009206", "next_hop_id": "626b3cbf-9de9-4d85-93da-185abc9ba904", "prev_hop_id": "a3a99140-41ef-4fdf-b1d1-70b947ab1491"}}' localhost:1337 kms.v1.KmsRoutingService/AddRoute +``` + +```sh + grpcurl -plaintext -import-path api/ -proto api/kms/v1/routing.proto -d '{"id": "afeda267-edce-4ab6-af51-a14cf7009206"}' localhost:1337 kms.v1.KmsRoutingService/GetRoute +``` diff --git a/kms/cmd/main.go b/kms/cmd/main.go index 4ce3a723e28399720d93abe656a611251387332f..dbae5a73f7b1b60690380e249296594da63a5a0d 100644 --- a/kms/cmd/main.go +++ b/kms/cmd/main.go @@ -33,7 +33,7 @@ func main() { flag.StringVar( &ctrlAddr, "ctrladdr", - "127.0.0.1:1337", + "127.0.0.1:1338", "address and port of the controller to manage this kms", ) @@ -68,6 +68,7 @@ func main() { go grpcServer.Start( func(server *grpc.Server, app *application.Application) { pb.RegisterHealthKmsServiceServer(server, app.HealthServer) + pb.RegisterKmsRoutingServiceServer(server, app.RoutingServer) }, ) diff --git a/kms/internal/application/app.go b/kms/internal/application/app.go index eac591d4b97973e7b2cdabba6bc50a2fc01567d5..debc80d3804d5d1cf1cd857f6cd4d8aecf266e07 100644 --- a/kms/internal/application/app.go +++ b/kms/internal/application/app.go @@ -1,7 +1,10 @@ package application import ( + "code.fbi.h-da.de/danet/costaquanta/kms/internal/core/ports" "code.fbi.h-da.de/danet/costaquanta/kms/internal/infrastructure/interaction" + "code.fbi.h-da.de/danet/costaquanta/kms/internal/infrastructure/store" + "code.fbi.h-da.de/danet/costaquanta/kms/internal/service" "code.fbi.h-da.de/danet/costaquanta/libs/logging" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.uber.org/zap" @@ -10,6 +13,10 @@ import ( type Application struct { tracer *sdktrace.TracerProvider HealthServer *interaction.HealthServer + + RoutingServer *interaction.RoutingServer + RoutingService ports.RoutingService + RoutingStore ports.RouteStore } func NewApplication(log *zap.Logger, tracer *sdktrace.TracerProvider) *Application { @@ -17,8 +24,22 @@ func NewApplication(log *zap.Logger, tracer *sdktrace.TracerProvider) *Applicati childTracer := tracer.Tracer("healthServer") healthSrv := interaction.NewHealthServer(healthServerLogger, childTracer) + routingStoreLogger := logging.CreateChildLogger(log, "routingStore") + routingStoreTracer := tracer.Tracer("routingStore") + routingStore := store.NewInMemoryRouteStore(routingStoreLogger, routingStoreTracer) + + routingServiceLogger := logging.CreateChildLogger(log, "routingService") + routingServiceTracer := tracer.Tracer("routingService") + rSrv := service.NewRoutingService(routingServiceLogger, routingServiceTracer, routingStore) + + routingLogger := logging.CreateChildLogger(log, "routingServer") + routingTracer := tracer.Tracer("routingServer") + routerSrv := interaction.NewRoutingServer(routingLogger, routingTracer, rSrv) + return &Application{ - HealthServer: healthSrv, - tracer: tracer, + HealthServer: healthSrv, + tracer: tracer, + RoutingServer: routerSrv, + RoutingService: rSrv, } } diff --git a/kms/internal/core/model/route.go b/kms/internal/core/model/route.go new file mode 100644 index 0000000000000000000000000000000000000000..e8dea598f4afa99fc8cdce1941d6085ade2033ac --- /dev/null +++ b/kms/internal/core/model/route.go @@ -0,0 +1,26 @@ +package model + +import "github.com/google/uuid" + +type Route struct { + // In other aspects used as route_id + ID uuid.UUID + // The ID of the KMS that a payload should be forwarded to if this route is used. + NextHopId string + // The ID of the KMS that a payload should be coming from if this route is used. + PrevHopId string + // The crypto algorithm that is used for this route. + // Is relevant for encrypting and decrypting a payload. + CryptoAlgorithm CryptoAlgorithm +} + +// Describes the crypto algorithms that are possible.enum +// More options can be added, this is a current selection useful for us. +type CryptoAlgorithm int32 + +const ( + CryptoAlgorithm_CRYPTO_ALGORITHM_UNSPECIFIED CryptoAlgorithm = 0 + CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM CryptoAlgorithm = 1 + CryptoAlgorithm_CRYPTO_ALGORITHM_OTP CryptoAlgorithm = 2 + CryptoAlgorithm_CRYPTO_ALGORITHM_HYBRID CryptoAlgorithm = 3 +) diff --git a/kms/internal/core/ports/service.go b/kms/internal/core/ports/service.go new file mode 100644 index 0000000000000000000000000000000000000000..5cd2fd4bcbf8fa9716a17b006a91f52c26070104 --- /dev/null +++ b/kms/internal/core/ports/service.go @@ -0,0 +1,15 @@ +package ports + +import ( + "context" + + "code.fbi.h-da.de/danet/costaquanta/kms/internal/core/model" +) + +type RoutingService interface { + AddRoute(context.Context, model.Route) (model.Route, error) + DeleteRoute(context.Context, string) error + GetRoute(context.Context, string) (model.Route, error) + GetRouteList(context.Context) ([]model.Route, error) + UpdateRoute(context.Context, model.Route) error +} diff --git a/kms/internal/core/ports/store.go b/kms/internal/core/ports/store.go new file mode 100644 index 0000000000000000000000000000000000000000..dc57456d7ea205a5f646fa024290ca0789b9f73c --- /dev/null +++ b/kms/internal/core/ports/store.go @@ -0,0 +1,15 @@ +package ports + +import ( + "context" + + "code.fbi.h-da.de/danet/costaquanta/kms/internal/core/model" +) + +type RouteStore interface { + AddRoute(context.Context, model.Route) (model.Route, error) + DeleteRoute(context.Context, string) error + GetRoute(context.Context, string) (model.Route, error) + GetRouteList(context.Context) ([]model.Route, error) + UpdateRoute(context.Context, model.Route) error +} diff --git a/kms/internal/infrastructure/interaction/routing.go b/kms/internal/infrastructure/interaction/routing.go new file mode 100644 index 0000000000000000000000000000000000000000..8291fcc7d630a4ff471d23411646e1f1e1a5ee07 --- /dev/null +++ b/kms/internal/infrastructure/interaction/routing.go @@ -0,0 +1,164 @@ +package interaction + +import ( + "context" + + pb "code.fbi.h-da.de/danet/costaquanta/gen/go/kms/v1" + sharedv1 "code.fbi.h-da.de/danet/costaquanta/gen/go/shared/v1" + "code.fbi.h-da.de/danet/costaquanta/kms/internal/core/model" + "code.fbi.h-da.de/danet/costaquanta/kms/internal/core/ports" + "github.com/google/uuid" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" +) + +type RoutingServer struct { + pb.UnimplementedKmsRoutingServiceServer + + logger *zap.SugaredLogger + tracer trace.Tracer + + srv ports.RoutingService +} + +func NewRoutingServer( + logger *zap.SugaredLogger, + tracer trace.Tracer, + srv ports.RoutingService, +) *RoutingServer { + s := &RoutingServer{ + logger: logger, + tracer: tracer, + srv: srv, + } + + return s +} + +func (r *RoutingServer) AddRoute( + ctx context.Context, + req *pb.AddRouteRequest, +) (*pb.AddRouteResponse, error) { + _, span := r.tracer.Start(ctx, "add-route") + defer span.End() + + uID, err := uuid.Parse(req.GetRoute().GetId()) + if err != nil { + return nil, err + } + + route := model.Route{ + ID: uID, + NextHopId: req.GetRoute().GetNextHopId(), + PrevHopId: req.GetRoute().GetPrevHopId(), + CryptoAlgorithm: model.CryptoAlgorithm(req.GetRoute().GetCryptoAlgorithm()), + } + + createdRoute, err := r.srv.AddRoute(ctx, route) + if err != nil { + return &pb.AddRouteResponse{}, err + } + + return &pb.AddRouteResponse{ + Route: &pb.Route{ + Id: createdRoute.ID.String(), + NextHopId: createdRoute.NextHopId, + PrevHopId: createdRoute.PrevHopId, + CryptoAlgorithm: sharedv1.CryptoAlgorithm(createdRoute.CryptoAlgorithm), + }, + }, nil +} + +func (r *RoutingServer) DeleteRoute( + ctx context.Context, + req *pb.DeleteRouteRequest, +) (*pb.DeleteRouteResponse, error) { + _, span := r.tracer.Start(ctx, "delete-route") + defer span.End() + + err := r.srv.DeleteRoute(ctx, req.GetId()) + if err != nil { + return nil, err + } + + return &pb.DeleteRouteResponse{}, nil +} + +func (r *RoutingServer) GetRoute( + ctx context.Context, + req *pb.GetRouteRequest, +) (*pb.GetRouteResponse, error) { + _, span := r.tracer.Start(ctx, "get-route") + defer span.End() + + route, err := r.srv.GetRoute(ctx, req.GetId()) + if err != nil { + return nil, err + } + + return &pb.GetRouteResponse{ + Route: &pb.Route{ + Id: route.ID.String(), + NextHopId: route.NextHopId, + PrevHopId: route.PrevHopId, + CryptoAlgorithm: sharedv1.CryptoAlgorithm(route.CryptoAlgorithm), + }, + }, nil +} + +func (r *RoutingServer) GetRouteList( + ctx context.Context, + req *pb.GetRouteListRequest, +) (*pb.GetRouteListResponse, error) { + _, span := r.tracer.Start(ctx, "get-route-list") + defer span.End() + + r.logger.Debugf("got get route list %+v", req) + + routes, err := r.srv.GetRouteList(ctx) + if err != nil { + return nil, err + } + + pbRoutes := make([]*pb.Route, len(routes)) + + for i, route := range routes { + pbRoutes[i] = &pb.Route{ + Id: route.ID.String(), + NextHopId: route.NextHopId, + PrevHopId: route.PrevHopId, + CryptoAlgorithm: sharedv1.CryptoAlgorithm(route.CryptoAlgorithm), + } + } + + return &pb.GetRouteListResponse{ + Routes: pbRoutes, + }, nil +} + +func (r *RoutingServer) UpdateRoute( + ctx context.Context, + req *pb.UpdateRouteRequest, +) (*pb.UpdateRouteResponse, error) { + _, span := r.tracer.Start(ctx, "update-route") + defer span.End() + + uID, err := uuid.Parse(req.GetRoute().GetId()) + if err != nil { + return nil, err + } + + route := model.Route{ + ID: uID, + NextHopId: req.GetRoute().GetNextHopId(), + PrevHopId: req.GetRoute().GetPrevHopId(), + CryptoAlgorithm: model.CryptoAlgorithm(req.GetRoute().GetCryptoAlgorithm()), + } + + err = r.srv.UpdateRoute(ctx, route) + if err != nil { + return nil, err + } + + return &pb.UpdateRouteResponse{}, nil +} diff --git a/kms/internal/infrastructure/interaction/routing_test.go b/kms/internal/infrastructure/interaction/routing_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ba61d83a1e5a3ed517890203ae3bd19398c38376 --- /dev/null +++ b/kms/internal/infrastructure/interaction/routing_test.go @@ -0,0 +1,464 @@ +package interaction + +import ( + "context" + "log" + "net" + "testing" + + pb "code.fbi.h-da.de/danet/costaquanta/gen/go/kms/v1" + pbs "code.fbi.h-da.de/danet/costaquanta/gen/go/shared/v1" + "code.fbi.h-da.de/danet/costaquanta/kms/internal/core/model" + "code.fbi.h-da.de/danet/costaquanta/kms/internal/core/ports" + "code.fbi.h-da.de/danet/costaquanta/kms/internal/infrastructure/store" + "code.fbi.h-da.de/danet/costaquanta/kms/internal/service" + "code.fbi.h-da.de/danet/costaquanta/libs/tracing" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/test/bufconn" +) + +func newRoutingServer() (pb.KmsRoutingServiceServer, ports.RouteStore) { + logs := zap.NewExample() + + traceProvider := tracing.GetTestTracer() + defer func() { + if err := traceProvider.Shutdown(context.Background()); err != nil { + logs.Info(err.Error()) + } + }() + + store := store.NewInMemoryRouteStore(logs.Sugar(), traceProvider.Tracer("test")) + svc := service.NewRoutingService(logs.Sugar(), traceProvider.Tracer("test"), store) + srv := NewRoutingServer( + zap.NewExample().Sugar(), + traceProvider.Tracer("test"), + svc, + ) + + return srv, store +} + +func getTestRoutingServer() (pb.KmsRoutingServiceClient, func(), ports.RouteStore) { + buffer := 1024 * 1024 + lis := bufconn.Listen(buffer) + + baseServer := grpc.NewServer() + src, store := newRoutingServer() + pb.RegisterKmsRoutingServiceServer(baseServer, src) + go func() { + if err := baseServer.Serve(lis); err != nil { + log.Printf("error serving server: %v", err) + } + }() + + conn, err := grpc.NewClient("passthrough:///bufnet", + grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { + return lis.Dial() + }), grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Printf("error connecting to server: %v", err) + } + + closer := func() { + err := lis.Close() + if err != nil { + log.Printf("error closing listener: %v", err) + } + baseServer.Stop() + } + + client := pb.NewKmsRoutingServiceClient(conn) + + return client, closer, store +} + +func TestRoutingServer_AddRoute(t *testing.T) { + t.Parallel() + + t.Run("Add new route", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + testUUID := uuid.New() + + client, closer, _ := getTestRoutingServer() + t.Cleanup(func() { + defer closer() + }) + + req := &pb.AddRouteRequest{ + Route: &pb.Route{ + Id: testUUID.String(), + NextHopId: "1337", + PrevHopId: "1338", + CryptoAlgorithm: pbs.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM, + }, + } + + resp, err := client.AddRoute(ctx, req) + require.NoError(t, err) + compareRoutes(t, req.GetRoute(), resp.GetRoute()) + }) + + t.Run("Add existing route should not fail", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + testUUID := uuid.New() + + client, closer, _ := getTestRoutingServer() + t.Cleanup(func() { + defer closer() + }) + + req := &pb.AddRouteRequest{ + Route: &pb.Route{ + Id: testUUID.String(), + NextHopId: "1337", + PrevHopId: "1338", + CryptoAlgorithm: pbs.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM, + }, + } + + resp, err := client.AddRoute(ctx, req) + require.NoError(t, err) + compareRoutes(t, req.GetRoute(), resp.GetRoute()) + + resp, err = client.AddRoute(ctx, req) + require.Error(t, err) + assert.Empty(t, resp) + }) + + t.Run("Should fail if route ID is empty", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + + client, closer, _ := getTestRoutingServer() + + t.Cleanup(func() { + defer closer() + }) + + req := &pb.AddRouteRequest{ + Route: &pb.Route{ + Id: "", + NextHopId: "1337", + PrevHopId: "1338", + CryptoAlgorithm: pbs.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM, + }, + } + + resp, err := client.AddRoute(ctx, req) + require.Error(t, err) + assert.Empty(t, resp) + }) +} + +func TestRoutingServer_DeleteRoute(t *testing.T) { + t.Parallel() + + t.Run("Delete an existing route should succeed", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + testUUID := uuid.New() + + client, closer, store := getTestRoutingServer() + routeInStore := model.Route{ + ID: testUUID, + NextHopId: "1337", + PrevHopId: "1338", + CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM, + } + _, err := store.AddRoute(ctx, routeInStore) + require.NoError(t, err) + + t.Cleanup(func() { + defer closer() + }) + + deleteReq := &pb.DeleteRouteRequest{ + Id: testUUID.String(), + } + + deleteResp, err := client.DeleteRoute(ctx, deleteReq) + require.NoError(t, err) + assert.NotNil(t, deleteResp) + + getResp, err := client.GetRoute(ctx, &pb.GetRouteRequest{ + Id: testUUID.String(), + }) + require.Error(t, err) + assert.Empty(t, getResp) + }) + + t.Run("Delete a non-existing route should fail", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + + client, closer, _ := getTestRoutingServer() + + t.Cleanup(func() { + defer closer() + }) + + deleteReq := &pb.DeleteRouteRequest{ + Id: uuid.New().String(), + } + + deleteResp, err := client.DeleteRoute(ctx, deleteReq) + require.Error(t, err) + assert.Empty(t, deleteResp) + }) +} + +func TestRoutingServer_GetRoute(t *testing.T) { + t.Parallel() + + t.Run("Get an existing route should succeed", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + testUUID := uuid.New() + + client, closer, store := getTestRoutingServer() + + routeInStore := model.Route{ + ID: testUUID, + NextHopId: "1337", + PrevHopId: "1338", + CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM, + } + _, err := store.AddRoute(ctx, routeInStore) + require.NoError(t, err) + + t.Cleanup(func() { + defer closer() + }) + + getResp, err := client.GetRoute(ctx, &pb.GetRouteRequest{ + Id: testUUID.String(), + }) + require.NoError(t, err) + compareRouteToModel(t, routeInStore, getResp.GetRoute()) + }) + + t.Run("Get a non-existing route should fail", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + + client, closer, _ := getTestRoutingServer() + + t.Cleanup(func() { + defer closer() + }) + + getResp, err := client.GetRoute(ctx, &pb.GetRouteRequest{ + Id: uuid.New().String(), + }) + require.Error(t, err) + assert.Empty(t, getResp) + }) + + t.Run("Get a route with an empty ID should fail", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + + client, closer, _ := getTestRoutingServer() + + t.Cleanup(func() { + defer closer() + }) + + getResp, err := client.GetRoute(ctx, &pb.GetRouteRequest{ + Id: "", + }) + require.Error(t, err) + assert.Empty(t, getResp) + }) +} + +func TestRoutingServer_GetRouteList(t *testing.T) { + t.Parallel() + + t.Run("Get an empty route list should succeed", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + + client, closer, _ := getTestRoutingServer() + + t.Cleanup(func() { + defer closer() + }) + + getResp, err := client.GetRouteList(ctx, &pb.GetRouteListRequest{}) + require.NoError(t, err) + assert.Empty(t, getResp.GetRoutes()) + }) + + t.Run("Get a non-empty route list should succeed", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + + testUUID1 := uuid.New() + testUUID2 := uuid.New() + + client, closer, store := getTestRoutingServer() + + routeInStore := model.Route{ + ID: testUUID1, + NextHopId: "1337", + PrevHopId: "1338", + CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM, + } + _, err := store.AddRoute(ctx, routeInStore) + require.NoError(t, err) + + routeInStore2 := model.Route{ + ID: testUUID2, + NextHopId: "1339", + PrevHopId: "1340", + CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM, + } + _, err = store.AddRoute(ctx, routeInStore2) + require.NoError(t, err) + + t.Cleanup(func() { + defer closer() + }) + + getResp, err := client.GetRouteList(ctx, &pb.GetRouteListRequest{}) + require.NoError(t, err) + + assert.Len(t, getResp.GetRoutes(), 2) + + for _, route := range getResp.GetRoutes() { + if route.GetId() == testUUID1.String() { + compareRouteToModel(t, routeInStore, route) + } else if route.GetId() == testUUID2.String() { + compareRouteToModel(t, routeInStore2, route) + } else { + t.Errorf("Unexpected route ID: %s", route.GetId()) + } + } + }) +} + +func TestRoutingServer_UpdateRoute(t *testing.T) { + t.Parallel() + + t.Run("Update an existing route should succeed", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + testUUID := uuid.New() + + client, closer, store := getTestRoutingServer() + + routeInStore := model.Route{ + ID: testUUID, + NextHopId: "1337", + PrevHopId: "1338", + CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM, + } + _, err := store.AddRoute(ctx, routeInStore) + require.NoError(t, err) + + t.Cleanup(func() { + defer closer() + }) + + updateReq := &pb.UpdateRouteRequest{ + Route: &pb.Route{ + Id: testUUID.String(), + NextHopId: "1339", + PrevHopId: "1340", + CryptoAlgorithm: pbs.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM, + }, + } + + updateResp, err := client.UpdateRoute(ctx, updateReq) + require.NoError(t, err) + assert.NotNil(t, updateResp) + + getResp, err := client.GetRoute(ctx, &pb.GetRouteRequest{ + Id: testUUID.String(), + }) + require.NoError(t, err) + compareRoutes(t, updateReq.GetRoute(), getResp.GetRoute()) + }) + + t.Run("Update a non-existing route should fail", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + + client, closer, _ := getTestRoutingServer() + + t.Cleanup(func() { + defer closer() + }) + + updateReq := &pb.UpdateRouteRequest{ + Route: &pb.Route{ + Id: uuid.New().String(), + NextHopId: "1339", + PrevHopId: "1340", + CryptoAlgorithm: pbs.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM, + }, + } + + updateResp, err := client.UpdateRoute(ctx, updateReq) + require.Error(t, err) + assert.Empty(t, updateResp) + }) + + t.Run("Update a route with an empty ID should fail", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + + client, closer, _ := getTestRoutingServer() + + t.Cleanup(func() { + defer closer() + }) + + updateReq := &pb.UpdateRouteRequest{ + Route: &pb.Route{ + Id: "", + NextHopId: "1339", + PrevHopId: "1340", + CryptoAlgorithm: pbs.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM, + }, + } + + updateResp, err := client.UpdateRoute(ctx, updateReq) + require.Error(t, err) + assert.Empty(t, updateResp) + }) +} + +func compareRouteToModel( + t *testing.T, + expected model.Route, + actual *pb.Route, +) { + t.Helper() + + assert.Equal(t, expected.ID.String(), actual.GetId()) + assert.Equal(t, expected.NextHopId, actual.GetNextHopId()) + assert.Equal(t, expected.PrevHopId, actual.GetPrevHopId()) + assert.Equal(t, expected.CryptoAlgorithm, model.CryptoAlgorithm(actual.GetCryptoAlgorithm())) +} + +func compareRoutes( + t *testing.T, + expected *pb.Route, + actual *pb.Route, +) { + t.Helper() + + assert.Equal(t, expected.GetId(), actual.GetId()) + assert.Equal(t, expected.GetNextHopId(), actual.GetNextHopId()) + assert.Equal(t, expected.GetPrevHopId(), actual.GetPrevHopId()) + assert.Equal(t, expected.GetCryptoAlgorithm(), actual.GetCryptoAlgorithm()) +} diff --git a/kms/internal/infrastructure/store/route.go b/kms/internal/infrastructure/store/route.go new file mode 100644 index 0000000000000000000000000000000000000000..a93b169cc25c662310042448ee34c8c26d7d866c --- /dev/null +++ b/kms/internal/infrastructure/store/route.go @@ -0,0 +1,168 @@ +package store + +import ( + "context" + "fmt" + "sync" + + "code.fbi.h-da.de/danet/costaquanta/kms/internal/core/model" + "github.com/google/uuid" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" +) + +type RouteStore interface { + AddRoute(context.Context, model.Route) (model.Route, error) + DeleteRoute(context.Context, string) error + GetRoute(context.Context, string) (model.Route, error) + GetRouteList(context.Context) ([]model.Route, error) + UpdateRoute(context.Context, model.Route) error +} + +type InMemoryRouteStore struct { + dataStore map[uuid.UUID]model.Route + + // Using a RWMutex to allow multiple readers concurrently. + // A write lock can only be acquired once all readers are done and release their locks. + mu sync.RWMutex + + logger *zap.SugaredLogger + tracer trace.Tracer +} + +func NewInMemoryRouteStore(logger *zap.SugaredLogger, tracer trace.Tracer) *InMemoryRouteStore { + return &InMemoryRouteStore{ + dataStore: make(map[uuid.UUID]model.Route), + mu: sync.RWMutex{}, + logger: logger, + tracer: tracer, + } +} + +func (s *InMemoryRouteStore) AddRoute(ctx context.Context, route model.Route) (model.Route, error) { + _, span := s.tracer.Start(ctx, "add-route") + defer span.End() + + s.logger.Debugw("Adding route to store", + "routeId", route.ID, + "nextHopId", route.NextHopId, + "prevHopId", route.PrevHopId, + "cryptoAlgorithm", route.CryptoAlgorithm) + + s.mu.Lock() + defer s.mu.Unlock() + + if _, exists := s.dataStore[route.ID]; exists { + s.logger.Errorw("Route already exists in store", + "routeId", route.ID.String()) + + return model.Route{}, fmt.Errorf("route %s already exists in store", route.ID) + } + + s.dataStore[route.ID] = route + + return route, nil +} + +func (s *InMemoryRouteStore) DeleteRoute(ctx context.Context, routeID string) error { + _, span := s.tracer.Start(ctx, "delete-route") + defer span.End() + + s.logger.Debugw("Deleting route from store", + "routeId", routeID) + + s.mu.Lock() + defer s.mu.Unlock() + + id, err := uuid.Parse(routeID) + if err != nil { + s.logger.Errorw("Failed to parse route ID", + "routeId", routeID, + "error", err) + + return fmt.Errorf("failed to parse route ID: %w", err) + } + + if _, exists := s.dataStore[id]; !exists { + s.logger.Errorw("Route does not exist in store", + "routeId", id.String()) + + return fmt.Errorf("route %s does not exist in store", id) + } + + delete(s.dataStore, id) + + return nil +} + +func (s *InMemoryRouteStore) GetRoute(ctx context.Context, routeID string) (model.Route, error) { + _, span := s.tracer.Start(ctx, "get-route") + defer span.End() + + s.logger.Debugw("Getting route from store", + "routeId", routeID) + + s.mu.RLock() + defer s.mu.RUnlock() + + id, err := uuid.Parse(routeID) + if err != nil { + s.logger.Errorw("Failed to parse route ID", + "routeId", routeID, + "error", err) + + return model.Route{}, fmt.Errorf("failed to parse route ID: %w", err) + } + + route, exists := s.dataStore[id] + if !exists { + s.logger.Errorw("Route does not exist in store", + "routeId", id.String()) + + return model.Route{}, fmt.Errorf("route %s does not exist in store", id) + } + + return route, nil +} + +func (s *InMemoryRouteStore) GetRouteList(ctx context.Context) ([]model.Route, error) { + _, span := s.tracer.Start(ctx, "get-route-list") + defer span.End() + + s.logger.Debug("Getting route list from store") + + s.mu.RLock() + defer s.mu.RUnlock() + + routes := make([]model.Route, 0, len(s.dataStore)) + for _, route := range s.dataStore { + routes = append(routes, route) + } + + return routes, nil +} + +func (s *InMemoryRouteStore) UpdateRoute(ctx context.Context, route model.Route) error { + _, span := s.tracer.Start(ctx, "update-route") + defer span.End() + + s.logger.Debugw("Updating route in store", + "routeId", route.ID, + "nextHopId", route.NextHopId, + "prevHopId", route.PrevHopId, + "cryptoAlgorithm", route.CryptoAlgorithm) + + s.mu.Lock() + defer s.mu.Unlock() + + if _, exists := s.dataStore[route.ID]; !exists { + s.logger.Errorw("Route does not exist in store", + "routeId", route.ID.String()) + + return fmt.Errorf("route %s does not exist in store", route.ID) + } + + s.dataStore[route.ID] = route + + return nil +} diff --git a/kms/internal/infrastructure/store/route_test.go b/kms/internal/infrastructure/store/route_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a86df2f06cae42936fbc3230563e0d73840261df --- /dev/null +++ b/kms/internal/infrastructure/store/route_test.go @@ -0,0 +1,220 @@ +package store + +import ( + "context" + "testing" + + "code.fbi.h-da.de/danet/costaquanta/kms/internal/core/model" + "code.fbi.h-da.de/danet/costaquanta/libs/logging" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace/noop" +) + +func createCtxAndStoreForTest() (*InMemoryRouteStore, context.Context) { + tracer := noop.NewTracerProvider().Tracer("routeStoreTest") + + parentLogger := logging.CreateProductionLogger("kms") + childLogger := logging.CreateChildLogger(parentLogger, "routeStoreTest") + + ctx := context.Background() + + return NewInMemoryRouteStore(childLogger, tracer), ctx +} + +func TestInMemoryRouteStore_AddRoute(t *testing.T) { + t.Parallel() + + t.Run("Add non-existing route to store", func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + + route := model.Route{ + ID: uuid.New(), + NextHopId: "1337", + PrevHopId: "42", + CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM, + } + + addedRoute, err := store.AddRoute(ctx, route) + require.NoError(t, err) + assert.Equal(t, route, addedRoute) + assert.Equal(t, route, store.dataStore[route.ID]) + }) + + t.Run("Add existing route to store should fail", func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + + route := model.Route{ + ID: uuid.New(), + NextHopId: "1337", + PrevHopId: "42", + CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM, + } + store.dataStore[route.ID] = route + + addedRoute, err := store.AddRoute(ctx, route) + require.Error(t, err) + assert.Equal(t, model.Route{}, addedRoute) + }) +} + +func TestInMemoryRouteStore_DeleteRoute(t *testing.T) { + t.Parallel() + + t.Run("Delete existing route from store", func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + + route := model.Route{ + ID: uuid.New(), + NextHopId: "1337", + PrevHopId: "42", + CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM, + } + store.dataStore[route.ID] = route + + err := store.DeleteRoute(ctx, route.ID.String()) + require.NoError(t, err) + + assert.NotContains(t, store.dataStore, route.ID) + }) + + t.Run("Delete non-existing route from store should fail", func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + + err := store.DeleteRoute(ctx, uuid.New().String()) + require.Error(t, err) + }) + + t.Run("Delete existing route twice from store should fail", func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + + route := model.Route{ + ID: uuid.New(), + NextHopId: "1337", + PrevHopId: "42", + CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM, + } + store.dataStore[route.ID] = route + + err := store.DeleteRoute(ctx, route.ID.String()) + require.NoError(t, err) + + err = store.DeleteRoute(ctx, route.ID.String()) + require.Error(t, err) + }) +} + +func TestInMemoryRouteStore_GetRoute(t *testing.T) { + t.Parallel() + + t.Run("Get existing route from store", func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + + route := model.Route{ + ID: uuid.New(), + NextHopId: "1337", + PrevHopId: "42", + CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM, + } + store.dataStore[route.ID] = route + + gotRoute, err := store.GetRoute(ctx, route.ID.String()) + require.NoError(t, err) + assert.Equal(t, route, gotRoute) + }) + + t.Run("Get non-existing route from store should fail", func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + + _, err := store.GetRoute(ctx, uuid.New().String()) + require.Error(t, err) + }) +} + +func TestInMemoryRouteStore_GetRouteList(t *testing.T) { + t.Parallel() + + t.Run("Get empty route list from store", func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + + routes, err := store.GetRouteList(ctx) + require.NoError(t, err) + assert.Empty(t, routes) + }) + + t.Run("Get non-empty route list from store", func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + + route1 := model.Route{ + ID: uuid.New(), + NextHopId: "1337", + PrevHopId: "42", + CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM, + } + route2 := model.Route{ + ID: uuid.New(), + NextHopId: "1338", + PrevHopId: "43", + CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM, + } + store.dataStore[route1.ID] = route1 + store.dataStore[route2.ID] = route2 + + routes, err := store.GetRouteList(ctx) + require.NoError(t, err) + + assert.Len(t, routes, 2) + }) +} + +func TestInMemoryRouteStore_UpdateRoute(t *testing.T) { + t.Parallel() + + t.Run("Update existing route in store", func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + + route := model.Route{ + ID: uuid.New(), + NextHopId: "1337", + PrevHopId: "42", + CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM, + } + store.dataStore[route.ID] = route + + updatedRoute := route + updatedRoute.NextHopId = "1338" + + err := store.UpdateRoute(ctx, updatedRoute) + require.NoError(t, err) + + gotRoute, err := store.GetRoute(ctx, updatedRoute.ID.String()) + require.NoError(t, err) + assert.Equal(t, updatedRoute, gotRoute) + }) + + t.Run("Update non-existing route in store should fail", func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + + route := model.Route{ + ID: uuid.New(), + NextHopId: "1337", + PrevHopId: "42", + CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM, + } + + err := store.UpdateRoute(ctx, route) + require.Error(t, err) + }) +} diff --git a/kms/internal/service/routing.go b/kms/internal/service/routing.go new file mode 100644 index 0000000000000000000000000000000000000000..86e4cc398bb708825fdfede7c8b1cc78699ec02c --- /dev/null +++ b/kms/internal/service/routing.go @@ -0,0 +1,93 @@ +package service + +import ( + "context" + + "code.fbi.h-da.de/danet/costaquanta/kms/internal/core/model" + "code.fbi.h-da.de/danet/costaquanta/kms/internal/core/ports" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" +) + +type Routing struct { + logger *zap.SugaredLogger + tracer trace.Tracer + + store ports.RouteStore +} + +func NewRoutingService( + logger *zap.SugaredLogger, + tracer trace.Tracer, + store ports.RouteStore, +) *Routing { + return &Routing{ + logger: logger, + tracer: tracer, + + store: store, + } +} + +func (r *Routing) AddRoute( + ctx context.Context, + route model.Route, +) (model.Route, error) { + _, span := r.tracer.Start(ctx, "add-route") + defer span.End() + + sr, err := r.store.AddRoute(ctx, route) + if err != nil { + return model.Route{}, err + } + + return sr, nil +} + +func (r *Routing) DeleteRoute(ctx context.Context, routeID string) error { + _, span := r.tracer.Start(ctx, "delete-route") + defer span.End() + + err := r.store.DeleteRoute(ctx, routeID) + if err != nil { + return err + } + + return nil +} + +func (r *Routing) GetRoute(ctx context.Context, routeID string) (model.Route, error) { + _, span := r.tracer.Start(ctx, "get-route") + defer span.End() + + route, err := r.store.GetRoute(ctx, routeID) + if err != nil { + return model.Route{}, err + } + + return route, nil +} + +func (r *Routing) GetRouteList(ctx context.Context) ([]model.Route, error) { + _, span := r.tracer.Start(ctx, "get-route-list") + defer span.End() + + routes, err := r.store.GetRouteList(ctx) + if err != nil { + return nil, err + } + + return routes, nil +} + +func (r *Routing) UpdateRoute(ctx context.Context, route model.Route) error { + _, span := r.tracer.Start(ctx, "update-route") + defer span.End() + + err := r.store.UpdateRoute(ctx, route) + if err != nil { + return err + } + + return nil +}