Skip to content

Commit da4eb61

Browse files
Support updating Bucket ACLs (#31)
S3 Buckets support Bucket ACLs in 3 ways: - Defining a "canned" ACL - through the `ACL` Spec field - Defining grant headers - through the `Grant*` Spec fields - Defining a list of grant objects - not supported through `CreateBucket`, and not necessary, so left out Each of these types is mutually exclusive, and therefore the user is expected to provide at most 1 of the 3. The existing code supports adding ACLs during the `CreateBucket` call, but the `ReadOne` does not provide these values. Instead, to describe these values, we must make a call to [`GetBucketAcl`](https://docs.aws.amazon.com/sdk-for-go/api/service/s3/#S3.GetBucketAcl). `GetBucketAcl` returns the ACLs as a list of grant objects. To compare these to the spec fields, we have to write custom code to convert from grant object to spec field type. This comparison is outlined in aws-controllers-k8s/community#895 , and this PR will close this issue. The order of precedence for diffing is canned ACL over `grant` types. For canned ACLs: A canned ACL is essentially a preset list of grants. Therefore, we can get back to the original canned ACL by comparing the list of grants to the known list of presets and their corresponding settings. However, multiple canned ACLs share the same presets. That is, `private`, `bucket-owner-read` and `bucket-owner-full-control` all return the same grant list. To compare this with `Spec.ACL` (a `string` field), I join the list of possible ACLs into a single string (delimited with `"|"`). In the pre-compare hook, I split the string and compare each of the possibilities to `Spec.ACL`. If any matches, I set that as the `latest.Spec.ACL`, otherwise I set it to `nil`. For grant headers: There is a many-to-one mapping of grants to grant headers, grouped by `Permission`. We are able to form the grant headers by aggregating by `Permission` and forming the header string by using `ID`, if the `Type` is `CanonicalUser`, or `URI`, if the `Type` is `Group`. Diff works as normal after that.
1 parent 14897c8 commit da4eb61

File tree

4 files changed

+539
-0
lines changed

4 files changed

+539
-0
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/aws/aws-sdk-go v1.37.10
88
github.com/go-logr/logr v0.1.0
99
github.com/spf13/pflag v1.0.5
10+
github.com/stretchr/testify v1.5.1
1011
k8s.io/api v0.18.2
1112
k8s.io/apimachinery v0.18.6
1213
k8s.io/client-go v0.18.2

pkg/resource/bucket/acl_custom.go

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://aws.amazon.com/apache2.0/
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
package bucket
15+
16+
import (
17+
"fmt"
18+
"strings"
19+
20+
svcsdk "github.com/aws/aws-sdk-go/service/s3"
21+
)
22+
23+
// Only some of these exist in the SDK, so duplicating them all here
24+
var (
25+
CannedACLPrivate = "private"
26+
CannedPublicRead = "public-read"
27+
CannedPublicReadWrite = "public-read-write"
28+
CannedAWSExecRead = "aws-exec-read"
29+
CannedAuthenticatedRead = "authenticated-read"
30+
CannedBucketOwnerRead = "bucket-owner-read"
31+
CannedBucketOwnerFullControl = "bucket-owner-full-control"
32+
CannedLogDeliveryWrite = "log-delivery-write"
33+
)
34+
35+
var (
36+
GranteeZATeamID = "6aa5a366c34c1cbe25dc49211496e913e0351eb0e8c37aa3477e40942ec6b97c"
37+
GranteeLogDeliveryURI = "http://acs.amazonaws.com/groups/s3/LogDelivery"
38+
GranteeAllUsersURI = "http://acs.amazonaws.com/groups/global/AllUsers"
39+
GranteeAuthenticatedUsersURI = "http://acs.amazonaws.com/groups/global/AuthenticatedUsers"
40+
)
41+
42+
var (
43+
HeaderUserIDFormat = "id=%s"
44+
HeaderURIFormat = "uri=%s"
45+
)
46+
47+
type aclGrantHeaders struct {
48+
FullControl string
49+
Read string
50+
ReadACP string
51+
Write string
52+
WriteACP string
53+
}
54+
55+
// hasOwnerFullControl returns true if any of the grants matches the owner
56+
// and has full control permissions.
57+
func hasOwnerFullControl(owner *svcsdk.Owner, grants []*svcsdk.Grant) bool {
58+
for _, grant := range grants {
59+
if grant.Grantee == nil ||
60+
grant.Grantee.ID == nil ||
61+
*grant.Grantee.ID != *owner.ID {
62+
continue
63+
}
64+
65+
return *grant.Permission == svcsdk.PermissionFullControl
66+
}
67+
return false
68+
}
69+
70+
// grantsContainPermission will return true if any of the grants have the
71+
// permission matching the one supplied.
72+
func grantsContainPermission(permission string, grants []*svcsdk.Grant) bool {
73+
for _, grant := range grants {
74+
if *grant.Permission == permission {
75+
return true
76+
}
77+
}
78+
return false
79+
}
80+
81+
// getGrantsByGroupURI searches a list of ACL grants for any that have a
82+
// group type grantee with the given URI.
83+
func getGrantsByGroupURI(uri string, grants []*svcsdk.Grant) []*svcsdk.Grant {
84+
matching := []*svcsdk.Grant{}
85+
86+
for _, grant := range grants {
87+
if grant.Grantee == nil {
88+
continue
89+
}
90+
91+
if *grant.Grantee.Type != svcsdk.TypeGroup {
92+
continue
93+
}
94+
95+
if *grant.Grantee.URI == uri {
96+
matching = append(matching, grant)
97+
}
98+
}
99+
return matching
100+
}
101+
102+
// getGrantsByCanonicalUserID searches a list of ACL grants for any that have a
103+
// canonical user type grantee with the given ID.
104+
func getGrantsByCanonicalUserID(id string, grants []*svcsdk.Grant) []*svcsdk.Grant {
105+
matching := []*svcsdk.Grant{}
106+
107+
for _, grant := range grants {
108+
if grant.Grantee == nil {
109+
continue
110+
}
111+
112+
if *grant.Grantee.Type != svcsdk.TypeCanonicalUser {
113+
continue
114+
}
115+
116+
if *grant.Grantee.ID == id {
117+
matching = append(matching, grant)
118+
}
119+
}
120+
return matching
121+
}
122+
123+
// getGrantsByPermission searches a list of ACL grants for any that have the
124+
// given permission.
125+
func getGrantsByPermission(permission string, grants []*svcsdk.Grant) []*svcsdk.Grant {
126+
matching := []*svcsdk.Grant{}
127+
128+
for _, grant := range grants {
129+
if *grant.Permission == permission {
130+
matching = append(matching, grant)
131+
}
132+
}
133+
return matching
134+
}
135+
136+
// formGrantHeader will form a grant header string from a list of grants
137+
func formGrantHeader(grants []*svcsdk.Grant) string {
138+
headers := []string{}
139+
for _, grant := range grants {
140+
if grant.Grantee == nil {
141+
continue
142+
}
143+
144+
if *grant.Grantee.Type == svcsdk.TypeGroup {
145+
headers = append(headers, fmt.Sprintf(HeaderURIFormat, *grant.Grantee.URI))
146+
}
147+
if *grant.Grantee.Type == svcsdk.TypeCanonicalUser {
148+
headers = append(headers, fmt.Sprintf(HeaderUserIDFormat, *grant.Grantee.ID))
149+
}
150+
}
151+
return strings.Join(headers, ",")
152+
}
153+
154+
// GetHeadersFromGrants will return a list of grant headers from grants
155+
func GetHeadersFromGrants(
156+
resp *svcsdk.GetBucketAclOutput,
157+
) aclGrantHeaders {
158+
headers := aclGrantHeaders{
159+
FullControl: formGrantHeader(getGrantsByPermission(svcsdk.PermissionFullControl, resp.Grants)),
160+
Read: formGrantHeader(getGrantsByPermission(svcsdk.PermissionRead, resp.Grants)),
161+
ReadACP: formGrantHeader(getGrantsByPermission(svcsdk.PermissionReadAcp, resp.Grants)),
162+
Write: formGrantHeader(getGrantsByPermission(svcsdk.PermissionWrite, resp.Grants)),
163+
WriteACP: formGrantHeader(getGrantsByPermission(svcsdk.PermissionWriteAcp, resp.Grants)),
164+
}
165+
166+
return headers
167+
}
168+
169+
// GetPossibleCannedACLsFromGrants will return a list of canned ACLs that match
170+
// the list of grants. This method will return nil if the grants did not match
171+
// any canned ACLs.
172+
func GetPossibleCannedACLsFromGrants(
173+
resp *svcsdk.GetBucketAclOutput,
174+
) []string {
175+
owner := resp.Owner
176+
grants := resp.Grants
177+
178+
// All canned ACLs include a grant with owner full control
179+
if !hasOwnerFullControl(owner, grants) {
180+
return []string{}
181+
}
182+
183+
switch len(grants) {
184+
case 1:
185+
return []string{CannedACLPrivate, CannedBucketOwnerRead, CannedBucketOwnerFullControl}
186+
case 2:
187+
execTeamGrant := getGrantsByCanonicalUserID(GranteeZATeamID, grants)
188+
if grantsContainPermission(svcsdk.PermissionRead, execTeamGrant) {
189+
return []string{CannedAWSExecRead}
190+
}
191+
192+
allUsersGrants := getGrantsByGroupURI(GranteeAllUsersURI, grants)
193+
if grantsContainPermission(svcsdk.PermissionRead, allUsersGrants) {
194+
return []string{CannedPublicRead}
195+
}
196+
197+
authenticatedUsersGrants := getGrantsByGroupURI(GranteeAuthenticatedUsersURI, grants)
198+
if grantsContainPermission(svcsdk.PermissionRead, authenticatedUsersGrants) {
199+
return []string{CannedAuthenticatedRead}
200+
}
201+
case 3:
202+
logDeliveryGrants := getGrantsByGroupURI(GranteeLogDeliveryURI, grants)
203+
if grantsContainPermission(svcsdk.PermissionWrite, logDeliveryGrants) &&
204+
grantsContainPermission(svcsdk.PermissionReadAcp, logDeliveryGrants) {
205+
return []string{CannedLogDeliveryWrite}
206+
}
207+
208+
allUsersGrants := getGrantsByGroupURI(GranteeAllUsersURI, grants)
209+
if grantsContainPermission(svcsdk.PermissionRead, allUsersGrants) &&
210+
grantsContainPermission(svcsdk.PermissionWrite, allUsersGrants) {
211+
return []string{CannedPublicReadWrite}
212+
}
213+
}
214+
215+
return []string{}
216+
}

0 commit comments

Comments
 (0)