From b2f38200f77ab7d8ef9b78813f6c2a9bcada2085 Mon Sep 17 00:00:00 2001 From: adfost Date: Tue, 28 Dec 2021 18:21:29 -0800 Subject: [PATCH] Custom Policies for Buckets (#1332) * custom policies * fixing error * add formatting --- models/bucket.go | 3 + models/set_bucket_policy_request.go | 3 + .../BucketDetails/BucketSummaryPanel.tsx | 3 + .../Buckets/BucketDetails/SetAccessPolicy.tsx | 79 ++++++++++++++----- .../src/screens/Console/Buckets/types.tsx | 1 + restapi/embedded_spec.go | 12 +++ restapi/user_buckets.go | 39 +++++---- restapi/user_buckets_test.go | 18 ++--- swagger-console.yml | 4 + 9 files changed, 116 insertions(+), 46 deletions(-) diff --git a/models/bucket.go b/models/bucket.go index 6486ce338..d89149c82 100644 --- a/models/bucket.go +++ b/models/bucket.go @@ -43,6 +43,9 @@ type Bucket struct { // creation date CreationDate string `json:"creation_date,omitempty"` + // definition + Definition string `json:"definition,omitempty"` + // details Details *BucketDetails `json:"details,omitempty"` diff --git a/models/set_bucket_policy_request.go b/models/set_bucket_policy_request.go index 87ec9cb89..446299b2c 100644 --- a/models/set_bucket_policy_request.go +++ b/models/set_bucket_policy_request.go @@ -39,6 +39,9 @@ type SetBucketPolicyRequest struct { // access // Required: true Access *BucketAccess `json:"access"` + + // definition + Definition string `json:"definition,omitempty"` } // Validate validates this set bucket policy request diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketSummaryPanel.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketSummaryPanel.tsx index 9f517b06d..c8364d5c3 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketSummaryPanel.tsx +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketSummaryPanel.tsx @@ -178,9 +178,11 @@ const BucketSummary = ({ const bucketName = match.params["bucketName"]; let accessPolicy = "n/a"; + let policyDefinition = ""; if (bucketInfo !== null) { accessPolicy = bucketInfo.access; + policyDefinition = bucketInfo.definition; } const displayGetBucketObjectLockConfiguration = hasPermission(bucketName, [ @@ -485,6 +487,7 @@ const BucketSummary = ({ bucketName={bucketName} open={accessPolicyScreenOpen} actualPolicy={accessPolicy} + actualDefinition={policyDefinition} closeModalAndRefresh={closeSetAccessPolicy} /> )} diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/SetAccessPolicy.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/SetAccessPolicy.tsx index 73910529d..91ef7c94c 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/SetAccessPolicy.tsx +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/SetAccessPolicy.tsx @@ -31,18 +31,37 @@ import api from "../../../../common/api"; import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper"; import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper"; import { ChangeAccessPolicyIcon } from "../../../../icons"; +import CodeMirrorWrapper from "../../Common/FormComponents/CodeMirrorWrapper/CodeMirrorWrapper"; const styles = (theme: Theme) => createStyles({ + codeMirrorContainer: { + marginBottom: 20, + paddingLeft: 15, + "&:nth-child(2) .MuiGrid-root:nth-child(3)": { + border: "1px solid #EAEAEA", + }, + "& label": { + marginBottom: ".5rem", + }, + "& label + div": { + display: "none", + }, + }, ...modalStyleUtils, ...spacingUtils, }); +createStyles({ + ...modalStyleUtils, + ...spacingUtils, +}); interface ISetAccessPolicyProps { classes: any; open: boolean; bucketName: string; actualPolicy: string; + actualDefinition: string; closeModalAndRefresh: () => void; setModalErrorSnackMessage: typeof setModalErrorSnackMessage; } @@ -52,11 +71,13 @@ const SetAccessPolicy = ({ open, bucketName, actualPolicy, + actualDefinition, closeModalAndRefresh, setModalErrorSnackMessage, }: ISetAccessPolicyProps) => { const [addLoading, setAddLoading] = useState(false); const [accessPolicy, setAccessPolicy] = useState(""); + const [policyDefinition, setPolicyDefinition] = useState(""); const addRecord = (event: React.FormEvent) => { event.preventDefault(); if (addLoading) { @@ -66,6 +87,7 @@ const SetAccessPolicy = ({ api .invoke("PUT", `/api/v1/buckets/${bucketName}/set-policy`, { access: accessPolicy, + definition: policyDefinition, }) .then((res) => { setAddLoading(false); @@ -79,7 +101,12 @@ const SetAccessPolicy = ({ useEffect(() => { setAccessPolicy(actualPolicy); - }, [setAccessPolicy, actualPolicy]); + setPolicyDefinition( + actualDefinition + ? JSON.stringify(JSON.parse(actualDefinition), null, 4) + : "" + ); + }, [setAccessPolicy, actualPolicy, setPolicyDefinition, actualDefinition]); return ( - - ) => { - setAccessPolicy(e.target.value as string); - }} - options={[ - { value: "PRIVATE", label: "Private" }, - { value: "PUBLIC", label: "Public" }, - ]} - /> + + + ) => { + setAccessPolicy(e.target.value as string); + }} + options={[ + { value: "PRIVATE", label: "Private" }, + { value: "PUBLIC", label: "Public" }, + { value: "CUSTOM", label: "Custom" }, + ]} + /> + + {accessPolicy === "CUSTOM" && ( + + { + setPolicyDefinition(value); + }} + /> + + )} diff --git a/portal-ui/src/screens/Console/Buckets/types.tsx b/portal-ui/src/screens/Console/Buckets/types.tsx index e91d05d82..f88693ae9 100644 --- a/portal-ui/src/screens/Console/Buckets/types.tsx +++ b/portal-ui/src/screens/Console/Buckets/types.tsx @@ -41,6 +41,7 @@ export interface Details { export interface BucketInfo { name: string; access: string; + definition: string; } export interface BucketList { diff --git a/restapi/embedded_spec.go b/restapi/embedded_spec.go index 982eb2bb0..0e8ee80c0 100644 --- a/restapi/embedded_spec.go +++ b/restapi/embedded_spec.go @@ -3735,6 +3735,9 @@ func init() { "creation_date": { "type": "string" }, + "definition": { + "type": "string" + }, "details": { "type": "object", "properties": { @@ -5294,6 +5297,9 @@ func init() { "properties": { "access": { "$ref": "#/definitions/bucketAccess" + }, + "definition": { + "type": "string" } } }, @@ -9626,6 +9632,9 @@ func init() { "creation_date": { "type": "string" }, + "definition": { + "type": "string" + }, "details": { "type": "object", "properties": { @@ -11185,6 +11194,9 @@ func init() { "properties": { "access": { "$ref": "#/definitions/bucketAccess" + }, + "definition": { + "type": "string" } } }, diff --git a/restapi/user_buckets.go b/restapi/user_buckets.go index 4ced01b5a..3d7f4a9c7 100644 --- a/restapi/user_buckets.go +++ b/restapi/user_buckets.go @@ -436,7 +436,7 @@ func getMakeBucketResponse(session *models.Principal, br *models.MakeBucketReque } // setBucketAccessPolicy set the access permissions on an existing bucket. -func setBucketAccessPolicy(ctx context.Context, client MinioClient, bucketName string, access models.BucketAccess) error { +func setBucketAccessPolicy(ctx context.Context, client MinioClient, bucketName string, access models.BucketAccess, policyDefinition string) error { if strings.TrimSpace(bucketName) == "" { return fmt.Errorf("error: bucket name not present") } @@ -444,18 +444,21 @@ func setBucketAccessPolicy(ctx context.Context, client MinioClient, bucketName s return fmt.Errorf("error: bucket access not present") } // Prepare policyJSON corresponding to the access type - if access != models.BucketAccessPRIVATE && access != models.BucketAccessPUBLIC { + if access != models.BucketAccessPRIVATE && access != models.BucketAccessPUBLIC && access != models.BucketAccessCUSTOM { return fmt.Errorf("access: `%s` not supported", access) } - bucketPolicy := consoleAccess2policyAccess(access) bucketAccessPolicy := policy.BucketAccessPolicy{Version: minioIAMPolicy.DefaultVersion} + if access == models.BucketAccessCUSTOM { + err := client.setBucketPolicyWithContext(ctx, bucketName, policyDefinition) + if err != nil { + return err + } + return nil + } + bucketPolicy := consoleAccess2policyAccess(access) bucketAccessPolicy.Statements = policy.SetPolicy(bucketAccessPolicy.Statements, bucketPolicy, bucketName, "") - // implemented like minio/mc/ s3Client.SetAccess() - if len(bucketAccessPolicy.Statements) == 0 { - return client.setBucketPolicyWithContext(ctx, bucketName, "") - } policyJSON, err := json.Marshal(bucketAccessPolicy) if err != nil { return err @@ -469,6 +472,7 @@ func getBucketSetPolicyResponse(session *models.Principal, bucketName string, re ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) defer cancel() + // get updated bucket details and return it mClient, err := newMinioClient(session) if err != nil { return nil, prepareError(err) @@ -476,11 +480,11 @@ func getBucketSetPolicyResponse(session *models.Principal, bucketName string, re // create a minioClient interface implementation // defining the client to be used minioClient := minioClient{client: mClient} - // set bucket access policy - if err := setBucketAccessPolicy(ctx, minioClient, bucketName, *req.Access); err != nil { + + if err := setBucketAccessPolicy(ctx, minioClient, bucketName, *req.Access, req.Definition); err != nil { return nil, prepareError(err) } - // get updated bucket details and return it + // set bucket access policy bucket, err := getBucketInfo(ctx, minioClient, bucketName) if err != nil { return nil, prepareError(err) @@ -548,19 +552,19 @@ func getBucketInfo(ctx context.Context, client MinioClient, bucketName string) ( LogError("error getting bucket policy: %v", err) } - var policyAccess policy.BucketPolicy if policyStr == "" { - policyAccess = policy.BucketPolicyNone + bucketAccess = models.BucketAccessPRIVATE } else { var p policy.BucketAccessPolicy if err = json.Unmarshal([]byte(policyStr), &p); err != nil { return nil, err } - policyAccess = policy.GetPolicy(p.Statements, bucketName, "") - } - bucketAccess = policyAccess2consoleAccess(policyAccess) - if bucketAccess == models.BucketAccessPRIVATE && policyStr != "" { - bucketAccess = models.BucketAccessCUSTOM + policyAccess := policy.GetPolicy(p.Statements, bucketName, "") + if len(p.Statements) > 0 && policyAccess == policy.BucketPolicyNone { + bucketAccess = models.BucketAccessCUSTOM + } else { + bucketAccess = policyAccess2consoleAccess(policyAccess) + } } bucketTags, err := client.GetBucketTagging(ctx, bucketName) if err != nil { @@ -574,6 +578,7 @@ func getBucketInfo(ctx context.Context, client MinioClient, bucketName string) ( return &models.Bucket{ Name: &bucketName, Access: &bucketAccess, + Definition: policyStr, CreationDate: "", // to be implemented Size: 0, // to be implemented Details: bucketDetails, diff --git a/restapi/user_buckets_test.go b/restapi/user_buckets_test.go index 26127412a..ecb140cdd 100644 --- a/restapi/user_buckets_test.go +++ b/restapi/user_buckets_test.go @@ -338,8 +338,8 @@ func TestBucketInfo(t *testing.T) { assert.Equal(outputExpected.CreationDate, bucketInfo.CreationDate) assert.Equal(outputExpected.Size, bucketInfo.Size) - // Test-3: getBucketInfo() get a bucket with CUSTOM access - // if bucket has a custom policy set it should return CUSTOM + // Test-3: getBucketInfo() get a bucket with PRIVATE access + // if bucket has a null statement, the the bucket is PRIVATE mockPolicy = "{\"Version\":\"2012-10-17\",\"Statement\":[]}" minioGetBucketPolicyMock = func(bucketName string) (string, error) { return mockPolicy, nil @@ -347,7 +347,7 @@ func TestBucketInfo(t *testing.T) { bucketToSet = "csbucket" outputExpected = &models.Bucket{ Name: swag.String(bucketToSet), - Access: models.NewBucketAccess(models.BucketAccessCUSTOM), + Access: models.NewBucketAccess(models.BucketAccessPRIVATE), CreationDate: "", // to be implemented Size: 0, // to be implemented } @@ -393,27 +393,27 @@ func TestSetBucketAccess(t *testing.T) { minioSetBucketPolicyWithContextMock = func(ctx context.Context, bucketName, policy string) error { return nil } - if err := setBucketAccessPolicy(ctx, minClient, "bucktest1", models.BucketAccessPUBLIC); err != nil { + if err := setBucketAccessPolicy(ctx, minClient, "bucktest1", models.BucketAccessPUBLIC, ""); err != nil { t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) } // Test-2: setBucketAccessPolicy() set private access - if err := setBucketAccessPolicy(ctx, minClient, "bucktest1", models.BucketAccessPRIVATE); err != nil { + if err := setBucketAccessPolicy(ctx, minClient, "bucktest1", models.BucketAccessPRIVATE, ""); err != nil { t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) } // Test-3: setBucketAccessPolicy() set invalid access, expected error - if err := setBucketAccessPolicy(ctx, minClient, "bucktest1", "other"); assert.Error(err) { + if err := setBucketAccessPolicy(ctx, minClient, "bucktest1", "other", ""); assert.Error(err) { assert.Equal("access: `other` not supported", err.Error()) } // Test-4: setBucketAccessPolicy() set access on empty bucket name, expected error - if err := setBucketAccessPolicy(ctx, minClient, "", models.BucketAccessPRIVATE); assert.Error(err) { + if err := setBucketAccessPolicy(ctx, minClient, "", models.BucketAccessPRIVATE, ""); assert.Error(err) { assert.Equal("error: bucket name not present", err.Error()) } // Test-5: setBucketAccessPolicy() set empty access on bucket, expected error - if err := setBucketAccessPolicy(ctx, minClient, "bucktest1", ""); assert.Error(err) { + if err := setBucketAccessPolicy(ctx, minClient, "bucktest1", "", ""); assert.Error(err) { assert.Equal("error: bucket access not present", err.Error()) } @@ -421,7 +421,7 @@ func TestSetBucketAccess(t *testing.T) { minioSetBucketPolicyWithContextMock = func(ctx context.Context, bucketName, policy string) error { return errors.New("error") } - if err := setBucketAccessPolicy(ctx, minClient, "bucktest1", models.BucketAccessPUBLIC); assert.Error(err) { + if err := setBucketAccessPolicy(ctx, minClient, "bucktest1", models.BucketAccessPUBLIC, ""); assert.Error(err) { assert.Equal("error", err.Error()) } diff --git a/swagger-console.yml b/swagger-console.yml index cdf167cc2..4302a4bb6 100644 --- a/swagger-console.yml +++ b/swagger-console.yml @@ -2417,6 +2417,8 @@ definitions: format: int64 access: $ref: "#/definitions/bucketAccess" + definition: + type: string rw_access: type: object properties: @@ -3044,6 +3046,8 @@ definitions: properties: access: $ref: "#/definitions/bucketAccess" + definition: + type: string bucketQuota: type: object properties: