diff --git a/cmd/osbuild-composer/composer.go b/cmd/osbuild-composer/composer.go index f4f6066ff..eafc7c5a5 100644 --- a/cmd/osbuild-composer/composer.go +++ b/cmd/osbuild-composer/composer.go @@ -111,17 +111,21 @@ func (c *Composer) InitAPI(cert, key string, l net.Listener) error { c.api = cloudapi.NewServer(c.workers, c.rpm, c.distros) c.koji = kojiapi.NewServer(c.logger, c.workers, c.rpm, c.distros) - tlsConfig, err := createTLSConfig(&connectionConfig{ - CACertFile: c.config.Koji.CA, - ServerKeyFile: key, - ServerCertFile: cert, - AllowedDomains: c.config.Koji.AllowedDomains, - }) - if err != nil { - return fmt.Errorf("Error creating TLS configuration: %v", err) - } + if len(c.config.ComposerAPI.IdentityFilter) > 0 { + c.apiListener = l + } else { + tlsConfig, err := createTLSConfig(&connectionConfig{ + CACertFile: c.config.Koji.CA, + ServerKeyFile: key, + ServerCertFile: cert, + AllowedDomains: c.config.Koji.AllowedDomains, + }) + if err != nil { + return fmt.Errorf("Error creating TLS configuration: %v", err) + } - c.apiListener = tls.NewListener(l, tlsConfig) + c.apiListener = tls.NewListener(l, tlsConfig) + } return nil } @@ -195,7 +199,7 @@ func (c *Composer) Start() error { // Add a "/" here, because http.ServeMux expects the // trailing slash for rooted subtrees, whereas the // handler functions don't. - mux.Handle(apiRoute+"/", c.api.Handler(apiRoute)) + mux.Handle(apiRoute+"/", c.api.Handler(apiRoute, c.config.ComposerAPI.IdentityFilter)) mux.Handle(kojiRoute+"/", c.koji.Handler(kojiRoute)) s := &http.Server{ diff --git a/cmd/osbuild-composer/config.go b/cmd/osbuild-composer/config.go index 3cd6d4e7d..f77af1384 100644 --- a/cmd/osbuild-composer/config.go +++ b/cmd/osbuild-composer/config.go @@ -15,6 +15,9 @@ type ComposerConfigFile struct { AllowedDomains []string `toml:"allowed_domains"` CA string `toml:"ca"` } `toml:"worker"` + ComposerAPI struct { + IdentityFilter []string `toml:"identity_filter"` + } `toml:"composer_api"` } func LoadConfig(name string) (*ComposerConfigFile, error) { diff --git a/internal/cloudapi/server.go b/internal/cloudapi/server.go index 840d47b65..0850c1347 100644 --- a/internal/cloudapi/server.go +++ b/internal/cloudapi/server.go @@ -3,12 +3,15 @@ package cloudapi import ( + "context" "crypto/rand" + "encoding/base64" "encoding/json" "fmt" "math" "math/big" "net/http" + "strings" "github.com/go-chi/chi" "github.com/google/uuid" @@ -23,9 +26,22 @@ import ( // Server represents the state of the cloud Server type Server struct { - workers *worker.Server - rpmMetadata rpmmd.RPMMD - distros *distroregistry.Registry + workers *worker.Server + rpmMetadata rpmmd.RPMMD + distros *distroregistry.Registry + identityFilter []string +} + +type contextKey int + +const ( + identityHeaderKey contextKey = iota +) + +type IdentityHeader struct { + Identity struct { + AccountNumber string `json:"account_number"` + } `json:"identity"` } // NewServer creates a new cloud server @@ -40,9 +56,13 @@ func NewServer(workers *worker.Server, rpmMetadata rpmmd.RPMMD, distros *distror // Create an http.Handler() for this server, that provides the composer API at // the given path. -func (server *Server) Handler(path string) http.Handler { +func (server *Server) Handler(path string, identityFilter []string) http.Handler { r := chi.NewRouter() + if len(identityFilter) > 0 { + server.identityFilter = identityFilter + r.Use(server.VerifyIdentityHeader) + } r.Route(path, func(r chi.Router) { HandlerFromMux(server, r) }) @@ -50,6 +70,37 @@ func (server *Server) Handler(path string) http.Handler { return r } +func (server *Server) VerifyIdentityHeader(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + idHeaderB64 := r.Header["X-Rh-Identity"] + if len(idHeaderB64) != 1 { + http.Error(w, "Auth header is not present", http.StatusNotFound) + return + } + + b64Result, err := base64.StdEncoding.DecodeString(idHeaderB64[0]) + if err != nil { + http.Error(w, "Auth header has incorrect format", http.StatusNotFound) + return + } + + var idHeader IdentityHeader + err = json.Unmarshal([]byte(strings.TrimSuffix(fmt.Sprintf("%s", b64Result), "\n")), &idHeader) + if err != nil { + http.Error(w, "Auth header has incorrect format", http.StatusNotFound) + return + } + + for _, i := range server.identityFilter { + if idHeader.Identity.AccountNumber == i { + next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), identityHeaderKey, idHeader))) + return + } + } + http.Error(w, "Account not allowed", http.StatusNotFound) + }) +} + // Compose handles a new /compose POST request func (server *Server) Compose(w http.ResponseWriter, r *http.Request) { contentType := r.Header["Content-Type"] diff --git a/test/cases/api.sh b/test/cases/api.sh index 75492a2f9..5fb1004e9 100755 --- a/test/cases/api.sh +++ b/test/cases/api.sh @@ -658,4 +658,43 @@ case $CLOUD_PROVIDER in ;; esac + + +# Verify the identityfilter +cat <