Repository: osbuild/osbuild-composer
Pull request #1585 (2021-07-23 13:06:28)

Weldr API: introduce Image Type denylist for filtering exposed images
Extend Weldr API to accept a list of denied image types, which should not be exposed via API for any supported distribution. This functionality will be needed to not expose image types which can't be successfully built outside of Red Hat VPN. Example of such images are the official RHEL EC2 images, which include RHUI client packages not available publicly.

Image Types are filters when listing available compose types and creating a new compose using Weldr API.

Extend osbuild-composer configuration to allow specifying the list of denied Image Types for Weldr API.

Add unit tests for implemented changes.

Add NEWS entry describing the newly introduced functionality.

**UPDATE:**
Changed the Image Type denylist in Weldr API from being applied to all distributions to being distribution-specific. Added support for using globing patterns as distribution and image type name.

This pull request includes:

- [X] adequate testing for the new functionality or fixed issue
- [X] adequate documentation informing people about the change such as
  - [X] create a file in [news/unreleased](https://github.com/osbuild/osbuild-composer/tree/main/docs/news/unreleased) directory if this change should be mentioned in the release news
  - [ ] submit a PR for the [guides](https://github.com/osbuild/guides) repository if this PR changed any behavior described there: https://www.osbuild.org/guides/

<!--
Thanks for proposing a change to osbuild-composer!

Please don't remove the above check list. These are things that each pull
request must have before it is merged. It helps maintainers to not forget
anything.

If the reason for ticking any of the boxes is ambiguous, please add a short
note explaining why.

For user-visible changes, "adequate documentation" is an entry describing the
change for users in docs/news. Please refer to docs/news/README.md for details.

In addition, if this pull request fixes a downstream issue, please refer to
test/README.md and add these additional items:

- [ ] 1st commit of any `rhbz#` related PR contains bug reproducer; CI reports FAIL or
- [ ] PR contains automated tests for new functionality and
- [ ] QE has approved reproducer/new tests and
- [ ] Subsequent commits provide bug fixes without modifying the reproducer; CI reports PASS and
- [ ] QE approves this PR; RHBZ status is set to `MODIFIED + Verified=Tested`
-->


bcl: I'm not sure why this is needed. If the system is a subscribed RHEL system, even if it is CentOS, then building these image types should work, right? (Not that we are going to officially support this, but it's useful for testing and is currently possible).

I'd rather see the distro code figure out what's supported in the current environment instead of needing to manually adjust a config file.

thozza: > I'm not sure why this is needed. If the system is a subscribed RHEL system, even if it is CentOS, then building these image types should work, right? (Not that we are going to officially support this, but it's useful for testing and is currently possible).

Unfortunately No. There are situations in which it won't work on subscribed system, regardless if it is RHEL, CentOS or Fedora. I should have probably explained the reason behind this change in a more detail.

I'm working on defining the official RHEL EC2 images, so that we can produce them as part of RHEL release. However these RHEL EC2 images include RHUI (Red Hat Update Infrastructure) client RPMs, which are not available publicly. Even if your system is subscribed, these RPMs won't be available via any CDN repository. They are unfortunately available only from internal repository behind Red Hat VPN. While this situation should be probably revisited and these RPMs should get published on CDN, it won't change for RHEL-8.5.

Note that the "problem" with RHEL EC2 images is not only about installation of a few additional RPMs, but also about applying different RHSM configuration when RHUI client RPMs are installed.

I originally planned define one EC2 image type shared by all use-cases (in reality represented by each composer API) and let the API caller to specify which subscription method should be used for the image. However this turned out to require more refactoring than originally anticipated and in order to make it with the change into 8.5, I agreed with @teg to define RHEL EC2 images as a separate image type. As a result, these image types will automatically show up in all composer APIs, however only Weldr API is problematic. Mainly because of the on-prem use case and the repo with RHUI client RPMs being private. Weldr API is a bit special compared to other APIs that we have, because the caller does not provide the complete list of repositories, which should be used for building the image. Cloud API is not used directly by any users, only via Image Builder. Image Builder explicitly limits the list of supported image types and provides the repo definitions for each image build. And lastly, the Koji API is actually the one which will be used to trigger builds of these RHEL EC2 images. The user who triggers the build has to provide all repo definitions used for building the image.

> 
> I'd rather see the distro code figure out what's supported in the current environment instead of needing to manually adjust a config file.

I think that the list of image types I used for unit testing the functionality may be a bit confusing as it may look like implying that we want to limit the list of supported image types per for specific distro or based on which distro you are running composer. However the main motivation is to have a mechanism to limit specific image types from being exposed via Weldr API (by default) and at the same time not limit composer to build them via other types of API.

So for the RHEL EC2 image types (which are not yet in composer) it can't be determined based on the current environment. 
thozza: > Looks good to me, thanks! Brian has a point with the denylist not being distro-specific. We might want to make it distro-specific because things might change in the future... @thozza wdyt? It seems slightly painful, unfortunately. :/

No, I agree... I just wanted to save myself a bit of a stress 😄  I'll look at it, it may not be that hard in the end.
thozza: I made the Image Type denylist distribution-specific. It is possible to filter an image type for any distributions using `*` as the distribution name. The same applies to matching image types, where `*` can be used as an image type name to filter all image types. However I'm not sure if the second case is actually useful. If not, I can remove it from the PR and keep only the ability to match any distribution name using `*`.

Patch:
diff --git a/cmd/osbuild-composer/composer.go b/cmd/osbuild-composer/composer.go
index f668e6eec..410332ce6 100644
--- a/cmd/osbuild-composer/composer.go
+++ b/cmd/osbuild-composer/composer.go
@@ -89,8 +89,9 @@ func NewComposer(config *ComposerConfigFile, stateDir, cacheDir string, logger *
 	return &c, nil
 }
 
-func (c *Composer) InitWeldr(repoPaths []string, weldrListener net.Listener) (err error) {
-	c.weldr, err = weldr.New(repoPaths, c.stateDir, c.rpm, c.distros, c.logger, c.workers)
+func (c *Composer) InitWeldr(repoPaths []string, weldrListener net.Listener,
+	distrosImageTypeDenylist map[string][]string) (err error) {
+	c.weldr, err = weldr.New(repoPaths, c.stateDir, c.rpm, c.distros, c.logger, c.workers, distrosImageTypeDenylist)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/osbuild-composer/config.go b/cmd/osbuild-composer/config.go
index 07b781800..1cc737e1a 100644
--- a/cmd/osbuild-composer/config.go
+++ b/cmd/osbuild-composer/config.go
@@ -28,6 +28,27 @@ type ComposerConfigFile struct {
 	ComposerAPI struct {
 		IdentityFilter []string `toml:"identity_filter"`
 	} `toml:"composer_api"`
+	WeldrAPI struct {
+		DistroConfigs map[string]WeldrDistroConfig `toml:"distros"`
+	} `toml:"weldr_api"`
+}
+
+type WeldrDistroConfig struct {
+	ImageTypeDenyList []string `toml:"image_type_denylist"`
+}
+
+// weldrDistrosImageTypeDenyList returns a map of distro-specific Image Type
+// deny lists for Weldr API.
+func (c *ComposerConfigFile) weldrDistrosImageTypeDenyList() map[string][]string {
+	distrosImageTypeDenyList := map[string][]string{}
+
+	for distro, distroConfig := range c.WeldrAPI.DistroConfigs {
+		if distroConfig.ImageTypeDenyList != nil {
+			distrosImageTypeDenyList[distro] = append([]string{}, distroConfig.ImageTypeDenyList...)
+		}
+	}
+
+	return distrosImageTypeDenyList
 }
 
 func LoadConfig(name string) (*ComposerConfigFile, error) {
@@ -66,6 +87,9 @@ func loadConfigFromEnv(intf interface{}) error {
 		case reflect.Slice:
 			// no-op
 			continue
+		case reflect.Map:
+			// no-op
+			continue
 		case reflect.Struct:
 			err := loadConfigFromEnv(fieldV.Addr().Interface())
 			if err != nil {
diff --git a/cmd/osbuild-composer/main.go b/cmd/osbuild-composer/main.go
index f10eaa821..03623b991 100644
--- a/cmd/osbuild-composer/main.go
+++ b/cmd/osbuild-composer/main.go
@@ -69,7 +69,7 @@ func main() {
 			log.Fatal("The osbuild-composer.socket unit is misconfigured. It should contain only one socket.")
 		}
 
-		err = composer.InitWeldr(repositoryConfigs, l[0])
+		err = composer.InitWeldr(repositoryConfigs, l[0], config.weldrDistrosImageTypeDenyList())
 		if err != nil {
 			log.Fatalf("Error initializing weldr API: %v", err)
 		}
diff --git a/docs/news/unreleased/weldr-image-type-denylist.md b/docs/news/unreleased/weldr-image-type-denylist.md
new file mode 100644
index 000000000..14845f6eb
--- /dev/null
+++ b/docs/news/unreleased/weldr-image-type-denylist.md
@@ -0,0 +1,24 @@
+# Weldr API: introduce the ablility to limit exposed Image Types by configuration
+
+Extend Weldr API to accept a map of distribution-specific lists of denied
+image types, which should not be exposed via API. It is allowed to use
+globing patterns as Distribution and Image Type names. This functionality
+is needed to not expose image types which can't be successfully built outside
+of Red Hat VPN.
+
+The list of denied Image Types is defined in `osbuild-composer` configuration,
+`/etc/osbuild-composer/osbuild-composer.toml`.
+
+Example configuration denying the building of `qcow2` and `vmdk` Image Types
+via Weldr API for any distribution:
+```toml
+[weldr_api.distros."*"]
+image_type_denylist = [ "qcow2", "vmdk" ]
+```
+
+Example configuration denying the building of `qcow2` and `vmdk` Image Types
+via Weldr API for `rhel-84` distribution:
+```toml
+[weldr_api.distros.rhel-84]
+image_type_denylist = [ "qcow2", "vmdk" ]
+```
diff --git a/internal/weldr/api.go b/internal/weldr/api.go
index bbd044590..c9f93ec99 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -24,6 +24,7 @@ import (
 	"time"
 
 	"github.com/BurntSushi/toml"
+	"github.com/gobwas/glob"
 	"github.com/google/uuid"
 	"github.com/julienschmidt/httprouter"
 
@@ -57,6 +58,9 @@ type API struct {
 	hostDistroName string                   // Name of the host distro
 	distroRegistry *distroregistry.Registry // Available distros
 	distros        []string                 // Supported distro names
+
+	//  List of ImageType names, which should not be exposed by the API
+	distrosImageTypeDenylist map[string][]string
 }
 
 type ComposeState int
@@ -117,26 +121,29 @@ var ValidBlueprintName = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
 // NewTestAPI is used for the test framework, sets up a single distro
 func NewTestAPI(rpm rpmmd.RPMMD, arch distro.Arch, dr *distroregistry.Registry,
 	rr *reporegistry.RepoRegistry, logger *log.Logger,
-	store *store.Store, workers *worker.Server, compatOutputDir string) *API {
+	store *store.Store, workers *worker.Server, compatOutputDir string,
+	distrosImageTypeDenylist map[string][]string) *API {
 
 	// Use the first entry as the host distribution
 	hostDistro := dr.GetDistro(dr.List()[0])
 	api := &API{
-		store:           store,
-		workers:         workers,
-		rpmmd:           rpm,
-		arch:            arch,
-		repoRegistry:    rr,
-		logger:          logger,
-		compatOutputDir: compatOutputDir,
-		hostDistroName:  hostDistro.Name(),
-		distroRegistry:  dr,
-		distros:         validDistros(rr, dr, arch.Name(), logger),
+		store:                    store,
+		workers:                  workers,
+		rpmmd:                    rpm,
+		arch:                     arch,
+		repoRegistry:             rr,
+		logger:                   logger,
+		compatOutputDir:          compatOutputDir,
+		hostDistroName:           hostDistro.Name(),
+		distroRegistry:           dr,
+		distros:                  validDistros(rr, dr, arch.Name(), logger),
+		distrosImageTypeDenylist: distrosImageTypeDenylist,
 	}
 	return setupRouter(api)
 }
 
-func New(repoPaths []string, stateDir string, rpm rpmmd.RPMMD, dr *distroregistry.Registry, logger *log.Logger, workers *worker.Server) (*API, error) {
+func New(repoPaths []string, stateDir string, rpm rpmmd.RPMMD, dr *distroregistry.Registry,
+	logger *log.Logger, workers *worker.Server, distrosImageTypeDenylist map[string][]string) (*API, error) {
 	if logger == nil {
 		logger = log.New(os.Stdout, "", 0)
 	}
@@ -168,16 +175,17 @@ func New(repoPaths []string, stateDir string, rpm rpmmd.RPMMD, dr *distroregistr
 	compatOutputDir := path.Join(stateDir, "outputs")
 
 	api := &API{
-		store:           store,
-		workers:         workers,
-		rpmmd:           rpm,
-		arch:            hostArch,
-		repoRegistry:    rr,
-		logger:          logger,
-		compatOutputDir: compatOutputDir,
-		hostDistroName:  hostDistro.Name(),
-		distroRegistry:  dr,
-		distros:         validDistros(rr, dr, hostArch.Name(), logger),
+		store:                    store,
+		workers:                  workers,
+		rpmmd:                    rpm,
+		arch:                     hostArch,
+		repoRegistry:             rr,
+		logger:                   logger,
+		compatOutputDir:          compatOutputDir,
+		hostDistroName:           hostDistro.Name(),
+		distroRegistry:           dr,
+		distros:                  validDistros(rr, dr, hostArch.Name(), logger),
+		distrosImageTypeDenylist: distrosImageTypeDenylist,
 	}
 	return setupRouter(api), nil
 }
@@ -378,10 +386,47 @@ func (api *API) openImageFile(composeId uuid.UUID, compose store.Compose) (io.Re
 	return reader, size, nil
 }
 
+// isImageTypeAllowed checks the given ImageType and Distro names against
+// the distro-specific ImageType Denylist provided to the API from configuration.
+// If the given ImageType is not allowed the method returns an `false`.
+// Otherwise `true` is returned.
+func (api *API) isImageTypeAllowed(distroName, imageType string) (bool, error) {
+	for deniedDistro, deniedImgTypes := range api.distrosImageTypeDenylist {
+		deniedDistroPattern, err := glob.Compile(deniedDistro)
+		if err != nil {
+			// the bool return value here does not have any real meaning
+			return true, err
+		}
+		if deniedDistroPattern.Match(distroName) {
+			for _, deniedImgType := range deniedImgTypes {
+				deniedImageTypePattern, err := glob.Compile(deniedImgType)
+				if err != nil {
+					// the bool return value here does not have any real meaning
+					return true, err
+				}
+
+				if deniedImageTypePattern.Match(imageType) {
+					return false, nil
+				}
+			}
+		}
+	}
+
+	return true, nil
+}
+
 // getImageType returns the ImageType for the selected distro
 // This is necessary because different distros support different image types, and the image
 // type may have a different package set than other distros.
 func (api *API) getImageType(distroName, imageType string) (distro.ImageType, error) {
+	imgAllowed, err := api.isImageTypeAllowed(distroName, imageType)
+	if err != nil {
+		return nil, fmt.Errorf("error while checking if image type is allowed: %v", err)
+	}
+	if !imgAllowed {
+		return nil, fmt.Errorf("image type %q for distro %q is denied by configuration", imageType, distroName)
+	}
+
 	distro := api.getDistro(distroName)
 	if distro == nil {
 		return nil, fmt.Errorf("GetDistro - unknown distribution: %s", distroName)
@@ -2146,8 +2191,8 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 	imageType, err := api.getImageType(distroName, cr.ComposeType)
 	if err != nil {
 		errors := responseError{
-			ID:  "UnknownComposeType",
-			Msg: fmt.Sprintf("Unknown compose type for architecture: %s", cr.ComposeType),
+			ID:  "ComposeError",
+			Msg: fmt.Sprintf("Failed to get compose type %q: %v", cr.ComposeType, err),
 		}
 		statusResponseError(writer, http.StatusBadRequest, errors)
 		return
@@ -2491,6 +2536,18 @@ func (api *API) composeTypesHandler(writer http.ResponseWriter, request *http.Re
 	}
 
 	for _, format := range arch.ListImageTypes() {
+		imgAllowed, err := api.isImageTypeAllowed(distroName, format)
+		if err != nil {
+			errors := responseError{
+				ID:  "InternalError",
+				Msg: fmt.Sprintf("Error while checking if image type is allowed: %v", err),
+			}
+			statusResponseError(writer, http.StatusInternalServerError, errors)
+			return
+		}
+		if !imgAllowed {
+			continue
+		}
 		reply.Types = append(reply.Types, composeType{format, true})
 	}
 


Test patch:
diff --git a/cmd/osbuild-composer/config_test.go b/cmd/osbuild-composer/config_test.go
index 909fc52e4..4e26a5aa8 100644
--- a/cmd/osbuild-composer/config_test.go
+++ b/cmd/osbuild-composer/config_test.go
@@ -36,6 +36,9 @@ func TestConfig(t *testing.T) {
 	require.Equal(t, config.Worker.AllowedDomains, []string{"osbuild.org"})
 	require.Equal(t, config.Worker.CA, "/etc/osbuild-composer/ca-crt.pem")
 
+	require.Equal(t, []string{"qcow2", "vmdk"}, config.WeldrAPI.DistroConfigs["*"].ImageTypeDenyList)
+	require.Equal(t, []string{"qcow2"}, config.WeldrAPI.DistroConfigs["rhel-84"].ImageTypeDenyList)
+
 	require.Equal(t, "overwrite-me-db", config.Worker.PGDatabase)
 
 	require.NoError(t, os.Setenv("PGDATABASE", "composer-db"))
@@ -44,3 +47,16 @@ func TestConfig(t *testing.T) {
 	require.NotNil(t, config)
 	require.Equal(t, "composer-db", config.Worker.PGDatabase)
 }
+
+func TestWeldrDistrosImageTypeDenyList(t *testing.T) {
+	config, err := LoadConfig("testdata/test.toml")
+	require.NoError(t, err)
+	require.NotNil(t, config)
+
+	expectedWeldrDistrosImageTypeDenyList := map[string][]string{
+		"*":       {"qcow2", "vmdk"},
+		"rhel-84": {"qcow2"},
+	}
+
+	require.Equal(t, expectedWeldrDistrosImageTypeDenyList, config.weldrDistrosImageTypeDenyList())
+}
diff --git a/cmd/osbuild-composer/testdata/test.toml b/cmd/osbuild-composer/testdata/test.toml
index 5ba153dbf..da6b3a977 100644
--- a/cmd/osbuild-composer/testdata/test.toml
+++ b/cmd/osbuild-composer/testdata/test.toml
@@ -6,3 +6,9 @@ ca = "/etc/osbuild-composer/ca-crt.pem"
 allowed_domains = [ "osbuild.org" ]
 ca = "/etc/osbuild-composer/ca-crt.pem"
 pg_database = "overwrite-me-db"
+
+[weldr_api.distros."*"]
+image_type_denylist = [ "qcow2", "vmdk" ]
+
+[weldr_api.distros.rhel-84]
+image_type_denylist = [ "qcow2" ]
diff --git a/internal/client/unit_test.go b/internal/client/unit_test.go
index ae596486e..a0ba48b2f 100644
--- a/internal/client/unit_test.go
+++ b/internal/client/unit_test.go
@@ -68,7 +68,7 @@ func executeTests(m *testing.M) int {
 	}
 
 	logger := log.New(os.Stdout, "", 0)
-	api := weldr.NewTestAPI(rpm, arch, dr, rr, logger, fixture.Store, fixture.Workers, "")
+	api := weldr.NewTestAPI(rpm, arch, dr, rr, logger, fixture.Store, fixture.Workers, "", nil)
 	server := http.Server{Handler: api}
 	defer server.Close()
 
diff --git a/internal/weldr/api_test.go b/internal/weldr/api_test.go
index 8bce68ce8..c72101543 100644
--- a/internal/weldr/api_test.go
+++ b/internal/weldr/api_test.go
@@ -61,7 +61,42 @@ func createWeldrAPI(tempdir string, fixtureGenerator rpmmd_mock.FixtureGenerator
 		panic(err)
 	}
 
-	return NewTestAPI(rpm, arch, dr, rr, nil, fixture.Store, fixture.Workers, ""), fixture.Store
+	return NewTestAPI(rpm, arch, dr, rr, nil, fixture.Store, fixture.Workers, "", nil), fixture.Store
+}
+
+// createWeldrAPI2 is an alternative function to createWeldrAPI, using different test architecture
+// with more than a single image type
+func createWeldrAPI2(tempdir string, fixtureGenerator rpmmd_mock.FixtureGenerator,
+	distroImageTypeDenylist map[string][]string) (*API, *store.Store) {
+	fixture := fixtureGenerator(tempdir)
+	rpm := rpmmd_mock.NewRPMMDMock(fixture)
+
+	rr := reporegistry.NewFromDistrosRepoConfigs(rpmmd.DistrosRepoConfigs{
+		test_distro.TestDistroName: {
+			test_distro.TestArch2Name: {
+				{Name: "test-id", BaseURL: "http://example.com/test/os/x86_64", CheckGPG: true},
+			},
+		},
+		test_distro.TestDistro2Name: {
+			test_distro.TestArch2Name: {
+				{Name: "test-id-2", BaseURL: "http://example.com/test-2/os/x86_64", CheckGPG: true},
+			},
+		},
+	})
+
+	distro1 := test_distro.New()
+	arch, err := distro1.GetArch(test_distro.TestArch2Name)
+	if err != nil {
+		panic(err)
+	}
+	distro2 := test_distro.New2()
+
+	dr, err := distroregistry.New(distro1, distro2)
+	if err != nil {
+		panic(err)
+	}
+
+	return NewTestAPI(rpm, arch, dr, rr, nil, fixture.Store, fixture.Workers, "", distroImageTypeDenylist), fixture.Store
 }
 
 func TestBasic(t *testing.T) {
@@ -736,6 +771,7 @@ func TestCompose(t *testing.T) {
 		{false, "POST", "/api/v1/compose", fmt.Sprintf(`{"blueprint_name": "test","compose_type":"%s","branch":"master","ostree":{"ref":"/bad/ref","parent":"","url":"http://ostree/"}}`, test_distro.TestImageTypeName), http.StatusBadRequest, `{"status":false,"errors":[{"id":"InvalidChars","msg":"Invalid ostree ref"}]}`, expectedComposeOSTreeURL, []string{"build_id"}},
 		{false, "POST", "/api/v1/compose", fmt.Sprintf(`{"blueprint_name": "test-distro-2","compose_type": "%s","branch": "master"}`, test_distro.TestImageTypeName), http.StatusOK, `{"status": true}`, expectedComposeGoodDistro, []string{"build_id"}},
 		{false, "POST", "/api/v1/compose", fmt.Sprintf(`{"blueprint_name": "test-fedora-1","compose_type": "%s","branch": "master"}`, test_distro.TestImageTypeName), http.StatusBadRequest, `{"status": false,"errors":[{"id":"DistroError", "msg":"Unknown distribution: fedora-1"}]}`, nil, []string{"build_id"}},
+		{false, "POST", "/api/v1/compose", `{"blueprint_name": "test-distro-2","compose_type": "imaginary_type","branch": "master"}`, http.StatusBadRequest, `{"status": false,"errors":[{"id":"ComposeError", "msg":"Failed to get compose type \"imaginary_type\": invalid image type: imaginary_type"}]}`, nil, []string{"build_id"}},
 	}
 
 	tempdir, err := ioutil.TempDir("", "weldr-tests-")
@@ -1444,3 +1480,264 @@ func TestModulesList(t *testing.T) {
 		test.TestRoute(t, api, true, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
 	}
 }
+
+func TestComposeTypes_ImageTypeDenylist(t *testing.T) {
+	var cases = []struct {
+		Path              string
+		ImageTypeDenylist map[string][]string
+		ExpectedStatus    int
+		ExpectedJSON      string
+	}{
+		{
+			"/api/v1/compose/types",
+			map[string][]string{},
+			http.StatusOK,
+			fmt.Sprintf(`{"types": [{"enabled":true, "name":%q},{"enabled":true, "name":%q}]}`, test_distro.TestImageTypeName, test_distro.TestImageType2Name),
+		},
+		{
+			"/api/v1/compose/types?distro=test-distro-2",
+			map[string][]string{},
+			http.StatusOK,
+			fmt.Sprintf(`{"types": [{"enabled":true, "name":%q},{"enabled":true, "name":%q}]}`, test_distro.TestImageTypeName, test_distro.TestImageType2Name),
+		},
+		{
+			"/api/v1/compose/types",
+			map[string][]string{test_distro.TestDistro2Name: {test_distro.TestImageTypeName}},
+			http.StatusOK,
+			fmt.Sprintf(`{"types": [{"enabled":true, "name":%q}]}`, test_distro.TestImageType2Name),
+		},
+		{
+			"/api/v1/compose/types?distro=test-distro-2",
+			map[string][]string{test_distro.TestDistro2Name: {test_distro.TestImageTypeName}},
+			http.StatusOK,
+			fmt.Sprintf(`{"types": [{"enabled":true, "name":%q}]}`, test_distro.TestImageType2Name),
+		},
+		{
+			"/api/v1/compose/types",
+			map[string][]string{test_distro.TestDistro2Name: {test_distro.TestImageTypeName, test_distro.TestImageType2Name}},
+			http.StatusOK,
+			`{"types": null}`,
+		},
+		{
+			"/api/v1/compose/types?distro=test-distro-2",
+			map[string][]string{test_distro.TestDistro2Name: {test_distro.TestImageTypeName, test_distro.TestImageType2Name}},
+			http.StatusOK,
+			`{"types": null}`,
+		},
+		{
+			"/api/v1/compose/types",
+			map[string][]string{"*": {test_distro.TestImageTypeName}},
+			http.StatusOK,
+			fmt.Sprintf(`{"types": [{"enabled":true, "name":%q}]}`, test_distro.TestImageType2Name),
+		},
+		{
+			"/api/v1/compose/types",
+			map[string][]string{"*": {test_distro.TestImageTypeName, test_distro.TestImageType2Name}},
+			http.StatusOK,
+			`{"types": null}`,
+		},
+		{
+			"/api/v1/compose/types",
+			map[string][]string{test_distro.TestDistro2Name: {"*"}},
+			http.StatusOK,
+			`{"types": null}`,
+		},
+		{
+			"/api/v1/compose/types?distro=test-distro-2",
+			map[string][]string{test_distro.TestDistroName: {"*"}},
+			http.StatusOK,
+			fmt.Sprintf(`{"types": [{"enabled":true, "name":%q}, {"enabled":true, "name":%q}]}`,
+				test_distro.TestImageTypeName, test_distro.TestImageType2Name),
+		},
+	}
+
+	tempdir, err := ioutil.TempDir("", "weldr-tests-")
+	require.NoError(t, err)
+	defer os.RemoveAll(tempdir)
+
+	for _, c := range cases {
+		api, _ := createWeldrAPI2(tempdir, rpmmd_mock.BaseFixture, c.ImageTypeDenylist)
+		test.TestRoute(t, api, true, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
+	}
+}
+
+func TestComposePOST_ImageTypeDenylist(t *testing.T) {
+	arch, err := test_distro.New2().GetArch(test_distro.TestArch2Name)
+	require.NoError(t, err)
+	imgType, err := arch.GetImageType(test_distro.TestImageTypeName)
+	require.NoError(t, err)
+	imgType2, err := arch.GetImageType(test_distro.TestImageType2Name)
+	require.NoError(t, err)
+	manifest, err := imgType.Manifest(nil, distro.ImageOptions{}, nil, nil, 0)
+	require.NoError(t, err)
+
+	expectedComposeLocal := &store.Compose{
+		Blueprint: &blueprint.Blueprint{
+			Name:           "test",
+			Version:        "0.0.0",
+			Packages:       []blueprint.Package{},
+			Modules:        []blueprint.Package{},
+			Groups:         []blueprint.Group{},
+			Customizations: nil,
+		},
+		ImageBuild: store.ImageBuild{
+			QueueStatus: common.IBWaiting,
+			ImageType:   imgType,
+			Manifest:    manifest,
+		},
+		Packages: []rpmmd.PackageSpec{},
+	}
+
+	expectedComposeLocal2 := &store.Compose{
+		Blueprint: &blueprint.Blueprint{
+			Name:           "test",
+			Version:        "0.0.0",
+			Packages:       []blueprint.Package{},
+			Modules:        []blueprint.Package{},
+			Groups:         []blueprint.Group{},
+			Customizations: nil,
+		},
+		ImageBuild: store.ImageBuild{
+			QueueStatus: common.IBWaiting,
+			ImageType:   imgType2,
+			Manifest:    manifest,
+		},
+		Packages: []rpmmd.PackageSpec{},
+	}
+
+	var cases = []struct {
+		Path              string
+		Body              string
+		imageTypeDenylist map[string][]string
+		ExpectedStatus    int
+		ExpectedJSON      string
+		ExpectedCompose   *store.Compose
+		IgnoreFields      []string
+	}{
+		{
+			"/api/v1/compose",
+			fmt.Sprintf(`{"blueprint_name": "test","compose_type": "%s","branch": "master"}`, test_distro.TestImageTypeName),
+			map[string][]string{},
+			http.StatusOK,
+			`{"status":true}`,
+			expectedComposeLocal,
+			[]string{"build_id"},
+		},
+		{
+			"/api/v1/compose",
+			fmt.Sprintf(`{"blueprint_name": "test","compose_type": "%s","branch": "master"}`, test_distro.TestImageType2Name),
+			map[string][]string{},
+			http.StatusOK,
+			`{"status": true}`,
+			expectedComposeLocal2,
+			[]string{"build_id"},
+		},
+		{
+			"/api/v1/compose",
+			fmt.Sprintf(`{"blueprint_name": "test","compose_type": "%s","branch": "master"}`, test_distro.TestImageTypeName),
+			map[string][]string{test_distro.TestDistro2Name: {test_distro.TestImageTypeName}},
+			http.StatusBadRequest,
+			fmt.Sprintf(`{"status":false,"errors":[{"id":"ComposeError","msg":"Failed to get compose type \"%[1]s\": image type \"%[1]s\" for distro \"%[2]s\" is denied by configuration"}]}`,
+				test_distro.TestImageTypeName, test_distro.TestDistro2Name),
+			expectedComposeLocal,
+			[]string{"build_id"},
+		},
+		{
+			"/api/v1/compose",
+			fmt.Sprintf(`{"blueprint_name": "test","compose_type": "%s","branch": "master"}`, test_distro.TestImageType2Name),
+			map[string][]string{test_distro.TestDistro2Name: {test_distro.TestImageTypeName}},
+			http.StatusOK,
+			`{"status": true}`,
+			expectedComposeLocal2,
+			[]string{"build_id"},
+		},
+		{
+			"/api/v1/compose",
+			fmt.Sprintf(`{"blueprint_name": "test","compose_type": "%s","branch": "master"}`, test_distro.TestImageTypeName),
+			map[string][]string{test_distro.TestDistro2Name: {test_distro.TestImageTypeName, test_distro.TestImageType2Name}},
+			http.StatusBadRequest,
+			fmt.Sprintf(`{"status":false,"errors":[{"id":"ComposeError","msg":"Failed to get compose type \"%[1]s\": image type \"%[1]s\" for distro \"%[2]s\" is denied by configuration"}]}`,
+				test_distro.TestImageTypeName, test_distro.TestDistro2Name),
+			expectedComposeLocal,
+			[]string{"build_id"},
+		},
+		{
+			"/api/v1/compose",
+			fmt.Sprintf(`{"blueprint_name": "test","compose_type": "%s","branch": "master"}`, test_distro.TestImageType2Name),
+			map[string][]string{test_distro.TestDistro2Name: {test_distro.TestImageTypeName, test_distro.TestImageType2Name}},
+			http.StatusBadRequest,
+			fmt.Sprintf(`{"status":false,"errors":[{"id":"ComposeError","msg":"Failed to get compose type \"%[1]s\": image type \"%[1]s\" for distro \"%[2]s\" is denied by configuration"}]}`,
+				test_distro.TestImageType2Name, test_distro.TestDistro2Name),
+			expectedComposeLocal2,
+			[]string{"build_id"},
+		},
+		{
+			"/api/v1/compose",
+			fmt.Sprintf(`{"blueprint_name": "test","compose_type": "%s","branch": "master"}`, test_distro.TestImageTypeName),
+			map[string][]string{"*": {test_distro.TestImageTypeName}},
+			http.StatusBadRequest,
+			fmt.Sprintf(`{"status":false,"errors":[{"id":"ComposeError","msg":"Failed to get compose type \"%[1]s\": image type \"%[1]s\" for distro \"%[2]s\" is denied by configuration"}]}`,
+				test_distro.TestImageTypeName, test_distro.TestDistro2Name),
+			expectedComposeLocal,
+			[]string{"build_id"},
+		},
+		{
+			"/api/v1/compose",
+			fmt.Sprintf(`{"blueprint_name": "test","compose_type": "%s","branch": "master"}`, test_distro.TestImageTypeName),
+			map[string][]string{test_distro.TestDistro2Name: {"*"}},
+			http.StatusBadRequest,
+			fmt.Sprintf(`{"status":false,"errors":[{"id":"ComposeError","msg":"Failed to get compose type \"%[1]s\": image type \"%[1]s\" for distro \"%[2]s\" is denied by configuration"}]}`,
+				test_distro.TestImageTypeName, test_distro.TestDistro2Name),
+			expectedComposeLocal,
+			[]string{"build_id"},
+		},
+		{
+			"/api/v1/compose",
+			fmt.Sprintf(`{"blueprint_name": "test","compose_type": "%s","branch": "master"}`, test_distro.TestImageTypeName),
+			map[string][]string{fmt.Sprintf("%s*", test_distro.TestDistroName): {fmt.Sprintf("%s*", test_distro.TestImageTypeName)}},
+			http.StatusBadRequest,
+			fmt.Sprintf(`{"status":false,"errors":[{"id":"ComposeError","msg":"Failed to get compose type \"%[1]s\": image type \"%[1]s\" for distro \"%[2]s\" is denied by configuration"}]}`,
+				test_distro.TestImageTypeName, test_distro.TestDistro2Name),
+			expectedComposeLocal,
+			[]string{"build_id"},
+		},
+		{
+			"/api/v1/compose",
+			fmt.Sprintf(`{"blueprint_name": "test","compose_type": "%s","branch": "master"}`, test_distro.TestImageType2Name),
+			map[string][]string{fmt.Sprintf("%s*", test_distro.TestDistroName): {fmt.Sprintf("%s*", test_distro.TestImageTypeName)}},
+			http.StatusBadRequest,
+			fmt.Sprintf(`{"status":false,"errors":[{"id":"ComposeError","msg":"Failed to get compose type \"%[1]s\": image type \"%[1]s\" for distro \"%[2]s\" is denied by configuration"}]}`,
+				test_distro.TestImageType2Name, test_distro.TestDistro2Name),
+			expectedComposeLocal,
+			[]string{"build_id"},
+		},
+	}
+
+	tempdir, err := ioutil.TempDir("", "weldr-tests-")
+	require.NoError(t, err)
+	defer os.RemoveAll(tempdir)
+
+	for _, c := range cases {
+		api, s := createWeldrAPI2(tempdir, rpmmd_mock.NoComposesFixture, c.imageTypeDenylist)
+		test.TestRoute(t, api, true, "POST", c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON, c.IgnoreFields...)
+
+		if c.ExpectedStatus != http.StatusOK {
+			continue
+		}
+
+		composes := s.GetAllComposes()
+		require.Equalf(t, 1, len(composes), "%s: bad compose count in store", c.Path)
+
+		var composeStruct store.Compose
+		for _, c := range composes {
+			composeStruct = c
+			break
+		}
+
+		require.NotNilf(t, composeStruct.ImageBuild.Manifest, "%s: the compose in the store did not contain a blueprint", c.Path)
+
+		if diff := cmp.Diff(composeStruct, *c.ExpectedCompose, test.IgnoreDates(), test.IgnoreUuids(), test.Ignore("Targets.Options.Location"), test.CompareImageTypes()); diff != "" {
+			t.Errorf("%s: compose in store isn't the same as expected, diff:\n%s", c.Path, diff)
+		}
+	}
+}