Skip to content

Commit

Permalink
add turnpike content guard
Browse files Browse the repository at this point in the history
Signed-off-by: Jonathan Holloway <jholloway@redhat.com>
  • Loading branch information
loadtheaccumulator committed Nov 13, 2024
1 parent 64200ba commit 7fe9ac7
Show file tree
Hide file tree
Showing 7 changed files with 321 additions and 10 deletions.
6 changes: 6 additions & 0 deletions pkg/clients/imagebuilder/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,12 @@ func (c *Client) ComposeInstaller(image *models.Image) (*models.Image, error) {
rhsm = false
}

if feature.PulpIntegration.IsEnabled() && image.Commit.Repo.PulpURL != "" {
repoURL = image.Commit.Repo.PulpURL
parsedURL, _ := url.Parse(repoURL)
c.log.WithField("redacted_url", parsedURL.Redacted()).Debug("Using Pulp repo URL for ISO installer request")
}

users := make([]User, 0)
if image.Installer != nil && image.Installer.Username != "" && image.Installer.SSHKey != "" {
users = append(users, User{Name: image.Installer.Username,
Expand Down
57 changes: 52 additions & 5 deletions pkg/clients/pulp/guards_composite.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"slices"

"github.com/google/uuid"
"github.com/redhatinsights/edge-api/pkg/ptr"
Expand Down Expand Up @@ -65,12 +66,44 @@ func (ps *PulpService) CompositeGuardReadByOrgID(ctx context.Context, orgID stri
return ps.CompositeGuardRead(ctx, id)
}

func contentGuardHrefsAreEqual(cghrefa, cghrefb []string) bool {
// compare how many hrefs are in the content guard versus expected
logrus.Debug("Comparing Content Guard hrefs")
if len(cghrefa) != len(cghrefb) {
logrus.WithFields(logrus.Fields{
"href_count_1": len(cghrefa),
"href_count_2": len(cghrefb),
}).Warning("Content Guards do not have the same number of hrefs")

return false
}

logrus.WithFields(logrus.Fields{
"href_count_1": len(cghrefa),
"href_count_2": len(cghrefb),
}).Debug("Content Guards have the same number of hrefs")

// compare content guard hrefs (actual vs. expected)
for i := range cghrefa {
if !slices.Contains(cghrefb, cghrefa[i]) {
logrus.WithField("href_mismatch", cghrefa[i]).Warning("Content Guard href mismatch")

return false
}
}

logrus.Debug("Content Guard has the expected hrefs")

return true
}

// CompositeGuardEnsure ensures that the composite guard is created and returns it. The method is idempotent.
func (ps *PulpService) CompositeGuardEnsure(ctx context.Context, orgID, headerHref, rbacHref string) (*CompositeContentGuardResponse, error) {
func (ps *PulpService) CompositeGuardEnsure(ctx context.Context, orgID string, pulpGuardHrefs []string) (*CompositeContentGuardResponse, error) {
cg, err := ps.CompositeGuardReadByOrgID(ctx, compositeGuardName(orgID))
// nolint: gocritic
if errors.Is(err, ErrRecordNotFound) {
cg, err = ps.CompositeGuardCreate(ctx, orgID, headerHref, rbacHref)
logrus.Warning("No composite guard found. Creating one.")
cg, err = ps.CompositeGuardCreate(ctx, orgID, pulpGuardHrefs...)
if err != nil {
return nil, err
}
Expand All @@ -83,14 +116,15 @@ func (ps *PulpService) CompositeGuardEnsure(ctx context.Context, orgID, headerHr
}

gs := ptr.FromOrEmpty(cg.Guards)
if !(len(gs) == 2 && (gs[0] != headerHref && gs[1] != rbacHref) || (gs[1] != headerHref && gs[0] != rbacHref)) {
if !contentGuardHrefsAreEqual(gs, pulpGuardHrefs) {
logrus.WithContext(ctx).Warnf("unexpected Composite Content Guard: %v, deleting it", gs)
err = ps.CompositeGuardDelete(ctx, ScanUUID(cg.PulpHref))
if err != nil {
return nil, err
}

cg, err = ps.CompositeGuardCreate(ctx, orgID, headerHref, rbacHref)
logrus.Warning("Matching composite guard not found. Creating one.")
cg, err = ps.CompositeGuardCreate(ctx, orgID, pulpGuardHrefs...)
if err != nil {
return nil, err
}
Expand All @@ -102,17 +136,30 @@ func (ps *PulpService) CompositeGuardEnsure(ctx context.Context, orgID, headerHr
// that the composite guard is not created or the guards are not the same as the ones provided, it will delete it
// and recreate it. This method is idempotent and will not create the guards if they already exist.
func (ps *PulpService) ContentGuardEnsure(ctx context.Context, orgID string) (*CompositeContentGuardResponse, error) {
var contentGuardHrefs []string

logrus.WithContext(ctx).Debug("Creating header guard")
hcg, err := ps.HeaderGuardEnsure(ctx, orgID)
if err != nil {
return nil, err
}
contentGuardHrefs = append(contentGuardHrefs, *hcg.PulpHref)

logrus.WithContext(ctx).Debug("Creating turnpike content guard")
tpcg, err := ps.TurnpikeGuardEnsure(ctx)
if err != nil {
return nil, err
}
contentGuardHrefs = append(contentGuardHrefs, *tpcg.PulpHref)

logrus.WithContext(ctx).Debug("Creating RBAC content guard")
rcg, err := ps.RbacGuardEnsure(ctx)
if err != nil {
return nil, err
}
contentGuardHrefs = append(contentGuardHrefs, *rcg.PulpHref)

ccg, err := ps.CompositeGuardEnsure(ctx, orgID, *hcg.PulpHref, *rcg.PulpHref)
ccg, err := ps.CompositeGuardEnsure(ctx, orgID, contentGuardHrefs)
if err != nil {
return nil, err
}
Expand Down
80 changes: 80 additions & 0 deletions pkg/clients/pulp/guards_composite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package pulp

import (
"fmt"
"testing"

"github.com/bxcodec/faker/v3"
"github.com/magiconair/properties/assert"
)

func TestContentGuardHrefsAreEqual(t *testing.T) {
var orgID = faker.UUIDDigit()

var href1 = fmt.Sprintf("/api/pulp/em%sd/api/v3/contentguards/core/header/%s/", orgID, faker.UUIDHyphenated())
var href2 = fmt.Sprintf("/api/pulp/em%sd/api/v3/contentguards/core/rbac/%s/", orgID, faker.UUIDHyphenated())
var href3 = fmt.Sprintf("/api/pulp/em%sd/api/v3/contentguards/core/header/%s/", orgID, faker.UUIDHyphenated())
var href4 = fmt.Sprintf("/api/pulp/em%sd/api/v3/contentguards/core/header/%s/", orgID, faker.UUIDHyphenated())

var slice1 = []string{href1, href2, href3} // first slice of hrefs
var slice2 = []string{href3, href2, href1} // same size, different order
var slice3 = []string{href1, href2, href3} // same size, same order
var slice4 = []string{href1, href2, href4} // same size, different content
var slice5 = []string{href1, href2} // different size
var slice6 = []string{href1, href2, ""} // samesize, empty string
var slice7 = []string{"", href2, href1} // one empty string
var slice8 = []string{"", "", ""} // all empty strings
var slice9 = []string{"", ""} // all empty strings, different size

t.Run("sameslice", func(t *testing.T) {
assert.Equal(t, contentGuardHrefsAreEqual(slice1, slice1), true)
})

t.Run("samesize_differentorder", func(t *testing.T) {
assert.Equal(t, contentGuardHrefsAreEqual(slice1, slice2), true)
})

t.Run("samesize_sameorder", func(t *testing.T) {
assert.Equal(t, contentGuardHrefsAreEqual(slice1, slice3), true)
})

t.Run("negative_samesize_differentcontent", func(t *testing.T) {
assert.Equal(t, contentGuardHrefsAreEqual(slice1, slice4), false)
})

t.Run("negative_differentsizes", func(t *testing.T) {
assert.Equal(t, contentGuardHrefsAreEqual(slice1, slice5), false)
})

t.Run("empty_both", func(t *testing.T) {
assert.Equal(t, contentGuardHrefsAreEqual([]string{}, []string{}), true)
})

t.Run("negative_empty_a", func(t *testing.T) {
assert.Equal(t, contentGuardHrefsAreEqual([]string{}, slice1), false)
})

t.Run("negative_empty_b", func(t *testing.T) {
assert.Equal(t, contentGuardHrefsAreEqual(slice1, []string{}), false)
})

t.Run("negative_samesize_emptystring", func(t *testing.T) {
assert.Equal(t, contentGuardHrefsAreEqual(slice1, slice6), false)
})

t.Run("samesize_oneemptystring_difforder", func(t *testing.T) {
assert.Equal(t, contentGuardHrefsAreEqual(slice6, slice7), true)
})

t.Run("negative_samesize_allemptystrings", func(t *testing.T) {
assert.Equal(t, contentGuardHrefsAreEqual(slice1, slice8), false)
})

t.Run("negative_differentsize_allemptystrings", func(t *testing.T) {
assert.Equal(t, contentGuardHrefsAreEqual(slice8, slice9), false)
})

t.Run("sameslice_allemptystrings", func(t *testing.T) {
assert.Equal(t, contentGuardHrefsAreEqual(slice8, slice8), true)
})
}
172 changes: 172 additions & 0 deletions pkg/clients/pulp/guards_turnpike.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package pulp

import (
"context"
"errors"
"fmt"

"github.com/google/uuid"
"github.com/redhatinsights/edge-api/config"
"github.com/redhatinsights/edge-api/pkg/ptr"
"github.com/sirupsen/logrus"
)

const TurnpikeGuardName = "ostree_turnpike_guard"
const TurnpikeJQFilter = ".identity.x509.subject_dn"

// TurnpikeGuardList returns a list of header guards. The nameFilter can be used to filter the results.
func (ps *PulpService) TurnpikeGuardList(ctx context.Context, nameFilter string) ([]HeaderContentGuardResponse, error) {
req := ContentguardsCoreHeaderListParams{
Limit: &DefaultPageSize,
}
if nameFilter != "" {
req.Name = &nameFilter
}

resp, err := ps.cwr.ContentguardsCoreHeaderListWithResponse(ctx, ps.dom, &req, addAuthenticationHeader)
if err != nil {
return nil, err
}

if resp.JSON200 == nil {
return nil, fmt.Errorf("unexpected response: %d, body: %s", resp.StatusCode(), string(resp.Body))
}

if resp.JSON200.Count > DefaultPageSize {
return nil, fmt.Errorf("default page size too small: %d", resp.JSON200.Count)
}

if resp.JSON200.Count == 0 || resp.JSON200.Results[0].PulpHref == nil {
return nil, ErrRecordNotFound
}

return resp.JSON200.Results, nil
}

// TurnpikeGuardRead returns the Turnpike guard with the given ID.
func (ps *PulpService) TurnpikeGuardRead(ctx context.Context, id uuid.UUID) (*HeaderContentGuardResponse, error) {
req := ContentguardsCoreHeaderReadParams{}
resp, err := ps.cwr.ContentguardsCoreHeaderReadWithResponse(ctx, ps.dom, id, &req, addAuthenticationHeader)

if err != nil {
return nil, err
}

if resp.JSON200 == nil {
return nil, fmt.Errorf("unexpected response: %d, body: %s", resp.StatusCode(), string(resp.Body))
}

return resp.JSON200, nil
}

// TurnpikeGuardFind returns the Turnpike guard.
func (ps *PulpService) TurnpikeGuardFind(ctx context.Context) (*HeaderContentGuardResponse, error) {
hgl, err := ps.TurnpikeGuardList(ctx, TurnpikeGuardName)
if err != nil {
logrus.WithFields(logrus.Fields{
"name": TurnpikeGuardName,
"error": err.Error(),
}).Error("Turnpike content guard not found")

if errors.Is(err, ErrRecordNotFound) {
return nil, ErrRecordNotFound
}

return nil, err
}

id := ScanUUID(hgl[0].PulpHref)
return ps.TurnpikeGuardRead(ctx, id)
}

// TurnpikeGuardCreate creates a new Turnpike guard and returns it.
func (ps *PulpService) TurnpikeGuardCreate(ctx context.Context) (*HeaderContentGuardResponse, error) {
cfg := config.Get()

req := HeaderContentGuard{
Name: TurnpikeGuardName,
Description: ptr.To("EDGE"),
HeaderName: "x-rh-identity",
HeaderValue: cfg.Pulp.GuardSubjectDN,
JqFilter: ptr.To(TurnpikeJQFilter),
}

resp, err := ps.cwr.ContentguardsCoreHeaderCreateWithResponse(ctx, ps.dom, req, addAuthenticationHeader)

if err != nil {
return nil, err
}

if resp.JSON201 == nil {
return nil, fmt.Errorf("unexpected response: %d, body: %s", resp.StatusCode(), string(resp.Body))
}

return resp.JSON201, nil
}

// TurnpikeGuardEnsure ensures that the Turnpike guard is created and returns it. The method is idempotent.
func (ps *PulpService) TurnpikeGuardEnsure(ctx context.Context) (*HeaderContentGuardResponse, error) {
cg, err := ps.TurnpikeGuardFind(ctx)
if errors.Is(err, ErrRecordNotFound) {
// turnpike guard is not found, so create one
cg, err = ps.TurnpikeGuardCreate(ctx)
if err != nil {
return nil, err
}

return cg, nil
}
if err != nil {
return nil, err
}
if cg == nil {
return nil, fmt.Errorf("unexpected nil guard")
}

return cg, nil
}

// TurnpikeGuardDelete deletes the Turnpike guard with the given ID.
func (ps *PulpService) TurnpikeGuardDelete(ctx context.Context, id uuid.UUID) error {
listParams := ContentguardsCoreHeaderListRolesParams{}
roles, err := ps.cwr.ContentguardsCoreHeaderListRolesWithResponse(ctx, ps.dom, id, &listParams, addAuthenticationHeader)

if err != nil {
return err
}

if roles.JSON200 == nil {
return fmt.Errorf("unexpected response: %d, body: %s", roles.StatusCode(), string(roles.Body))
}

for _, role := range roles.JSON200.Roles {
nr := NestedRole{
Role: role.Role,
Users: &[]string{},
}

logrus.WithContext(ctx).Warnf("removing Turnpike guardrole: %s", role.Role)
removed, err := ps.cwr.ContentguardsCoreHeaderRemoveRoleWithResponse(ctx, ps.dom, id, nr, addAuthenticationHeader)

if err != nil {
return err
}

if removed.JSON201 == nil {
return fmt.Errorf("unexpected response: %d, body: %s", removed.StatusCode(), string(removed.Body))
}
}

resp, err := ps.cwr.ContentguardsCoreHeaderDelete(ctx, ps.dom, id, addAuthenticationHeader)

if err != nil {
return err
}
resp.Body.Close()

if resp.StatusCode != 204 {
return fmt.Errorf("unexpected response: %d", resp.StatusCode)
}

return nil
}
2 changes: 1 addition & 1 deletion pkg/services/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -958,7 +958,7 @@ func (s *ImageService) CreateRepoForImage(ctx context.Context, img *models.Image
"aws_status": repo.Status,
"pulp_url": parsedPulpURL.Redacted(),
"pulp_status": repo.PulpStatus,
}).Info("OSTree repo is ready")
}).Info("OSTree repo process complete")

if repo.Status != models.RepoStatusSuccess && repo.PulpStatus != models.RepoStatusSuccess {
return nil, goErrors.New("No repo has been created")
Expand Down
Loading

0 comments on commit 7fe9ac7

Please sign in to comment.