Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MGMT-17413: Change pull secret validation on register/update cluster/infraenv to run only against the required images #6158

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 6 additions & 8 deletions cmd/main.go
Expand Up @@ -336,19 +336,17 @@ func main() {
instructionApi := hostcommands.NewInstructionManager(log.WithField("pkg", "instructions"), db, hwValidator,
releaseHandler, Options.InstructionConfig, connectivityValidator, eventsHandler, versionHandler, osImages, Options.EnableKubeAPI)

images := []string{
publicRegistries := map[string]bool{}
validations.ParsePublicRegistries(publicRegistries, Options.ValidationsConfig.PublicRegistries)
pullSecretValidator, err := validations.NewPullSecretValidator(
publicRegistries,
authHandler,
Options.ReleaseImageMirror,
Options.BMConfig.AgentDockerImg,
Options.InstructionConfig.InstallerImage,
Options.InstructionConfig.ControllerImage,
Options.InstructionConfig.AgentImage,
}

for _, releaseImage := range releaseImagesArray {
images = append(images, *releaseImage.URL)
danmanor marked this conversation as resolved.
Show resolved Hide resolved
}

pullSecretValidator, err := validations.NewPullSecretValidator(Options.ValidationsConfig, authHandler, images...)
)
failOnError(err, "failed to create pull secret validator")

log.Println("DeployTarget: " + Options.DeployTarget)
Expand Down
14 changes: 7 additions & 7 deletions internal/bminventory/inventory.go
Expand Up @@ -177,7 +177,7 @@ type InstallerInternals interface {
GetKnownHostApprovedCounts(clusterID strfmt.UUID) (registered, approved int, err error)
HostWithCollectedLogsExists(clusterId strfmt.UUID) (bool, error)
GetKnownApprovedHosts(clusterId strfmt.UUID) ([]*common.Host, error)
ValidatePullSecret(secret string, username string) error
ValidatePullSecret(secret string, username string, releaseImageURL string) error
GetInfraEnvInternal(ctx context.Context, params installer.GetInfraEnvParams) (*common.InfraEnv, error)
V2UpdateHostInstallProgressInternal(ctx context.Context, params installer.V2UpdateHostInstallProgressParams) error
}
Expand Down Expand Up @@ -285,8 +285,8 @@ func NewBareMetalInventory(
}
}

func (b *bareMetalInventory) ValidatePullSecret(secret string, username string) error {
return b.secretValidator.ValidatePullSecret(secret, username)
func (b *bareMetalInventory) ValidatePullSecret(secret string, username string, releaseImageURL string) error {
return b.secretValidator.ValidatePullSecret(secret, username, releaseImageURL)
}

func (b *bareMetalInventory) updatePullSecret(pullSecret string, log logrus.FieldLogger) (string, error) {
Expand Down Expand Up @@ -633,7 +633,7 @@ func (b *bareMetalInventory) RegisterClusterInternal(
}

pullSecret := swag.StringValue(params.NewClusterParams.PullSecret)
err = b.ValidatePullSecret(pullSecret, ocm.UserNameFromContext(ctx))
err = b.ValidatePullSecret(pullSecret, ocm.UserNameFromContext(ctx), *releaseImage.URL)
if err != nil {
err = errors.Wrap(secretValidationToUserError(err), "pull secret for new cluster is invalid")
return nil, common.NewApiError(http.StatusBadRequest, err)
Expand Down Expand Up @@ -1847,7 +1847,7 @@ func (b *bareMetalInventory) validateAndUpdateClusterParams(ctx context.Context,
log := logutil.FromContext(ctx, b.log)

if swag.StringValue(params.ClusterUpdateParams.PullSecret) != "" {
if err := b.ValidatePullSecret(*params.ClusterUpdateParams.PullSecret, ocm.UserNameFromContext(ctx)); err != nil {
if err := b.ValidatePullSecret(*params.ClusterUpdateParams.PullSecret, ocm.UserNameFromContext(ctx), cluster.OcpReleaseImage); err != nil {
log.WithError(err).Errorf("Pull secret for cluster %s is invalid", params.ClusterID)
return installer.V2UpdateClusterParams{}, err
}
Expand Down Expand Up @@ -4645,7 +4645,7 @@ func (b *bareMetalInventory) RegisterInfraEnvInternal(
}

pullSecret := swag.StringValue(params.InfraenvCreateParams.PullSecret)
err = b.ValidatePullSecret(pullSecret, ocm.UserNameFromContext(ctx))
err = b.ValidatePullSecret(pullSecret, ocm.UserNameFromContext(ctx), "")
if err != nil {
err = errors.Wrap(secretValidationToUserError(err), "pull secret for new infraEnv is invalid")
return common.NewApiError(http.StatusBadRequest, err)
Expand Down Expand Up @@ -5109,7 +5109,7 @@ func (b *bareMetalInventory) validateAndUpdateInfraEnvParams(ctx context.Context
log := logutil.FromContext(ctx, b.log)

if params.InfraEnvUpdateParams.PullSecret != "" {
if err := b.ValidatePullSecret(params.InfraEnvUpdateParams.PullSecret, ocm.UserNameFromContext(ctx)); err != nil {
if err := b.ValidatePullSecret(params.InfraEnvUpdateParams.PullSecret, ocm.UserNameFromContext(ctx), ""); err != nil {
log.WithError(err).Errorf("Pull secret for infraEnv %s is invalid", params.InfraEnvID)
return installer.UpdateInfraEnvParams{}, err
}
Expand Down
52 changes: 26 additions & 26 deletions internal/bminventory/inventory_test.go

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions internal/bminventory/mock_installer_internal.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

246 changes: 246 additions & 0 deletions internal/cluster/validations/pull_secret_validation.go
@@ -0,0 +1,246 @@
package validations

import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"strings"

"github.com/openshift/assisted-service/pkg/auth"
"github.com/openshift/assisted-service/pkg/ocm"
"github.com/pkg/errors"
)

const (
dockerHubRegistry = "docker.io"
dockerHubLegacyAuth = "https://index.docker.io/v1/"
stageRegistry = "registry.stage.redhat.io"
)

// PullSecretValidator is used run validations on a provided pull secret
// it verifies the format of the pull secrete and access to required image registries
//
//go:generate mockgen -source=pull_secret_validation.go -package=validations -destination=mock_pull_secret_validation.go
type PullSecretValidator interface {
ValidatePullSecret(secret string, username string, releaseImageURL string) error
}

func ParsePublicRegistries(publicRegistries map[string]bool, publicRegistriesLiteral string) {
if publicRegistriesLiteral == "" {
return
}

for _, registry := range strings.Split(publicRegistriesLiteral, ",") {
publicRegistries[registry] = true
}
}

type registryPullSecretValidator struct {
publicRegistries map[string]bool
registriesWithAuth map[string]bool
authHandler auth.Authenticator
}

type imagePullSecret struct {
Auths map[string]map[string]interface{} `json:"auths"`
}

type PullSecretCreds struct {
Username string
Password string
Registry string
AuthRaw string
Email string
}

// PullSecretError distinguishes secret validation errors produced by this package from other types of errors
type PullSecretError struct {
Msg string
Cause error
}

func (e *PullSecretError) Error() string {
return e.Msg
}

func (e *PullSecretError) Unwrap() error {
return e.Cause
}

// ParsePullSecret validates the format of a pull secret and converts the secret string into individual credentail entries
func ParsePullSecret(secret string) (map[string]PullSecretCreds, error) {
result := make(map[string]PullSecretCreds)
var s imagePullSecret

err := json.Unmarshal([]byte(strings.TrimSpace(secret)), &s)
if err != nil {
return nil, &PullSecretError{Msg: "pull secret must be a well-formed JSON", Cause: err}
}

if len(s.Auths) == 0 {
return nil, &PullSecretError{Msg: "pull secret must contain 'auths' JSON-object field"}
}

for d, a := range s.Auths {

_, authPresent := a["auth"]
_, credsStorePresent := a["credsStore"]
if !authPresent && !credsStorePresent {
return nil, &PullSecretError{Msg: fmt.Sprintf("invalid pull secret: %q JSON-object requires either 'auth' or 'credsStore' field", d)}
}

var authRaw string
if auth, ok := a["auth"].(string); authPresent && !ok {
return nil, &PullSecretError{Msg: fmt.Sprintf("invalid pull secret: 'auth' field of %q is %v but should be a string", d, a["auth"])}
} else {
authRaw = auth
}
data, err := base64.StdEncoding.DecodeString(authRaw)
if err != nil {
return nil, &PullSecretError{Msg: fmt.Sprintf("invalid pull secret: 'auth' field of %q is not base64-encoded", d)}
}

res := bytes.Split(data, []byte(":"))
if len(res) != 2 {
return nil, &PullSecretError{Msg: fmt.Sprintf("invalid pull secret: 'auth' for %s is not in 'user:password' format", d)}
}

var email string
if emailString, ok := a["email"].(string); ok {
email = emailString
}

result[d] = PullSecretCreds{
Password: string(res[1]),
Username: string(res[0]),
AuthRaw: authRaw,
Registry: d,
Email: email,
}

}
return result, nil
}

func AddRHRegPullSecret(secret, rhCred string) (string, error) {
if rhCred == "" {
return "", errors.Errorf("invalid pull secret")
}
var s imagePullSecret
err := json.Unmarshal([]byte(strings.TrimSpace(secret)), &s)
if err != nil {
return secret, errors.Errorf("invalid pull secret: %v", err)
}
s.Auths[stageRegistry] = make(map[string]interface{})
s.Auths[stageRegistry]["auth"] = base64.StdEncoding.EncodeToString([]byte(rhCred))
ps, err := json.Marshal(s)
if err != nil {
return secret, err
}
return string(ps), nil
}

func NewPullSecretValidator(publicRegistries map[string]bool, authHandler auth.Authenticator, images ...string) (PullSecretValidator, error) {
registriesWithAuth := map[string]bool{}
for _, image := range images {
registryWithAuth, err := getRegistryAuthStatus(publicRegistries, image)
if err != nil {
return nil, err
}

if registryWithAuth != nil {
registriesWithAuth[*registryWithAuth] = true
}
}

return &registryPullSecretValidator{
publicRegistries: publicRegistries,
registriesWithAuth: registriesWithAuth,
authHandler: authHandler,
}, nil
}

func validateRegistryWithAuth(registry string, credentials map[string]PullSecretCreds) error {
// Both "docker.io" and "https://index.docker.io/v1/" are acceptable for DockerHub login
if registry == dockerHubRegistry {
if _, ok := credentials[dockerHubLegacyAuth]; ok {
return nil
}
}

// We add auth for stage registry automatically
if registry == stageRegistry {
return nil
}

if _, ok := credentials[registry]; !ok {
return &PullSecretError{Msg: fmt.Sprintf("pull secret must contain auth for %q", registry)}
}

return nil
}

// ValidatePullSecret validates that a pull secret is well formed and contains all required data
func (v *registryPullSecretValidator) ValidatePullSecret(secret string, username string, releaseImageURL string) error {
creds, err := ParsePullSecret(secret)
if err != nil {
return err
}

// only check for cloud creds if we're authenticating against Red Hat SSO
if v.authHandler.AuthType() == auth.TypeRHSSO {
r, ok := creds["cloud.openshift.com"]
if !ok {
return &PullSecretError{Msg: "pull secret must contain auth for \"cloud.openshift.com\""}
}

var user interface{}
user, err = v.authHandler.AuthAgentAuth(r.AuthRaw)
if err != nil {
return &PullSecretError{Msg: "failed to authenticate the pull secret token"}
}

if (user.(*ocm.AuthPayload)).Username != username {
return &PullSecretError{Msg: "pull secret token does not match current user"}
}
}

for registry := range v.registriesWithAuth {
if err = validateRegistryWithAuth(registry, creds); err != nil {
return err
}
}

registryWithAuth, err := getRegistryAuthStatus(v.publicRegistries, releaseImageURL)
if err != nil {
return err
}

if registryWithAuth != nil {
if err := validateRegistryWithAuth(*registryWithAuth, creds); err != nil {
return err
}
}

return nil
}

// getRegistryAuthStatus takes a release image reference and a set of ignorarble registries,
// and returns the image's registry if it requires authentication and it is not ignorable
func getRegistryAuthStatus(ignorableImages map[string]bool, image string) (*string, error) {
if image == "" {
return nil, nil
}

registry, err := ParseRegistry(image)
if err != nil {
return nil, errors.Wrapf(err, "error occurred while trying to parse the registry out of '%s'", image)
}

if (registry == dockerHubRegistry && ignorableImages[dockerHubLegacyAuth]) || ignorableImages[registry] {
return nil, nil
}

return &registry, nil
}