From 2c171aa493892a1b6542899412f0b8da032a41ec Mon Sep 17 00:00:00 2001 From: Henry Barreto Date: Wed, 22 May 2024 17:09:33 -0300 Subject: [PATCH] feature(gateway): add route and chores for connector --- gateway/conf.d/shellhub.conf | 18 +++ pkg/api/authorizer/auth_test.go | 6 + pkg/api/authorizer/permissions.go | 12 ++ pkg/models/connector.go | 41 +++++++ pkg/validator/validator.go | 33 +++++ pkg/validator/validator_test.go | 196 ++++++++++++++++++++++++++++++ 6 files changed, 306 insertions(+) create mode 100644 pkg/models/connector.go diff --git a/gateway/conf.d/shellhub.conf b/gateway/conf.d/shellhub.conf index d9e0bdba473..5cee561dc46 100644 --- a/gateway/conf.d/shellhub.conf +++ b/gateway/conf.d/shellhub.conf @@ -222,6 +222,24 @@ server { } {{ end -}} + {{ if bool (env.Getenv "SHELLHUB_CLOUD") -}} + location /api/connector { + set $upstream cloud-api:8080; + auth_request /auth; + auth_request_set $tenant_id $upstream_http_x_tenant_id; + auth_request_set $username $upstream_http_x_username; + auth_request_set $id $upstream_http_x_id; + auth_request_set $role $upstream_http_x_role; + error_page 500 =401 /auth; + rewrite ^/api/(.*)$ /api/$1 break; + proxy_set_header X-Tenant-ID $tenant_id; + proxy_set_header X-Username $username; + proxy_set_header X-ID $id; + proxy_set_header X-Role $role; + proxy_pass http://$upstream; + } + {{ end -}} + {{ if bool (env.Getenv "SHELLHUB_ENTERPRISE") -}} location /api/firewall { set $upstream cloud-api:8080; diff --git a/pkg/api/authorizer/auth_test.go b/pkg/api/authorizer/auth_test.go index 65ecfc39616..cd3d42e311a 100644 --- a/pkg/api/authorizer/auth_test.go +++ b/pkg/api/authorizer/auth_test.go @@ -107,6 +107,9 @@ func TestRolePermissions(t *testing.T) { authorizer.APIKeyCreate, authorizer.APIKeyUpdate, authorizer.APIKeyDelete, + authorizer.ConnectorDelete, + authorizer.ConnectorUpdate, + authorizer.ConnectorSet, }, }, { @@ -149,6 +152,9 @@ func TestRolePermissions(t *testing.T) { authorizer.APIKeyCreate, authorizer.APIKeyUpdate, authorizer.APIKeyDelete, + authorizer.ConnectorDelete, + authorizer.ConnectorUpdate, + authorizer.ConnectorSet, }, }, { diff --git a/pkg/api/authorizer/permissions.go b/pkg/api/authorizer/permissions.go index a55082bb123..04758f84182 100644 --- a/pkg/api/authorizer/permissions.go +++ b/pkg/api/authorizer/permissions.go @@ -55,6 +55,10 @@ const ( APIKeyCreate APIKeyUpdate APIKeyDelete + + ConnectorDelete + ConnectorUpdate + ConnectorSet ) var observerPermissions = []Permission{ @@ -122,6 +126,10 @@ var adminPermissions = []Permission{ APIKeyCreate, APIKeyUpdate, APIKeyDelete, + + ConnectorDelete, + ConnectorUpdate, + ConnectorSet, } var ownerPermissions = []Permission{ @@ -176,4 +184,8 @@ var ownerPermissions = []Permission{ APIKeyCreate, APIKeyUpdate, APIKeyDelete, + + ConnectorDelete, + ConnectorUpdate, + ConnectorSet, } diff --git a/pkg/models/connector.go b/pkg/models/connector.go new file mode 100644 index 00000000000..663f259e68e --- /dev/null +++ b/pkg/models/connector.go @@ -0,0 +1,41 @@ +package models + +type ConnectorStatus struct { + // State of connection. + State string `json:"state" bson:"state"` + // Message contains a message what caused the current [State]. + Message string `json:"message" bson:"message"` +} + +type ConnectorTLS struct { + // CA is the Certificate Authority used to generate the [Cert] for the server and the client. + CA string `json:"ca" bson:"ca" validate:"required,certPEM"` + // Cert is generated from [CA] certificate and used by the client to authorize the connection to the Container Engine. + Cert string `json:"cert" bson:"cert" validate:"required,certPEM"` + // Key is the private key for the certificate on [Cert] field. + Key string `json:"key" bson:"key" validate:"required,privateKeyPEM"` +} + +// ConnectorData contains the mutable data for each Connector. +type ConnectorData struct { + // Enable indicates if the Connection's connection is enable. + Enable *bool `json:"enable" bson:"enable,omitempty"` + // Secure indicates if the Connector use HTTPS for authentication. + Secure *bool `json:"secure" bson:"secure,omitempty"` + // Address is the address to the Container Engine. + Address *string `json:"address" bson:"address,omitempty" validate:"required,hostname_rfc1123"` + // Port is the port to Container Engine. + Port *uint `json:"port" bson:"port,omitempty" validate:"required,min=1,max=65535"` + // TLS stores the configuration for authenticate using TLS on the Container Engine. + TLS *ConnectorTLS `json:"tls,omitempty" bson:"tls,omitempty" validate:"required_if=Secure true"` +} + +type Connector struct { + // UID is the unique identifier of Connector. + UID string `json:"uid" bson:"uid"` + // TenantID indicate which namespace this connector is related. + TenantID string `json:"tenant_id" bson:"tenant_id"` + // Status shows the connection status for the connector. + Status ConnectorStatus `json:"status" bson:"-"` + ConnectorData `bson:",inline"` +} diff --git a/pkg/validator/validator.go b/pkg/validator/validator.go index 25d5c9059df..a9556214ce8 100644 --- a/pkg/validator/validator.go +++ b/pkg/validator/validator.go @@ -1,6 +1,8 @@ package validator import ( + "crypto/x509" + "encoding/pem" "errors" "fmt" "reflect" @@ -37,6 +39,9 @@ const ( UserPasswordTag = "password" // DeviceNameTag contains the rule to validate the device's name. DeviceNameTag = "device_name" + // PrivateKeyPEMTag contains the rule to validate a private key. + PrivateKeyPEMTag = "privateKeyPEM" + CertPEMTag = "certPEM" ) // Rules is a slice that contains all validation rules. @@ -121,6 +126,34 @@ var Rules = []Rule{ }, Error: fmt.Errorf("role must be \"owner\", \"administrator\", \"operator\" or \"observer\""), }, + { + Tag: PrivateKeyPEMTag, + Handler: func(field validator.FieldLevel) bool { + block, _ := pem.Decode([]byte(field.Field().String())) + if block == nil { + return false + } + + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + + return err == nil && key != nil + }, + Error: fmt.Errorf("the private key is invalid"), + }, + { + Tag: CertPEMTag, + Handler: func(field validator.FieldLevel) bool { + block, _ := pem.Decode([]byte(field.Field().String())) + if block == nil { + return false + } + + cert, err := x509.ParseCertificate(block.Bytes) + + return err == nil && cert != nil + }, + Error: fmt.Errorf("the cert is invalid"), + }, } // Validator is the ShellHub validator. diff --git a/pkg/validator/validator_test.go b/pkg/validator/validator_test.go index afb69eeecc0..00e6cd3ff2e 100644 --- a/pkg/validator/validator_test.go +++ b/pkg/validator/validator_test.go @@ -260,3 +260,199 @@ func TestDeviceName(t *testing.T) { }) } } + +func TestKeyPEM(t *testing.T) { + tests := []struct { + description string + value string + want bool + }{ + { + description: "failed when the private key is empty", + value: "", + want: false, + }, + { + description: "failed when the private key does not have the header", + value: ` +MC4CAQAwBQYDK2VwBCIEIA2Ecxi0E2XsKUNRYBv98VRbpsjl/kD7l7XOa/aKYitU +-----END PRIVATE KEY-----`, + want: false, + }, + { + description: "failed when the private key does not have the footer", + value: `-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIA2Ecxi0E2XsKUNRYBv98VRbpsjl/kD7l7XOa/aKYitU +`, + want: false, + }, + { + description: "failed when the private key does not have header neither footer", + value: ` +MC4CAQAwBQYDK2VwBCIEIA2Ecxi0E2XsKUNRYBv98VRbpsjl/kD7l7XOa/aKYitU +`, + want: false, + }, + { + description: "success when the private key is a valid ED25519", + value: `-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIA2Ecxi0E2XsKUNRYBv98VRbpsjl/kD7l7XOa/aKYitU +-----END PRIVATE KEY-----`, + want: true, + }, + { + description: "success when the private key is a valid RSA4096", + value: `-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDt92hrf1PDvCAw +NaEv1xjfL2QCyEsA7zxBGPIIA5ETsB41LW3yS98oy8F/L72BDEepmsw49DaQLbIZ +JrjXyT4dtYKN9oPgv5uwwmwPrWexsiDiVA968DOgSWj4S4MIDLAwd3gBqrQgqFut +Iwgt58KzhKYn/J9+1q/G8ecKzRre7c7/PQbCHEH4A/XiIudyuSf49ziU+U7dq9rZ +IAiyG2xMAKZnjANP0dQj8gaAJCD1qesyoIUXrHCuesrZEEY1gov6ZxUeR62KQgIF +JDQ8nrGgPRc/AjNcLhLKH5xaRqfbEv3WyYw1Ag4Fc1ZtIOgLbMr9BRcxnrhCAIBD +4ASU+63N5zxC/K0JOPy4iSa8+uMXoYD4eJIUI4e9cuAp976zCsrd6d2QEDZmly2/ +KGrcTunlNQ49LfqV9LQWnumRoQ5vhlOHWAQmY48svf45PGeQrrbLUfV24uO4Zzwn +CCCHBUUUwTlasZi1zwHgZ1rmqOjemnGn6HJ9T64tFypUQKOiS5NxeAajszQLf3Gf +IE8ZibE+uxZQyvRexmyUt+RaOQfyAKtnczyOd9LU4/JqVtbKYtuxltw503gS+Ruz +xcHuFEv/takSszbr9mKAj/pT0MEKE9nJLP2gcqw0j2fdjfWWejPGwWlxJ98sPlw8 +eh4KNOtphFmgbjIUTrjfS6G+3cbOwQIDAQABAoICAHDw8hnHCjoFcR+AbJqYk6Dl +zKk3Z8WvReE9li2wh6wY9BVYFO0hDm692f3j6iSz79Uy94d2fOkMDxG525Pq2vTd +v3NiUzAZsKqBdCkyq1reiJXywJAgLdh+zve9Wxi4cOzn3sinvKsdTLmNPWYQL8vl +ArlKwGZCPZYGHJp3QzAYHRzt2WXKZJLySkKEP2YnM64Jo8ys0L4LwSg4+HeT5V/j +FRdjD/VTyMQwq94oh44hEdRq9BAK00Y0WE8SVsgxx/7V6uN+sIJEltHa34H/7Zz4 +Ma7BfB/dbCSLQTllfGhRCLHm4YkNCxuSJKxRqGA3x9Wzk1EFHD2TIE1WpsYQ92ku +ZrYt9XsVQVEvoJpo9qfpJwtYkbSJIcOVzRSuPX5xb3q+rPT1aGfJPtZUtfwokL0O +iRK60eGntenSlJNPrbgTjr2JULd4rlZy4CGYy6frVBCYjDr/f+Li25Ya17VCezZV +1R9TbTORaKlbTc0gonaXuVX5G23DdrpMFvlBspL8fx4c9Ewy+8D9EdO5w2j+pFaI +rj7JL4hTIWKv8YG3jACuXvGKy9ikQXq1h6hDtpeqJ1y7CGq1JIEWh4IGHZTHA+WD +kRPe0YtZ5092OZcT43h8Gr/Qg4nS0qwmUc5eEs33F3PKumzNeZ2cfHiTLWd5PRMW +WBWu+o/bN79VANWQiCtDAoIBAQD73UbT++YxfM51I62XGjjdc8CSZd0a2zk3nvpo +8JeBrWnfefmRuA2QaLyC+u9py5RTHMeq1EjncMBE48LSaRUisjfORtJ+D6ZUovGm +++BJKBt/VuBu3Opnrz/opscWJhVzwPoMa/oKvkhA02dS+y+sQ7feUJm3nkVQ3peq +U/WDtEFWgqHa89SPssNYdH7t4M9OX/L0q1hN6LN1umvPUm4P2vT/d58EaxxuQ4Z6 +qtfFSr6IRBChPUOoVCZPmB81I9qDyU8sbnZsl8evxZ/cwMrJn1GdEcAm/9r/+K05 +HCw1Whs6ZVepqYf5yX84V7FNoar16txMQGJaWHfFgouumMSPAoIBAQDx37XtoC+n +FRCRVjcAc86GXaH5g3fFU6seg1Mkoe4H3vA7EMosZKJ37V8G7lTyR5C4BFIcRyX9 +bWXpP2Aubyqq4aq6wunratU8VgdmboKh1ADQ/tQd9HCNpJtAmI8hfan3Vxv4lKED +WgcraaWHa7VOrjfJsaMC9SV9vDBVNfY+dzz4OZEafjKGySkTMoBrWfEfO+Q0sVDR +acmE/g3cTEjlvDarWG5yquSBEidO/4eZRhyx76wERAi77eOUGak83rOoaRdfrWim +Zi6C8H/5hvhrBSn+TbUK05rF9vVvrs1kRB4qgnFm4aFFbKLyjuHEtkulM1BvMR+a +15l/ES7ikv+vAoIBAQDIYOd0x7gALzdiYpw81xPeu7S9xGUAdOE0qzq2OpOPDBRr +Q3OWx0OjXHB+FH5dQSYkaYVBF9tYpo+RP1NEa23xSLC1YAsfV/wQ4gI3w7RQ/6PA +z7GHAiNLklXaFrXVnT779M/7CfzIh1KcoJRXpJftCYNDUAS73SNwj2dCj8GIouRI +m22B8PNvz90yhpxlTLIhvJxio9+BPF1qkIItU3tVCfJZPSY6Ma1Q3FAlT76SrECh +0OUaIs+tICXKtVA+yuOSbZqb0tZM1wR7h1MEIi4z8pjPycuCO5RUidfm088oMyPu +daokxUf1JqYcgUgCZ1jIha32zFJzZmcDsDTJF6lpAoIBAAwBc7FQ0yyy8fiU0/QU +y3qF6UVOTkKgLY09LYJS+1KusTPtWGutrxbO1HmumM7R2JAZvs2ihnM22+kg+TA0 +2mRTATt181B5JA5zorhl4dwQft3g2DyIZpHRSteA+xHJgAdD7qJ/FiLpdBOmkc3P +/dbi9OfxBkteSbcdATUpkYh2OLOFf/tVqkJgd8Z5KkCp3TsUqPYomv9aBeOxDJUT +wEaO+hO1Nv5AF0mE0iisrFliTohSgjJQAjL50uMGBw17bGV+medo3xnrVoGvWFrV +ZT1Cq1vxFXxtFnCfGn2pqo5Ah1LK2MAnkO62PrxVdUVjWwvfKS3rvUrdSsQw4Sfj +7gcCggEAJk/ydgLGXs1Ti5g5yxe8HkrOM/zycUymeSt3j0EpfXYQEPKmS/337kpT +VvMc7QlFZnjdidRrlCxqnLJZ8kcbLDMRikU+IWikpWUBvlk3mSp3Z98otz1OBBJV +C08w1DePdRSEJgiMdqfjtIg6Dg9R0CpaQ/YLolkkhJ5LekaBvQJqNQT7wgG9NHvG +5p5q2wJfrbxoZX2gGRuqMhNfx9pJJbZdP08DWfeja8MG+JkZqMiKEDPlZTWHSLf3 +uccmoL1Os2G6iqnhL+rIFf637U2B/DinlaODYsM1b96MrrpLgBHU/4OcwsN0t751 +rRrVfCKhbJKpjAZq5U9VKt9LcGe9kA== +-----END PRIVATE KEY-----`, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + data := struct { + Key string `validate:"required,privateKeyPEM"` + }{ + Key: tt.value, + } + + ok, _ := New().Struct(data) + + assert.Equal(t, tt.want, ok) + }) + } +} + +func TestCertPEM(t *testing.T) { + tests := []struct { + description string + value string + want bool + }{ + { + description: "failed when the cert is empty", + value: "", + want: false, + }, + { + description: "failed when the cert does not have the header", + value: ` +MC4CAQAwBQYDK2VwBCIEIA2Ecxi0E2XsKUNRYBv98VRbpsjl/kD7l7XOa/aKYitU +-----END CERTIFICATE-----`, + want: false, + }, + { + description: "failed when the cert does not have the footer", + value: `-----BEGIN CERTIFICATE----- +MC4CAQAwBQYDK2VwBCIEIA2Ecxi0E2XsKUNRYBv98VRbpsjl/kD7l7XOa/aKYitU +`, + want: false, + }, + { + description: "failed when the cert does not have header neither footer", + value: ` +MC4CAQAwBQYDK2VwBCIEIA2Ecxi0E2XsKUNRYBv98VRbpsjl/kD7l7XOa/aKYitU +`, + want: false, + }, + { + description: "success when the cert is a valid", + value: `-----BEGIN CERTIFICATE----- +MIIFUTCCAzmgAwIBAgIUGOBHWPTiCbwt8iLWYNZwKTDbONUwDQYJKoZIhvcNAQEL +BQAwWzELMAkGA1UEBhMCQlIxDjAMBgNVBAgMBUJhaGlhMRQwEgYDVQQHDAtYaXF1 +ZS1YaXF1ZTEQMA4GA1UECgwHSGVucnknczEUMBIGA1UEAwwLZGVsbGcxNTU1MTAw +HhcNMjQwNTI5MTk1NzA0WhcNMjUwNTI5MTk1NzA0WjARMQ8wDQYDVQQDDAZjbGll +bnQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDt92hrf1PDvCAwNaEv +1xjfL2QCyEsA7zxBGPIIA5ETsB41LW3yS98oy8F/L72BDEepmsw49DaQLbIZJrjX +yT4dtYKN9oPgv5uwwmwPrWexsiDiVA968DOgSWj4S4MIDLAwd3gBqrQgqFutIwgt +58KzhKYn/J9+1q/G8ecKzRre7c7/PQbCHEH4A/XiIudyuSf49ziU+U7dq9rZIAiy +G2xMAKZnjANP0dQj8gaAJCD1qesyoIUXrHCuesrZEEY1gov6ZxUeR62KQgIFJDQ8 +nrGgPRc/AjNcLhLKH5xaRqfbEv3WyYw1Ag4Fc1ZtIOgLbMr9BRcxnrhCAIBD4ASU ++63N5zxC/K0JOPy4iSa8+uMXoYD4eJIUI4e9cuAp976zCsrd6d2QEDZmly2/KGrc +TunlNQ49LfqV9LQWnumRoQ5vhlOHWAQmY48svf45PGeQrrbLUfV24uO4ZzwnCCCH +BUUUwTlasZi1zwHgZ1rmqOjemnGn6HJ9T64tFypUQKOiS5NxeAajszQLf3GfIE8Z +ibE+uxZQyvRexmyUt+RaOQfyAKtnczyOd9LU4/JqVtbKYtuxltw503gS+RuzxcHu +FEv/takSszbr9mKAj/pT0MEKE9nJLP2gcqw0j2fdjfWWejPGwWlxJ98sPlw8eh4K +NOtphFmgbjIUTrjfS6G+3cbOwQIDAQABo1cwVTATBgNVHSUEDDAKBggrBgEFBQcD +AjAdBgNVHQ4EFgQUzvw/tD0WsD5q2K2wSokjLEReY6wwHwYDVR0jBBgwFoAU9Nw4 +MqfdGEeRWXI2H1ChuK2k9qEwDQYJKoZIhvcNAQELBQADggIBAIQp2CQyPjaqbXZc +hiR0VWwAyifttrHJJ59VCFovH4/LW8oPbg8w7JP4bfm9iTbo7yTqDV6BfOWat4Qf +T5o0HVcmxKEY7X6bEAmTFfSsNs6NTuaIE8QSFpJpKvLGIjulSqhayjSPuqJavluc +lGa1vUPeIqZAKPDFwrdqMXg/Q7DMhg9su7QPfNVu2E2Hrq++PaXPnWZlu3/yu5FH +2qjoS/xeG8QL8STzqVxqsmcGXkI8FYT2Goidb5eNPSqJflntgm0FzZ/YYvCpZbdC +8/Qjg+CnopfuyLS72iZvW4tSv/9plBsiu6UqhbjBz9xQZbBDpvUOyUvK+L8URmWB +21xTMtqdqk3iG3qAFGnaz0EM0Tg4MEopzYMieob2XoxjSH55ykj33LF/sZeNVPzK +gXi2bqLzL5I1kTPF+Irrg5z7FBTcXRVdPcvqjxGfbyVVmaxNmC26ozIF94rYUOIr +JeUB+pKG1xX/fhUAMeLvEkJ6GOl6ldnTqPJrNAZzwAqW5ra0H9kIbmf1fGPpezaa +KdtGUV3wYjChWAuSa0S3mP1qD+sRNS5NtR7efemmoUbR+hCg2Vyo5osRSJ9dkQJf +PNcoe7LEpZdYQvPI5v1fqVcFpOdOCckDdaGb3XPpd69LGdFD0jHOzF9eIavv9ewV +eiDIAGdPArZi+JWdNsp+TK4MJjcy +-----END CERTIFICATE-----`, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + data := struct { + Cert string `validate:"required,certPEM"` + }{ + Cert: tt.value, + } + + ok, _ := New().Struct(data) + + assert.Equal(t, tt.want, ok) + }) + } +}