From f13f44587e0be28d031eec5ca731d26f6eef14f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Pab=C3=B3n?= Date: Wed, 14 Nov 2018 14:54:16 -0800 Subject: [PATCH] HTTPS and Auth support for the SDK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the REST Gateway would directly connect to the gRPC port. Now with HTTPS/TLS support, this is more complicated since the CERTS would have a CN not labeled 'localhost'. For simplicity the SDK now also has a unix domain socket. This UDS is used for local access and for the REST Gateway to communicate with the gRPC server. All models support token authentication and authorization based on JWTs. All SDK unit tests run with HTTPS enabled. Signed-off-by: Luis Pabón --- api/server/sdk/alerts.go | 4 +- api/server/sdk/cloud_backup.go | 2 +- api/server/sdk/cluster.go | 2 +- api/server/sdk/cluster_pair.go | 20 +- api/server/sdk/cluster_test.go | 27 +- api/server/sdk/credentials.go | 2 +- api/server/sdk/identity.go | 2 +- api/server/sdk/node.go | 2 +- api/server/sdk/objectstore.go | 2 +- api/server/sdk/rest_gateway.go | 153 +++++++ api/server/sdk/schedule_policy.go | 2 +- api/server/sdk/sdk_test.go | 53 ++- api/server/sdk/server.go | 474 +++++++++++---------- api/server/sdk/server_interceptors.go | 209 +++++++++ api/server/sdk/server_interceptors_test.go | 106 +++++ api/server/sdk/test_certs/server-cert.pem | 32 ++ api/server/sdk/test_certs/server-key.pem | 52 +++ api/server/sdk/utils.go | 10 + api/server/sdk/volume.go | 2 +- cmd/osd/main.go | 57 +++ hack/generate-https-certs.sh | 4 + hack/generate-jwt-rsa-keys.sh | 7 + pkg/auth/auth.go | 148 +++++++ pkg/grpcserver/grpcserver.go | 2 +- pkg/grpcserver/grpcutil.go | 55 +++ 25 files changed, 1184 insertions(+), 245 deletions(-) create mode 100644 api/server/sdk/rest_gateway.go create mode 100644 api/server/sdk/server_interceptors.go create mode 100644 api/server/sdk/server_interceptors_test.go create mode 100644 api/server/sdk/test_certs/server-cert.pem create mode 100644 api/server/sdk/test_certs/server-key.pem create mode 100755 hack/generate-https-certs.sh create mode 100755 hack/generate-jwt-rsa-keys.sh create mode 100644 pkg/auth/auth.go create mode 100644 pkg/grpcserver/grpcutil.go diff --git a/api/server/sdk/alerts.go b/api/server/sdk/alerts.go index f3fc829b5..6b47c1c4b 100644 --- a/api/server/sdk/alerts.go +++ b/api/server/sdk/alerts.go @@ -38,7 +38,7 @@ const ( // alertsServer pointer properly instantiated with a valid // alerts.filterDeleter. type alertsServer struct { - server *Server + server serverAccessor } func (s *alertsServer) alert() alerts.FilterDeleter { @@ -48,7 +48,7 @@ func (s *alertsServer) alert() alerts.FilterDeleter { // NewAlertsServer provides an instance of alerts server interface. func NewAlertsServer(filterDeleter alerts.FilterDeleter) api.OpenStorageAlertsServer { return &alertsServer{ - server: &Server{alertHandler: filterDeleter}, + server: &sdkGrpcServer{alertHandler: filterDeleter}, } } diff --git a/api/server/sdk/cloud_backup.go b/api/server/sdk/cloud_backup.go index 7e851a309..0e878b143 100644 --- a/api/server/sdk/cloud_backup.go +++ b/api/server/sdk/cloud_backup.go @@ -27,7 +27,7 @@ import ( // CloudBackupServer is an implementation of the gRPC OpenStorageCloudBackup interface type CloudBackupServer struct { - server *Server + server serverAccessor } func (s *CloudBackupServer) driver() volume.VolumeDriver { diff --git a/api/server/sdk/cluster.go b/api/server/sdk/cluster.go index 7146213ff..ebcd3917b 100644 --- a/api/server/sdk/cluster.go +++ b/api/server/sdk/cluster.go @@ -27,7 +27,7 @@ import ( // ClusterServer is an implementation of the gRPC OpenStorageClusterServer interface type ClusterServer struct { - server *Server + server serverAccessor } func (s *ClusterServer) cluster() cluster.Cluster { diff --git a/api/server/sdk/cluster_pair.go b/api/server/sdk/cluster_pair.go index 36e1f2381..7e73ffee3 100644 --- a/api/server/sdk/cluster_pair.go +++ b/api/server/sdk/cluster_pair.go @@ -27,7 +27,7 @@ import ( // ClusterPairServer is an implementation of the gRPC OpenStorageClusterServer interface type ClusterPairServer struct { - server *Server + server serverAccessor } func (s *ClusterPairServer) cluster() cluster.Cluster { @@ -39,6 +39,9 @@ func (s *ClusterPairServer) Create( ctx context.Context, req *api.SdkClusterPairCreateRequest, ) (*api.SdkClusterPairCreateResponse, error) { + if s.cluster() == nil { + return nil, status.Error(codes.Unavailable, "Resource has not been initialized") + } if req.GetRequest() == nil { return nil, status.Errorf(codes.InvalidArgument, "Must supply valid request") @@ -60,6 +63,9 @@ func (s *ClusterPairServer) Inspect( ctx context.Context, req *api.SdkClusterPairInspectRequest, ) (*api.SdkClusterPairInspectResponse, error) { + if s.cluster() == nil { + return nil, status.Error(codes.Unavailable, "Resource has not been initialized") + } if len(req.GetId()) == 0 { return nil, status.Error(codes.InvalidArgument, "Must supply cluster ID") @@ -78,6 +84,9 @@ func (s *ClusterPairServer) Enumerate( ctx context.Context, req *api.SdkClusterPairEnumerateRequest, ) (*api.SdkClusterPairEnumerateResponse, error) { + if s.cluster() == nil { + return nil, status.Error(codes.Unavailable, "Resource has not been initialized") + } resp, err := s.cluster().EnumeratePairs() if err != nil { @@ -93,6 +102,9 @@ func (s *ClusterPairServer) GetToken( ctx context.Context, req *api.SdkClusterPairGetTokenRequest, ) (*api.SdkClusterPairGetTokenResponse, error) { + if s.cluster() == nil { + return nil, status.Error(codes.Unavailable, "Resource has not been initialized") + } resp, err := s.cluster().GetPairToken(false) if err != nil { @@ -108,6 +120,9 @@ func (s *ClusterPairServer) ResetToken( ctx context.Context, req *api.SdkClusterPairResetTokenRequest, ) (*api.SdkClusterPairResetTokenResponse, error) { + if s.cluster() == nil { + return nil, status.Error(codes.Unavailable, "Resource has not been initialized") + } resp, err := s.cluster().GetPairToken(true) if err != nil { @@ -123,6 +138,9 @@ func (s *ClusterPairServer) Delete( ctx context.Context, req *api.SdkClusterPairDeleteRequest, ) (*api.SdkClusterPairDeleteResponse, error) { + if s.cluster() == nil { + return nil, status.Error(codes.Unavailable, "Resource has not been initialized") + } if len(req.GetClusterId()) == 0 { return nil, status.Error(codes.InvalidArgument, "Must supply valid cluster ID") diff --git a/api/server/sdk/cluster_test.go b/api/server/sdk/cluster_test.go index 6c9671914..429c5f140 100644 --- a/api/server/sdk/cluster_test.go +++ b/api/server/sdk/cluster_test.go @@ -18,6 +18,7 @@ package sdk import ( "context" + "io/ioutil" "testing" "github.com/golang/mock/gomock" @@ -33,27 +34,30 @@ func TestNewSdkServerBadParameters(t *testing.T) { s, err := New(nil) assert.Nil(t, s) assert.NotNil(t, err) + assert.Contains(t, err.Error(), "configuration") s, err = New(&ServerConfig{}) assert.Nil(t, s) assert.NotNil(t, err) - assert.Contains(t, err.Error(), "Unable to setup server") + assert.Contains(t, err.Error(), "Must provide unix domain") s, err = New(&ServerConfig{ Net: "test", }) assert.Nil(t, s) assert.NotNil(t, err) - assert.Contains(t, err.Error(), "Unable to setup server") + assert.Contains(t, err.Error(), "Must provide unix domain") s, err = New(&ServerConfig{ - Net: "test", - Address: "blah", - DriverName: "name", + Net: "test", + Socket: "blah", + RestPort: testRESTPort, + AccessOutput: ioutil.Discard, + AuditOutput: ioutil.Discard, }) assert.Nil(t, s) assert.NotNil(t, err) - assert.Contains(t, err.Error(), "Unable to get driver") + assert.Contains(t, err.Error(), "Address must be") // Add driver to registry mc := gomock.NewController(t) @@ -64,13 +68,16 @@ func TestNewSdkServerBadParameters(t *testing.T) { }) defer volumedrivers.Remove("mock") s, err = New(&ServerConfig{ - Net: "test", - Address: "blah", - DriverName: "mock", + Net: "test", + Address: "blah", + DriverName: "mock", + RestPort: testRESTPort, + AccessOutput: ioutil.Discard, + AuditOutput: ioutil.Discard, }) assert.Nil(t, s) assert.NotNil(t, err) - assert.Contains(t, err.Error(), "Unable to setup server") + assert.Contains(t, err.Error(), "Must provide unix domain") } func TestSdkClusterInspectCurrent(t *testing.T) { diff --git a/api/server/sdk/credentials.go b/api/server/sdk/credentials.go index b35feb1e9..824353778 100644 --- a/api/server/sdk/credentials.go +++ b/api/server/sdk/credentials.go @@ -28,7 +28,7 @@ import ( // CredentialServer is an implementation of the gRPC OpenStorageCredential interface type CredentialServer struct { - server *Server + server serverAccessor } func (s *CredentialServer) driver() volume.VolumeDriver { diff --git a/api/server/sdk/identity.go b/api/server/sdk/identity.go index fc9e60311..477744f01 100644 --- a/api/server/sdk/identity.go +++ b/api/server/sdk/identity.go @@ -28,7 +28,7 @@ import ( // IdentityServer is an implementation of the gRPC OpenStorageIdentityServer interface type IdentityServer struct { - server *Server + server serverAccessor } func (s *IdentityServer) driver() volume.VolumeDriver { diff --git a/api/server/sdk/node.go b/api/server/sdk/node.go index 0679b43ba..f66c003b9 100644 --- a/api/server/sdk/node.go +++ b/api/server/sdk/node.go @@ -28,7 +28,7 @@ import ( // NodeServer is an implementation of the gRPC OpenStorageNodeServer interface type NodeServer struct { - server *Server + server serverAccessor } func (s *NodeServer) cluster() cluster.Cluster { diff --git a/api/server/sdk/objectstore.go b/api/server/sdk/objectstore.go index 7fb39e8a4..921edbeb7 100644 --- a/api/server/sdk/objectstore.go +++ b/api/server/sdk/objectstore.go @@ -29,7 +29,7 @@ import ( // Objectstoreserver is an implementation of the gRPC OpenStorageObjectstore interface type ObjectstoreServer struct { - server *Server + server serverAccessor } func (s *ObjectstoreServer) cluster() cluster.Cluster { diff --git a/api/server/sdk/rest_gateway.go b/api/server/sdk/rest_gateway.go new file mode 100644 index 000000000..6dc23fc6a --- /dev/null +++ b/api/server/sdk/rest_gateway.go @@ -0,0 +1,153 @@ +/* +Package sdk is the gRPC implementation of the SDK gRPC server +Copyright 2018 Portworx + +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 sdk + +import ( + "context" + "fmt" + "mime" + "net/http" + + "github.com/gobuffalo/packr" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/sirupsen/logrus" + "google.golang.org/grpc" + + "github.com/libopenstorage/openstorage/api" + "github.com/libopenstorage/openstorage/pkg/grpcserver" +) + +type sdkRestGateway struct { + config ServerConfig + restPort string + grpcServer *sdkGrpcServer + server *http.Server +} + +func newSdkRestGateway(config *ServerConfig, grpcServer *sdkGrpcServer) (*sdkRestGateway, error) { + return &sdkRestGateway{ + config: *config, + restPort: config.RestPort, + grpcServer: grpcServer, + }, nil +} + +func (s *sdkRestGateway) Start() error { + mux, err := s.restServerSetupHandlers() + if err != nil { + return err + } + + // Create object here so that we can access its Close receiver. + address := ":" + s.restPort + s.server = &http.Server{ + Addr: address, + Handler: mux, + } + + ready := make(chan bool) + go func() { + ready <- true + var err error + if s.config.Tls != nil { + err = s.server.ListenAndServeTLS(s.config.Tls.CertFile, s.config.Tls.KeyFile) + } else { + err = s.server.ListenAndServe() + } + + if err == http.ErrServerClosed || err == nil { + return + } else { + logrus.Fatalf("Unable to start SDK REST gRPC Gateway: %s\n", + err.Error()) + } + }() + <-ready + logrus.Infof("SDK gRPC REST Gateway started on port :%s", s.restPort) + + return nil +} + +func (s *sdkRestGateway) Stop() { + if err := s.server.Close(); err != nil { + logrus.Fatalf("REST GW STOP error: %v", err) + } +} + +// restServerSetupHandlers sets up the handlers to the swagger ui and +// to the gRPC REST Gateway. +func (s *sdkRestGateway) restServerSetupHandlers() (*http.ServeMux, error) { + + // Create an HTTP server router + mux := http.NewServeMux() + + // Swagger files using packr + swaggerUIBox := packr.NewBox("./swagger-ui") + swaggerJSONBox := packr.NewBox("./api") + mime.AddExtensionType(".svg", "image/svg+xml") + + // Handler to return swagger.json + mux.HandleFunc("/swagger.json", func(w http.ResponseWriter, r *http.Request) { + w.Write(swaggerJSONBox.Bytes("api.swagger.json")) + }) + + // Handler to access the swagger ui. The UI pulls the swagger + // json file from /swagger.json + // The link below MUST have th last '/'. It is really important. + prefix := "/swagger-ui/" + mux.Handle(prefix, + http.StripPrefix(prefix, http.FileServer(swaggerUIBox))) + + // Create a router just for HTTP REST gRPC Server Gateway + gmux := runtime.NewServeMux() + + // Connect to gRPC unix domain socket + conn, err := grpcserver.Connect( + s.grpcServer.Address(), + []grpc.DialOption{grpc.WithInsecure()}) + if err != nil { + return nil, fmt.Errorf("Failed to connect to gRPC handler: %v", err) + } + + // REST Gateway Handlers + handlers := []func(context.Context, *runtime.ServeMux, *grpc.ClientConn) (err error){ + api.RegisterOpenStorageClusterHandler, + api.RegisterOpenStorageNodeHandler, + api.RegisterOpenStorageVolumeHandler, + api.RegisterOpenStorageObjectstoreHandler, + api.RegisterOpenStorageCredentialsHandler, + api.RegisterOpenStorageSchedulePolicyHandler, + api.RegisterOpenStorageCloudBackupHandler, + api.RegisterOpenStorageIdentityHandler, + api.RegisterOpenStorageMountAttachHandler, + api.RegisterOpenStorageAlertsHandler, + api.RegisterOpenStorageClusterPairHandler, + api.RegisterOpenStorageMigrateHandler, + } + + // Register the REST Gateway handlers + for _, handler := range handlers { + err := handler(context.Background(), gmux, conn) + if err != nil { + return nil, err + } + } + + // Pass all other unhandled paths to the gRPC gateway + mux.Handle("/", gmux) + return mux, nil +} diff --git a/api/server/sdk/schedule_policy.go b/api/server/sdk/schedule_policy.go index 2134240af..ac426bae0 100644 --- a/api/server/sdk/schedule_policy.go +++ b/api/server/sdk/schedule_policy.go @@ -29,7 +29,7 @@ import ( // SchedulePolicyServer is an implementation of the gRPC OpenStorageSchedulePolicy interface type SchedulePolicyServer struct { - server *Server + server serverAccessor } func (s *SchedulePolicyServer) cluster() cluster.Cluster { diff --git a/api/server/sdk/sdk_test.go b/api/server/sdk/sdk_test.go index 02a3fcc5b..e7e2ec123 100644 --- a/api/server/sdk/sdk_test.go +++ b/api/server/sdk/sdk_test.go @@ -18,8 +18,10 @@ package sdk import ( "context" + "io/ioutil" "net/http" "net/http/httptest" + "os" "testing" "github.com/golang/mock/gomock" @@ -40,11 +42,15 @@ import ( "github.com/stretchr/testify/assert" "google.golang.org/grpc" "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" "google.golang.org/grpc/status" ) const ( mockDriverName = "mock" + testUds = "/tmp/sdk-test.sock" + testHttpsPort = "34000" + testRESTPort = "34001" ) // testServer is a simple struct used abstract @@ -59,6 +65,10 @@ type testServer struct { gw *httptest.Server } +func init() { + logrus.SetLevel(logrus.PanicLevel) +} + func setupMockDriver(tester *testServer, t *testing.T) { volumedrivers.Add(mockDriverName, func(map[string]string) (volume.VolumeDriver, error) { return tester.m, nil @@ -84,23 +94,35 @@ func newTestServer(t *testing.T) *testServer { var err error // Setup simple driver + os.Remove(testUds) tester.server, err = New(&ServerConfig{ DriverName: mockDriverName, Net: "tcp", - Address: "127.0.0.1:0", + Address: ":" + testHttpsPort, + RestPort: testRESTPort, + Socket: testUds, Cluster: tester.c, AlertsFilterDeleter: tester.a, + AccessOutput: ioutil.Discard, + AuditOutput: ioutil.Discard, + Tls: &TLSConfig{ + CertFile: "test_certs/server-cert.pem", + KeyFile: "test_certs/server-key.pem", + }, }) assert.Nil(t, err) err = tester.server.Start() assert.Nil(t, err) + grpccreds, err := credentials.NewClientTLSFromFile("test_certs/server-cert.pem", "") + assert.Nil(t, err) + // Setup a connection to the driver - tester.conn, err = grpc.Dial(tester.server.Address(), grpc.WithInsecure()) + tester.conn, err = grpc.Dial("localhost:"+testHttpsPort, grpc.WithTransportCredentials(grpccreds)) assert.Nil(t, err) // Setup REST gateway - mux, err := tester.server.restServerSetupHandlers() + mux, err := tester.server.restGateway.restServerSetupHandlers() assert.NoError(t, err) assert.NotNil(t, mux) tester.gw = httptest.NewServer(mux) @@ -138,7 +160,11 @@ func (s *testServer) Conn() *grpc.ClientConn { } func (s *testServer) Server() grpcserver.Server { - return s.server + return s.server.netServer +} + +func (s *testServer) UdsServer() grpcserver.Server { + return s.server.udsServer } func (s *testServer) GatewayURL() string { @@ -206,19 +232,32 @@ func TestSdkWithNoVolumeDriverThenAddOne(t *testing.T) { // Setup SDK Server with no volume driver alert, err := alerts.NewFilterDeleter(kv) assert.NoError(t, err) + + os.Remove(testUds) server, err := New(&ServerConfig{ Net: "tcp", - Address: "127.0.0.1:0", + Address: ":" + testHttpsPort, + RestPort: testRESTPort, + Socket: testUds, Cluster: cm, AlertsFilterDeleter: alert, + AccessOutput: ioutil.Discard, + AuditOutput: ioutil.Discard, + Tls: &TLSConfig{ + CertFile: "test_certs/server-cert.pem", + KeyFile: "test_certs/server-key.pem", + }, }) assert.Nil(t, err) err = server.Start() assert.Nil(t, err) + defer server.Stop() + + grpccreds, err := credentials.NewClientTLSFromFile("test_certs/server-cert.pem", "") + assert.Nil(t, err) // Setup a connection to the driver - conn, err := grpc.Dial(server.Address(), grpc.WithInsecure()) - assert.NoError(t, err) + conn, err := grpc.Dial("localhost:"+testHttpsPort, grpc.WithTransportCredentials(grpccreds)) // Setup API names that depend on the volume driver // To get the names, look at api.pb.go and search for grpc.Invoke or c.cc.Invoke diff --git a/api/server/sdk/server.go b/api/server/sdk/server.go index c1082b51b..bd8da8107 100644 --- a/api/server/sdk/server.go +++ b/api/server/sdk/server.go @@ -17,27 +17,43 @@ limitations under the License. package sdk import ( - "context" "fmt" - "mime" - "net/http" + "io" + "os" "sync" - "github.com/gobuffalo/packr" + "github.com/sirupsen/logrus" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "github.com/grpc-ecosystem/go-grpc-middleware" - grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery" - "github.com/grpc-ecosystem/grpc-gateway/runtime" + grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth" + "github.com/libopenstorage/openstorage/alerts" "github.com/libopenstorage/openstorage/api" "github.com/libopenstorage/openstorage/api/spec" "github.com/libopenstorage/openstorage/cluster" + "github.com/libopenstorage/openstorage/pkg/auth" "github.com/libopenstorage/openstorage/pkg/grpcserver" "github.com/libopenstorage/openstorage/volume" volumedrivers "github.com/libopenstorage/openstorage/volume/drivers" - "github.com/sirupsen/logrus" - "google.golang.org/grpc" ) +const ( + // Default audig log location + defaultAuditLog = "/var/log/openstorage-audit.log" + // Default access log location + defaultAccessLog = "/var/log/openstorage-access.log" +) + +// TLSConfig points to the cert files needed for HTTPS +type TLSConfig struct { + // CertFile is the path to the cert file + CertFile string + // KeyFile is the path to the key file + KeyFile string +} + // ServerConfig provides the configuration to the SDK server type ServerConfig struct { // Net is the transport for gRPC: unix, tcp, etc. @@ -49,20 +65,64 @@ type ServerConfig struct { // RestAdress is the port number. Example: 9110 // For the gRPC REST Gateway. RestPort string + // Unix domain socket for local communication. This socket + // will be used by the REST Gateway to communicate with the gRPC server. + // Only set for testing. Having a '%s' can be supported to use the + // name of the driver as the driver name. + Socket string + // (optional) Location for audit log. + // If not provided, it will go to /var/log/openstorage-audit.log + AuditOutput io.Writer + // (optional) Location of access log. + // This is useful when authorization is not running. + // If not provided, it will go to /var/log/openstorage-access.log + AccessOutput io.Writer // (optional) The OpenStorage driver to use DriverName string // (optional) Cluster interface Cluster cluster.Cluster // AlertsFilterDeleter AlertsFilterDeleter alerts.FilterDeleter + // Authentication configuration + Auth *auth.JwtAuthConfig + // Tls configuration + Tls *TLSConfig } // Server is an implementation of the gRPC SDK interface type Server struct { + config ServerConfig + netServer *sdkGrpcServer + udsServer *sdkGrpcServer + restGateway *sdkRestGateway + + accessLog *os.File + auditLog *os.File +} + +type serverAccessor interface { + alert() alerts.FilterDeleter + cluster() cluster.Cluster + driver() volume.VolumeDriver +} + +type logger struct { + log *logrus.Entry +} + +type sdkGrpcServer struct { *grpcserver.GrpcServer - restPort string - lock sync.RWMutex + restPort string + lock sync.RWMutex + name string + authenticator auth.Authenticator + config ServerConfig + + // Loggers + log *logrus.Entry + auditLogOutput io.Writer + accessLogOutput io.Writer // Interface implementations clusterHandler cluster.Cluster @@ -83,14 +143,140 @@ type Server struct { } // Interface check -var _ grpcserver.Server = &Server{} +var _ grpcserver.Server = &sdkGrpcServer{} -// New creates a new SDK gRPC server +// New creates a new SDK server func New(config *ServerConfig) (*Server, error) { + + if config == nil { + return nil, fmt.Errorf("Must provide configuration") + } + + // Check if the socket is provided to enable the REST gateway to communicate + // to the unix domain socket + if len(config.Socket) == 0 { + return nil, fmt.Errorf("Must provide unix domain socket for SDK") + } + if len(config.RestPort) == 0 { + return nil, fmt.Errorf("Must provide REST Gateway port for the SDK") + } + + // Set default log locations + var ( + accessLog, auditLog *os.File + err error + ) + if config.AuditOutput == nil { + auditLog, err = openLog(defaultAuditLog) + if err != nil { + return nil, err + } + config.AuditOutput = auditLog + } + if config.AccessOutput == nil { + accessLog, err := openLog(defaultAccessLog) + if err != nil { + return nil, err + } + config.AccessOutput = accessLog + } + + // Create a gRPC server on the network + netServer, err := newSdkGrpcServer(config) + if err != nil { + return nil, err + } + + // Create a gRPC server on a unix domain socket + udsConfig := *config + udsConfig.Net = "unix" + udsConfig.Address = config.Socket + udsConfig.Tls = nil + udsServer, err := newSdkGrpcServer(&udsConfig) + if err != nil { + return nil, err + } + + // Create REST Gateway and connect it to the unix domain socket server + restGateway, err := newSdkRestGateway(config, udsServer) + if err != nil { + return nil, err + } + + return &Server{ + config: *config, + netServer: netServer, + udsServer: udsServer, + restGateway: restGateway, + auditLog: auditLog, + accessLog: accessLog, + }, nil +} + +// Start all servers +func (s *Server) Start() error { + if err := s.netServer.Start(); err != nil { + return err + } else if err := s.udsServer.Start(); err != nil { + return err + } else if err := s.restGateway.Start(); err != nil { + return err + } + + return nil +} + +func (s *Server) Stop() { + s.netServer.Stop() + s.udsServer.Stop() + s.restGateway.Stop() + + if s.accessLog != nil { + s.accessLog.Close() + } + if s.auditLog != nil { + s.auditLog.Close() + } +} + +func (s *Server) Address() string { + return s.netServer.Address() +} + +func (s *Server) UdsAddress() string { + return s.udsServer.Address() +} + +// UseCluster will setup a new cluster object for the gRPC handlers +func (s *Server) UseCluster(c cluster.Cluster) { + s.netServer.useCluster(c) + s.udsServer.useCluster(c) +} + +// UseVolumeDriver will setup a new driver object for the gRPC handlers +func (s *Server) UseVolumeDriver(d volume.VolumeDriver) { + s.netServer.useVolumeDriver(d) + s.udsServer.useVolumeDriver(d) +} + +// UseAlert will setup a new alert object for the gRPC handlers +func (s *Server) UseAlert(a alerts.FilterDeleter) { + s.netServer.useAlert(a) + s.udsServer.useAlert(a) +} + +// New creates a new SDK gRPC server +func newSdkGrpcServer(config *ServerConfig) (*sdkGrpcServer, error) { if nil == config { return nil, fmt.Errorf("Configuration must be provided") } + // Create a log object for this server + name := "SDK-" + config.Net + log := logrus.WithFields(logrus.Fields{ + "name": name, + }) + // Save the driver for future calls var ( d volume.VolumeDriver @@ -103,22 +289,39 @@ func New(config *ServerConfig) (*Server, error) { } } + // Setup authentication + var authenticator auth.Authenticator + if config.Auth != nil { + authenticator, err = auth.New(config.Auth) + if err != nil { + return nil, err + } + log.Info(name + " authentication enabled") + } else { + log.Info(name + " authentication disabled") + } + // Create gRPC server gServer, err := grpcserver.New(&grpcserver.GrpcServerConfig{ - Name: "SDK", + Name: name, Net: config.Net, Address: config.Address, }) if err != nil { - return nil, fmt.Errorf("Unable to setup server: %v", err) + return nil, fmt.Errorf("Unable to setup %s server: %v", name, err) } - s := &Server{ - GrpcServer: gServer, - restPort: config.RestPort, - clusterHandler: config.Cluster, - driverHandler: d, - alertHandler: config.AlertsFilterDeleter, + s := &sdkGrpcServer{ + GrpcServer: gServer, + accessLogOutput: config.AccessOutput, + auditLogOutput: config.AuditOutput, + config: *config, + name: name, + log: log, + authenticator: authenticator, + clusterHandler: config.Cluster, + driverHandler: d, + alertHandler: config.AlertsFilterDeleter, } s.identityServer = &IdentityServer{ server: s, @@ -157,14 +360,37 @@ func New(config *ServerConfig) (*Server, error) { // Start is used to start the server. // It will return an error if the server is already running. -func (s *Server) Start() error { +func (s *sdkGrpcServer) Start() error { + // Setup https if certs have been provided opts := make([]grpc.ServerOption, 0) - opts = append(opts, grpc.UnaryInterceptor( - grpc_middleware.ChainUnaryServer( - s.rwlockIntercepter, - grpc_recovery.UnaryServerInterceptor(), - ))) + if s.config.Tls != nil { + creds, err := credentials.NewServerTLSFromFile(s.config.Tls.CertFile, s.config.Tls.KeyFile) + if err != nil { + return fmt.Errorf("Failed to create credentials from cert files: %v", err) + } + opts = append(opts, grpc.Creds(creds)) + s.log.Info("SDK TLS enabled") + } else { + s.log.Info("SDK TLS disabled") + } + + // Setup authentication and authorization using interceptors if auth is enabled + if s.config.Auth != nil { + opts = append(opts, grpc.UnaryInterceptor( + grpc_middleware.ChainUnaryServer( + s.rwlockIntercepter, + grpc_auth.UnaryServerInterceptor(s.auth), + s.authorizationServerInterceptor, + s.loggerServerInterceptor, + ))) + } else { + opts = append(opts, grpc.UnaryInterceptor( + grpc_middleware.ChainUnaryServer( + s.rwlockIntercepter, + s.loggerServerInterceptor, + ))) + } // Start the gRPC Server err := s.GrpcServer.StartWithServer(func() *grpc.Server { @@ -188,30 +414,24 @@ func (s *Server) Start() error { return err } - if len(s.restPort) != 0 { - return s.startRestServer() - } return nil } -// UseCluster will setup a new cluster object for the gRPC handlers -func (s *Server) UseCluster(c cluster.Cluster) { +func (s *sdkGrpcServer) useCluster(c cluster.Cluster) { s.lock.Lock() defer s.lock.Unlock() s.clusterHandler = c } -// UseVolumeDriver will setup a new driver object for the gRPC handlers -func (s *Server) UseVolumeDriver(d volume.VolumeDriver) { +func (s *sdkGrpcServer) useVolumeDriver(d volume.VolumeDriver) { s.lock.Lock() defer s.lock.Unlock() s.driverHandler = d } -// UseAlert will setup a new alert object for the gRPC handlers -func (s *Server) UseAlert(a alerts.FilterDeleter) { +func (s *sdkGrpcServer) useAlert(a alerts.FilterDeleter) { s.lock.Lock() defer s.lock.Unlock() @@ -219,192 +439,14 @@ func (s *Server) UseAlert(a alerts.FilterDeleter) { } // Accessors -func (s *Server) driver() volume.VolumeDriver { +func (s *sdkGrpcServer) driver() volume.VolumeDriver { return s.driverHandler } -func (s *Server) cluster() cluster.Cluster { +func (s *sdkGrpcServer) cluster() cluster.Cluster { return s.clusterHandler } -func (s *Server) alert() alerts.FilterDeleter { +func (s *sdkGrpcServer) alert() alerts.FilterDeleter { return s.alertHandler } - -// startRestServer starts the HTTP/REST gRPC gateway. -func (s *Server) startRestServer() error { - - mux, err := s.restServerSetupHandlers() - if err != nil { - return err - } - - ready := make(chan bool) - go func() { - ready <- true - err := http.ListenAndServe(":"+s.restPort, mux) - if err != nil { - logrus.Fatalf("Unable to start SDK REST gRPC Gateway: %s\n", - err.Error()) - } - }() - <-ready - logrus.Infof("SDK gRPC REST Gateway started on port :%s", s.restPort) - - return nil -} - -// restServerSetupHandlers sets up the handlers to the swagger ui and -// to the gRPC REST Gateway. -func (s *Server) restServerSetupHandlers() (*http.ServeMux, error) { - - // Create an HTTP server router - mux := http.NewServeMux() - - // Swagger files using packr - swaggerUIBox := packr.NewBox("./swagger-ui") - swaggerJSONBox := packr.NewBox("./api") - mime.AddExtensionType(".svg", "image/svg+xml") - - // Handler to return swagger.json - mux.HandleFunc("/swagger.json", func(w http.ResponseWriter, r *http.Request) { - w.Write(swaggerJSONBox.Bytes("api.swagger.json")) - }) - - // Handler to access the swagger ui. The UI pulls the swagger - // json file from /swagger.json - // The link below MUST have th last '/'. It is really important. - prefix := "/swagger-ui/" - mux.Handle(prefix, - http.StripPrefix(prefix, http.FileServer(swaggerUIBox))) - - // Create a router just for HTTP REST gRPC Server Gateway - gmux := runtime.NewServeMux( - runtime.WithMarshalerOption( - runtime.MIMEWildcard, - &runtime.JSONPb{OrigName: true, EmitDefaults: true})) - err := api.RegisterOpenStorageClusterHandlerFromEndpoint( - context.Background(), - gmux, - s.Address(), - []grpc.DialOption{grpc.WithInsecure()}) - if err != nil { - return nil, err - } - - err = api.RegisterOpenStorageNodeHandlerFromEndpoint( - context.Background(), - gmux, - s.Address(), - []grpc.DialOption{grpc.WithInsecure()}) - if err != nil { - return nil, err - } - - err = api.RegisterOpenStorageVolumeHandlerFromEndpoint( - context.Background(), - gmux, - s.Address(), - []grpc.DialOption{grpc.WithInsecure()}) - if err != nil { - return nil, err - } - - err = api.RegisterOpenStorageObjectstoreHandlerFromEndpoint( - context.Background(), - gmux, - s.Address(), - []grpc.DialOption{grpc.WithInsecure()}) - if err != nil { - return nil, err - } - - err = api.RegisterOpenStorageCredentialsHandlerFromEndpoint( - context.Background(), - gmux, - s.Address(), - []grpc.DialOption{grpc.WithInsecure()}) - if err != nil { - return nil, err - } - - err = api.RegisterOpenStorageSchedulePolicyHandlerFromEndpoint( - context.Background(), - gmux, - s.Address(), - []grpc.DialOption{grpc.WithInsecure()}) - if err != nil { - return nil, err - } - - err = api.RegisterOpenStorageCloudBackupHandlerFromEndpoint( - context.Background(), - gmux, - s.Address(), - []grpc.DialOption{grpc.WithInsecure()}) - if err != nil { - return nil, err - } - - err = api.RegisterOpenStorageIdentityHandlerFromEndpoint( - context.Background(), - gmux, - s.Address(), - []grpc.DialOption{grpc.WithInsecure()}) - if err != nil { - return nil, err - } - - err = api.RegisterOpenStorageMountAttachHandlerFromEndpoint( - context.Background(), - gmux, - s.Address(), - []grpc.DialOption{grpc.WithInsecure()}) - if err != nil { - return nil, err - } - - err = api.RegisterOpenStorageAlertsHandlerFromEndpoint( - context.Background(), - gmux, - s.Address(), - []grpc.DialOption{grpc.WithInsecure()}) - if err != nil { - return nil, err - } - err = api.RegisterOpenStorageClusterPairHandlerFromEndpoint( - context.Background(), - gmux, - s.Address(), - []grpc.DialOption{grpc.WithInsecure()}) - if err != nil { - return nil, err - } - - err = api.RegisterOpenStorageMigrateHandlerFromEndpoint( - context.Background(), - gmux, - s.Address(), - []grpc.DialOption{grpc.WithInsecure()}) - if err != nil { - return nil, err - } - - // Pass all other unhandled paths to the gRPC gateway - mux.Handle("/", gmux) - - return mux, nil -} - -// This interceptor provides a way to lock out any calls while we adjust the server -func (s *Server) rwlockIntercepter( - ctx context.Context, - req interface{}, - info *grpc.UnaryServerInfo, - handler grpc.UnaryHandler, -) (interface{}, error) { - s.lock.RLock() - defer s.lock.RUnlock() - - return handler(ctx, req) -} diff --git a/api/server/sdk/server_interceptors.go b/api/server/sdk/server_interceptors.go new file mode 100644 index 000000000..297c5cfc1 --- /dev/null +++ b/api/server/sdk/server_interceptors.go @@ -0,0 +1,209 @@ +/* +Package sdk is the gRPC implementation of the SDK gRPC server +Copyright 2018 Portworx + +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 sdk + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + sdk_auth "github.com/libopenstorage/openstorage-sdk-auth/pkg/auth" + "github.com/pborman/uuid" + "github.com/sirupsen/logrus" + + grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// Keys to store data in gRPC context. Use these keys to retrieve +// the data from the gRPC context +type InterceptorContextkey string + +const ( + // Key to store in the token claims in gRPC context + InterceptorContextTokenKey InterceptorContextkey = "tokenclaims" + + // Metedata context key where the token is found. + // This key must be used by the caller as the key for the token in + // the metedata of the context. The generated Rest Gateway also uses this + // key as the location of the raw token coming from the standard REST + // header: Authorization: bearer + ContextMetadataTokenKey = "bearer" +) + +var ( + defaultRoles = map[string][]sdk_auth.Rule{ + "admin": { + { + Services: []string{"*"}, + Apis: []string{"*"}, + }, + }, + } +) + +// This interceptor provides a way to lock out any calls while we adjust the server +func (s *sdkGrpcServer) rwlockIntercepter( + ctx context.Context, + req interface{}, + info *grpc.UnaryServerInfo, + handler grpc.UnaryHandler, +) (interface{}, error) { + s.lock.RLock() + defer s.lock.RUnlock() + + return handler(ctx, req) +} + +// Authenticate user and add authorization information back in the context +func (s *sdkGrpcServer) auth(ctx context.Context) (context.Context, error) { + token, err := grpc_auth.AuthFromMD(ctx, ContextMetadataTokenKey) + if err != nil { + return nil, err + } + + // Authenticate user + claims, err := s.authenticator.AuthenticateToken(token) + if err != nil { + return nil, status.Error(codes.PermissionDenied, err.Error()) + } + + // Add authorization information back into the context so that other + // functions can get access to this information + ctx = context.WithValue(ctx, InterceptorContextTokenKey, claims) + + return ctx, nil +} + +func (s *sdkGrpcServer) loggerServerInterceptor( + ctx context.Context, + req interface{}, + info *grpc.UnaryServerInfo, + handler grpc.UnaryHandler, +) (interface{}, error) { + reqid := uuid.New() + log := logrus.New() + log.Out = s.accessLogOutput + logger := log.WithFields(logrus.Fields{ + "method": info.FullMethod, + "reqid": reqid, + }) + + logger.Info("Start") + ts := time.Now() + i, err := handler(ctx, req) + duration := time.Now().Sub(ts) + if err != nil { + logger.WithFields(logrus.Fields{"duration": duration}).Infof("Failed: %v", err) + } else { + logger.WithFields(logrus.Fields{"duration": duration}).Info("Successful") + } + + return i, err +} + +func (s *sdkGrpcServer) authorizationServerInterceptor( + ctx context.Context, + req interface{}, + info *grpc.UnaryServerInfo, + handler grpc.UnaryHandler, +) (interface{}, error) { + claims, ok := ctx.Value(InterceptorContextTokenKey).(*sdk_auth.Claims) + if !ok { + return nil, status.Errorf(codes.Internal, "Authorization called without token") + } + + // Setup auditor log + claimsJSON, err := json.Marshal(claims) + if err != nil { + logrus.Warningf("Unable to unmarshal claims: %v", err) + } + log := logrus.New() + log.Out = s.auditLogOutput + logger := log.WithFields(logrus.Fields{ + "name": claims.Name, + "email": claims.Email, + "role": claims.Role, + "claims": string(claimsJSON), + "method": info.FullMethod, + }) + + // Determine rules + var rules []sdk_auth.Rule + if len(claims.Rules) != 0 { + rules = claims.Rules + } else { + if len(claims.Role) == 0 { + return nil, status.Error(codes.PermissionDenied, "Access denied, no roles or rules set") + } + rules, ok = defaultRoles[claims.Role] + if !ok { + return nil, status.Errorf( + codes.PermissionDenied, + "Access denied, unknown role: %s", claims.Role) + } + } + + // Authorize + if err := authorizeClaims(rules, info.FullMethod); err != nil { + logger.Infof("Access denied") + return nil, status.Errorf( + codes.PermissionDenied, + "Access to %s denied", + info.FullMethod) + } + + logger.Info("Authorized") + return handler(ctx, req) +} + +func authorizeClaims(rules []sdk_auth.Rule, fullmethod string) error { + + var reqService, reqApi string + + // String: "/openstorage.api.OpenStorage/" + parts := strings.Split(fullmethod, "/") + + if len(parts) > 1 { + reqService = strings.TrimPrefix(strings.ToLower(parts[1]), "openstorage.api.openstorage") + } + + if len(parts) > 2 { + reqApi = strings.ToLower(parts[2]) + } + + // Go through each rule until a match is found + for _, rule := range rules { + for _, service := range rule.Services { + if service == "*" || + service == reqService { + for _, api := range rule.Apis { + if api == "*" || + api == reqApi { + return nil + } + } + } + } + } + + return fmt.Errorf("no accessable rule to authorize access found") +} diff --git a/api/server/sdk/server_interceptors_test.go b/api/server/sdk/server_interceptors_test.go new file mode 100644 index 000000000..35da21939 --- /dev/null +++ b/api/server/sdk/server_interceptors_test.go @@ -0,0 +1,106 @@ +/* +Package sdk is the gRPC implementation of the SDK gRPC server +Copyright 2018 Portworx + +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 sdk + +import ( + "testing" + + sdk_auth "github.com/libopenstorage/openstorage-sdk-auth/pkg/auth" + "github.com/stretchr/testify/assert" +) + +func TestAuthorizeClaims(t *testing.T) { + + tests := []struct { + denied bool + fullmethod string + rules []sdk_auth.Rule + role string + }{ + { + denied: false, + fullmethod: "/openstorage.api.OpenStorageVolumes/Enumerate", + role: "admin", + }, + { + denied: false, + fullmethod: "/openstorage.api.OpenStorageFutureService/SomeCallInTheFuture", + role: "admin", + }, + { + denied: true, + fullmethod: "/openstorage.api.OpenStorageFutureService/SomeCallInTheFuture", + rules: []sdk_auth.Rule{}, + }, + { + denied: false, + fullmethod: "/openstorage.api.OpenStorageFutureService/SomeCallInTheFuture", + rules: []sdk_auth.Rule{ + { + Services: []string{"futureservice"}, + Apis: []string{"*"}, + }, + }, + }, + { + denied: true, + fullmethod: "/openstorage.api.OpenStorageFutureService/SomeCallInTheFuture", + rules: []sdk_auth.Rule{ + { + Services: []string{"futureservice"}, + Apis: []string{"anothercall"}, + }, + }, + }, + { + denied: true, + fullmethod: "/openstorage.api.OpenStorageFutureService/SomeCallInTheFuture", + rules: []sdk_auth.Rule{ + { + Services: []string{"*"}, + Apis: []string{"anothercall"}, + }, + }, + }, + { + denied: false, + fullmethod: "/openstorage.api.OpenStorageFutureService/SomeCallInTheFuture", + rules: []sdk_auth.Rule{ + { + Services: []string{"cluster", "volume", "futureservice"}, + Apis: []string{"somecallinthefuture"}, + }, + }, + }, + } + + for _, test := range tests { + var rules []sdk_auth.Rule + if len(test.role) != 0 { + rules = defaultRoles[test.role] + } else { + rules = test.rules + } + + err := authorizeClaims(rules, test.fullmethod) + if test.denied { + assert.NotNil(t, err, test.fullmethod, rules) + } else { + assert.Nil(t, err, test.fullmethod, rules) + } + } +} diff --git a/api/server/sdk/test_certs/server-cert.pem b/api/server/sdk/test_certs/server-cert.pem new file mode 100644 index 000000000..06c62b0a4 --- /dev/null +++ b/api/server/sdk/test_certs/server-cert.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFfzCCA2egAwIBAgIJAJNUtz17R8K5MA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV +BAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQg +Q29tcGFueSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xODExMTMyMjQyMjNa +Fw0xOTExMTMyMjQyMjNaMFYxCzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0 +IENpdHkxHDAaBgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQxEjAQBgNVBAMMCWxv +Y2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOBS9qfxrE6N ++NC2aKLaDjD0/eqqZSUezraWnGbCtPcZzr8RwBdqC+8EfYZa36JffoWocsJ7GznA +UyFsFNEscJ0+Z/wD8xXusrjiBpxwB6xu5Vc3xpsAjWXF//+g50UHtC2Uov0GIP7n +w+REFwHXqE0ZqmMGI/oYPQlvQMqd2coFdp0q6Nhaea6DHhGfmVk0utj5uANoXF2G +8YDq99xKI5SWMqkXJpjJOSH6lcwwlWDtC09M+u149NqDRKHUMFSC6l2q1PcQbW8t +gxE2U5rrfmzamtVEjNnfwKevnJyVQTjHoFuanlJOXa6HKSPwSYcwa7PpapUBGCtU +7fzBqkwOYdY2voyCVyMXOVQaRS8KFXdWkX48duCDkvzKatzTv7zAbS5pPmQYGty8 +eT4qMPR7j6wZkztWjZ0V4wliX9ritfMEFl9PN5OzqXUGhwyAiy3/V42464RI8pT5 +FsckxNRkIlofy/m7OIRXlmTWQunbkUmQSJfLZLWHHwTjH+3kA9MUYqcBkwmyDSq7 +IK4uL6eRnM8T3ef/o3SOFXCfu/WmGPcbXx1Ne9ZwCKh/Sw0uBrt1exsIY/nl5xo5 +T5nsJ93W9bKPaUQVDLl2pRWsc7SE+FTz2+XUSGJlhdlqOzuxjVNbYCU+IWuhblBP +c7vv+jgVrUhsPgIZnuK9sk11HPUIDvzNAgMBAAGjUDBOMB0GA1UdDgQWBBQjbpUZ +6y2kQ6pj7TmQFa0ObdM+NDAfBgNVHSMEGDAWgBQjbpUZ6y2kQ6pj7TmQFa0ObdM+ +NDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQB59Q3g6pHISqCxHBdP +EO0uTWVSmZoudncpJozUM3ut7+9ZRNobTPC4HlIBZbzjplEP/LtIf9OQ/a1jIpIh +JUEnqaRbpWIN/D4/LvMkJXqhlQlZWUPaUWJgfs2BZSKfbNCdrBl/OPgoXoH6F7p8 +NoiY8VGIe4VKgxyJWphjole/9FSo3fJPF8Qa16kJZnC/eaQub2su0jOsuo4HfPPN +IaKC3paKaKvOXvtF+NqGFtwpnEAbNP8owL0jF7B0Jr9T+TYP2IFWgOtWE70STOcV +NJ0UoycMLNY4it5M3OG2/jO+1kvAEfPG+nPv08Iy1rwIo3MbGNe2GrRokT1Jwafn +A7JPsRi1jUkqlRdiYThn4mCTn9DuOkni5BuHMh5liiFsMrw3l8SvgSQyWVAlXsD1 +sgWE2N7Q1OPoLcfaQk/Phs2b9ZVIXle1EIfalP7hgz9rNC/7KmK0VIeG5SYyBSy5 +/EY29UDRgFkq2QyPuYoD1ylZq0pSl+ZNW5amTn2tYWyUwlJbqgp7HG2X3pTEzAir +KpnGTTQKdnumejr8LOsc9A/uE1mDoUW6x1COZHAROsZik7Beqz1qiyjZdrl+WrTC +8hvGA04k2fRrBTsqLD5tj35NrKhIdgUx+liqHzY0nZBZLgV2u6/bXUVuUuSHBs6R +uuEh2b9vyDnIBLrWKdcp8Ynshg== +-----END CERTIFICATE----- diff --git a/api/server/sdk/test_certs/server-key.pem b/api/server/sdk/test_certs/server-key.pem new file mode 100644 index 000000000..3dc6eda7d --- /dev/null +++ b/api/server/sdk/test_certs/server-key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +cbJ8SoyVfJzOeDBHU2HRYL6woY3UQV6/ +-----END PRIVATE KEY----- diff --git a/api/server/sdk/utils.go b/api/server/sdk/utils.go index cc604b315..b13638b5a 100644 --- a/api/server/sdk/utils.go +++ b/api/server/sdk/utils.go @@ -18,6 +18,8 @@ limitations under the License. package sdk import ( + "fmt" + "os" "time" "github.com/libopenstorage/openstorage/api" @@ -229,3 +231,11 @@ func retainInternalSpecYamlByteToSdkSched( } return scheds, nil } + +func openLog(logfile string) (*os.File, error) { + file, err := os.OpenFile(logfile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + return nil, fmt.Errorf("Unable to open logfile %s: %v", logfile, err) + } + return file, nil +} diff --git a/api/server/sdk/volume.go b/api/server/sdk/volume.go index 3812ed135..d039d6d10 100644 --- a/api/server/sdk/volume.go +++ b/api/server/sdk/volume.go @@ -25,7 +25,7 @@ import ( // VolumeServer is an implementation of the gRPC OpenStorageVolume interface type VolumeServer struct { specHandler spec.SpecHandler - server *Server + server serverAccessor } func (s *VolumeServer) cluster() cluster.Cluster { diff --git a/cmd/osd/main.go b/cmd/osd/main.go index ea348c8e0..87bfabfe7 100644 --- a/cmd/osd/main.go +++ b/cmd/osd/main.go @@ -25,6 +25,7 @@ package main import ( "fmt" + "io/ioutil" "net/url" "os" "runtime" @@ -46,6 +47,7 @@ import ( "github.com/libopenstorage/openstorage/csi" "github.com/libopenstorage/openstorage/graph/drivers" "github.com/libopenstorage/openstorage/objectstore" + "github.com/libopenstorage/openstorage/pkg/auth" "github.com/libopenstorage/openstorage/schedpolicy" "github.com/libopenstorage/openstorage/volume" "github.com/libopenstorage/openstorage/volume/drivers" @@ -337,12 +339,18 @@ func start(c *cli.Context) error { csiServer.Start() // Start SDK Server for this driver + sdksocket := fmt.Sprintf("/var/lib/osd/driver/%s-sdk.sock", d) + os.Remove(sdksocket) + sdkServer, err := sdk.New(&sdk.ServerConfig{ Net: "tcp", Address: ":" + c.String("sdkport"), RestPort: c.String("sdkrestport"), + Socket: sdksocket, DriverName: d, Cluster: cm, + Auth: setupAuth(), + Tls: setupSdkTls(), }) if err != nil { return fmt.Errorf("Failed to start SDK server for driver %s: %v", d, err) @@ -404,3 +412,52 @@ func wrapAction(f func(*cli.Context) error) func(*cli.Context) { } } } + +func setupAuth() *auth.JwtAuthConfig { + var err error + rsaFile := os.Getenv("OPENSTORAGE_AUTH_RSA_PUBKEY") + ecdsFile := os.Getenv("OPENSTORAGE_AUTH_ECDS_PUBKEY") + sharedsecret := os.Getenv("OPENSTORAGE_AUTH_SHAREDSECRET") + + if len(rsaFile) == 0 && + len(ecdsFile) == 0 && + len(sharedsecret) == 0 { + return nil + } + + authConfig := &auth.JwtAuthConfig{ + SharedSecret: []byte(sharedsecret), + } + + // Read RSA file + if len(rsaFile) != 0 { + authConfig.RsaPublicPem, err = ioutil.ReadFile(rsaFile) + if err != nil { + logrus.Errorf("Failed to read %s", rsaFile) + } + } + + // Read Ecds file + if len(ecdsFile) != 0 { + authConfig.ECDSPublicPem, err = ioutil.ReadFile(ecdsFile) + if err != nil { + logrus.Errorf("Failed to read %s", ecdsFile) + } + } + + return authConfig +} + +func setupSdkTls() *sdk.TLSConfig { + certFile := os.Getenv("OPENSTORAGE_CERTFILE") + keyFile := os.Getenv("OPENSTORAGE_KEYFILE") + if len(certFile) != 0 && len(keyFile) != 0 { + logrus.Infof("TLS %s and %s", certFile, keyFile) + return &sdk.TLSConfig{ + CertFile: certFile, + KeyFile: keyFile, + } + } + + return nil +} diff --git a/hack/generate-https-certs.sh b/hack/generate-https-certs.sh new file mode 100755 index 000000000..cd56723c8 --- /dev/null +++ b/hack/generate-https-certs.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +openssl req -x509 -newkey rsa:4096 -keyout server-key.pem -out server-cert.pem -days 365 -nodes + diff --git a/hack/generate-jwt-rsa-keys.sh b/hack/generate-jwt-rsa-keys.sh new file mode 100755 index 000000000..e5e2428c3 --- /dev/null +++ b/hack/generate-jwt-rsa-keys.sh @@ -0,0 +1,7 @@ +#!/bin/sh +# https://cloud.google.com/iot/docs/how-tos/credentials/keys +# +# Generate RSA256 +openssl genrsa -out rsa_private.pem 2048 +openssl rsa -in rsa_private.pem -pubout -out rsa_public.pem + diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go new file mode 100644 index 000000000..f44367947 --- /dev/null +++ b/pkg/auth/auth.go @@ -0,0 +1,148 @@ +/* +Package auth can be used for authentication and authorization +Copyright 2018 Portworx + +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 auth + +import ( + "encoding/json" + "fmt" + "strings" + + jwt "github.com/dgrijalva/jwt-go" + sdk_auth "github.com/libopenstorage/openstorage-sdk-auth/pkg/auth" +) + +var ( + // Required claim keys. Please see openstorage-sdk-auth for more information + requiredClaims = []string{"exp", "iat", "name", "email"} +) + +// Authenticator interface validates and extracts the claims from a raw token +type Authenticator interface { + AuthenticateToken(string) (*sdk_auth.Claims, error) +} + +// JwtAuthConfig provides JwtAuthenticator the keys to validate the token +type JwtAuthConfig struct { + SharedSecret []byte + RsaPublicPem []byte + ECDSPublicPem []byte +} + +// JwtAuthenticator definition. It contains the raw bytes of the keys and their +// objects as returned by the Jwt package +type JwtAuthenticator struct { + config JwtAuthConfig + rsaKey interface{} + ecdsKey interface{} + sharedSecretKey interface{} +} + +// New returns a JwtAuthenticator +func New(config *JwtAuthConfig) (*JwtAuthenticator, error) { + + if config == nil { + return nil, fmt.Errorf("Must provide configuration") + } + + // Check at least one is set + if len(config.SharedSecret) == 0 && + len(config.RsaPublicPem) == 0 && + len(config.ECDSPublicPem) == 0 { + return nil, fmt.Errorf("Server was passed empty authentication information with no shared secret or pem files set") + } + + authenticator := &JwtAuthenticator{ + config: *config, + } + + var err error + if len(config.SharedSecret) != 0 { + authenticator.sharedSecretKey = config.SharedSecret + } + if len(config.RsaPublicPem) != 0 { + authenticator.rsaKey, err = jwt.ParseRSAPublicKeyFromPEM(config.RsaPublicPem) + if err != nil { + return nil, fmt.Errorf("Unable to parse rsa public key: %v", err) + } + } + if len(config.ECDSPublicPem) != 0 { + authenticator.ecdsKey, err = jwt.ParseECPublicKeyFromPEM(config.ECDSPublicPem) + if err != nil { + return nil, fmt.Errorf("Unable to parse ecds public key: %v", err) + } + } + + return authenticator, nil +} + +// AuthenticateToken determines if a token is valid and if it is, returns +// the information in the claims. +func (j *JwtAuthenticator) AuthenticateToken(rawtoken string) (*sdk_auth.Claims, error) { + + // Parse token + token, err := jwt.Parse(rawtoken, func(token *jwt.Token) (interface{}, error) { + + // Verify Method + if strings.HasPrefix(token.Method.Alg(), "RS") { + // RS256, RS384, or RS512 + return j.rsaKey, nil + } else if strings.HasPrefix(token.Method.Alg(), "ES") { + // ES256, ES384, or ES512 + return j.ecdsKey, nil + } else if strings.HasPrefix(token.Method.Alg(), "HS") { + // HS256, HS384, or HS512 + return j.sharedSecretKey, nil + } + return nil, fmt.Errorf("Unknown token algorithm: %s", token.Method.Alg()) + }) + if err != nil { + return nil, err + } + + if !token.Valid { + return nil, fmt.Errorf("Token failed validation") + } + + // Get claims + claims, ok := token.Claims.(jwt.MapClaims) + if claims == nil || !ok { + return nil, fmt.Errorf("No claims found in token") + } + + // Check for required claims + for _, requiredClaim := range requiredClaims { + if _, ok := claims[requiredClaim]; !ok { + // Claim missing + return nil, fmt.Errorf("Required claim %v missing from token", requiredClaim) + } + } + + // Token now has been verified. + // Claims holds all the authorization information. + // Here we need to first decode it then unmarshal it from JSON + parts := strings.Split(token.Raw, ".") + claimBytes, err := jwt.DecodeSegment(parts[1]) + if err != nil { + return nil, fmt.Errorf("Failed to decode claims: %v", err) + } + var sdkClaims sdk_auth.Claims + err = json.Unmarshal(claimBytes, &sdkClaims) + if err != nil { + return nil, fmt.Errorf("Unable to get sdkclaims: %v", err) + } + return &sdkClaims, nil +} diff --git a/pkg/grpcserver/grpcserver.go b/pkg/grpcserver/grpcserver.go index 3dab24edb..29694574e 100644 --- a/pkg/grpcserver/grpcserver.go +++ b/pkg/grpcserver/grpcserver.go @@ -88,7 +88,7 @@ func (s *GrpcServer) Start(register func(grpcServer *grpc.Server)) error { return nil } -// Start is used to start the server. +// StartWithServer is used to start the server. // It will return an error if the server is already runnig. func (s *GrpcServer) StartWithServer(server func() *grpc.Server) error { s.lock.Lock() diff --git a/pkg/grpcserver/grpcutil.go b/pkg/grpcserver/grpcutil.go new file mode 100644 index 000000000..8db8e08a2 --- /dev/null +++ b/pkg/grpcserver/grpcutil.go @@ -0,0 +1,55 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + 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 grpcserver + +import ( + "context" + "fmt" + "net" + "net/url" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/connectivity" +) + +// Connect address by grpc +func Connect(address string, dialOptions []grpc.DialOption) (*grpc.ClientConn, error) { + u, err := url.Parse(address) + if err == nil && (!u.IsAbs() || u.Scheme == "unix") { + dialOptions = append(dialOptions, + grpc.WithDialer( + func(addr string, timeout time.Duration) (net.Conn, error) { + return net.DialTimeout("unix", u.Path, timeout) + })) + } + + conn, err := grpc.Dial(address, dialOptions...) + if err != nil { + return nil, err + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + for { + if !conn.WaitForStateChange(ctx, conn.GetState()) { + return conn, fmt.Errorf("Connection timed out") + } + if conn.GetState() == connectivity.Ready { + return conn, nil + } + } +}