Skip to content
This repository has been archived by the owner on Mar 16, 2024. It is now read-only.

change: move actual permissions to .status + apply IRAs to consumed permissions #2226

Merged
merged 13 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceRoot}",
"program": "${workspaceRoot}/main.go",
"args": ["controller"],
},
{
Expand Down
230 changes: 227 additions & 3 deletions integration/client/imagerules/imageroleauthorizations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
adminv1 "github.com/acorn-io/runtime/pkg/apis/admin.acorn.io/v1"
apiv1 "github.com/acorn-io/runtime/pkg/apis/api.acorn.io/v1"
internalv1 "github.com/acorn-io/runtime/pkg/apis/internal.acorn.io/v1"
v1 "github.com/acorn-io/runtime/pkg/apis/internal.acorn.io/v1"
internaladminv1 "github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1"
"github.com/acorn-io/runtime/pkg/awspermissions"
"github.com/acorn-io/runtime/pkg/client"
Expand Down Expand Up @@ -135,7 +136,7 @@ func TestImageRoleAuthorizations(t *testing.T) {
},
Spec: internalv1.AppInstanceSpec{
Image: tagName,
Permissions: []internalv1.Permissions{
GrantedPermissions: []internalv1.Permissions{
{
ServiceName: "rootapp",
Rules: []internalv1.PolicyRule{{
Expand Down Expand Up @@ -215,7 +216,7 @@ func TestImageRoleAuthorizations(t *testing.T) {
require.Equal(t, 1, len(app.Status.Staged.ImagePermissionsDenied), "should have 1 denied permission (rootapp) with no IRA defined")
expectedDeniedPermissionsRootapp := []internalv1.Permissions{
{
ServiceName: tagName,
ServiceName: "rootapp",
Rules: []internalv1.PolicyRule{
{
PolicyRule: rbacv1.PolicyRule{
Expand Down Expand Up @@ -305,7 +306,7 @@ func TestImageRoleAuthorizations(t *testing.T) {
require.Equal(t, 1, len(nestedApp.Status.Staged.ImagePermissionsDenied), "should have 1 denied permission (foo.awsapp)")
expectedDeniedPermissionsFooAwsapp := []internalv1.Permissions{
{
ServiceName: nestedImageTagName,
ServiceName: "awsapp",
Rules: []internalv1.PolicyRule{
{
PolicyRule: rbacv1.PolicyRule{
Expand Down Expand Up @@ -363,4 +364,227 @@ func TestImageRoleAuthorizations(t *testing.T) {
})

require.Equal(t, 0, len(nestedApp.Status.Staged.ImagePermissionsDenied), "should have 0 denied permissions")

// --------------------------------------------------------------------
// Run #5 - Now we're updating the granted permissions to include permissions that the image is not authorized to have -> expect denied permissions
// --------------------------------------------------------------------

rmapp(ctx, app)

nappinstance := blueprint.DeepCopy()
nappinstance.Spec.GrantedPermissions = append(app.Spec.GrantedPermissions, internalv1.Permissions{
ServiceName: "rootapp",
Rules: []internalv1.PolicyRule{{
PolicyRule: rbacv1.PolicyRule{
APIGroups: []string{"*"},
Verbs: []string{"*"},
Resources: []string{"*"},
},
}},
})

app = createWaitLoop(ctx, nappinstance)

require.Equal(t, 1, len(app.Status.Staged.ImagePermissionsDenied), "should have 1 denied permission (rootapp)")

rmapp(ctx, app)
}

func TestImageRoleAuthorizationConsumerPerms(t *testing.T) {
helper.StartController(t)

ctx := helper.GetCTX(t)
c, _ := helper.ClientAndProject(t)
kclient := helper.MustReturn(kclient.Default)

// enable image role authorizations in acorn config
helper.EnableFeatureWithRestore(t, ctx, kclient, profiles.FeatureImageRoleAuthorizations)

// Delete any existing rules from this project namespace
err := kclient.DeleteAllOf(ctx, &internaladminv1.ImageRoleAuthorizationInstance{}, cclient.InNamespace(c.GetNamespace()))
if err != nil {
t.Fatal(err)
}
err = kclient.DeleteAllOf(ctx, &internaladminv1.ClusterImageRoleAuthorizationInstance{})
if err != nil {
t.Fatal(err)
}

// Build Image
image, err := c.AcornImageBuild(ctx, "./testdata/serviceconsumer/Acornfile", &client.AcornImageBuildOptions{
Cwd: "./testdata/serviceconsumer/",
})
if err != nil {
t.Fatal(err)
}

// Prep: Create a role that we can use later
role := &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: "test:get:secrets",
Namespace: c.GetNamespace(),
},
Rules: []rbacv1.PolicyRule{{
APIGroups: []string{""},
Verbs: []string{"get"},
Resources: []string{"secrets"},
}},
}

err = apply.Ensure(ctx, kclient, role)
if err != nil {
t.Fatal(err)
}

// Integration tests don't have proper privileges so we will by pass the permission validation
appInstance := &internalv1.AppInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "test-consumer",
Namespace: c.GetProject(),
},
Spec: internalv1.AppInstanceSpec{
Image: image.ID,
GrantedPermissions: []internalv1.Permissions{{
ServiceName: "producer.default",
Rules: []internalv1.PolicyRule{{
PolicyRule: rbacv1.PolicyRule{
APIGroups: []string{""},
Verbs: []string{"get"},
Resources: []string{"secrets"},
},
}},
}},
},
Status: internalv1.AppInstanceStatus{},
}
if err := apply.Ensure(ctx, kclient, appInstance); err != nil {
t.Fatal(err)
}

app, err := c.AppGet(ctx, appInstance.Name)
if err != nil {
t.Fatal(err)
}
app = helper.WaitForObject(t, helper.Watcher(t, c), &apiv1.AppList{}, app, func(obj *apiv1.App) bool {
return obj.Status.AppStatus.Services["producer"].ServiceAcornName != ""
})

serviceAcorn, err := c.AppGet(ctx, app.Status.AppStatus.Services["producer"].ServiceAcornName)
require.NoError(t, err, "should not error while getting service acorn")

serviceAcorn = helper.WaitForObject(t, helper.Watcher(t, c), &apiv1.AppList{}, serviceAcorn, func(obj *apiv1.App) bool {
return obj.Status.Staged.PermissionsChecked
})

require.Equal(t, 1, len(serviceAcorn.Status.Staged.ImagePermissionsDenied), "should have 1 denied permissions")

// Create IRA to allow service Acorn to have the permissions
ira := &adminv1.ImageRoleAuthorization{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: c.GetNamespace(),
},
Spec: internaladminv1.ImageRoleAuthorizationInstanceSpec{
ImageSelector: internalv1.ImageSelector{
NamePatterns: []string{
serviceAcorn.Status.Staged.AppImage.ID,
serviceAcorn.Status.Staged.AppImage.Digest,
"**@" + serviceAcorn.Status.Staged.AppImage.Digest,
},
},
Roles: internaladminv1.RoleAuthorizations{
Scopes: []string{"account"},
RoleRefs: []internaladminv1.RoleRef{
{
Name: "test:get:secrets",
Kind: "Role",
},
},
},
},
}

// Ensure that the selector matches the service acorn image now
err = imageselector.MatchImage(ctx, kclient, c.GetNamespace(), serviceAcorn.Status.Staged.AppImage.Name, "", serviceAcorn.Status.Staged.AppImage.Digest, ira.Spec.ImageSelector, imageselector.MatchImageOpts{})
require.NoError(t, err, "should not error while matching image against pattern %s", ira.Spec.ImageSelector.NamePatterns[0])

err = apply.Ensure(ctx, kclient, ira)
if err != nil {
t.Fatal(err)
}

// Wait for permissions to be authorized (check gets re-triggered by the creation of the IRA)
serviceAcorn = helper.WaitForObject(t, helper.Watcher(t, c), &apiv1.AppList{}, serviceAcorn, func(obj *apiv1.App) bool {
return len(obj.Status.Staged.ImagePermissionsDenied) == 0
})

helper.WaitForObject(t, helper.Watcher(t, c), &apiv1.AppList{}, serviceAcorn, func(obj *apiv1.App) bool {
return obj.Status.Ready
})

// Service Acorn is ready, so App should try to consume permissions that it's not allowed to have
app = helper.WaitForObject(t, helper.Watcher(t, c), &apiv1.AppList{}, app, func(obj *apiv1.App) bool {
return len(obj.Status.DeniedConsumerPermissions) > 0
})

expectedPerms := []internalv1.Permissions{
{
ServiceName: "kubetest",
Rules: []internalv1.PolicyRule{
{
PolicyRule: rbacv1.PolicyRule{
Verbs: []string{"get"},
APIGroups: []string{""},
Resources: []string{"secrets"},
ResourceNames: []string{"foo"},
NonResourceURLs: []string(nil),
}, Scopes: []string(nil),
},
},
ZZ_ClusterRules: []v1.PolicyRule(nil),
},
{
ServiceName: "test",
Rules: []internalv1.PolicyRule{
{
PolicyRule: rbacv1.PolicyRule{
Verbs: []string{"get"},
APIGroups: []string{""},
Resources: []string{"secrets"},
ResourceNames: []string{"foo"},
NonResourceURLs: []string(nil),
},
Scopes: []string(nil),
},
},
ZZ_ClusterRules: []v1.PolicyRule(nil),
},
}

require.Equal(t, expectedPerms, app.Status.DeniedConsumerPermissions)
require.Empty(t, app.Status.Staged.ImagePermissionsDenied)
require.True(t, app.Status.Condition("consumer-permissions").Error)
require.Contains(t, app.Status.Condition("consumer-permissions").Message, "cannot run current image due to unauthorized permissions given to it by consumed services: rules needed:")

// Now update IRA to allow consumed permissions
ira.Spec.ImageSelector.NamePatterns = []string{"**"} // simply catch-all wildcard

err = apply.Ensure(ctx, kclient, ira)
if err != nil {
t.Fatal(err)
}

// Denied Consumer Permissions should vanish
app = helper.WaitForObject(t, helper.Watcher(t, c), &apiv1.AppList{}, app, func(obj *apiv1.App) bool {
return len(obj.Status.DeniedConsumerPermissions) == 0
})

require.True(t, app.Status.Condition("consumer-permissions").Success)

app = helper.WaitForObject(t, helper.Watcher(t, c), &apiv1.AppList{}, app, func(obj *apiv1.App) bool {
return obj.Status.Ready
})

require.True(t, app.Status.Ready) // should be impossible to not be ready at this point given that we wait for it ¯\_(ツ)_/¯
require.Equal(t, expectedPerms, app.Status.Permissions)
}
27 changes: 27 additions & 0 deletions integration/client/imagerules/testdata/serviceconsumer/Acornfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
services: producer: build: acornfile: "service.acorn"

jobs: test: {
consumes: "producer"
image: "ghcr.io/acorn-io/images-mirror/busybox:latest"
command: "/test.sh"
files: "/test.sh": """
#!/bin/sh
set -e -x
[ "$foo" == "envvalue" ]
[ "$(cat /secret-file)" == "filevalue" ]
"""
}

jobs: kubetest: {
consumes: "producer"
image: "cgr.dev/chainguard/kubectl:latest-dev"
env: NAMESPACE: "@{acorn.project}"
entrypoint: "/run.sh"
files: "/run.sh": """
#!/bin/sh
set -e -x
[ "${NAMESPACE}" == "@{acorn.project}" ]
kubectl -n ${NAMESPACE} get secret foo 2>&1 | grep "NotFound"
kubectl -n ${NAMESPACE} get secret bar 2>&1 | grep "Forbidden"
"""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
services: default: {
default: true
// You this must be a superset or the exact same permissions
// that you will grant to the consumer
consumer: permissions: rules: [{
verbs: ["get"]
apiGroups: [""]
resources: ["secrets"]
}]
generated: job: "default-svc"
}

secrets: asecret: data: {
env: "envvalue"
file: "filevalue"
}

jobs: "default-svc": {
image: "ghcr.io/acorn-io/images-mirror/busybox:latest"
command: "/run.sh"
files: "/run.sh": """
#!/bin/sh
cat > /run/secrets/output << EOF
services: default: {
secrets: ["asecret"]
consumer: {
permissions: rules: [{
verbs:["get"]
apiGroups: [""]
resources: ["secrets"]
resourceNames: ["foo"]
}]
env: foo: "secret://asecret/env"
files: "/secret-file": "secret://asecret/file"
}
}
EOF
"""
}
2 changes: 1 addition & 1 deletion integration/run/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func TestServiceConsumer(t *testing.T) {
},
Spec: v1.AppInstanceSpec{
Image: image.ID,
Permissions: []v1.Permissions{{
GrantedPermissions: []v1.Permissions{{
ServiceName: "producer.default",
Rules: []v1.PolicyRule{{
PolicyRule: rbacv1.PolicyRule{
Expand Down
5 changes: 3 additions & 2 deletions pkg/apis/api.acorn.io/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ type ImageDetails struct {
AppImage v1.AppImage `json:"appImage,omitempty"`
AppSpec *v1.AppSpec `json:"appSpec,omitempty"`
Params *v1.ParamSpec `json:"params,omitempty"`
Permissions []v1.Permissions `json:"permissions,omitempty"`
Permissions []v1.Permissions `json:"permissions,omitempty"` // Permissions requested by the image, including nested images
SignatureDigest string `json:"signatureDigest,omitempty"`
Readme string `json:"readme,omitempty"`
NestedImages []NestedImage `json:"nestedImages,omitempty"`
Expand All @@ -289,7 +289,8 @@ func (i ImageDetails) GetParseError() string {
return ""
}

func (i *ImageDetails) GetPermissions() (result []v1.Permissions) {
// GetAllImagesRequestedPermissions returns the list of all permissions from the image and its nested images.
func (i *ImageDetails) GetAllImagesRequestedPermissions() (result []v1.Permissions) {
result = append(result, i.Permissions...)
for _, nested := range i.NestedImages {
result = append(result, nested.Permissions...)
Expand Down
Loading