This repository has been archived by the owner on Mar 16, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 101
/
permissions_check.go
180 lines (156 loc) · 6.71 KB
/
permissions_check.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
package permissions
import (
"errors"
"fmt"
"strings"
"github.com/acorn-io/baaah/pkg/router"
"github.com/acorn-io/baaah/pkg/uncached"
apiv1 "github.com/acorn-io/runtime/pkg/apis/api.acorn.io/v1"
v1 "github.com/acorn-io/runtime/pkg/apis/internal.acorn.io/v1"
"github.com/acorn-io/runtime/pkg/config"
"github.com/acorn-io/runtime/pkg/imagerules"
"github.com/acorn-io/runtime/pkg/labels"
"github.com/acorn-io/runtime/pkg/profiles"
"github.com/acorn-io/runtime/pkg/tags"
"github.com/acorn-io/z"
"github.com/google/go-containerregistry/pkg/name"
"golang.org/x/exp/maps"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/strings/slices"
)
// CopyPromoteStagedAppImage copies the staged app image to the app image if
// - the staged app image is set
// - the permissions have been checked
// - there are no missing permissions
// - there are no image permissions denied (if ImageRoleAuthorizations are enabled)
// - the image is allowed by the image allow rules (if enabled)
func CopyPromoteStagedAppImage(req router.Request, _ router.Response) error {
app := req.Object.(*v1.AppInstance)
if app.Status.Staged.AppImage.ID != "" &&
app.Status.Staged.PermissionsChecked &&
len(app.Status.Staged.PermissionsMissing) == 0 &&
len(app.Status.Staged.ImagePermissionsDenied) == 0 &&
z.Dereference(app.Status.Staged.ImageAllowed) {
app.Status.AppImage = app.Status.Staged.AppImage
app.Status.Permissions = app.Status.Staged.AppScopedPermissions
}
return nil
}
/*
CheckPermissions checks various things related to permissions
a) if the image is allowed by the image allow rules (if enabled)
b) [if ImageRoleAuthorizations are enabled] if the authorized permissions for the appImage cover the permissions granted explicitly by the user or implicitly to the image (Authorized >= Granted)
c) if the permissions granted by the user or implicitly to the image cover the permissions requested by the app (Granted >= Requested)
*Note*: One thing that we do not cover here is permissions consumed from a service. Since that's dynamic and we cannot know the generated permissions at this point,
the IRA check (if enabled) will happen later, just before the actual Kubernetes resources get applied (once the service instances are ready).
*/
func CheckPermissions(req router.Request, _ router.Response) error {
app := req.Object.(*v1.AppInstance)
// Reset staged status fields if the respective feature is disabled
iraEnabled, err := config.GetFeature(req.Ctx, req.Client, profiles.FeatureImageRoleAuthorizations)
if err != nil {
return err
}
if !iraEnabled {
app.Status.Staged.ImagePermissionsDenied = nil
}
// Early exit
if app.Status.Staged.AppImage.ID == "" ||
app.Status.Staged.AppImage.Digest == app.Status.AppImage.Digest && app.Status.Staged.PermissionsObservedGeneration == app.Generation {
// IAR disabled? Allow the Image if we're not re-checking permissions
if enabled, err := config.GetFeature(req.Ctx, req.Client, profiles.FeatureImageAllowRules); err != nil {
return err
} else if !enabled {
app.Status.Staged.ImageAllowed = z.Pointer(true)
}
return nil
}
if err := checkImageAllowed(req.Ctx, req.Client, app); err != nil {
return err
}
var (
appImage = app.Status.Staged.AppImage
imageName = appImage.ID
details = &apiv1.ImageDetails{
DeployArgs: app.Spec.DeployArgs,
Profiles: app.Spec.GetProfiles(app.Status.GetDevMode()),
IncludeNested: true,
}
)
if !tags.IsLocalReference(imageName) {
ref, err := name.ParseReference(imageName)
if err != nil {
return err
}
imageName = ref.Context().Digest(appImage.Digest).String()
}
err = req.Client.SubResource("details").Create(req.Ctx, uncached.Get(&apiv1.Image{
ObjectMeta: metav1.ObjectMeta{
Name: strings.ReplaceAll(imageName, "/", "+"),
Namespace: app.Namespace,
},
}), details)
if err != nil {
return err
} else if details.GetParseError() != "" {
return errors.New(details.GetParseError())
}
if details.AppImage.Digest != appImage.Digest {
return fmt.Errorf("failed to lookup image [%s], resolved to digest [%s] but expected [%s]", imageName,
details.AppImage.Digest, appImage.Digest)
}
scopedGrantedPerms := GetAppScopedPermissions(app, details.AppSpec)
app.Status.Staged.AppScopedPermissions = scopedGrantedPerms
// If iraEnabled, check if the Acorn images are authorized to request the defined permissions.
if iraEnabled {
imageName := appImage.Name
// E.g. for child Acorns, the appImage.Name is the image ID, but we need the original image name (with registry/repo)
// to check for the signatures
if oi, ok := app.GetAnnotations()[labels.AcornOriginalImage]; ok {
imageName = oi
}
authzPerms, err := imagerules.GetAuthorizedPermissions(req.Ctx, req.Client, app.Namespace, imageName, appImage.Digest)
if err != nil {
return err
}
// Need to deepcopy here since otherwise we'd override the name in the original object which we still need for other authz checks
// For IRA Checks, we use the image name as the service name
copyWithName := func(perms []v1.Permissions, name string) []v1.Permissions {
nperms := make([]v1.Permissions, len(perms))
for i := range perms {
nperms[i] = perms[i].DeepCopy().Get()
nperms[i].ServiceName = name
}
return nperms
}
var deniedPerms []v1.Permissions
for _, p := range scopedGrantedPerms {
if d, granted := v1.GrantsAll(app.Namespace, []v1.Permissions{p}, copyWithName(authzPerms, p.ServiceName)); !granted {
deniedPerms = append(deniedPerms, d...)
}
}
app.Status.Staged.ImagePermissionsDenied = deniedPerms
}
// This is checking if the user granted all permissions that the app requires
missing, _ := v1.GrantsAll(app.Namespace, details.GetAllImagesRequestedPermissions(), app.Spec.GetGrantedPermissions()) // Check: Granted Permissions is a superset of the requested permissions -> this includes nested images
app.Status.Staged.PermissionsObservedGeneration = app.Generation
app.Status.Staged.PermissionsChecked = true
app.Status.Staged.PermissionsMissing = missing
return nil
}
func GetAppScopedPermissions(app *v1.AppInstance, appSpec *v1.AppSpec) []v1.Permissions {
// ServiceNames of the current app level (i.e. not nested Acorns/Services)
svcnames := maps.Keys(appSpec.Containers)
svcnames = append(svcnames, maps.Keys(appSpec.Functions)...)
svcnames = append(svcnames, maps.Keys(appSpec.Jobs)...)
svcnames = append(svcnames, maps.Keys(appSpec.Services)...)
// Only consider the scope of the current app level (i.e. not nested Acorns/Services)
grantedPerms := app.Spec.GetGrantedPermissions()
scopedGrantedPerms := []v1.Permissions{}
for i, p := range grantedPerms {
if slices.Contains(svcnames, p.ServiceName) {
scopedGrantedPerms = append(scopedGrantedPerms, grantedPerms[i])
}
}
return scopedGrantedPerms
}