diff --git a/pkg/networkservice/chains/nsmgr/server.go b/pkg/networkservice/chains/nsmgr/server.go index f0c6557c7..590a611b3 100644 --- a/pkg/networkservice/chains/nsmgr/server.go +++ b/pkg/networkservice/chains/nsmgr/server.go @@ -43,6 +43,7 @@ import ( "github.com/networkservicemesh/sdk/pkg/networkservice/common/metrics" "github.com/networkservicemesh/sdk/pkg/networkservice/core/adapters" "github.com/networkservicemesh/sdk/pkg/registry" + registryauthorize "github.com/networkservicemesh/sdk/pkg/registry/common/authorize" "github.com/networkservicemesh/sdk/pkg/registry/common/begin" "github.com/networkservicemesh/sdk/pkg/registry/common/clientconn" registryclientinfo "github.com/networkservicemesh/sdk/pkg/registry/common/clientinfo" @@ -77,6 +78,8 @@ type nsmgrServer struct { type serverOptions struct { authorizeServer networkservice.NetworkServiceServer authorizeMonitorConnectionServer networkservice.MonitorConnectionServer + authorizeNSRegistryServer registryapi.NetworkServiceRegistryServer + authorizeNSERegistryServer registryapi.NetworkServiceEndpointRegistryServer dialOptions []grpc.DialOption dialTimeout time.Duration regURL *url.URL @@ -130,6 +133,26 @@ func WithAuthorizeMonitorConnectionServer(authorizeMonitorConnectionServer netwo } } +// WithAuthorizeNSRegistryServer sets authorization NetworkServiceRegistry chain element +func WithAuthorizeNSRegistryServer(authorizeNSRegistryServer registryapi.NetworkServiceRegistryServer) Option { + if authorizeNSRegistryServer == nil { + panic("authorizeNSRegistryServer cannot be nil") + } + return func(o *serverOptions) { + o.authorizeNSRegistryServer = authorizeNSRegistryServer + } +} + +// WithAuthorizeNSERegistryServer sets authorization NetworkServiceEndpointRegistry chain element +func WithAuthorizeNSERegistryServer(authorizeNSERegistryServer registryapi.NetworkServiceEndpointRegistryServer) Option { + if authorizeNSERegistryServer == nil { + panic("authorizeNSERegistryServer cannot be nil") + } + return func(o *serverOptions) { + o.authorizeNSERegistryServer = authorizeNSERegistryServer + } +} + // 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) { @@ -162,6 +185,8 @@ func NewServer(ctx context.Context, tokenGenerator token.GeneratorFunc, options opts := &serverOptions{ authorizeServer: authorize.NewServer(authorize.Any()), authorizeMonitorConnectionServer: authmonitor.NewMonitorConnectionServer(authmonitor.Any()), + authorizeNSRegistryServer: registryauthorize.NewNetworkServiceRegistryServer(registryauthorize.Any()), + authorizeNSERegistryServer: registryauthorize.NewNetworkServiceEndpointRegistryServer(registryauthorize.Any()), name: "nsmgr-" + uuid.New().String(), forwarderServiceName: "forwarder", } @@ -187,6 +212,7 @@ func NewServer(ctx context.Context, tokenGenerator token.GeneratorFunc, options } nsRegistry = chain.NewNetworkServiceRegistryServer( + opts.authorizeNSRegistryServer, nsRegistry, ) @@ -214,6 +240,7 @@ func NewServer(ctx context.Context, tokenGenerator token.GeneratorFunc, options var nseRegistry = chain.NewNetworkServiceEndpointRegistryServer( begin.NewNetworkServiceEndpointRegistryServer(), + opts.authorizeNSERegistryServer, registryclientinfo.NewNetworkServiceEndpointRegistryServer(), expire.NewNetworkServiceEndpointRegistryServer(ctx, time.Minute), registryrecvfd.NewNetworkServiceEndpointRegistryServer(), // Allow to receive a passed files diff --git a/pkg/networkservice/common/authorize/server.go b/pkg/networkservice/common/authorize/server.go index 8939e6576..1caae8417 100644 --- a/pkg/networkservice/common/authorize/server.go +++ b/pkg/networkservice/common/authorize/server.go @@ -30,6 +30,7 @@ import ( "github.com/networkservicemesh/sdk/pkg/networkservice/core/next" "github.com/networkservicemesh/sdk/pkg/tools/opa" "github.com/networkservicemesh/sdk/pkg/tools/spire" + "github.com/networkservicemesh/sdk/pkg/tools/stringset" ) type authorizeServer struct { @@ -76,7 +77,7 @@ func (a *authorizeServer) Request(ctx context.Context, request *networkservice.N connID := conn.GetPath().GetPathSegments()[index-1].GetId() ids, ok := a.spiffeIDConnectionMap.Load(spiffeID) if !ok { - ids = &spire.ConnectionIDSet{} + ids = new(stringset.StringSet) } ids.Store(connID, struct{}{}) a.spiffeIDConnectionMap.Store(spiffeID, ids) diff --git a/pkg/registry/common/authorize/common.go b/pkg/registry/common/authorize/common.go new file mode 100644 index 000000000..d0849d1c0 --- /dev/null +++ b/pkg/registry/common/authorize/common.go @@ -0,0 +1,70 @@ +// 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" + + "github.com/spiffe/go-spiffe/v2/spiffeid" + + "github.com/networkservicemesh/sdk/pkg/tools/stringset" +) + +// RegistryOpaInput represents input for policies in authorizNSEServer and authorizeNSServer +type RegistryOpaInput struct { + SpiffeID string `json:"spiffe_id"` + ResourceName string `json:"resource_name"` + SpiffeIDResourcesMap map[string][]string `json:"spiffe_id_resources_map"` +} + +// Policy represents authorization policy for network service. +type Policy interface { + // Check checks authorization + Check(ctx context.Context, input interface{}) error +} + +type policiesList []Policy + +func (l *policiesList) check(ctx context.Context, input RegistryOpaInput) error { + if l == nil { + return nil + } + for _, policy := range *l { + if policy == nil { + continue + } + if err := policy.Check(ctx, input); err != nil { + return err + } + } + return nil +} + +func getRawMap(m *spiffeIDResourcesMap) map[string][]string { + rawMap := make(map[string][]string) + m.Range(func(key spiffeid.ID, value *stringset.StringSet) bool { + id := key.String() + rawMap[id] = make([]string, 0) + value.Range(func(key string, value struct{}) bool { + rawMap[id] = append(rawMap[id], key) + return true + }) + return true + }) + + return rawMap +} diff --git a/pkg/registry/common/authorize/common_test.go b/pkg/registry/common/authorize/common_test.go new file mode 100644 index 000000000..5ecd1531b --- /dev/null +++ b/pkg/registry/common/authorize/common_test.go @@ -0,0 +1,61 @@ +// 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/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "math/big" + "net/url" + "time" + + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/peer" +) + +func generateCert(u *url.URL) []byte { + ca := &x509.Certificate{ + SerialNumber: big.NewInt(1653), + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + URIs: []*url.URL{u}, + } + + priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + pub := &priv.PublicKey + + certBytes, _ := x509.CreateCertificate(rand.Reader, ca, ca, pub, priv) + return certBytes +} + +func withPeer(ctx context.Context, certBytes []byte) (context.Context, error) { + x509cert, err := x509.ParseCertificate(certBytes) + if err != nil { + return nil, err + } + + authInfo := &credentials.TLSInfo{ + State: tls.ConnectionState{ + PeerCertificates: []*x509.Certificate{x509cert}, + }, + } + return peer.NewContext(ctx, &peer.Peer{AuthInfo: authInfo}), nil +} diff --git a/pkg/registry/common/authorize/gen.go b/pkg/registry/common/authorize/gen.go new file mode 100644 index 000000000..02af0348c --- /dev/null +++ b/pkg/registry/common/authorize/gen.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 authorize + +import ( + "sync" +) + +//go:generate go-syncmap -output sync_map.gen.go -type spiffeIDResourcesMap + +// spiffeIDResourcesMap - sync.Map with key == spiffeID and value == list of NSEs associated with spiffeID +type spiffeIDResourcesMap sync.Map diff --git a/pkg/registry/common/authorize/ns_server.go b/pkg/registry/common/authorize/ns_server.go new file mode 100644 index 000000000..9b51ea49b --- /dev/null +++ b/pkg/registry/common/authorize/ns_server.go @@ -0,0 +1,119 @@ +// 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 ( + "context" + + "github.com/golang/protobuf/ptypes/empty" + + "github.com/networkservicemesh/api/pkg/api/registry" + + "github.com/networkservicemesh/sdk/pkg/registry/core/next" + "github.com/networkservicemesh/sdk/pkg/tools/opa" + "github.com/networkservicemesh/sdk/pkg/tools/spire" + "github.com/networkservicemesh/sdk/pkg/tools/stringset" +) + +type authorizeNSServer struct { + policies policiesList + spiffeIDNSsMap *spiffeIDResourcesMap +} + +// NewNetworkServiceRegistryServer - returns a new authorization registry.NetworkServiceRegistryServer +// Authorize registry server checks spiffeID of NS. +func NewNetworkServiceRegistryServer(opts ...Option) registry.NetworkServiceRegistryServer { + o := &options{ + policies: policiesList{opa.WithRegistryClientAllowedPolicy()}, + spiffeIDResourcesMap: new(spiffeIDResourcesMap), + } + + for _, opt := range opts { + opt(o) + } + + return &authorizeNSServer{ + policies: o.policies, + spiffeIDNSsMap: o.spiffeIDResourcesMap, + } +} + +func (s *authorizeNSServer) Register(ctx context.Context, ns *registry.NetworkService) (*registry.NetworkService, error) { + spiffeID, err := spire.SpiffeIDFromContext(ctx) + if err != nil && len(s.policies) == 0 { + return next.NetworkServiceRegistryServer(ctx).Register(ctx, ns) + } + + rawMap := getRawMap(s.spiffeIDNSsMap) + input := RegistryOpaInput{ + SpiffeID: spiffeID.String(), + ResourceName: ns.Name, + SpiffeIDResourcesMap: rawMap, + } + if err := s.policies.check(ctx, input); err != nil { + return nil, err + } + + nsNames, ok := s.spiffeIDNSsMap.Load(spiffeID) + if !ok { + nsNames = new(stringset.StringSet) + } + nsNames.Store(ns.Name, struct{}{}) + s.spiffeIDNSsMap.Store(spiffeID, nsNames) + + return next.NetworkServiceRegistryServer(ctx).Register(ctx, ns) +} + +func (s *authorizeNSServer) Find(query *registry.NetworkServiceQuery, server registry.NetworkServiceRegistry_FindServer) error { + return next.NetworkServiceRegistryServer(server.Context()).Find(query, server) +} + +func (s *authorizeNSServer) Unregister(ctx context.Context, ns *registry.NetworkService) (*empty.Empty, error) { + spiffeID, err := spire.SpiffeIDFromContext(ctx) + if err != nil && len(s.policies) == 0 { + return next.NetworkServiceRegistryServer(ctx).Unregister(ctx, ns) + } + + rawMap := getRawMap(s.spiffeIDNSsMap) + input := RegistryOpaInput{ + SpiffeID: spiffeID.String(), + ResourceName: ns.Name, + SpiffeIDResourcesMap: rawMap, + } + if err := s.policies.check(ctx, input); err != nil { + return nil, err + } + + nsNames, ok := s.spiffeIDNSsMap.Load(spiffeID) + if ok { + nsNames.Delete(ns.Name) + namesEmpty := true + nsNames.Range(func(key string, value struct{}) bool { + namesEmpty = false + return true + }) + + if namesEmpty { + s.spiffeIDNSsMap.Delete(spiffeID) + } else { + s.spiffeIDNSsMap.Store(spiffeID, nsNames) + } + } + + return next.NetworkServiceRegistryServer(ctx).Unregister(ctx, ns) +} diff --git a/pkg/registry/common/authorize/ns_server_test.go b/pkg/registry/common/authorize/ns_server_test.go new file mode 100644 index 000000000..35cdfca92 --- /dev/null +++ b/pkg/registry/common/authorize/ns_server_test.go @@ -0,0 +1,61 @@ +// 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" + "net/url" + "testing" + + "github.com/networkservicemesh/api/pkg/api/registry" + "github.com/stretchr/testify/require" + + "github.com/networkservicemesh/sdk/pkg/registry/common/authorize" + + "go.uber.org/goleak" +) + +func TestAuthzNetworkServiceRegistry(t *testing.T) { + t.Cleanup(func() { goleak.VerifyNone(t) }) + server := authorize.NewNetworkServiceRegistryServer() + + nsReg := ®istry.NetworkService{Name: "ns-1"} + + u1, _ := url.Parse("spiffe://test.com/workload1") + u2, _ := url.Parse("spiffe://test.com/workload2") + cert1 := generateCert(u1) + cert2 := generateCert(u2) + cert1Ctx, err := withPeer(context.Background(), cert1) + require.NoError(t, err) + cert2Ctx, err := withPeer(context.Background(), cert2) + require.NoError(t, err) + + _, err = server.Register(cert1Ctx, nsReg) + require.NoError(t, err) + + _, err = server.Register(cert2Ctx, nsReg) + require.Error(t, err) + + _, err = server.Register(cert1Ctx, nsReg) + require.NoError(t, err) + + _, err = server.Unregister(cert2Ctx, nsReg) + require.Error(t, err) + + _, err = server.Unregister(cert1Ctx, nsReg) + require.NoError(t, err) +} diff --git a/pkg/registry/common/authorize/nse_server.go b/pkg/registry/common/authorize/nse_server.go new file mode 100644 index 000000000..eaf818604 --- /dev/null +++ b/pkg/registry/common/authorize/nse_server.go @@ -0,0 +1,119 @@ +// 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 ( + "context" + + "github.com/golang/protobuf/ptypes/empty" + + "github.com/networkservicemesh/api/pkg/api/registry" + + "github.com/networkservicemesh/sdk/pkg/registry/core/next" + "github.com/networkservicemesh/sdk/pkg/tools/opa" + "github.com/networkservicemesh/sdk/pkg/tools/spire" + "github.com/networkservicemesh/sdk/pkg/tools/stringset" +) + +type authorizeNSEServer struct { + policies policiesList + spiffeIDNSEsMap *spiffeIDResourcesMap +} + +// NewNetworkServiceEndpointRegistryServer - returns a new authorization registry.NetworkServiceEndpointRegistryServer +// Authorize registry server checks spiffeID of NSE. +func NewNetworkServiceEndpointRegistryServer(opts ...Option) registry.NetworkServiceEndpointRegistryServer { + o := &options{ + policies: policiesList{opa.WithRegistryClientAllowedPolicy()}, + spiffeIDResourcesMap: new(spiffeIDResourcesMap), + } + + for _, opt := range opts { + opt(o) + } + + return &authorizeNSEServer{ + policies: o.policies, + spiffeIDNSEsMap: o.spiffeIDResourcesMap, + } +} + +func (s *authorizeNSEServer) Register(ctx context.Context, nse *registry.NetworkServiceEndpoint) (*registry.NetworkServiceEndpoint, error) { + spiffeID, err := spire.SpiffeIDFromContext(ctx) + if err != nil && len(s.policies) == 0 { + return next.NetworkServiceEndpointRegistryServer(ctx).Register(ctx, nse) + } + + rawMap := getRawMap(s.spiffeIDNSEsMap) + input := RegistryOpaInput{ + SpiffeID: spiffeID.String(), + ResourceName: nse.Name, + SpiffeIDResourcesMap: rawMap, + } + if err := s.policies.check(ctx, input); err != nil { + return nil, err + } + + nseNames, ok := s.spiffeIDNSEsMap.Load(spiffeID) + if !ok { + nseNames = new(stringset.StringSet) + } + nseNames.LoadOrStore(nse.Name, struct{}{}) + s.spiffeIDNSEsMap.Store(spiffeID, nseNames) + + return next.NetworkServiceEndpointRegistryServer(ctx).Register(ctx, nse) +} + +func (s *authorizeNSEServer) Find(query *registry.NetworkServiceEndpointQuery, server registry.NetworkServiceEndpointRegistry_FindServer) error { + return next.NetworkServiceEndpointRegistryServer(server.Context()).Find(query, server) +} + +func (s *authorizeNSEServer) Unregister(ctx context.Context, nse *registry.NetworkServiceEndpoint) (*empty.Empty, error) { + spiffeID, err := spire.SpiffeIDFromContext(ctx) + if err != nil && len(s.policies) == 0 { + return next.NetworkServiceEndpointRegistryServer(ctx).Unregister(ctx, nse) + } + + rawMap := getRawMap(s.spiffeIDNSEsMap) + input := RegistryOpaInput{ + SpiffeID: spiffeID.String(), + ResourceName: nse.Name, + SpiffeIDResourcesMap: rawMap, + } + if err := s.policies.check(ctx, input); err != nil { + return nil, err + } + + nseNames, ok := s.spiffeIDNSEsMap.Load(spiffeID) + if ok { + nseNames.Delete(nse.Name) + namesEmpty := true + nseNames.Range(func(key string, value struct{}) bool { + namesEmpty = false + return true + }) + + if namesEmpty { + s.spiffeIDNSEsMap.Delete(spiffeID) + } else { + s.spiffeIDNSEsMap.Store(spiffeID, nseNames) + } + } + + return next.NetworkServiceEndpointRegistryServer(ctx).Unregister(ctx, nse) +} diff --git a/pkg/registry/common/authorize/nse_server_test.go b/pkg/registry/common/authorize/nse_server_test.go new file mode 100644 index 000000000..0a0f6afa7 --- /dev/null +++ b/pkg/registry/common/authorize/nse_server_test.go @@ -0,0 +1,61 @@ +// 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" + "net/url" + "testing" + + "github.com/networkservicemesh/api/pkg/api/registry" + "github.com/stretchr/testify/require" + + "github.com/networkservicemesh/sdk/pkg/registry/common/authorize" + + "go.uber.org/goleak" +) + +func TestAuthzEndpointRegistry(t *testing.T) { + t.Cleanup(func() { goleak.VerifyNone(t) }) + server := authorize.NewNetworkServiceEndpointRegistryServer() + + nseReg := ®istry.NetworkServiceEndpoint{Name: "nse-1"} + + u1, _ := url.Parse("spiffe://test.com/workload1") + u2, _ := url.Parse("spiffe://test.com/workload2") + cert1 := generateCert(u1) + cert2 := generateCert(u2) + cert1Ctx, err := withPeer(context.Background(), cert1) + require.NoError(t, err) + cert2Ctx, err := withPeer(context.Background(), cert2) + require.NoError(t, err) + + _, err = server.Register(cert1Ctx, nseReg) + require.NoError(t, err) + + _, err = server.Register(cert2Ctx, nseReg) + require.Error(t, err) + + _, err = server.Register(cert1Ctx, nseReg) + require.NoError(t, err) + + _, err = server.Unregister(cert2Ctx, nseReg) + require.Error(t, err) + + _, err = server.Unregister(cert1Ctx, nseReg) + require.NoError(t, err) +} diff --git a/pkg/registry/common/authorize/options.go b/pkg/registry/common/authorize/options.go new file mode 100644 index 000000000..43c46c027 --- /dev/null +++ b/pkg/registry/common/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 + +type options struct { + policies policiesList + spiffeIDResourcesMap *spiffeIDResourcesMap +} + +// Option is authorization option for server +type Option func(*options) + +// Any authorizes any call of request/close +func Any() Option { + return func(o *options) { + o.policies = nil + } +} + +// WithPolicies sets custom policies for registry +func WithPolicies(p ...Policy) Option { + return func(o *options) { + o.policies = p + } +} + +// WithSpiffeIDResourcesMap sets map to keep spiffeIDResourcesMap to authorize connections with Registry Authorize Chain Element +func WithSpiffeIDResourcesMap(m *spiffeIDResourcesMap) Option { + return func(o *options) { + o.spiffeIDResourcesMap = m + } +} diff --git a/pkg/registry/common/authorize/sync_map.gen.go b/pkg/registry/common/authorize/sync_map.gen.go new file mode 100644 index 000000000..9d941f01e --- /dev/null +++ b/pkg/registry/common/authorize/sync_map.gen.go @@ -0,0 +1,77 @@ +// Code generated by "-output sync_map.gen.go -type spiffeIDResourcesMap -output sync_map.gen.go -type spiffeIDResourcesMap"; DO NOT EDIT. +package authorize + +import ( + "sync" // Used by sync.Map. + + "github.com/spiffe/go-spiffe/v2/spiffeid" + + "github.com/networkservicemesh/sdk/pkg/tools/stringset" +) + +// Generate code that will fail if the constants change value. +func _() { + // An "cannot convert spiffeIDResourcesMap literal (type spiffeIDResourcesMap) 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)(spiffeIDResourcesMap{}) +} + +var _nil_spiffeIDResourcesMap_stringset_StringSet_value = func() (val *stringset.StringSet) { 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 *spiffeIDResourcesMap) Load(key spiffeid.ID) (*stringset.StringSet, bool) { + value, ok := (*sync.Map)(m).Load(key) + if value == nil { + return _nil_spiffeIDResourcesMap_stringset_StringSet_value, ok + } + return value.(*stringset.StringSet), ok +} + +// Store sets the value for a key. +func (m *spiffeIDResourcesMap) Store(key spiffeid.ID, value *stringset.StringSet) { + (*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 *spiffeIDResourcesMap) LoadOrStore(key spiffeid.ID, value *stringset.StringSet) (*stringset.StringSet, bool) { + actual, loaded := (*sync.Map)(m).LoadOrStore(key, value) + if actual == nil { + return _nil_spiffeIDResourcesMap_stringset_StringSet_value, loaded + } + return actual.(*stringset.StringSet), 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 *spiffeIDResourcesMap) LoadAndDelete(key spiffeid.ID) (value *stringset.StringSet, loaded bool) { + actual, loaded := (*sync.Map)(m).LoadAndDelete(key) + if actual == nil { + return _nil_spiffeIDResourcesMap_stringset_StringSet_value, loaded + } + return actual.(*stringset.StringSet), loaded +} + +// Delete deletes the value for a key. +func (m *spiffeIDResourcesMap) 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 *spiffeIDResourcesMap) Range(f func(key spiffeid.ID, value *stringset.StringSet) bool) { + (*sync.Map)(m).Range(func(key, value interface{}) bool { + return f(key.(spiffeid.ID), value.(*stringset.StringSet)) + }) +} diff --git a/pkg/tools/cidr/group.go b/pkg/tools/cidr/group.go index 635988543..542ce95b4 100644 --- a/pkg/tools/cidr/group.go +++ b/pkg/tools/cidr/group.go @@ -25,11 +25,13 @@ import ( // Groups allows parsing cidr groups. // Example: [v1, v2, v3], [v4], v5 => -// [][]*net.IPNet{ -// {v1, v2, v3}, -// {v4}, -// {v5}, -// } +// +// [][]*net.IPNet{ +// {v1, v2, v3}, +// {v4}, +// {v5}, +// } +// // Can be used for env configuration. // See at https://github.com/kelseyhightower/envconfig#custom-decoders type Groups [][]*net.IPNet diff --git a/pkg/tools/monitorconnection/authorize/server.go b/pkg/tools/monitorconnection/authorize/server.go index 8d9d3a8fa..b73e7c2c4 100644 --- a/pkg/tools/monitorconnection/authorize/server.go +++ b/pkg/tools/monitorconnection/authorize/server.go @@ -24,6 +24,7 @@ import ( "github.com/networkservicemesh/sdk/pkg/tools/monitorconnection/next" "github.com/networkservicemesh/sdk/pkg/tools/opa" "github.com/networkservicemesh/sdk/pkg/tools/spire" + "github.com/networkservicemesh/sdk/pkg/tools/stringset" ) type authorizeMonitorConnectionsServer struct { @@ -57,8 +58,9 @@ type MonitorOpaInput struct { 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 { + func(sid spiffeid.ID, connIds *stringset.StringSet) bool { connIds.Range( func(connId string, _ struct{}) bool { ids := simpleMap[sid.String()] diff --git a/pkg/tools/monitorconnection/authorize/server_test.go b/pkg/tools/monitorconnection/authorize/server_test.go index d47f106a4..2d6720f0a 100644 --- a/pkg/tools/monitorconnection/authorize/server_test.go +++ b/pkg/tools/monitorconnection/authorize/server_test.go @@ -35,6 +35,7 @@ import ( "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/sdk/pkg/tools/stringset" "github.com/networkservicemesh/api/pkg/api/networkservice" @@ -188,7 +189,7 @@ func TestAuthzEndpoint(t *testing.T) { } spiffeIDConnectionMap := spire.SpiffeIDConnectionMap{} for spiffeIDstr, connIds := range s.spiffeIDConnMap { - connIDMap := spire.ConnectionIDSet{} + connIDMap := stringset.StringSet{} for _, connID := range connIds { connIDMap.Store(connID, struct{}{}) } @@ -233,7 +234,7 @@ func TestAuthorize_ShouldCorrectlyWorkWithHeal(t *testing.T) { require.NoError(t, err) spiffeIDConnectionMap := spire.SpiffeIDConnectionMap{} - connMap := spire.ConnectionIDSet{} + connMap := stringset.StringSet{} var placer struct{} connMap.Store("conn1", placer) var spiffeID spiffeid.ID diff --git a/pkg/tools/opa/policies.go b/pkg/tools/opa/policies.go index aac4f5c48..b62a5f832 100644 --- a/pkg/tools/opa/policies.go +++ b/pkg/tools/opa/policies.go @@ -36,6 +36,9 @@ var tokensExpiredPolicySource string //go:embed policies/service_connection.rego var tokensServiceConnectionPolicySource string +//go:embed policies/registry_client_allowed.rego +var registryClientAllowedPolicySource string + // WithTokensValidPolicy returns default policy for checking that all tokens in the path can be decoded. func WithTokensValidPolicy() *AuthorizationPolicy { return &AuthorizationPolicy{ @@ -88,3 +91,12 @@ func WithMonitorConnectionServerPolicy() *AuthorizationPolicy { checker: True("service_connection"), } } + +// WithRegistryClientAllowedPolicy returns policy for checking resource (nse or ns) registration/unregistration validity +func WithRegistryClientAllowedPolicy() *AuthorizationPolicy { + return &AuthorizationPolicy{ + policySource: registryClientAllowedPolicySource, + query: "registry_client_allowed", + checker: True("registry_client_allowed"), + } +} diff --git a/pkg/tools/opa/policies/registry_client_allowed.rego b/pkg/tools/opa/policies/registry_client_allowed.rego new file mode 100644 index 000000000..59d7acb75 --- /dev/null +++ b/pkg/tools/opa/policies/registry_client_allowed.rego @@ -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 nsm + +default registry_client_allowed = false + +# new NSE case +registry_client_allowed { + nses := { nse | nse := input.spiffe_id_resources_map[_][_]; nse == input.resource_name } + count(nses) == 0 +} + +# refresh/unregister NSE case +registry_client_allowed { + input.spiffe_id_resources_map[input.spiffe_id][_] == input.resource_name +} \ No newline at end of file diff --git a/pkg/tools/opa/registry_client_allowed_test.go b/pkg/tools/opa/registry_client_allowed_test.go new file mode 100644 index 000000000..3b388c1b8 --- /dev/null +++ b/pkg/tools/opa/registry_client_allowed_test.go @@ -0,0 +1,64 @@ +// 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 opa_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/networkservicemesh/sdk/pkg/registry/common/authorize" + "github.com/networkservicemesh/sdk/pkg/tools/opa" +) + +func TestRegistryClientAllowedPolicy(t *testing.T) { + var p = opa.WithRegistryClientAllowedPolicy() + spiffeIDResourcesMap := map[string][]string{ + "id1": {"nse1", "nse2"}, + "id2": {"nse3", "nse4"}, + } + + samples := []struct { + nseName string + spiffeID string + valid bool + }{ + {spiffeID: "id1", nseName: "nse1", valid: true}, + {spiffeID: "id1", nseName: "nse3", valid: false}, + {spiffeID: "id1", nseName: "nse5", valid: true}, + {spiffeID: "id3", nseName: "nse5", valid: true}, + {spiffeID: "id3", nseName: "nse2", valid: false}, + } + + ctx := context.Background() + + for _, sample := range samples { + var input = authorize.RegistryOpaInput{ + SpiffeIDResourcesMap: spiffeIDResourcesMap, + SpiffeID: sample.spiffeID, + ResourceName: sample.nseName, + } + + err := p.Check(ctx, input) + if sample.valid { + require.NoError(t, err) + } else { + require.Error(t, err) + } + } +} diff --git a/pkg/tools/spire/gen_spiffeid_conn_map.go b/pkg/tools/spire/gen_spiffeid_conn_map.go index 694ef6c26..599b39190 100644 --- a/pkg/tools/spire/gen_spiffeid_conn_map.go +++ b/pkg/tools/spire/gen_spiffeid_conn_map.go @@ -20,7 +20,7 @@ import ( "sync" ) -//go:generate go-syncmap -output spiffe_id_connection_map.gen.go -type SpiffeIDConnectionMap +//go:generate go-syncmap -output spiffe_id_connection_map.gen.go -type SpiffeIDConnectionMap -// SpiffeIDConnectionMap - sync.Map with key == spiffeid.ID and value == *ConnectionIDSet +// SpiffeIDConnectionMap - sync.Map with key == spiffeid.ID and value == *StringSet 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 index 4d89119ba..f521d6bb0 100644 --- a/pkg/tools/spire/spiffe_id_connection_map.gen.go +++ b/pkg/tools/spire/spiffe_id_connection_map.gen.go @@ -1,11 +1,12 @@ -// 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" - +// Code generated by "-output spiffe_id_connection_map.gen.go -type SpiffeIDConnectionMap -output spiffe_id_connection_map.gen.go -type SpiffeIDConnectionMap"; DO NOT EDIT. package spire import ( - "github.com/spiffe/go-spiffe/v2/spiffeid" "sync" // Used by sync.Map. + + "github.com/spiffe/go-spiffe/v2/spiffeid" + + "github.com/networkservicemesh/sdk/pkg/tools/stringset" ) // Generate code that will fail if the constants change value. @@ -15,43 +16,43 @@ func _() { _ = (sync.Map)(SpiffeIDConnectionMap{}) } -var _nil_SpiffeIDConnectionMap_ConnectionIDSet_value = func() (val *ConnectionIDSet) { return }() +var _nil_SpiffeIDConnectionMap_stringset_StringSet_value = func() (val *stringset.StringSet) { 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) { +func (m *SpiffeIDConnectionMap) Load(key spiffeid.ID) (*stringset.StringSet, bool) { value, ok := (*sync.Map)(m).Load(key) if value == nil { - return _nil_SpiffeIDConnectionMap_ConnectionIDSet_value, ok + return _nil_SpiffeIDConnectionMap_stringset_StringSet_value, ok } - return value.(*ConnectionIDSet), ok + return value.(*stringset.StringSet), ok } // Store sets the value for a key. -func (m *SpiffeIDConnectionMap) Store(key spiffeid.ID, value *ConnectionIDSet) { +func (m *SpiffeIDConnectionMap) Store(key spiffeid.ID, value *stringset.StringSet) { (*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) { +func (m *SpiffeIDConnectionMap) LoadOrStore(key spiffeid.ID, value *stringset.StringSet) (*stringset.StringSet, bool) { actual, loaded := (*sync.Map)(m).LoadOrStore(key, value) if actual == nil { - return _nil_SpiffeIDConnectionMap_ConnectionIDSet_value, loaded + return _nil_SpiffeIDConnectionMap_stringset_StringSet_value, loaded } - return actual.(*ConnectionIDSet), loaded + return actual.(*stringset.StringSet), 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) { +func (m *SpiffeIDConnectionMap) LoadAndDelete(key spiffeid.ID) (value *stringset.StringSet, loaded bool) { actual, loaded := (*sync.Map)(m).LoadAndDelete(key) if actual == nil { - return _nil_SpiffeIDConnectionMap_ConnectionIDSet_value, loaded + return _nil_SpiffeIDConnectionMap_stringset_StringSet_value, loaded } - return actual.(*ConnectionIDSet), loaded + return actual.(*stringset.StringSet), loaded } // Delete deletes the value for a key. @@ -69,8 +70,8 @@ func (m *SpiffeIDConnectionMap) Delete(key spiffeid.ID) { // // 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) { +func (m *SpiffeIDConnectionMap) Range(f func(key spiffeid.ID, value *stringset.StringSet) bool) { (*sync.Map)(m).Range(func(key, value interface{}) bool { - return f(key.(spiffeid.ID), value.(*ConnectionIDSet)) + return f(key.(spiffeid.ID), value.(*stringset.StringSet)) }) } diff --git a/pkg/tools/spire/gen_conn_map.go b/pkg/tools/stringset/gen.go similarity index 76% rename from pkg/tools/spire/gen_conn_map.go rename to pkg/tools/stringset/gen.go index f63e7d2f5..8790ac370 100644 --- a/pkg/tools/spire/gen_conn_map.go +++ b/pkg/tools/stringset/gen.go @@ -14,13 +14,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -package spire +package stringset import ( "sync" ) -//go:generate go-syncmap -output connection_id_set.gen.go -type ConnectionIDSet +//go:generate go-syncmap -output sync_map.gen.go -type StringSet -// ConnectionIDSet - sync.Map with key == string and value == bool -type ConnectionIDSet sync.Map +// StringSet - sync.Map with key == string and value == struct{} +type StringSet sync.Map diff --git a/pkg/tools/spire/connection_id_set.gen.go b/pkg/tools/stringset/sync_map.gen.go similarity index 59% rename from pkg/tools/spire/connection_id_set.gen.go rename to pkg/tools/stringset/sync_map.gen.go index 1526a87c7..cc4a23fde 100644 --- a/pkg/tools/spire/connection_id_set.gen.go +++ b/pkg/tools/stringset/sync_map.gen.go @@ -1,7 +1,5 @@ -// 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 +// Code generated by "-output sync_map.gen.go -type StringSet -output sync_map.gen.go -type StringSet"; DO NOT EDIT. +package stringset import ( "sync" // Used by sync.Map. @@ -9,52 +7,52 @@ import ( // 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. + // An "cannot convert StringSet literal (type StringSet) 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{}) + _ = (sync.Map)(StringSet{}) } -var _nil_ConnectionIDSet_struct___value = func() (val struct{}) { return }() +var _nil_StringSet_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) { +func (m *StringSet) Load(key string) (struct{}, bool) { value, ok := (*sync.Map)(m).Load(key) if value == nil { - return _nil_ConnectionIDSet_struct___value, ok + return _nil_StringSet_struct___value, ok } return value.(struct{}), ok } // Store sets the value for a key. -func (m *ConnectionIDSet) Store(key string, value struct{}) { +func (m *StringSet) 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) { +func (m *StringSet) 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 _nil_StringSet_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) { +func (m *StringSet) LoadAndDelete(key string) (value struct{}, loaded bool) { actual, loaded := (*sync.Map)(m).LoadAndDelete(key) if actual == nil { - return _nil_ConnectionIDSet_struct___value, loaded + return _nil_StringSet_struct___value, loaded } return actual.(struct{}), loaded } // Delete deletes the value for a key. -func (m *ConnectionIDSet) Delete(key string) { +func (m *StringSet) Delete(key string) { (*sync.Map)(m).Delete(key) } @@ -68,7 +66,7 @@ func (m *ConnectionIDSet) Delete(key string) { // // 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) { +func (m *StringSet) Range(f func(key string, value struct{}) bool) { (*sync.Map)(m).Range(func(key, value interface{}) bool { return f(key.(string), value.(struct{})) })