mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-07 12:37:41 +01:00
s3: require their own actions to change a bucket's CORS or versioning (#12433)
* s3: require their own actions to change a bucket's CORS or versioning PUT/DELETE ?cors and PUT ?versioning were authorized as Write, so an identity granted only object writes on a bucket could replace its CORS rules or suspend versioning. They now need PutBucketCors, DeleteBucketCors and PutBucketVersioning (or Admin), and inline policies granting s3:PutBucketCors, s3:DeleteBucketCors or s3:PutBucketVersioning map to those actions instead of Write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3: test that object Write cannot change bucket CORS or versioning Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * admin: offer the bucket CORS and versioning permissions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3: test each bucket config action with its own identity Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3a579ece3b
commit
3e64326f2b
7 files changed
+109
-7
No files matched your search
@@ -588,3 +588,80 @@ func TestRouting_BucketLifecycleWritesNeedAdmin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouting_BucketCorsAndVersioningWritesNeedTheirActions(t *testing.T) {
|
||||
const bucket = "config-bucket"
|
||||
configFile := filepath.Join(t.TempDir(), "s3.json")
|
||||
require.NoError(t, os.WriteFile(configFile, []byte(`{
|
||||
"identities": [
|
||||
{"name":"writer","credentials":[{"accessKey":"WRITERKEY","secretKey":"writersecret"}],"actions":["Read:`+bucket+`","Write:`+bucket+`","List:`+bucket+`","Tagging:`+bucket+`"]},
|
||||
{"name":"bucketadmin","credentials":[{"accessKey":"BUCKETADMINKEY","secretKey":"bucketadminsecret"}],"actions":["Admin:`+bucket+`"]},
|
||||
{"name":"corsputter","credentials":[{"accessKey":"CORSPUTKEY","secretKey":"corsputsecret"}],"actions":["PutBucketCors:`+bucket+`"]},
|
||||
{"name":"corsdeleter","credentials":[{"accessKey":"CORSDELKEY","secretKey":"corsdelsecret"}],"actions":["DeleteBucketCors:`+bucket+`"]},
|
||||
{"name":"versioner","credentials":[{"accessKey":"VERSIONERKEY","secretKey":"versionersecret"}],"actions":["PutBucketVersioning:`+bucket+`"]},
|
||||
{"name":"policyuser","credentials":[{"accessKey":"POLICYUSERKEY","secretKey":"policyusersecret"}],"policyNames":["ConfigPolicy"]}
|
||||
],
|
||||
"policies": [
|
||||
{"name":"ConfigPolicy","content":"{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"s3:PutBucketCors\",\"s3:DeleteBucketCors\",\"s3:PutBucketVersioning\"],\"Resource\":[\"arn:aws:s3:::`+bucket+`\"]}]}"}
|
||||
]
|
||||
}`), 0600))
|
||||
|
||||
s3a := setupRoutingTestServer(t)
|
||||
s3a.iam = NewIdentityAccessManagementWithStore(&S3ApiServerOption{Config: configFile}, nil, "memory")
|
||||
require.True(t, s3a.iam.isEnabled())
|
||||
s3a.cb = &CircuitBreaker{s3a: s3a}
|
||||
s3a.bucketConfigCache = NewBucketConfigCache(time.Minute)
|
||||
s3a.bucketConfigCache.SetNegativeCache(bucket)
|
||||
router := mux.NewRouter()
|
||||
s3a.registerRouter(router)
|
||||
|
||||
send := func(accessKey, secretKey, method, query, body string) *httptest.ResponseRecorder {
|
||||
req := httptest.NewRequest(method, "http://localhost:8333/"+bucket+"?"+query, strings.NewReader(body))
|
||||
creds := credentials.NewStaticCredentials(accessKey, secretKey, "")
|
||||
_, err := v4.NewSigner(creds).Sign(req, strings.NewReader(body), "s3", "us-east-1", time.Now())
|
||||
require.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
router.ServeHTTP(rr, req)
|
||||
return rr
|
||||
}
|
||||
|
||||
const corsXML = `<CORSConfiguration><CORSRule><AllowedOrigin>*</AllowedOrigin><AllowedMethod>GET</AllowedMethod></CORSRule></CORSConfiguration>`
|
||||
const versioningXML = `<VersioningConfiguration><Status>Suspended</Status></VersioningConfiguration>`
|
||||
singleActionKeys := map[string]string{"CORSPUTKEY": "corsputsecret", "CORSDELKEY": "corsdelsecret", "VERSIONERKEY": "versionersecret"}
|
||||
for _, tc := range []struct {
|
||||
method, query, body, allowedKey string
|
||||
}{
|
||||
{http.MethodPut, "cors", corsXML, "CORSPUTKEY"},
|
||||
{http.MethodDelete, "cors", "", "CORSDELKEY"},
|
||||
{http.MethodPut, "versioning", versioningXML, "VERSIONERKEY"},
|
||||
} {
|
||||
denied := [][2]string{{"WRITERKEY", "writersecret"}}
|
||||
allowed := [][2]string{{"BUCKETADMINKEY", "bucketadminsecret"}, {"POLICYUSERKEY", "policyusersecret"}}
|
||||
for key, secret := range singleActionKeys {
|
||||
if key == tc.allowedKey {
|
||||
allowed = append(allowed, [2]string{key, secret})
|
||||
} else {
|
||||
denied = append(denied, [2]string{key, secret})
|
||||
}
|
||||
}
|
||||
for _, key := range denied {
|
||||
rr := send(key[0], key[1], tc.method, tc.query, tc.body)
|
||||
assert.Equal(t, http.StatusForbidden, rr.Code, "%s %s ?%s", key[0], tc.method, tc.query)
|
||||
assert.Contains(t, rr.Body.String(), "AccessDenied", "%s %s ?%s", key[0], tc.method, tc.query)
|
||||
}
|
||||
for _, key := range allowed {
|
||||
rr := send(key[0], key[1], tc.method, tc.query, tc.body)
|
||||
assert.Equal(t, http.StatusNotFound, rr.Code, "%s %s ?%s", key[0], tc.method, tc.query)
|
||||
assert.Contains(t, rr.Body.String(), "NoSuchBucket", "%s %s ?%s", key[0], tc.method, tc.query)
|
||||
}
|
||||
}
|
||||
|
||||
for policyAction, identityAction := range map[string]string{
|
||||
"s3:PutBucketCors": s3_constants.ACTION_PUT_BUCKET_CORS,
|
||||
"s3:DeleteBucketCors": s3_constants.ACTION_DELETE_BUCKET_CORS,
|
||||
"s3:PutBucketVersioning": s3_constants.ACTION_PUT_BUCKET_VERSIONING,
|
||||
} {
|
||||
assert.Equal(t, identityAction, iamMapToStatementAction(policyAction), policyAction)
|
||||
assert.Equal(t, policyAction, "s3:"+iamMapToIdentitiesAction(identityAction), identityAction)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user