diff --git a/pkg/networkservice/chains/endpoint/server.go b/pkg/networkservice/chains/endpoint/server.go index d4fc340d8..480cc6e80 100644 --- a/pkg/networkservice/chains/endpoint/server.go +++ b/pkg/networkservice/chains/endpoint/server.go @@ -1,6 +1,6 @@ -// Copyright (c) 2020-2021 Cisco Systems, Inc. +// Copyright (c) 2020-2022 Cisco Systems, Inc. // -// Copyright (c) 2020-2021 Doc.ai and/or its affiliates. +// Copyright (c) 2020-2022 Doc.ai and/or its affiliates. // // SPDX-License-Identifier: Apache-2.0 // @@ -41,6 +41,8 @@ import ( "github.com/networkservicemesh/sdk/pkg/networkservice/common/updatetoken" "github.com/networkservicemesh/sdk/pkg/networkservice/core/chain" "github.com/networkservicemesh/sdk/pkg/tools/grpcutils" + authmonitor "github.com/networkservicemesh/sdk/pkg/tools/monitorconnection/authorize" + "github.com/networkservicemesh/sdk/pkg/tools/monitorconnection/next" "github.com/networkservicemesh/sdk/pkg/tools/token" ) @@ -60,9 +62,10 @@ type endpoint struct { } type serverOptions struct { - name string - authorizeServer networkservice.NetworkServiceServer - additionalFunctionality []networkservice.NetworkServiceServer + name string + authorizeServer networkservice.NetworkServiceServer + authorizeMonitorConnectionServer networkservice.MonitorConnectionServer + additionalFunctionality []networkservice.NetworkServiceServer } // Option modifies server option value @@ -85,6 +88,16 @@ func WithAuthorizeServer(authorizeServer networkservice.NetworkServiceServer) Op } } +// WithAuthorizeMonitorConnectionServer sets authorization MonitorConnectionServer chain element +func WithAuthorizeMonitorConnectionServer(authorizeMonitorConnectionServer networkservice.MonitorConnectionServer) Option { + if authorizeMonitorConnectionServer == nil { + panic("authorizeMonitorConnectionServer cannot be nil") + } + return func(o *serverOptions) { + o.authorizeMonitorConnectionServer = authorizeMonitorConnectionServer + } +} + // WithAdditionalFunctionality sets additional NetworkServiceServer chain elements to be included in the chain func WithAdditionalFunctionality(additionalFunctionality ...networkservice.NetworkServiceServer) Option { return func(o *serverOptions) { @@ -95,12 +108,14 @@ func WithAdditionalFunctionality(additionalFunctionality ...networkservice.Netwo // NewServer - returns a NetworkServiceMesh client as a chain of the standard Client pieces plus whatever func NewServer(ctx context.Context, tokenGenerator token.GeneratorFunc, options ...Option) Endpoint { opts := &serverOptions{ - name: "endpoint-" + uuid.New().String(), - authorizeServer: authorize.NewServer(authorize.Any()), + name: "endpoint-" + uuid.New().String(), + authorizeServer: authorize.NewServer(authorize.Any()), + authorizeMonitorConnectionServer: authmonitor.NewMonitorConnectionServer(authmonitor.Any()), } for _, opt := range options { opt(opts) } + var mcsPtr networkservice.MonitorConnectionServer rv := &endpoint{} rv.NetworkServiceServer = chain.NewNetworkServiceServer( @@ -111,10 +126,10 @@ func NewServer(ctx context.Context, tokenGenerator token.GeneratorFunc, options opts.authorizeServer, metadata.NewServer(), timeout.NewServer(ctx), - monitor.NewServer(ctx, &rv.MonitorConnectionServer), + monitor.NewServer(ctx, &mcsPtr), trimpath.NewServer(), }, opts.additionalFunctionality...)...) - + rv.MonitorConnectionServer = next.NewMonitorConnectionServer(opts.authorizeMonitorConnectionServer, mcsPtr) return rv } diff --git a/pkg/networkservice/chains/nsmgr/server.go b/pkg/networkservice/chains/nsmgr/server.go index 0550d2c83..856f46f22 100644 --- a/pkg/networkservice/chains/nsmgr/server.go +++ b/pkg/networkservice/chains/nsmgr/server.go @@ -58,6 +58,7 @@ import ( registryadapter "github.com/networkservicemesh/sdk/pkg/registry/core/adapters" "github.com/networkservicemesh/sdk/pkg/registry/core/chain" "github.com/networkservicemesh/sdk/pkg/tools/grpcutils" + authmonitor "github.com/networkservicemesh/sdk/pkg/tools/monitorconnection/authorize" "github.com/networkservicemesh/sdk/pkg/tools/token" ) @@ -74,13 +75,14 @@ type nsmgrServer struct { } type serverOptions struct { - authorizeServer networkservice.NetworkServiceServer - dialOptions []grpc.DialOption - dialTimeout time.Duration - regURL *url.URL - name string - url string - forwarderServiceName string + authorizeServer networkservice.NetworkServiceServer + authorizeMonitorConnectionServer networkservice.MonitorConnectionServer + dialOptions []grpc.DialOption + dialTimeout time.Duration + regURL *url.URL + name string + url string + forwarderServiceName string } // Option modifies server option value @@ -118,6 +120,16 @@ func WithAuthorizeServer(authorizeServer networkservice.NetworkServiceServer) Op } } +// WithAuthorizeMonitorConnectionServer sets authorization MonitorConnectionServer chain element +func WithAuthorizeMonitorConnectionServer(authorizeMonitorConnectionServer networkservice.MonitorConnectionServer) Option { + if authorizeMonitorConnectionServer == nil { + panic("authorizeMonitorConnectionServer cannot be nil") + } + return func(o *serverOptions) { + o.authorizeMonitorConnectionServer = authorizeMonitorConnectionServer + } +} + // WithRegistry sets URL and dial options to reach the upstream registry, if not passed memory storage will be used. func WithRegistry(regURL *url.URL) Option { return func(o *serverOptions) { @@ -147,16 +159,16 @@ var _ Nsmgr = (*nsmgrServer)(nil) // options - a set of Nsmgr options. func NewServer(ctx context.Context, tokenGenerator token.GeneratorFunc, options ...Option) Nsmgr { opts := &serverOptions{ - authorizeServer: authorize.NewServer(authorize.Any()), - name: "nsmgr-" + uuid.New().String(), - forwarderServiceName: "forwarder", + authorizeServer: authorize.NewServer(authorize.Any()), + authorizeMonitorConnectionServer: authmonitor.NewMonitorConnectionServer(authmonitor.Any()), + name: "nsmgr-" + uuid.New().String(), + forwarderServiceName: "forwarder", } for _, opt := range options { opt(opts) } rv := &nsmgrServer{} - var nsRegistry = memory.NewNetworkServiceRegistryServer() if opts.regURL != nil { // Use remote registry @@ -212,6 +224,7 @@ func NewServer(ctx context.Context, tokenGenerator token.GeneratorFunc, options rv.Endpoint = endpoint.NewServer(ctx, tokenGenerator, endpoint.WithName(opts.name), endpoint.WithAuthorizeServer(opts.authorizeServer), + endpoint.WithAuthorizeMonitorConnectionServer(opts.authorizeMonitorConnectionServer), endpoint.WithAdditionalFunctionality( adapters.NewClientToServer(clientinfo.NewClient()), discoverforwarder.NewServer( diff --git a/pkg/networkservice/chains/nsmgrproxy/server.go b/pkg/networkservice/chains/nsmgrproxy/server.go index 6eb8184b8..bce155f6f 100644 --- a/pkg/networkservice/chains/nsmgrproxy/server.go +++ b/pkg/networkservice/chains/nsmgrproxy/server.go @@ -50,6 +50,7 @@ import ( "github.com/networkservicemesh/sdk/pkg/tools/fs" "github.com/networkservicemesh/sdk/pkg/tools/grpcutils" "github.com/networkservicemesh/sdk/pkg/tools/log" + authmonitor "github.com/networkservicemesh/sdk/pkg/tools/monitorconnection/authorize" "github.com/networkservicemesh/sdk/pkg/tools/token" ) @@ -67,12 +68,13 @@ type nsmgrProxyServer struct { } type serverOptions struct { - name string - mapipFilePath string - listenOn *url.URL - authorizeServer networkservice.NetworkServiceServer - dialOptions []grpc.DialOption - dialTimeout time.Duration + name string + mapipFilePath string + listenOn *url.URL + authorizeServer networkservice.NetworkServiceServer + authorizeMonitorConnectionServer networkservice.MonitorConnectionServer + dialOptions []grpc.DialOption + dialTimeout time.Duration } func (s *serverOptions) openMapIPChannel(ctx context.Context) <-chan map[string]string { @@ -117,6 +119,16 @@ func WithAuthorizeServer(authorizeServer networkservice.NetworkServiceServer) Op } } +// WithAuthorizeMonitorConnectionServer sets authorization MonitorConnectionServer chain element +func WithAuthorizeMonitorConnectionServer(authorizeMonitorConnectionServer networkservice.MonitorConnectionServer) Option { + if authorizeMonitorConnectionServer == nil { + panic("authorizeMonitorConnectionServer cannot be nil") + } + return func(o *serverOptions) { + o.authorizeMonitorConnectionServer = authorizeMonitorConnectionServer + } +} + // WithListenOn sets current listenOn url func WithListenOn(u *url.URL) Option { return func(o *serverOptions) { @@ -148,12 +160,12 @@ func WithDialTimeout(dialTimeout time.Duration) Option { // NewServer creates new proxy NSMgr func NewServer(ctx context.Context, regURL, proxyURL *url.URL, tokenGenerator token.GeneratorFunc, options ...Option) nsmgr.Nsmgr { rv := new(nsmgrProxyServer) - opts := &serverOptions{ - name: "nsmgr-proxy-" + uuid.New().String(), - authorizeServer: authorize.NewServer(authorize.Any()), - listenOn: &url.URL{Scheme: "unix", Host: "listen.on"}, - mapipFilePath: "map-ip.yaml", + name: "nsmgr-proxy-" + uuid.New().String(), + authorizeServer: authorize.NewServer(authorize.Any()), + authorizeMonitorConnectionServer: authmonitor.NewMonitorConnectionServer(authmonitor.Any()), + listenOn: &url.URL{Scheme: "unix", Host: "listen.on"}, + mapipFilePath: "map-ip.yaml", } for _, opt := range options { opt(opts) @@ -185,6 +197,7 @@ func NewServer(ctx context.Context, regURL, proxyURL *url.URL, tokenGenerator to rv.Endpoint = endpoint.NewServer(ctx, tokenGenerator, endpoint.WithName(opts.name), endpoint.WithAuthorizeServer(opts.authorizeServer), + endpoint.WithAuthorizeMonitorConnectionServer(opts.authorizeMonitorConnectionServer), endpoint.WithAdditionalFunctionality( interdomainbypass.NewServer(&interdomainBypassNSEServer, opts.listenOn), discover.NewServer(nsClient, nseClient), diff --git a/pkg/networkservice/common/authorize/client.go b/pkg/networkservice/common/authorize/client.go index 146e15b89..3a33fa00f 100644 --- a/pkg/networkservice/common/authorize/client.go +++ b/pkg/networkservice/common/authorize/client.go @@ -1,6 +1,6 @@ -// Copyright (c) 2020-2021 Doc.ai and/or its affiliates. +// Copyright (c) 2020-2022 Doc.ai and/or its affiliates. // -// Copyright (c) 2020-2021 Cisco Systems, Inc. +// Copyright (c) 2020-2022 Cisco Systems, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -42,19 +42,20 @@ type authorizeClient struct { // NewClient - returns a new authorization networkservicemesh.NetworkServiceClient // Authorize client checks rigiht side of path. func NewClient(opts ...Option) networkservice.NetworkServiceClient { - var result = &authorizeClient{ - policies: []Policy{ + o := &options{ + policies: policiesList{ opa.WithTokensValidPolicy(), opa.WithNextTokenSignedPolicy(), opa.WithTokensExpiredPolicy(), opa.WithTokenChainPolicy(), }, } - - for _, o := range opts { - o.apply(&result.policies) + for _, opt := range opts { + opt(o) + } + var result = &authorizeClient{ + policies: o.policies, } - return result } diff --git a/pkg/networkservice/common/authorize/options.go b/pkg/networkservice/common/authorize/options.go index 2e2d3e834..6f426caa2 100644 --- a/pkg/networkservice/common/authorize/options.go +++ b/pkg/networkservice/common/authorize/options.go @@ -1,6 +1,6 @@ -// Copyright (c) 2020-2021 Doc.ai and/or its affiliates. +// Copyright (c) 2020-2022 Doc.ai and/or its affiliates. // -// Copyright (c) 2020-2021 Cisco Systems, Inc. +// Copyright (c) 2020-2022 Cisco Systems, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -18,11 +18,16 @@ package authorize -// Option is authorization option for server -type Option interface { - apply(*policiesList) +import "github.com/networkservicemesh/sdk/pkg/tools/spire" + +type options struct { + policies policiesList + spiffeIDConnectionMap *spire.SpiffeIDConnectionMap } +// Option is authorization option for network service server +type Option func(*options) + // Any authorizes any call of request/close func Any() Option { return WithPolicies(nil) @@ -30,13 +35,14 @@ func Any() Option { // WithPolicies sets custom policies func WithPolicies(p ...Policy) Option { - return optionFunc(func(l *policiesList) { - *l = p - }) + return func(o *options) { + o.policies = p + } } -type optionFunc func(*policiesList) - -func (f optionFunc) apply(a *policiesList) { - f(a) +// WithSpiffeIDConnectionMap sets map to keep spiffeIDConnectionMap to authorize connections with MonitorConnectionServer +func WithSpiffeIDConnectionMap(s *spire.SpiffeIDConnectionMap) Option { + return func(o *options) { + o.spiffeIDConnectionMap = s + } } diff --git a/pkg/networkservice/common/authorize/server.go b/pkg/networkservice/common/authorize/server.go index d8b568cd2..8939e6576 100644 --- a/pkg/networkservice/common/authorize/server.go +++ b/pkg/networkservice/common/authorize/server.go @@ -1,6 +1,6 @@ -// Copyright (c) 2020-2021 Doc.ai and/or its affiliates. +// Copyright (c) 2020-2022 Doc.ai and/or its affiliates. // -// Copyright (c) 2020-2021 Cisco Systems, Inc. +// Copyright (c) 2020-2022 Cisco Systems, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -29,40 +29,58 @@ import ( "github.com/networkservicemesh/sdk/pkg/networkservice/core/next" "github.com/networkservicemesh/sdk/pkg/tools/opa" + "github.com/networkservicemesh/sdk/pkg/tools/spire" ) type authorizeServer struct { - policies policiesList + policies policiesList + spiffeIDConnectionMap *spire.SpiffeIDConnectionMap } // NewServer - returns a new authorization networkservicemesh.NetworkServiceServers // Authorize server checks left side of Path. func NewServer(opts ...Option) networkservice.NetworkServiceServer { - var s = &authorizeServer{ - policies: []Policy{ + o := &options{ + policies: policiesList{ opa.WithTokensValidPolicy(), opa.WithPrevTokenSignedPolicy(), opa.WithTokensExpiredPolicy(), opa.WithTokenChainPolicy(), }, + spiffeIDConnectionMap: &spire.SpiffeIDConnectionMap{}, + } + for _, opt := range opts { + opt(o) } - for _, o := range opts { - o.apply(&s.policies) + var s = &authorizeServer{ + policies: o.policies, + spiffeIDConnectionMap: o.spiffeIDConnectionMap, } return s } func (a *authorizeServer) Request(ctx context.Context, request *networkservice.NetworkServiceRequest) (*networkservice.Connection, error) { - var index = request.GetConnection().GetPath().GetIndex() + conn := request.GetConnection() + var index = conn.GetPath().GetIndex() var leftSide = &networkservice.Path{ Index: index, - PathSegments: request.GetConnection().GetPath().GetPathSegments()[:index+1], + PathSegments: conn.GetPath().GetPathSegments()[:index+1], } if _, ok := peer.FromContext(ctx); ok { if err := a.policies.check(ctx, leftSide); err != nil { return nil, err } } + + if spiffeID, err := spire.SpiffeIDFromContext(ctx); err == nil { + connID := conn.GetPath().GetPathSegments()[index-1].GetId() + ids, ok := a.spiffeIDConnectionMap.Load(spiffeID) + if !ok { + ids = &spire.ConnectionIDSet{} + } + ids.Store(connID, struct{}{}) + a.spiffeIDConnectionMap.Store(spiffeID, ids) + } return next.Server(ctx).Request(ctx, request) } @@ -72,6 +90,25 @@ func (a *authorizeServer) Close(ctx context.Context, conn *networkservice.Connec Index: index, PathSegments: conn.GetPath().GetPathSegments()[:index+1], } + if spiffeID, err := spire.SpiffeIDFromContext(ctx); err == nil { + connID := conn.GetPath().GetPathSegments()[index-1].GetId() + ids, ok := a.spiffeIDConnectionMap.Load(spiffeID) + if ok { + if _, ok := ids.Load(connID); ok { + ids.Delete(connID) + } + } + idsEmpty := true + ids.Range(func(_ string, _ struct{}) bool { + idsEmpty = false + return true + }) + if idsEmpty { + a.spiffeIDConnectionMap.Delete(spiffeID) + } else { + a.spiffeIDConnectionMap.Store(spiffeID, ids) + } + } if _, ok := peer.FromContext(ctx); ok { if err := a.policies.check(ctx, leftSide); err != nil { return nil, err diff --git a/pkg/networkservice/core/next/tail_client.go b/pkg/networkservice/core/next/tail_client.go index c7e763ad1..268076c0f 100644 --- a/pkg/networkservice/core/next/tail_client.go +++ b/pkg/networkservice/core/next/tail_client.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020 Cisco Systems, Inc. +// Copyright (c) 2020-2022 Cisco Systems, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -25,7 +25,7 @@ import ( "github.com/networkservicemesh/api/pkg/api/networkservice" ) -// tailServer is a simple implementation of networkservice.NetworkServiceServer that is called at the end of a chain +// tailClient is a simple implementation of networkservice.NetworkServiceClient that is called at the end of a chain // to insure that we never call a method on a nil object type tailClient struct{} diff --git a/pkg/tools/monitorconnection/authorize/common.go b/pkg/tools/monitorconnection/authorize/common.go new file mode 100644 index 000000000..7a1c792ba --- /dev/null +++ b/pkg/tools/monitorconnection/authorize/common.go @@ -0,0 +1,44 @@ +// Copyright (c) 2022 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authorize + +import ( + "context" +) + +// Policy represents authorization policy for monitor connection. +type Policy interface { + // Check checks authorization + Check(ctx context.Context, input interface{}) error +} + +type policiesList []Policy + +func (l *policiesList) check(ctx context.Context, srv MonitorOpaInput) error { + if l == nil { + return nil + } + for _, policy := range *l { + if policy == nil { + continue + } + if err := policy.Check(ctx, srv); err != nil { + return err + } + } + return nil +} diff --git a/pkg/tools/monitorconnection/authorize/options.go b/pkg/tools/monitorconnection/authorize/options.go new file mode 100644 index 000000000..d0435d485 --- /dev/null +++ b/pkg/tools/monitorconnection/authorize/options.go @@ -0,0 +1,46 @@ +// Copyright (c) 2022 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authorize + +import "github.com/networkservicemesh/sdk/pkg/tools/spire" + +type options struct { + policies policiesList + spiffeIDConnectionMap *spire.SpiffeIDConnectionMap +} + +// Option is authorization option for monitor connection server +type Option func(*options) + +// Any authorizes any call of request/close +func Any() Option { + return WithPolicies(nil) +} + +// WithPolicies sets custom policies +func WithPolicies(p ...Policy) Option { + return func(o *options) { + o.policies = p + } +} + +// WithSpiffeIDConnectionMap sets map to keep spiffeIDConnectionMap to authorize connections with MonitorServer +func WithSpiffeIDConnectionMap(s *spire.SpiffeIDConnectionMap) Option { + return func(o *options) { + o.spiffeIDConnectionMap = s + } +} diff --git a/pkg/tools/monitorconnection/authorize/server.go b/pkg/tools/monitorconnection/authorize/server.go new file mode 100644 index 000000000..8d9d3a8fa --- /dev/null +++ b/pkg/tools/monitorconnection/authorize/server.go @@ -0,0 +1,89 @@ +// Copyright (c) 2022 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package authorize provides authz checks for incoming or returning connections. +package authorize + +import ( + "github.com/networkservicemesh/api/pkg/api/networkservice" + "github.com/spiffe/go-spiffe/v2/spiffeid" + + "github.com/networkservicemesh/sdk/pkg/tools/monitorconnection/next" + "github.com/networkservicemesh/sdk/pkg/tools/opa" + "github.com/networkservicemesh/sdk/pkg/tools/spire" +) + +type authorizeMonitorConnectionsServer struct { + policies policiesList + spiffeIDConnectionMap *spire.SpiffeIDConnectionMap +} + +// NewMonitorConnectionServer - returns a new authorization networkservicemesh.MonitorConnectionServer +func NewMonitorConnectionServer(opts ...Option) networkservice.MonitorConnectionServer { + o := &options{ + policies: policiesList{opa.WithMonitorConnectionServerPolicy()}, + spiffeIDConnectionMap: &spire.SpiffeIDConnectionMap{}, + } + for _, opt := range opts { + opt(o) + } + var s = &authorizeMonitorConnectionsServer{ + policies: o.policies, + spiffeIDConnectionMap: o.spiffeIDConnectionMap, + } + return s +} + +// MonitorOpaInput - used to pass complex structure to monitor policies +type MonitorOpaInput struct { + SpiffeIDConnectionMap map[string][]string `json:"spiffe_id_connection_map"` + SelectorConnectionIds []string `json:"selector_connection_ids"` + ServiceSpiffeID string `json:"service_spiffe_id"` +} + +func (a *authorizeMonitorConnectionsServer) MonitorConnections(in *networkservice.MonitorScopeSelector, srv networkservice.MonitorConnection_MonitorConnectionsServer) error { + ctx := srv.Context() + simpleMap := make(map[string][]string) + a.spiffeIDConnectionMap.Range( + func(sid spiffeid.ID, connIds *spire.ConnectionIDSet) bool { + connIds.Range( + func(connId string, _ struct{}) bool { + ids := simpleMap[sid.String()] + ids = append(ids, connId) + simpleMap[sid.String()] = ids + return true + }, + ) + return true + }, + ) + + connIDs := make([]string, 0) + for _, v := range in.PathSegments { + connIDs = append(connIDs, v.GetId()) + } + spiffeID, _ := spire.SpiffeIDFromContext(ctx) + err := a.policies.check(ctx, MonitorOpaInput{ + ServiceSpiffeID: spiffeID.String(), + SpiffeIDConnectionMap: simpleMap, + SelectorConnectionIds: connIDs, + }) + if err != nil { + return err + } + + return next.MonitorConnectionServer(ctx).MonitorConnections(in, srv) +} diff --git a/pkg/tools/monitorconnection/authorize/server_test.go b/pkg/tools/monitorconnection/authorize/server_test.go new file mode 100644 index 000000000..d47f106a4 --- /dev/null +++ b/pkg/tools/monitorconnection/authorize/server_test.go @@ -0,0 +1,247 @@ +// Copyright (c) 2022 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authorize_test + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "testing" + "time" + + "go.uber.org/goleak" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/peer" + "google.golang.org/grpc/status" + + "github.com/spiffe/go-spiffe/v2/spiffeid" + + "github.com/networkservicemesh/sdk/pkg/tools/monitorconnection/authorize" + "github.com/networkservicemesh/sdk/pkg/tools/opa" + "github.com/networkservicemesh/sdk/pkg/tools/spire" + + "github.com/networkservicemesh/api/pkg/api/networkservice" + + "github.com/stretchr/testify/require" +) + +const ( + // certPem is a X.509 certificate with spiffeId = "spiffe://test.com/workload" + certPem = `-----BEGIN CERTIFICATE----- +MIIBvjCCAWWgAwIBAgIQbnFakUhzr52nHoLGltZDyDAKBggqhkjOPQQDAjAdMQsw +CQYDVQQGEwJVUzEOMAwGA1UEChMFU1BJUkUwHhcNMjAwMTAxMDEwMTAxWhcNMzAw +MTAxMDEwMTAxWjAdMQswCQYDVQQGEwJVUzEOMAwGA1UEChMFU1BJUkUwWTATBgcq +hkjOPQIBBggqhkjOPQMBBwNCAASlFpbASv+NIyVdFwTp22JR5gx7D6LJ01Z8Wz0S +ZiBneWRAcYUBBQY6zKwr/RQtCDxUcFfFyq4zEfUD29a5Phnoo4GGMIGDMA4GA1Ud +DwEB/wQEAwIDqDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0T +AQH/BAIwADAdBgNVHQ4EFgQUJJpYlJa1eNEcks+zJcwKClopSAowJQYDVR0RBB4w +HIYac3BpZmZlOi8vdGVzdC5jb20vd29ya2xvYWQwCgYIKoZIzj0EAwIDRwAwRAIg +Dk6tlURSF8ULhNbnyUxFQ33rDic2dX8jOIstV2dWErwCIDRH2yw0swTcUMQWYgHy +aMp+T747AZGjOEfwHb9/w+7m +-----END CERTIFICATE----- +` + spiffeID1 = "spiffe://test.com/workload" + spiffeID2 = "spiffe://test.com/anotherWorkload" +) + +func getContextWithTLSCert() (context.Context, error) { + block, _ := pem.Decode([]byte(certPem)) + x509cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + + authInfo := &credentials.TLSInfo{ + State: tls.ConnectionState{ + PeerCertificates: []*x509.Certificate{x509cert}, + }, + } + + return peer.NewContext(context.Background(), &peer.Peer{AuthInfo: authInfo}), nil +} + +func testPolicy() authorize.Policy { + return opa.WithPolicyFromSource(` + package test + + default allow = false + + allow { + conn_ids := {y | y = input.spiffe_id_connection_map[input.service_spiffe_id][_]} + path_conn_ids := {x | x = input.selector_connection_ids[_]} + count(path_conn_ids) > 0 + count(conn_ids) > 0 + inter := conn_ids & path_conn_ids + count(inter) > 0 + } +`, "allow", opa.True) +} + +type testEmptyMCMCServer struct { + networkservice.MonitorConnection_MonitorConnectionsServer + context context.Context +} + +func (t *testEmptyMCMCServer) Send(event *networkservice.ConnectionEvent) error { + return nil +} + +func (t *testEmptyMCMCServer) Context() context.Context { + return t.context +} +func TestAuthzEndpoint(t *testing.T) { + t.Cleanup(func() { goleak.VerifyNone(t) }) + suits := []struct { + name string + baseCtx bool + pathSegments []*networkservice.PathSegment + spiffeIDConnMap map[string][]string + denied bool + }{ + { + name: "simple negative test without peer context", + baseCtx: false, + pathSegments: make([]*networkservice.PathSegment, 0), + denied: true, + }, + { + name: "simple negative test with peer context", + baseCtx: true, + pathSegments: make([]*networkservice.PathSegment, 0), + denied: true, + }, + { + name: "positive test with several spiffeIds objects", + baseCtx: true, + pathSegments: []*networkservice.PathSegment{{Id: "conn1"}}, + spiffeIDConnMap: map[string][]string{spiffeID1: {"conn1"}, spiffeID2: {"conn2"}}, + denied: false, + }, + { + name: "positive test with several connection ids in the path", + baseCtx: true, + pathSegments: []*networkservice.PathSegment{{Id: "conn1"}, {Id: "conn2"}}, + spiffeIDConnMap: map[string][]string{spiffeID1: {"conn1"}}, + denied: false, + }, + { + name: "positive test with several connection ids in the spiffeID map", + baseCtx: true, + pathSegments: []*networkservice.PathSegment{{Id: "conn1"}}, + spiffeIDConnMap: map[string][]string{spiffeID1: {"conn1", "conn2"}}, + denied: false, + }, + { + name: "negative test without peer context", + baseCtx: false, + pathSegments: []*networkservice.PathSegment{{Id: "conn1"}}, + spiffeIDConnMap: map[string][]string{spiffeID1: {"conn1"}}, + denied: true, + }, + { + name: "negative test with empty path", + baseCtx: true, + pathSegments: make([]*networkservice.PathSegment, 0), + spiffeIDConnMap: map[string][]string{spiffeID1: {"conn1"}}, + denied: true, + }, + { + name: "negative test without peer context", + baseCtx: false, + pathSegments: []*networkservice.PathSegment{{Id: "conn1"}}, + spiffeIDConnMap: make(map[string][]string), + denied: true, + }, + { + name: "negative test with wrong spiffeID in the map", + baseCtx: true, + pathSegments: []*networkservice.PathSegment{{Id: "conn1"}}, + spiffeIDConnMap: map[string][]string{spiffeID2: {"conn1"}}, + denied: true, + }, + } + + for i := range suits { + s := suits[i] + t.Run(s.name, func(t *testing.T) { + var err error + baseCtx := context.Background() + if s.baseCtx { + baseCtx, err = getContextWithTLSCert() + require.NoError(t, err) + } + spiffeIDConnectionMap := spire.SpiffeIDConnectionMap{} + for spiffeIDstr, connIds := range s.spiffeIDConnMap { + connIDMap := spire.ConnectionIDSet{} + for _, connID := range connIds { + connIDMap.Store(connID, struct{}{}) + } + var spiffeID spiffeid.ID + spiffeID, err = spiffeid.FromString(spiffeIDstr) + require.NoError(t, err) + spiffeIDConnectionMap.Store(spiffeID, &connIDMap) + } + ctx, cancel := context.WithTimeout(baseCtx, time.Second) + defer cancel() + srv := authorize.NewMonitorConnectionServer(authorize.WithSpiffeIDConnectionMap(&spiffeIDConnectionMap), authorize.WithPolicies(testPolicy())) + checkResult := func(err error) { + if !s.denied { + require.Nil(t, err, "monitorConnections expected to be not denied: ") + return + } + require.NotNil(t, err, "monitorConnections expected to be denied") + s, ok := status.FromError(err) + require.True(t, ok, "error without error status code"+err.Error()) + require.Equal(t, s.Code(), codes.PermissionDenied, "wrong error status code") + } + err = srv.MonitorConnections(&networkservice.MonitorScopeSelector{PathSegments: s.pathSegments}, &testEmptyMCMCServer{context: ctx}) + checkResult(err) + }) + } +} + +func TestAuthorize_ShouldCorrectlyWorkWithHeal(t *testing.T) { + t.Cleanup(func() { goleak.VerifyNone(t) }) + peerCtx, err := getContextWithTLSCert() + require.NoError(t, err) + ctx, cancel := context.WithTimeout(peerCtx, time.Second) + defer cancel() + + selector := &networkservice.MonitorScopeSelector{ + PathSegments: []*networkservice.PathSegment{{Id: "conn1"}}, + } + // simulate heal request + err = authorize.NewMonitorConnectionServer( + authorize.Any()).MonitorConnections( + selector, &testEmptyMCMCServer{context: ctx}) + require.NoError(t, err) + + spiffeIDConnectionMap := spire.SpiffeIDConnectionMap{} + connMap := spire.ConnectionIDSet{} + var placer struct{} + connMap.Store("conn1", placer) + var spiffeID spiffeid.ID + spiffeID, err = spiffeid.FromString(spiffeID1) + require.NoError(t, err) + spiffeIDConnectionMap.Store(spiffeID, &connMap) + err = authorize.NewMonitorConnectionServer( + authorize.WithSpiffeIDConnectionMap(&spiffeIDConnectionMap)).MonitorConnections( + selector, &testEmptyMCMCServer{context: ctx}) + require.NoError(t, err) +} diff --git a/pkg/tools/monitorconnection/next/common_test.go b/pkg/tools/monitorconnection/next/common_test.go new file mode 100644 index 000000000..d95798d6d --- /dev/null +++ b/pkg/tools/monitorconnection/next/common_test.go @@ -0,0 +1,41 @@ +// Copyright (c) 2022 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package next_test + +import "context" + +type contextKeyType string + +const ( + visitKey contextKeyType = "visitKey" +) + +func visit(ctx context.Context) context.Context { + if v, ok := ctx.Value(visitKey).(*int); ok { + *v++ + return ctx + } + val := 0 + return context.WithValue(ctx, visitKey, &val) +} + +func visitValue(ctx context.Context) int { + if v, ok := ctx.Value(visitKey).(*int); ok { + return *v + } + return 0 +} diff --git a/pkg/tools/monitorconnection/next/context.go b/pkg/tools/monitorconnection/next/context.go new file mode 100644 index 000000000..14f8d4f23 --- /dev/null +++ b/pkg/tools/monitorconnection/next/context.go @@ -0,0 +1,50 @@ +// Copyright (c) 2022 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package next provides a mechanism for chained networkservice.MonitorConnection{Server,Client}s to call +// the next element in the chain. +package next + +import ( + "context" + + "github.com/networkservicemesh/api/pkg/api/networkservice" +) + +type contextKeyType string + +const ( + nextMonitorConnectionServerKey contextKeyType = "NextMonitorConnectionServer" +) + +// withNextMonitorConnectionServer - +// Wraps 'parent' in a new Context that has the Server networkservice.MonitorConnectionServer to be called in the chain +func withNextMonitorConnectionServer(parent context.Context, next networkservice.MonitorConnectionServer) context.Context { + if parent == nil { + parent = context.Background() + } + return context.WithValue(parent, nextMonitorConnectionServerKey, next) +} + +// MonitorConnectionServer - +// Returns the networkservice.MonitorConnectionServer to be called in the chain from the context.Context +func MonitorConnectionServer(ctx context.Context) networkservice.MonitorConnectionServer { + rv, ok := ctx.Value(nextMonitorConnectionServerKey).(networkservice.MonitorConnectionServer) + if ok && rv != nil { + return rv + } + return &tailMonitorConnectionsServer{} +} diff --git a/pkg/tools/monitorconnection/next/server.go b/pkg/tools/monitorconnection/next/server.go new file mode 100644 index 000000000..adcb3ce50 --- /dev/null +++ b/pkg/tools/monitorconnection/next/server.go @@ -0,0 +1,73 @@ +// Copyright (c) 2022 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package next + +import ( + "context" + + "github.com/networkservicemesh/api/pkg/api/networkservice" + + "github.com/networkservicemesh/sdk/pkg/tools/monitorconnection/streamcontext" +) + +// MonitorConnectionsServerWrapper - a function that wraps around a networkservice.MonitorConnectionServer +type MonitorConnectionsServerWrapper func(networkservice.MonitorConnectionServer) networkservice.MonitorConnectionServer + +// MonitorConnectionsServerChainer - a function that chains together a list of networkservice.MonitorConnectionServers +type MonitorConnectionsServerChainer func(...networkservice.MonitorConnectionServer) networkservice.MonitorConnectionServer + +type nextMonitorConnectionServer struct { + servers []networkservice.MonitorConnectionServer + index int + nextParent networkservice.MonitorConnectionServer +} + +// NewWrappedMonitorConnectionServer - creates a chain of servers with each one wrapped in wrapper +func NewWrappedMonitorConnectionServer(wrapper MonitorConnectionsServerWrapper, servers ...networkservice.MonitorConnectionServer) networkservice.MonitorConnectionServer { + rv := &nextMonitorConnectionServer{servers: make([]networkservice.MonitorConnectionServer, 0, len(servers))} + for _, srv := range servers { + rv.servers = append(rv.servers, wrapper(srv)) + } + return rv +} + +// NewMonitorConnectionServer - chains together servers into a single networkservice.MonitorConnectionServer +func NewMonitorConnectionServer(servers ...networkservice.MonitorConnectionServer) networkservice.MonitorConnectionServer { + return NewWrappedMonitorConnectionServer( + func(server networkservice.MonitorConnectionServer) networkservice.MonitorConnectionServer { + return server + }, servers...) +} + +func (n *nextMonitorConnectionServer) MonitorConnections(in *networkservice.MonitorScopeSelector, srv networkservice.MonitorConnection_MonitorConnectionsServer) error { + server, ctx := n.getServerAndContext(srv.Context()) + return server.MonitorConnections(in, streamcontext.MonitorConnectionMonitorConnectionsServer(ctx, srv)) +} + +func (n *nextMonitorConnectionServer) getServerAndContext(ctx context.Context) (networkservice.MonitorConnectionServer, context.Context) { + nextParent := n.nextParent + if n.index == 0 { + nextParent = MonitorConnectionServer(ctx) + if len(n.servers) == 0 { + return nextParent, ctx + } + } + if n.index+1 < len(n.servers) { + return n.servers[n.index], withNextMonitorConnectionServer(ctx, &nextMonitorConnectionServer{nextParent: nextParent, servers: n.servers, index: n.index + 1}) + } + return n.servers[n.index], withNextMonitorConnectionServer(ctx, nextParent) +} diff --git a/pkg/tools/monitorconnection/next/server_test.go b/pkg/tools/monitorconnection/next/server_test.go new file mode 100644 index 000000000..50d2b0066 --- /dev/null +++ b/pkg/tools/monitorconnection/next/server_test.go @@ -0,0 +1,107 @@ +// Copyright (c) 2022 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package next_test + +import ( + "context" + "fmt" + "sync" + "testing" + + "github.com/networkservicemesh/sdk/pkg/tools/monitorconnection/next" + "github.com/networkservicemesh/sdk/pkg/tools/monitorconnection/streamcontext" + + "github.com/networkservicemesh/api/pkg/api/networkservice" + "github.com/stretchr/testify/assert" +) + +type testEmptyMCServer struct{} + +func (t *testEmptyMCServer) MonitorConnections(in *networkservice.MonitorScopeSelector, srv networkservice.MonitorConnection_MonitorConnectionsServer) error { + return nil +} + +type testEmptyMCMCServer struct { + networkservice.MonitorConnection_MonitorConnectionsServer + context context.Context +} + +func (t *testEmptyMCMCServer) Send(event *networkservice.ConnectionEvent) error { + return nil +} + +func (t *testEmptyMCMCServer) Context() context.Context { + return t.context +} + +func emptyMCServer() networkservice.MonitorConnectionServer { + return &testEmptyMCServer{} +} + +type testVisitMCServer struct{} + +func (t *testVisitMCServer) MonitorConnections(in *networkservice.MonitorScopeSelector, srv networkservice.MonitorConnection_MonitorConnectionsServer) error { + srv = streamcontext.MonitorConnectionMonitorConnectionsServer(visit(srv.Context()), srv) + rv := next.MonitorConnectionServer(srv.Context()).MonitorConnections(in, srv) + return rv +} + +func visitMCServer() networkservice.MonitorConnectionServer { + return &testVisitMCServer{} +} + +func TestNewMonitorConnectionsServerShouldNotPanic(t *testing.T) { + assert.NotPanics(t, func() { + _ = next.NewMonitorConnectionServer().MonitorConnections( + nil, &testEmptyMCMCServer{context: context.Background()}) + _ = next.NewWrappedMonitorConnectionServer(func(server networkservice.MonitorConnectionServer) networkservice.MonitorConnectionServer { + return server + }).MonitorConnections(nil, &testEmptyMCMCServer{context: context.Background()}) + }) +} + +func TestNSServerBranches(t *testing.T) { + servers := [][]networkservice.MonitorConnectionServer{ + {visitMCServer()}, + {visitMCServer(), visitMCServer()}, + {visitMCServer(), visitMCServer(), visitMCServer()}, + {emptyMCServer(), visitMCServer(), visitMCServer()}, + {visitMCServer(), emptyMCServer(), visitMCServer()}, + {visitMCServer(), visitMCServer(), emptyMCServer()}, + {next.NewMonitorConnectionServer(), next.NewMonitorConnectionServer(visitMCServer(), next.NewMonitorConnectionServer()), visitMCServer()}, + } + expects := []int{1, 2, 3, 0, 1, 2, 2, 2} + for i, sample := range servers { + s := next.NewMonitorConnectionServer(sample...) + ctx := visit(context.Background()) + eventSrv := &testEmptyMCMCServer{context: ctx} + _ = s.MonitorConnections(nil, eventSrv) + assert.Equal(t, expects[i], visitValue(eventSrv.Context()), fmt.Sprintf("sample index: %v", i)) + } +} +func TestDataRaceMonitorConnectionServer(t *testing.T) { + s := next.NewMonitorConnectionServer(emptyMCServer()) + wg := sync.WaitGroup{} + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + _ = s.MonitorConnections(nil, &testEmptyMCMCServer{context: context.Background()}) + }() + } + wg.Wait() +} diff --git a/pkg/tools/monitorconnection/next/tail_server.go b/pkg/tools/monitorconnection/next/tail_server.go new file mode 100644 index 000000000..04ba87a86 --- /dev/null +++ b/pkg/tools/monitorconnection/next/tail_server.go @@ -0,0 +1,30 @@ +// Copyright (c) 2022 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package next + +import ( + "github.com/networkservicemesh/api/pkg/api/networkservice" +) + +// tailMonitorConnectionsServer is a simple implementation of networkservice.MonitorConnectionServer that is called at the end of a chain +// to insure that we never call a method on a nil object + +type tailMonitorConnectionsServer struct{} + +func (t *tailMonitorConnectionsServer) MonitorConnections(in *networkservice.MonitorScopeSelector, sv networkservice.MonitorConnection_MonitorConnectionsServer) error { + return nil +} diff --git a/pkg/tools/monitorconnection/streamcontext/stream_context.go b/pkg/tools/monitorconnection/streamcontext/stream_context.go new file mode 100644 index 000000000..8b2b6f2d7 --- /dev/null +++ b/pkg/tools/monitorconnection/streamcontext/stream_context.go @@ -0,0 +1,68 @@ +// Copyright (c) 2022 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package streamcontext provides API to extend context for authorize monitor connection server +package streamcontext + +import ( + "context" + + "github.com/networkservicemesh/api/pkg/api/networkservice" + + "github.com/networkservicemesh/sdk/pkg/tools/extend" +) + +type monitorConnectionMonitorConnectionsServer struct { + networkservice.MonitorConnection_MonitorConnectionsServer + ctx context.Context +} + +func (s *monitorConnectionMonitorConnectionsServer) Context() context.Context { + return s.ctx +} + +// MonitorConnectionMonitorConnectionsServer extends context for MonitorConnection_MonitorConnectionsServer +func MonitorConnectionMonitorConnectionsServer(ctx context.Context, server networkservice.MonitorConnection_MonitorConnectionsServer) networkservice.MonitorConnection_MonitorConnectionsServer { + if server != nil { + ctx = extend.WithValuesFromContext(server.Context(), ctx) + } + + return &monitorConnectionMonitorConnectionsServer{ + ctx: ctx, + MonitorConnection_MonitorConnectionsServer: server, + } +} + +type monitorConnectionMonitorConnectionsClient struct { + networkservice.MonitorConnection_MonitorConnectionsClient + ctx context.Context +} + +func (s *monitorConnectionMonitorConnectionsClient) Context() context.Context { + return s.ctx +} + +// MonitorConnectionMonitorConnectionsClient extends context for MonitorConnection_MonitorConnectionsClient +func MonitorConnectionMonitorConnectionsClient(ctx context.Context, client networkservice.MonitorConnection_MonitorConnectionsClient) networkservice.MonitorConnection_MonitorConnectionsClient { + if client != nil { + ctx = extend.WithValuesFromContext(client.Context(), ctx) + } + + return &monitorConnectionMonitorConnectionsClient{ + ctx: ctx, + MonitorConnection_MonitorConnectionsClient: client, + } +} diff --git a/pkg/tools/opa/opainput.go b/pkg/tools/opa/opainput.go index 53afa123a..87fed58a2 100644 --- a/pkg/tools/opa/opainput.go +++ b/pkg/tools/opa/opainput.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020 Doc.ai and/or its affiliates. +// Copyright (c) 2020-2022 Doc.ai and/or its affiliates. // // SPDX-License-Identifier: Apache-2.0 // @@ -23,7 +23,6 @@ import ( "encoding/pem" "github.com/pkg/errors" - "google.golang.org/grpc/peer" "google.golang.org/grpc/credentials" @@ -38,7 +37,7 @@ func PreparedOpaInput(ctx context.Context, model interface{}) (map[string]interf p, ok := peer.FromContext(ctx) var cert *x509.Certificate if ok { - cert = parseX509Cert(p.AuthInfo) + cert = ParseX509Cert(p.AuthInfo) } var pemcert string if cert != nil { @@ -55,7 +54,8 @@ func pemEncodingX509Cert(cert *x509.Certificate) string { return string(certpem) } -func parseX509Cert(authInfo credentials.AuthInfo) *x509.Certificate { +// ParseX509Cert - parses x509 certificate from the passed credentials.AuthInfo +func ParseX509Cert(authInfo credentials.AuthInfo) *x509.Certificate { var peerCert *x509.Certificate switch v := authInfo.(type) { diff --git a/pkg/tools/opa/policies.go b/pkg/tools/opa/policies.go index a7e5db815..aac4f5c48 100644 --- a/pkg/tools/opa/policies.go +++ b/pkg/tools/opa/policies.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2021 Doc.ai and/or its affiliates. +// Copyright (c) 2020-2022 Doc.ai and/or its affiliates. // // SPDX-License-Identifier: Apache-2.0 // @@ -33,6 +33,9 @@ var tokensChainedPolicySource string //go:embed policies/tokens_expired.rego var tokensExpiredPolicySource string +//go:embed policies/service_connection.rego +var tokensServiceConnectionPolicySource string + // WithTokensValidPolicy returns default policy for checking that all tokens in the path can be decoded. func WithTokensValidPolicy() *AuthorizationPolicy { return &AuthorizationPolicy{ @@ -77,3 +80,11 @@ func WithTokensExpiredPolicy() *AuthorizationPolicy { checker: False("tokens_expired"), } } + +func WithMonitorConnectionServerPolicy() *AuthorizationPolicy { + return &AuthorizationPolicy{ + policySource: tokensServiceConnectionPolicySource, + query: "service_connection", + checker: True("service_connection"), + } +} diff --git a/pkg/tools/opa/policies/service_connection.rego b/pkg/tools/opa/policies/service_connection.rego new file mode 100644 index 000000000..79921b42c --- /dev/null +++ b/pkg/tools/opa/policies/service_connection.rego @@ -0,0 +1,28 @@ +# Copyright (c) 2020 Cisco and/or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +package nsm + +default service_connection = false + +service_connection { + conn_ids := {y | y = input.spiffe_id_connection_map[input.service_spiffe_id][_]} + path_conn_ids := {x | x = input.selector_connection_ids[_]} + count(path_conn_ids) > 0 + count(conn_ids) > 0 + inter := conn_ids & path_conn_ids + count(inter) > 0 +} diff --git a/pkg/tools/opa/service_connection_policy_test.go b/pkg/tools/opa/service_connection_policy_test.go new file mode 100644 index 000000000..63697bb05 --- /dev/null +++ b/pkg/tools/opa/service_connection_policy_test.go @@ -0,0 +1,53 @@ +// Copyright (c) 2022 Doc.ai and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package opa_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/networkservicemesh/sdk/pkg/tools/monitorconnection/authorize" + "github.com/networkservicemesh/sdk/pkg/tools/opa" +) + +func TestWithServiceConnectionPolicy(t *testing.T) { + var p = opa.WithMonitorConnectionServerPolicy() + var input = authorize.MonitorOpaInput{ + SelectorConnectionIds: []string{"conn1"}, + SpiffeIDConnectionMap: map[string][]string{ + spiffeID: {"conn1"}, + }, + ServiceSpiffeID: spiffeID, + } + var invalidInput = authorize.MonitorOpaInput{ + SelectorConnectionIds: []string{"conn1"}, + SpiffeIDConnectionMap: map[string][]string{ + spiffeID: {"conn2"}, + }, + ServiceSpiffeID: spiffeID, + } + + ctx := context.Background() + + err := p.Check(ctx, input) + require.NoError(t, err) + + err = p.Check(ctx, invalidInput) + require.Error(t, err) +} diff --git a/pkg/tools/sandbox/node.go b/pkg/tools/sandbox/node.go index 9af355122..15b2406a6 100644 --- a/pkg/tools/sandbox/node.go +++ b/pkg/tools/sandbox/node.go @@ -40,6 +40,7 @@ import ( "github.com/networkservicemesh/sdk/pkg/registry/common/sendfd" "github.com/networkservicemesh/sdk/pkg/registry/core/chain" "github.com/networkservicemesh/sdk/pkg/tools/log" + authmonitor "github.com/networkservicemesh/sdk/pkg/tools/monitorconnection/authorize" "github.com/networkservicemesh/sdk/pkg/tools/token" ) @@ -69,6 +70,7 @@ func (n *Node) NewNSMgr( options := []nsmgr.Option{ nsmgr.WithName(name), nsmgr.WithAuthorizeServer(authorize.NewServer(authorize.Any())), + nsmgr.WithAuthorizeMonitorConnectionServer(authmonitor.NewMonitorConnectionServer(authmonitor.Any())), nsmgr.WithDialOptions(dialOptions...), nsmgr.WithDialTimeout(DialTimeout), } diff --git a/pkg/tools/spire/connection_id_set.gen.go b/pkg/tools/spire/connection_id_set.gen.go new file mode 100644 index 000000000..1526a87c7 --- /dev/null +++ b/pkg/tools/spire/connection_id_set.gen.go @@ -0,0 +1,75 @@ +// Code generated by "-output connection_id_set.gen.go -type ConnectionIDSet -output connection_id_set.gen.go -type ConnectionIDSet"; DO NOT EDIT. +// Install -output connection_id_set.gen.go -type ConnectionIDSet by "go get -u github.com/searKing/golang/tools/-output connection_id_set.gen.go -type ConnectionIDSet" + +package spire + +import ( + "sync" // Used by sync.Map. +) + +// Generate code that will fail if the constants change value. +func _() { + // An "cannot convert ConnectionIDSet literal (type ConnectionIDSet) to type sync.Map" compiler error signifies that the base type have changed. + // Re-run the go-syncmap command to generate them again. + _ = (sync.Map)(ConnectionIDSet{}) +} + +var _nil_ConnectionIDSet_struct___value = func() (val struct{}) { return }() + +// Load returns the value stored in the map for a key, or nil if no +// value is present. +// The ok result indicates whether value was found in the map. +func (m *ConnectionIDSet) Load(key string) (struct{}, bool) { + value, ok := (*sync.Map)(m).Load(key) + if value == nil { + return _nil_ConnectionIDSet_struct___value, ok + } + return value.(struct{}), ok +} + +// Store sets the value for a key. +func (m *ConnectionIDSet) Store(key string, value struct{}) { + (*sync.Map)(m).Store(key, value) +} + +// LoadOrStore returns the existing value for the key if present. +// Otherwise, it stores and returns the given value. +// The loaded result is true if the value was loaded, false if stored. +func (m *ConnectionIDSet) LoadOrStore(key string, value struct{}) (struct{}, bool) { + actual, loaded := (*sync.Map)(m).LoadOrStore(key, value) + if actual == nil { + return _nil_ConnectionIDSet_struct___value, loaded + } + return actual.(struct{}), loaded +} + +// LoadAndDelete deletes the value for a key, returning the previous value if any. +// The loaded result reports whether the key was present. +func (m *ConnectionIDSet) LoadAndDelete(key string) (value struct{}, loaded bool) { + actual, loaded := (*sync.Map)(m).LoadAndDelete(key) + if actual == nil { + return _nil_ConnectionIDSet_struct___value, loaded + } + return actual.(struct{}), loaded +} + +// Delete deletes the value for a key. +func (m *ConnectionIDSet) Delete(key string) { + (*sync.Map)(m).Delete(key) +} + +// Range calls f sequentially for each key and value present in the map. +// If f returns false, range stops the iteration. +// +// Range does not necessarily correspond to any consistent snapshot of the Map's +// contents: no key will be visited more than once, but if the value for any key +// is stored or deleted concurrently, Range may reflect any mapping for that key +// from any point during the Range call. +// +// Range may be O(N) with the number of elements in the map even if f returns +// false after a constant number of calls. +func (m *ConnectionIDSet) Range(f func(key string, value struct{}) bool) { + (*sync.Map)(m).Range(func(key, value interface{}) bool { + return f(key.(string), value.(struct{})) + }) +} diff --git a/pkg/tools/spire/gen_conn_map.go b/pkg/tools/spire/gen_conn_map.go new file mode 100644 index 000000000..f63e7d2f5 --- /dev/null +++ b/pkg/tools/spire/gen_conn_map.go @@ -0,0 +1,26 @@ +// Copyright (c) 2022 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package spire + +import ( + "sync" +) + +//go:generate go-syncmap -output connection_id_set.gen.go -type ConnectionIDSet + +// ConnectionIDSet - sync.Map with key == string and value == bool +type ConnectionIDSet sync.Map diff --git a/pkg/tools/spire/gen_spiffeid_conn_map.go b/pkg/tools/spire/gen_spiffeid_conn_map.go new file mode 100644 index 000000000..694ef6c26 --- /dev/null +++ b/pkg/tools/spire/gen_spiffeid_conn_map.go @@ -0,0 +1,26 @@ +// Copyright (c) 2022 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package spire + +import ( + "sync" +) + +//go:generate go-syncmap -output spiffe_id_connection_map.gen.go -type SpiffeIDConnectionMap + +// SpiffeIDConnectionMap - sync.Map with key == spiffeid.ID and value == *ConnectionIDSet +type SpiffeIDConnectionMap sync.Map diff --git a/pkg/tools/spire/spiffe_id_connection_map.gen.go b/pkg/tools/spire/spiffe_id_connection_map.gen.go new file mode 100644 index 000000000..4d89119ba --- /dev/null +++ b/pkg/tools/spire/spiffe_id_connection_map.gen.go @@ -0,0 +1,76 @@ +// Code generated by "-output spiffe_id_connection_map.gen.go -type SpiffeIDConnectionMap -output spiffe_id_connection_map.gen.go -type SpiffeIDConnectionMap"; DO NOT EDIT. +// Install -output spiffe_id_connection_map.gen.go -type SpiffeIDConnectionMap by "go get -u github.com/searKing/golang/tools/-output spiffe_id_connection_map.gen.go -type SpiffeIDConnectionMap" + +package spire + +import ( + "github.com/spiffe/go-spiffe/v2/spiffeid" + "sync" // Used by sync.Map. +) + +// Generate code that will fail if the constants change value. +func _() { + // An "cannot convert SpiffeIDConnectionMap literal (type SpiffeIDConnectionMap) to type sync.Map" compiler error signifies that the base type have changed. + // Re-run the go-syncmap command to generate them again. + _ = (sync.Map)(SpiffeIDConnectionMap{}) +} + +var _nil_SpiffeIDConnectionMap_ConnectionIDSet_value = func() (val *ConnectionIDSet) { return }() + +// Load returns the value stored in the map for a key, or nil if no +// value is present. +// The ok result indicates whether value was found in the map. +func (m *SpiffeIDConnectionMap) Load(key spiffeid.ID) (*ConnectionIDSet, bool) { + value, ok := (*sync.Map)(m).Load(key) + if value == nil { + return _nil_SpiffeIDConnectionMap_ConnectionIDSet_value, ok + } + return value.(*ConnectionIDSet), ok +} + +// Store sets the value for a key. +func (m *SpiffeIDConnectionMap) Store(key spiffeid.ID, value *ConnectionIDSet) { + (*sync.Map)(m).Store(key, value) +} + +// LoadOrStore returns the existing value for the key if present. +// Otherwise, it stores and returns the given value. +// The loaded result is true if the value was loaded, false if stored. +func (m *SpiffeIDConnectionMap) LoadOrStore(key spiffeid.ID, value *ConnectionIDSet) (*ConnectionIDSet, bool) { + actual, loaded := (*sync.Map)(m).LoadOrStore(key, value) + if actual == nil { + return _nil_SpiffeIDConnectionMap_ConnectionIDSet_value, loaded + } + return actual.(*ConnectionIDSet), loaded +} + +// LoadAndDelete deletes the value for a key, returning the previous value if any. +// The loaded result reports whether the key was present. +func (m *SpiffeIDConnectionMap) LoadAndDelete(key spiffeid.ID) (value *ConnectionIDSet, loaded bool) { + actual, loaded := (*sync.Map)(m).LoadAndDelete(key) + if actual == nil { + return _nil_SpiffeIDConnectionMap_ConnectionIDSet_value, loaded + } + return actual.(*ConnectionIDSet), loaded +} + +// Delete deletes the value for a key. +func (m *SpiffeIDConnectionMap) Delete(key spiffeid.ID) { + (*sync.Map)(m).Delete(key) +} + +// Range calls f sequentially for each key and value present in the map. +// If f returns false, range stops the iteration. +// +// Range does not necessarily correspond to any consistent snapshot of the Map's +// contents: no key will be visited more than once, but if the value for any key +// is stored or deleted concurrently, Range may reflect any mapping for that key +// from any point during the Range call. +// +// Range may be O(N) with the number of elements in the map even if f returns +// false after a constant number of calls. +func (m *SpiffeIDConnectionMap) Range(f func(key spiffeid.ID, value *ConnectionIDSet) bool) { + (*sync.Map)(m).Range(func(key, value interface{}) bool { + return f(key.(spiffeid.ID), value.(*ConnectionIDSet)) + }) +} diff --git a/pkg/tools/spire/start.go b/pkg/tools/spire/start.go index b26d18fcc..3ea394c08 100644 --- a/pkg/tools/spire/start.go +++ b/pkg/tools/spire/start.go @@ -23,6 +23,8 @@ package spire import ( "context" + "crypto/x509" + "errors" "fmt" "io/ioutil" "os" @@ -33,7 +35,12 @@ import ( "github.com/edwarnicke/exechelper" "github.com/sirupsen/logrus" + "github.com/spiffe/go-spiffe/v2/spiffeid" + "github.com/spiffe/go-spiffe/v2/svid/x509svid" "github.com/spiffe/go-spiffe/v2/workloadapi" + "google.golang.org/grpc/peer" + + "github.com/networkservicemesh/sdk/pkg/tools/opa" ) type contextKeyType string @@ -273,3 +280,21 @@ func execHealthCheck(ctx context.Context, cmdStr string, options ...*exechelper. } } } + +// SpiffeIDFromContext - returns spiffe ID of the service from the peer context +func SpiffeIDFromContext(ctx context.Context) (spiffeid.ID, error) { + p, ok := peer.FromContext(ctx) + var cert *x509.Certificate + if !ok { + return spiffeid.ID{}, errors.New("fail to get peer from context") + } + cert = opa.ParseX509Cert(p.AuthInfo) + if cert != nil { + spiffeID, err := x509svid.IDFromCert(cert) + if err == nil { + return spiffeID, nil + } + return spiffeid.ID{}, errors.New("fail to get Spiffe ID from certificate") + } + return spiffeid.ID{}, errors.New("fail to get certificate from peer") +}