diff --git a/cmd/garm/main.go b/cmd/garm/main.go index 6b85a4ed..6fd06b6b 100644 --- a/cmd/garm/main.go +++ b/cmd/garm/main.go @@ -32,6 +32,8 @@ import ( "garm/runner" "garm/util" + "github.com/gorilla/handlers" + "github.com/gorilla/mux" "github.com/pkg/errors" ) @@ -117,6 +119,12 @@ func main() { } router := routers.NewAPIRouter(controller, logWriter, jwtMiddleware, initMiddleware, instanceMiddleware) + corsMw := mux.CORSMethodMiddleware(router) + router.Use(corsMw) + + allowedOrigins := handlers.AllowedOrigins(cfg.APIServer.CORSOrigins) + methodsOk := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "OPTIONS", "DELETE"}) + headersOk := handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}) tlsCfg, err := cfg.APIServer.APITLSConfig() if err != nil { @@ -127,7 +135,7 @@ func main() { Addr: cfg.APIServer.BindAddress(), TLSConfig: tlsCfg, // Pass our instance of gorilla/mux in. - Handler: router, + Handler: handlers.CORS(methodsOk, headersOk, allowedOrigins)(router), } listener, err := net.Listen("tcp", srv.Addr) diff --git a/config/config.go b/config/config.go index 1ac80e23..d479371d 100644 --- a/config/config.go +++ b/config/config.go @@ -141,10 +141,19 @@ func (c *Config) Validate() error { return errors.Wrap(err, "validating jwt config") } + providerNames := map[string]int{} + for _, provider := range c.Providers { if err := provider.Validate(); err != nil { return errors.Wrap(err, "validating provider") } + providerNames[provider.Name] += 1 + } + + for name, count := range providerNames { + if count > 1 { + return fmt.Errorf("duplicate provider name %s", name) + } } return nil @@ -165,10 +174,12 @@ func (d *Default) Validate() error { return fmt.Errorf("missing callback_url") } - if d.ConfigDir != "" { - if _, err := os.Stat(d.ConfigDir); err != nil { - return errors.Wrap(err, "accessing config dir") - } + if d.ConfigDir == "" { + return fmt.Errorf("config_dir cannot be empty") + } + + if _, err := os.Stat(d.ConfigDir); err != nil { + return errors.Wrap(err, "accessing config dir") } return nil diff --git a/config/config_test.go b/config/config_test.go index d912156b..ada97c4e 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1 +1,175 @@ +// Copyright 2022 Cloudbase Solutions SRL +// +// 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 config + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + EncryptionPassphrase = "bocyasicgatEtenOubwonIbsudNutDom" +) + +func getDefaultSectionConfig(configDir string) Default { + return Default{ + ConfigDir: configDir, + CallbackURL: "https://garm.example.com/", + LogFile: filepath.Join(configDir, "garm.log"), + } +} + +func getDefaultTLSConfig() TLSConfig { + return TLSConfig{ + CRT: "../testdata/certs/srv-pub.pem", + Key: "../testdata/certs/srv-key.pem", + CACert: "../testdata/certs/ca-pub.pem", + } +} + +func getDefaultAPIServerConfig() APIServer { + return APIServer{ + Bind: "0.0.0.0", + Port: 9998, + UseTLS: true, + TLSConfig: getDefaultTLSConfig(), + CORSOrigins: []string{}, + } +} + +func getDefaultDatabaseConfig(dir string) Database { + return Database{ + Debug: false, + DbBackend: SQLiteBackend, + SQLite: SQLite{ + DBFile: filepath.Join(dir, "garm.db"), + }, + Passphrase: EncryptionPassphrase, + } +} + +func getDefaultProvidersConfig() []Provider { + lxdConfig := getDefaultLXDConfig() + return []Provider{ + { + Name: "test_lxd", + ProviderType: LXDProvider, + Description: "test LXD provider", + LXD: lxdConfig, + }, + } +} + +func getDefaultGithubConfig() []Github { + return []Github{ + { + Name: "dummy_creds", + Description: "dummy github credentials", + OAuth2Token: "bogus", + }, + } +} + +func getDefaultJWTCofig() JWTAuth { + return JWTAuth{ + Secret: EncryptionPassphrase, + TimeToLive: "48h", + } +} + +func getDefaultConfig(t *testing.T) Config { + dir, err := ioutil.TempDir("", "garm-config-test") + if err != nil { + t.Fatalf("failed to create temporary directory: %s", err) + } + t.Cleanup(func() { os.RemoveAll(dir) }) + + return Config{ + Default: getDefaultSectionConfig(dir), + APIServer: getDefaultAPIServerConfig(), + Database: getDefaultDatabaseConfig(dir), + Providers: getDefaultProvidersConfig(), + Github: getDefaultGithubConfig(), + JWTAuth: getDefaultJWTCofig(), + } +} + +func TestConfig(t *testing.T) { + cfg := getDefaultConfig(t) + + err := cfg.Validate() + assert.Nil(t, err) +} + +func TestDefaultSectionConfig(t *testing.T) { + dir, err := ioutil.TempDir("", "garm-config-test") + if err != nil { + t.Fatalf("failed to create temporary directory: %s", err) + } + t.Cleanup(func() { os.RemoveAll(dir) }) + cfg := getDefaultSectionConfig(dir) + + tests := []struct { + name string + cfg Default + errString string + }{ + { + name: "Config is valid", + cfg: cfg, + errString: "", + }, + { + name: "CallbackURL cannot be empty", + cfg: Default{ + CallbackURL: "", + ConfigDir: cfg.ConfigDir, + }, + errString: "missing callback_url", + }, + { + name: "ConfigDir cannot be empty", + cfg: Default{ + CallbackURL: cfg.CallbackURL, + ConfigDir: "", + }, + errString: "config_dir cannot be empty", + }, + { + name: "config_dir must exist and be accessible", + cfg: Default{ + CallbackURL: cfg.CallbackURL, + ConfigDir: "/i/do/not/exist", + }, + errString: "accessing config dir: stat /i/do/not/exist:.*", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := tc.cfg.Validate() + if tc.errString == "" { + assert.Nil(t, err) + } else { + assert.NotNil(t, err) + assert.Regexp(t, tc.errString, err.Error()) + } + }) + } +} diff --git a/config/external.go b/config/external.go index 4e2b55bb..93dcc7c6 100644 --- a/config/external.go +++ b/config/external.go @@ -1,3 +1,17 @@ +// Copyright 2022 Cloudbase Solutions SRL +// +// 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 config import ( diff --git a/config/external_test.go b/config/external_test.go index 5332e2e1..ad67a9c2 100644 --- a/config/external_test.go +++ b/config/external_test.go @@ -1,3 +1,17 @@ +// Copyright 2022 Cloudbase Solutions SRL +// +// 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 config import ( diff --git a/config/lxd_test.go b/config/lxd_test.go index 85d267d5..163f13eb 100644 --- a/config/lxd_test.go +++ b/config/lxd_test.go @@ -1,3 +1,17 @@ +// Copyright 2022 Cloudbase Solutions SRL +// +// 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 config import ( diff --git a/testdata/certs/ca-pub.pem b/testdata/certs/ca-pub.pem new file mode 100644 index 00000000..dcfd8819 --- /dev/null +++ b/testdata/certs/ca-pub.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIRAMrfG/ZCst3T0RtRgcnY5h0wDQYJKoZIhvcNAQELBQAw +MDEcMBoGA1UEChMTQ2xvdWRiYXNlIFNvbHV0aW9uczEQMA4GA1UEAxMHUm9vdCBD +QTAeFw0yMjA3MDQxODM3MjdaFw0zMjA3MDExODM3MjdaMDAxHDAaBgNVBAoTE0Ns +b3VkYmFzZSBTb2x1dGlvbnMxEDAOBgNVBAMTB1Jvb3QgQ0EwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCooxNCaSfs8UhVmavARonC5S2mFLCSLXrHGPhS ++hjAgqQrF8pI/LvJmsV1LAjYjIAqwE9eDIbuyj8nBuaPJYFoS4McaKZmuo/UYqgx +5K+rDfMdDUFIP0XGTopJFcfKguQQU6Tx+8v5Ubc8C9WjRFDRbmR5ihNzKb1Eb/y1 +OrwmsNtMmgyZFCm6yDMymXFgqTo58zTj9d04uwumVLjSoJxPEqytf9LpKJoeoj7O +tnSq8OMPyhsPu3LgZyvyB65ehb1NChica99Dh5bee4muQAkYGUqbRXgbau6edhsQ +MtXM4mzaXdQIMva3rPfnBfPuhc9WjnKmmmOsWDv510nbl0hVAgMBAAGjYTBfMA4G +A1UdDwEB/wQEAwICBDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUoesgOFlL4kKwT1enjyGkxCEUPSowDQYJ +KoZIhvcNAQELBQADggEBAHlWX8iDkTtADKMAbfBMzvsolzQjZALMab2Jdco7QKtO +i3g6k7UWsgAO+cbH8XCM87ty6JuPUFJNibLWHl1A4dSqwbGGNw+wLVLqNzu8dDqC +E1WG3rIFkXFa3z3eZhauBcdp2FCLbuHtD4g/yGHE/LExnIZeHMpbF2MuC0V34PhY +daj2FUYJe+hmiKRajXGYjv6jOTAbylLK8qzF7HmTnLIJ0hahmKnykC2FiwAVpxZC +T0OcNEXjR1FJfSVHJC2OCOZXPftP7ssZmO18j35UMGk/oxkrUz0839rQtT5oZPI6 +UuNSyP3+BcQtdrDUiCOum651ojwvEO4umlQX3zGmo7U= +-----END CERTIFICATE----- diff --git a/testdata/certs/srv-key.pem b/testdata/certs/srv-key.pem new file mode 100644 index 00000000..71148cb2 --- /dev/null +++ b/testdata/certs/srv-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAyUksz2y+noPUEebojEhMmMUWV9Co+wEWx7QUnWuT9ywaU+Jl +4hucM8W+2kgLz4eo0Ih9TOgMasSeCXFQCYdHA6AHzUmzO54i3kuliRFP5QXnOoPS +Z5t8GrcnuX3eZ/VAlgdMygs74R1fBn3vCNIY9PVdoaR7dd66Nx1U13ayHG3Z9LnM +FMa0T2snnjONv3iL2ctZIlOKbYgOvKa9yjyGdSne20RJG659dpF8/0ovz6BmaRVl +5sE1sfF9Y84WXkZjDAvfg8rtTXSwSsuZeRlPz+wEP3tKtzCjzcUr61bd7M456pwD +9Y19dRsGEqP4A5tZ/xTeHol604n9STcKdxfVfwIDAQABAoIBAQC+SACmbcSdkT3v +VnxL8SUH3njKtySndFYWn7RFRKdyKC0TU5jA8RzvDGbpbuZLX5FE4WKiS1NBs3E6 +t/XTrCCD2srFVICtQUbxIk3wj38ZoEUA2hEThLcDglV5UvXnDL/rlCcRcSFAJEXq +my5XBlY1a6cB4b4cpVsgMwg7T6f8mmcQv7Y5CjIH3P8RSjAQpoDRPCcDnEWieDnV +NDTPFKvOdXe01XMJ7YJ9jd3RE4exra3xeDJgB0EPxexlDpcLMxDEt3JfEbzr3xdc +sSXQbVzriYRv8jYV0xAond6itVDfoH6qGVGP+YiSKLeHbcML8WjeFtUhG8gYl/OX +RkrHzVGRAoGBAOYbyN6cb7EWDyr6V+s8o/lD67Zs1SCoa7mMxjbTsjuGuEjNsoLU +CetAUCbay7NZl37fwm7QE5PbOjr1ED6tgSi3Za9M/3GITFIrqRVbfIP5nyfJgbUQ +QLBZ9sb22rrWz5IWFx0jxWQzO6g3ZEPgRt47OO9xBmfJr77A+FRzNnEFAoGBAN/v +KQu2xBIdq4aiai/EepHG0YLToMVCgx0CZiajdl9QhJW4oBBYnYDm/elZSdksDmjy +8J80yUeZBQIkL6o2qIBrkrrcPl019KUtljiTDNQQhQqRqmN/HZxZXFXDVoMLvdJf +LQejtmT93pD4SsrQ8T5fV8qDjybSSKtw5A1zMMOzAoGAJZN389I78wEVLQGAhet7 +I2NSBJI1I4YdQUmZvK5JNqtDQLBGbFLP49i2vgGQrhL9SDyl9Y6JA+YS1jnak0Gm +C84XDs9WD2YggHKcw94SsUJ8GPUw9y5WQfYO9GKvST922fY9hAapXzl2jnutJeBj +8jpdi8w7LYuj5VSBuNivlTkCgYAibAbpMAzvo/Pr3CdqdE9K/T7TV2iNKe4xlV7S +baoSeLQIt7qTCKwdVmSNbBY39de5Ni7aqiiOgu0MKTfSeFhqdR627of/l/2lLl67 +D4+XQXrR5xZD+RQ6JlpVLJOtzS4+mja3x+iPmZ6Otjv49SlAJzO9g3+LviNBhzbn +Al/qlQKBgQCOx15o+udsT2LlVOUL6Gt6TBoin6BmepGK4plFPVs83pM2Ce7oYhr/ +MBpgYO/CiYzFkWJdY91IHRswt8MYqIxpBZNv1TZntHlHVItEzgidSU8BkxMte8u4 +iAvNm8KCSwFVopwcXaGOxtqUAgX7rJo9+y22KBMtYBAq1RWDI5DlOg== +-----END RSA PRIVATE KEY----- diff --git a/testdata/certs/srv-pub.pem b/testdata/certs/srv-pub.pem new file mode 100644 index 00000000..24c1712e --- /dev/null +++ b/testdata/certs/srv-pub.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID1DCCArygAwIBAgIRAMlzfk77HwnF4vq4FSp5cvEwDQYJKoZIhvcNAQELBQAw +MDEcMBoGA1UEChMTQ2xvdWRiYXNlIFNvbHV0aW9uczEQMA4GA1UEAxMHUm9vdCBD +QTAeFw0yMjA3MDQxODM3MjdaFw0zMjA3MDExODM3MjdaMDMxHDAaBgNVBAoTE0Ns +b3VkYmFzZSBTb2x1dGlvbnMxEzARBgNVBAMTCnJlcGxpY2F0b3IwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJSSzPbL6eg9QR5uiMSEyYxRZX0Kj7ARbH +tBSda5P3LBpT4mXiG5wzxb7aSAvPh6jQiH1M6AxqxJ4JcVAJh0cDoAfNSbM7niLe +S6WJEU/lBec6g9Jnm3watye5fd5n9UCWB0zKCzvhHV8Gfe8I0hj09V2hpHt13ro3 +HVTXdrIcbdn0ucwUxrRPayeeM42/eIvZy1kiU4ptiA68pr3KPIZ1Kd7bREkbrn12 +kXz/Si/PoGZpFWXmwTWx8X1jzhZeRmMMC9+Dyu1NdLBKy5l5GU/P7AQ/e0q3MKPN +xSvrVt3szjnqnAP1jX11GwYSo/gDm1n/FN4eiXrTif1JNwp3F9V/AgMBAAGjgeUw +geIwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB +/wQCMAAwgawGA1UdEQSBpDCBoYIEZ2FybYIKZ2FybS5sb2NhbIIJbG9jYWxob3N0 +hwQKxnUxhxAqAQT5AEsXXoAAAAAAAAAEhxD+gAAAAAAAAAIWPv/+yxHIhwQK9/YB +hxD+gAAAAAAAAAIWPv/+WxRvhwRkeIJUhxD9ehFcoeCrEkhDzZZieIJUhxD+gAAA +AAAAAGBnFVzKxAsbhwSsEQABhxD+gAAAAAAAAABCKP/+U3+HMA0GCSqGSIb3DQEB +CwUAA4IBAQBxo48eWMMSYBcPvON8fVURTw58OSuW4iw7GTOCZIluQybXfhg7b/CC +BOHwrW6F+j3iIpgQx5AVofGldGKRcCdqg+uToP6rZQvHkoIGwbkTvFLxSnhrr0rc +4fvkgloVEV2Oyf7lFDES64MKh8U+baXgwyUl++vrwSmTv2BCCAG5XNFN8jvAEHLu +wCB6NY6X2RriUGoBr1dIJeDWjYMLQqBh2m9+8vCLsQ7A7TzDJfF7JxhHV94qZ4c4 +LKaBdKxl4UGr6HuRMPapsoQqVWV+ZjZrPf4LSoj3DenNwzOc/Rm3Nya8xDCCLxqu +RgNpSos4SD2WTF1RG2buzmgrsdvd3mWX +-----END CERTIFICATE-----