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

Commit

Permalink
change: move actual permissions to .status + apply IRAs to consumed p…
Browse files Browse the repository at this point in the history
…ermissions (#2226)

- The actually granted and authorized permissions for every app will now go to AppInstances' `.status.permissions` field and contain **only** those permissions required for this specific AppInstance, **not** for nested Acorns
- Permissions are promoted from `.spec` to `.status` along with the staged AppImage when all checks (including ImageRoleAuthorizations) pass
- Permissions consumed from nested Services are added after all that and thus we have a new handler for them that only allows the consuming App to run if it's authorized to hold the permissions consumed from the producing service
  - Those will be merged into the `.status.permissions` field such that this field reflects truly all permissions given to that AppInstance
  • Loading branch information
iwilltry42 authored Oct 16, 2023
1 parent 0db995b commit 14748bc
Show file tree
Hide file tree
Showing 47 changed files with 1,182 additions and 146 deletions.
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

0 comments on commit 14748bc

Please sign in to comment.