diff --git a/browser/app/js/browser/MainContent.js b/browser/app/js/browser/MainContent.js index 6fa052fc4..f8fff3fde 100644 --- a/browser/app/js/browser/MainContent.js +++ b/browser/app/js/browser/MainContent.js @@ -19,6 +19,7 @@ import MobileHeader from "./MobileHeader" import Header from "./Header" import ObjectsSection from "../objects/ObjectsSection" import MainActions from "./MainActions" +import BucketPolicyModal from "../buckets/BucketPolicyModal" import MakeBucketModal from "../buckets/MakeBucketModal" import UploadModal from "../uploads/UploadModal" import ObjectsBulkActions from "../objects/ObjectsBulkActions" @@ -30,6 +31,7 @@ export const MainContent = () => (
+ diff --git a/browser/app/js/buckets/Bucket.js b/browser/app/js/buckets/Bucket.js index bd05209b1..25cddf6d7 100644 --- a/browser/app/js/buckets/Bucket.js +++ b/browser/app/js/buckets/Bucket.js @@ -16,6 +16,7 @@ import React from "react" import classNames from "classnames" +import BucketDropdown from "./BucketDropdown" export const Bucket = ({ bucket, isActive, selectBucket }) => { return ( @@ -36,6 +37,7 @@ export const Bucket = ({ bucket, isActive, selectBucket }) => { > {bucket} + ) } diff --git a/browser/app/js/buckets/BucketDropdown.js b/browser/app/js/buckets/BucketDropdown.js new file mode 100644 index 000000000..3f7654df0 --- /dev/null +++ b/browser/app/js/buckets/BucketDropdown.js @@ -0,0 +1,92 @@ +/* + * Minio Cloud Storage (C) 2018 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from "react" +import classNames from "classnames" +import { connect } from "react-redux" +import * as actionsBuckets from "./actions" +import { getCurrentBucket } from "./selectors" +import Dropdown from "react-bootstrap/lib/Dropdown" + +export class BucketDropdown extends React.Component { + constructor(props) { + super(props) + this.state = { + showBucketDropdown: false + } + } + + toggleDropdown() { + if (this.state.showBucketDropdown) { + this.setState({ + showBucketDropdown: false + }) + } else { + this.setState({ + showBucketDropdown: true + }) + } + } + + render() { + const { bucket, showBucketPolicy, deleteBucket, currentBucket } = this.props + return ( + + + + + +
  • + { + e.stopPropagation() + this.toggleDropdown() + showBucketPolicy() + }} + > + Edit policy + +
  • +
  • + { + e.stopPropagation() + this.toggleDropdown() + deleteBucket(bucket) + }} + > + Delete + +
  • +
    +
    + ) + } +} + +const mapDispatchToProps = dispatch => { + return { + deleteBucket: bucket => dispatch(actionsBuckets.deleteBucket(bucket)), + showBucketPolicy: () => dispatch(actionsBuckets.showBucketPolicy()) + } +} + +export default connect(state => state, mapDispatchToProps)(BucketDropdown) diff --git a/browser/app/js/buckets/BucketList.js b/browser/app/js/buckets/BucketList.js index 303b7e73c..45e6cda14 100644 --- a/browser/app/js/buckets/BucketList.js +++ b/browser/app/js/buckets/BucketList.js @@ -28,7 +28,7 @@ export class BucketList extends React.Component { componentWillMount() { const { fetchBuckets, setBucketList, selectBucket } = this.props if (web.LoggedIn()) { - fetchBuckets() + fetchBuckets("list") } else { const { bucket, prefix } = pathSlice(history.location.pathname) if (bucket) { @@ -63,7 +63,7 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { - fetchBuckets: () => dispatch(actionsBuckets.fetchBuckets()), + fetchBuckets: action => dispatch(actionsBuckets.fetchBuckets(action)), setBucketList: buckets => dispatch(actionsBuckets.setList(buckets)), selectBucket: bucket => dispatch(actionsBuckets.selectBucket(bucket)) } diff --git a/browser/app/js/buckets/BucketPolicyModal.js b/browser/app/js/buckets/BucketPolicyModal.js new file mode 100644 index 000000000..4b7d8ece0 --- /dev/null +++ b/browser/app/js/buckets/BucketPolicyModal.js @@ -0,0 +1,61 @@ +/* + * Minio Cloud Storage (C) 2018 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from "react" +import { connect } from "react-redux" +import { Modal, ModalHeader } from "react-bootstrap" +import * as actionsBuckets from "./actions" +import PolicyInput from "./PolicyInput" +import Policy from "./Policy" + +export const BucketPolicyModal = ({ showBucketPolicy, currentBucket, hideBucketPolicy, policies }) => { + return ( + + + Bucket Policy ( + { currentBucket }) + + +
    + + { policies.map((policy, i) => + ) } +
    +
    + ) +} + +const mapStateToProps = state => { + return { + currentBucket: state.buckets.currentBucket, + showBucketPolicy: state.buckets.showBucketPolicy, + policies: state.buckets.policies + } +} + +const mapDispatchToProps = dispatch => { + return { + hideBucketPolicy: () => dispatch(actionsBuckets.hideBucketPolicy()) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(BucketPolicyModal) \ No newline at end of file diff --git a/browser/app/js/buckets/Policy.js b/browser/app/js/buckets/Policy.js new file mode 100644 index 000000000..2256a9ae5 --- /dev/null +++ b/browser/app/js/buckets/Policy.js @@ -0,0 +1,93 @@ +/* + * Minio Cloud Storage (C) 2018 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { READ_ONLY, WRITE_ONLY, READ_WRITE } from '../constants' + +import React from "react" +import { connect } from "react-redux" +import classnames from "classnames" +import * as actionsBuckets from "./actions" +import * as actionsAlert from "../alert/actions" +import web from "../web" + +export class Policy extends React.Component { + removePolicy(e) { + e.preventDefault() + const {currentBucket, prefix, fetchPolicies, showAlert} = this.props + web. + SetBucketPolicy({ + bucketName: currentBucket, + prefix: prefix, + policy: 'none' + }) + .then(() => { + fetchPolicies(currentBucket) + }) + .catch(e => showAlert('danger', e.message)) + } + + render() { + const {policy, prefix} = this.props + let newPrefix = prefix + + if (newPrefix === '') + newPrefix = '*' + + return ( +
    +
    + { newPrefix } +
    +
    + +
    +
    + +
    +
    + ) + } +} + +const mapStateToProps = state => { + return { + currentBucket: state.buckets.currentBucket + } +} + +const mapDispatchToProps = dispatch => { + return { + fetchPolicies: bucket => dispatch(actionsBuckets.fetchPolicies(bucket)), + showAlert: (type, message) => + dispatch(actionsAlert.set({ type: type, message: message })) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(Policy) \ No newline at end of file diff --git a/browser/app/js/buckets/PolicyInput.js b/browser/app/js/buckets/PolicyInput.js new file mode 100644 index 000000000..2ed103262 --- /dev/null +++ b/browser/app/js/buckets/PolicyInput.js @@ -0,0 +1,115 @@ +/* + * Minio Cloud Storage (C) 2018 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { READ_ONLY, WRITE_ONLY, READ_WRITE } from '../constants' + +import React from "react" +import { connect } from "react-redux" +import classnames from "classnames" +import * as actionsBuckets from "./actions" +import * as actionsAlert from "../alert/actions" +import web from "../web" + +export class PolicyInput extends React.Component { + componentDidMount() { + const { currentBucket, fetchPolicies } = this.props + fetchPolicies(currentBucket) + } + + componentWillUnmount() { + const { setPolicies } = this.props + setPolicies([]) + } + + handlePolicySubmit(e) { + e.preventDefault() + const { currentBucket, fetchPolicies, showAlert } = this.props + + if (this.prefix.value === "*") + this.prefix.value = "" + + let policyAlreadyExists = this.props.policies.some( + elem => this.prefix.value === elem.prefix && this.policy.value === elem.policy + ) + if (policyAlreadyExists) { + showAlert("danger", "Policy for this prefix already exists.") + return + } + + web. + SetBucketPolicy({ + bucketName: currentBucket, + prefix: this.prefix.value, + policy: this.policy.value + }) + .then(() => { + fetchPolicies(currentBucket) + this.prefix.value = '' + }) + .catch(e => showAlert("danger", e.message)) + } + + render() { + return ( +
    +
    + this.prefix = prefix } + className="form-control" + placeholder="Prefix" + /> +
    +
    + +
    +
    + +
    +
    + ) + } +} + +const mapStateToProps = state => { + return { + currentBucket: state.buckets.currentBucket, + policies: state.buckets.policies + } +} + +const mapDispatchToProps = dispatch => { + return { + fetchPolicies: bucket => dispatch(actionsBuckets.fetchPolicies(bucket)), + setPolicies: policies => dispatch(actionsBuckets.setPolicies(policies)), + showAlert: (type, message) => + dispatch(actionsAlert.set({ type: type, message: message })) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(PolicyInput) \ No newline at end of file diff --git a/browser/app/js/buckets/__tests__/BucketDropdown.test.js b/browser/app/js/buckets/__tests__/BucketDropdown.test.js new file mode 100644 index 000000000..6aeb5f03a --- /dev/null +++ b/browser/app/js/buckets/__tests__/BucketDropdown.test.js @@ -0,0 +1,62 @@ +/* + * Minio Cloud Storage (C) 2018 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from "react" +import { shallow, mount } from "enzyme" +import { BucketDropdown } from "../BucketDropdown" + +describe("BucketDropdown", () => { + it("should render without crashing", () => { + shallow() + }) + + it("should call toggleDropdown on dropdown toggle", () => { + const spy = jest.spyOn(BucketDropdown.prototype, 'toggleDropdown') + const wrapper = shallow( + + ) + wrapper + .find("Uncontrolled(Dropdown)") + .simulate("toggle") + expect(spy).toHaveBeenCalled() + spy.mockReset() + spy.mockRestore() + }) + + it("should call showBucketPolicy when Edit Policy link is clicked", () => { + const showBucketPolicy = jest.fn() + const wrapper = shallow( + + ) + wrapper + .find("li a") + .at(0) + .simulate("click", { stopPropagation: jest.fn() }) + expect(showBucketPolicy).toHaveBeenCalled() + }) + + it("should call deleteBucket when Delete link is clicked", () => { + const deleteBucket = jest.fn() + const wrapper = shallow( + + ) + wrapper + .find("li a") + .at(1) + .simulate("click", { stopPropagation: jest.fn() }) + expect(deleteBucket).toHaveBeenCalledWith("test") + }) +}) diff --git a/browser/app/js/buckets/__tests__/BucketPolicyModal.test.js b/browser/app/js/buckets/__tests__/BucketPolicyModal.test.js new file mode 100644 index 000000000..fa86f77eb --- /dev/null +++ b/browser/app/js/buckets/__tests__/BucketPolicyModal.test.js @@ -0,0 +1,43 @@ +/* + * Minio Cloud Storage (C) 2018 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from "react" +import { shallow, mount } from "enzyme" +import { BucketPolicyModal } from "../BucketPolicyModal" +import { READ_ONLY, WRITE_ONLY, READ_WRITE } from "../../constants" + +describe("BucketPolicyModal", () => { + it("should render without crashing", () => { + shallow() + }) + + it("should call hideBucketPolicy when close button is clicked", () => { + const hideBucketPolicy = jest.fn() + const wrapper = shallow( + + ) + wrapper.find("button").simulate("click") + expect(hideBucketPolicy).toHaveBeenCalled() + }) + + it("should include the PolicyInput and Policy components when there are any policies", () => { + const wrapper = shallow( + + ) + expect(wrapper.find("Connect(PolicyInput)").length).toBe(1) + expect(wrapper.find("Connect(Policy)").length).toBe(1) + }) +}) diff --git a/browser/app/js/buckets/__tests__/Policy.test.js b/browser/app/js/buckets/__tests__/Policy.test.js new file mode 100644 index 000000000..52d34fcd2 --- /dev/null +++ b/browser/app/js/buckets/__tests__/Policy.test.js @@ -0,0 +1,63 @@ +/* + * Minio Cloud Storage (C) 2018 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from "react" +import { shallow, mount } from "enzyme" +import { Policy } from "../Policy" +import { READ_ONLY, WRITE_ONLY, READ_WRITE } from "../../constants" +import web from "../../web" + +jest.mock("../../web", () => ({ + SetBucketPolicy: jest.fn(() => { + return Promise.resolve() + }) +})) + +describe("Policy", () => { + it("should render without crashing", () => { + shallow() + }) + + it("should call web.setBucketPolicy and fetchPolicies on submit", () => { + const fetchPolicies = jest.fn() + const wrapper = shallow( + + ) + wrapper.find("button").simulate("click", { preventDefault: jest.fn() }) + + expect(web.SetBucketPolicy).toHaveBeenCalledWith({ + bucketName: "bucket", + prefix: "foo", + policy: "none" + }) + + setImmediate(() => { + expect(fetchPolicies).toHaveBeenCalledWith("bucket") + }) + }) + + it("should change the empty string to '*' while displaying prefixes", () => { + const wrapper = shallow( + + ) + expect(wrapper.find(".pmbl-item").at(0).text()).toEqual("*") + }) +}) diff --git a/browser/app/js/buckets/__tests__/PolicyInput.test.js b/browser/app/js/buckets/__tests__/PolicyInput.test.js new file mode 100644 index 000000000..a3d961cd2 --- /dev/null +++ b/browser/app/js/buckets/__tests__/PolicyInput.test.js @@ -0,0 +1,77 @@ +/* + * Minio Cloud Storage (C) 2018 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from "react" +import { shallow, mount } from "enzyme" +import { PolicyInput } from "../PolicyInput" +import { READ_ONLY, WRITE_ONLY, READ_WRITE } from "../../constants" +import web from "../../web" + +jest.mock("../../web", () => ({ + SetBucketPolicy: jest.fn(() => { + return Promise.resolve() + }) +})) + +describe("PolicyInput", () => { + it("should render without crashing", () => { + const fetchPolicies = jest.fn() + shallow() + }) + + it("should call fetchPolicies after the component has mounted", () => { + const fetchPolicies = jest.fn() + const wrapper = shallow( + + ) + setImmediate(() => { + expect(fetchPolicies).toHaveBeenCalled() + }) + }) + + it("should call web.setBucketPolicy and fetchPolicies on submit", () => { + const fetchPolicies = jest.fn() + const wrapper = shallow( + + ) + wrapper.instance().prefix = { value: "baz" } + wrapper.instance().policy = { value: READ_ONLY } + wrapper.find("button").simulate("click", { preventDefault: jest.fn() }) + + expect(web.SetBucketPolicy).toHaveBeenCalledWith({ + bucketName: "bucket", + prefix: "baz", + policy: READ_ONLY + }) + + setImmediate(() => { + expect(fetchPolicies).toHaveBeenCalledWith("bucket") + }) + }) + + it("should change the prefix '*' to an empty string", () => { + const fetchPolicies = jest.fn() + const wrapper = shallow( + + ) + wrapper.instance().prefix = { value: "*" } + wrapper.instance().policy = { value: READ_ONLY } + + wrapper.find("button").simulate("click", { preventDefault: jest.fn() }) + + expect(wrapper.instance().prefix).toEqual({ value: "" }) + }) +}) diff --git a/browser/app/js/buckets/__tests__/actions.test.js b/browser/app/js/buckets/__tests__/actions.test.js index 323c9316f..4d14dd245 100644 --- a/browser/app/js/buckets/__tests__/actions.test.js +++ b/browser/app/js/buckets/__tests__/actions.test.js @@ -26,6 +26,9 @@ jest.mock("../../web", () => ({ }), MakeBucket: jest.fn(() => { return Promise.resolve() + }), + DeleteBucket: jest.fn(() => { + return Promise.resolve() }) })) @@ -43,7 +46,7 @@ describe("Buckets actions", () => { { type: "buckets/SET_LIST", buckets: ["test1", "test2"] }, { type: "buckets/SET_CURRENT_BUCKET", bucket: "test1" } ] - return store.dispatch(actionsBuckets.fetchBuckets()).then(() => { + return store.dispatch(actionsBuckets.fetchBuckets("list")).then(() => { const actions = store.getActions() expect(actions).toEqual(expectedActions) }) @@ -57,7 +60,7 @@ describe("Buckets actions", () => { { type: "buckets/SET_CURRENT_BUCKET", bucket: "test2" } ] window.location - return store.dispatch(actionsBuckets.fetchBuckets()).then(() => { + return store.dispatch(actionsBuckets.fetchBuckets("list")).then(() => { const actions = store.getActions() expect(actions).toEqual(expectedActions) }) @@ -71,7 +74,7 @@ describe("Buckets actions", () => { { type: "buckets/SET_CURRENT_BUCKET", bucket: "test1" } ] window.location - return store.dispatch(actionsBuckets.fetchBuckets()).then(() => { + return store.dispatch(actionsBuckets.fetchBuckets("list")).then(() => { const actions = store.getActions() expect(actions).toEqual(expectedActions) }) @@ -107,6 +110,36 @@ describe("Buckets actions", () => { expect(actions).toEqual(expectedActions) }) + it("creates buckets/SHOW_BUCKET_POLICY for showBucketPolicy", () => { + const store = mockStore() + const expectedActions = [ + { type: "buckets/SHOW_BUCKET_POLICY", show: true } + ] + store.dispatch(actionsBuckets.showBucketPolicy()) + const actions = store.getActions() + expect(actions).toEqual(expectedActions) + }) + + it("creates buckets/SHOW_BUCKET_POLICY for hideBucketPolicy", () => { + const store = mockStore() + const expectedActions = [ + { type: "buckets/SHOW_BUCKET_POLICY", show: false } + ] + store.dispatch(actionsBuckets.hideBucketPolicy()) + const actions = store.getActions() + expect(actions).toEqual(expectedActions) + }) + + it("creates buckets/SET_POLICIES action", () => { + const store = mockStore() + const expectedActions = [ + { type: "buckets/SET_POLICIES", policies: ["test1", "test2"] } + ] + store.dispatch(actionsBuckets.setPolicies(["test1", "test2"])) + const actions = store.getActions() + expect(actions).toEqual(expectedActions) + }) + it("creates buckets/ADD action", () => { const store = mockStore() const expectedActions = [{ type: "buckets/ADD", bucket: "test" }] @@ -115,6 +148,14 @@ describe("Buckets actions", () => { expect(actions).toEqual(expectedActions) }) + it("creates buckets/REMOVE action", () => { + const store = mockStore() + const expectedActions = [{ type: "buckets/REMOVE", bucket: "test" }] + store.dispatch(actionsBuckets.removeBucket("test")) + const actions = store.getActions() + expect(actions).toEqual(expectedActions) + }) + it("creates buckets/ADD and buckets/SET_CURRENT_BUCKET after creating the bucket", () => { const store = mockStore() const expectedActions = [ @@ -126,4 +167,19 @@ describe("Buckets actions", () => { expect(actions).toEqual(expectedActions) }) }) + + it("creates alert/SET, buckets/REMOVE, buckets/SET_LIST and buckets/SET_CURRENT_BUCKET " + + "after deleting the bucket", () => { + const store = mockStore() + const expectedActions = [ + { type: "alert/SET", alert: {id: 0, message: "Bucket 'test3' has been deleted.", type: "info"} }, + { type: "buckets/REMOVE", bucket: "test3" }, + { type: "buckets/SET_LIST", buckets: ["test1", "test2"] }, + { type: "buckets/SET_CURRENT_BUCKET", bucket: "test1" } + ] + return store.dispatch(actionsBuckets.deleteBucket("test3")).then(() => { + const actions = store.getActions() + expect(actions).toEqual(expectedActions) + }) + }) }) diff --git a/browser/app/js/buckets/__tests__/reducer.test.js b/browser/app/js/buckets/__tests__/reducer.test.js index 6906198e2..96b729ea7 100644 --- a/browser/app/js/buckets/__tests__/reducer.test.js +++ b/browser/app/js/buckets/__tests__/reducer.test.js @@ -22,8 +22,10 @@ describe("buckets reducer", () => { const initialState = reducer(undefined, {}) expect(initialState).toEqual({ list: [], + policies: [], filter: "", currentBucket: "", + showBucketPolicy: false, showMakeBucketModal: false }) }) @@ -47,6 +49,17 @@ describe("buckets reducer", () => { expect(newState.list).toEqual(["test3", "test1", "test2"]) }) + it("should handle REMOVE", () => { + const newState = reducer( + { list: ["test1", "test2"] }, + { + type: actions.REMOVE, + bucket: "test2" + } + ) + expect(newState.list).toEqual(["test1"]) + }) + it("should handle SET_FILTER", () => { const newState = reducer(undefined, { type: actions.SET_FILTER, @@ -63,6 +76,22 @@ describe("buckets reducer", () => { expect(newState.currentBucket).toEqual("test") }) + it("should handle SET_POLICIES", () => { + const newState = reducer(undefined, { + type: actions.SET_POLICIES, + policies: ["test1", "test2"] + }) + expect(newState.policies).toEqual(["test1", "test2"]) + }) + + it("should handle SHOW_BUCKET_POLICY", () => { + const newState = reducer(undefined, { + type: actions.SHOW_BUCKET_POLICY, + show: true + }) + expect(newState.showBucketPolicy).toBeTruthy() + }) + it("should handle SHOW_MAKE_BUCKET_MODAL", () => { const newState = reducer(undefined, { type: actions.SHOW_MAKE_BUCKET_MODAL, diff --git a/browser/app/js/buckets/actions.js b/browser/app/js/buckets/actions.js index 79e580d63..7a68f52d0 100644 --- a/browser/app/js/buckets/actions.js +++ b/browser/app/js/buckets/actions.js @@ -22,11 +22,14 @@ import { pathSlice } from "../utils" export const SET_LIST = "buckets/SET_LIST" export const ADD = "buckets/ADD" +export const REMOVE = "buckets/REMOVE" export const SET_FILTER = "buckets/SET_FILTER" export const SET_CURRENT_BUCKET = "buckets/SET_CURRENT_BUCKET" export const SHOW_MAKE_BUCKET_MODAL = "buckets/SHOW_MAKE_BUCKET_MODAL" +export const SHOW_BUCKET_POLICY = "buckets/SHOW_BUCKET_POLICY" +export const SET_POLICIES = "buckets/SET_POLICIES" -export const fetchBuckets = () => { +export const fetchBuckets = action => { return function(dispatch) { return web.ListBuckets().then(res => { const buckets = res.buckets ? res.buckets.map(bucket => bucket.name) : [] @@ -38,6 +41,9 @@ export const fetchBuckets = () => { } else { dispatch(selectBucket(buckets[0])) } + } else if (action === "delete") { + dispatch(selectBucket("")) + history.replace("/") } }) } @@ -92,11 +98,43 @@ export const makeBucket = bucket => { } } +export const deleteBucket = bucket => { + return function(dispatch) { + return web + .DeleteBucket({ + bucketName: bucket + }) + .then(() => { + dispatch( + alertActions.set({ + type: "info", + message: "Bucket '" + bucket + "' has been deleted." + }) + ) + dispatch(removeBucket(bucket)) + dispatch(fetchBuckets("delete")) + }) + .catch(err => { + dispatch( + alertActions.set({ + type: "danger", + message: err.message + }) + ) + }) + } +} + export const addBucket = bucket => ({ type: ADD, bucket }) +export const removeBucket = bucket => ({ + type: REMOVE, + bucket +}) + export const showMakeBucketModal = () => ({ type: SHOW_MAKE_BUCKET_MODAL, show: true @@ -106,3 +144,42 @@ export const hideMakeBucketModal = () => ({ type: SHOW_MAKE_BUCKET_MODAL, show: false }) + +export const fetchPolicies = bucket => { + return function(dispatch) { + return web + .ListAllBucketPolicies({ + bucketName: bucket + }) + .then(res => { + let policies = res.policies + if(policies) + dispatch(setPolicies(policies)) + else + dispatch(setPolicies([])) + }) + .catch(err => { + dispatch( + alertActions.set({ + type: "danger", + message: err.message + }) + ) + }) + } +} + +export const setPolicies = policies => ({ + type: SET_POLICIES, + policies +}) + +export const showBucketPolicy = () => ({ + type: SHOW_BUCKET_POLICY, + show: true +}) + +export const hideBucketPolicy = () => ({ + type: SHOW_BUCKET_POLICY, + show: false +}) \ No newline at end of file diff --git a/browser/app/js/buckets/reducer.js b/browser/app/js/buckets/reducer.js index 7bebdef21..ed4d556fb 100644 --- a/browser/app/js/buckets/reducer.js +++ b/browser/app/js/buckets/reducer.js @@ -16,12 +16,22 @@ import * as actionsBuckets from "./actions" +const removeBucket = (list, action) => { + const idx = list.findIndex(bucket => bucket === action.bucket) + if (idx == -1) { + return list + } + return [...list.slice(0, idx), ...list.slice(idx + 1)] +} + export default ( state = { list: [], filter: "", currentBucket: "", - showMakeBucketModal: false + showMakeBucketModal: false, + policies: [], + showBucketPolicy: false }, action ) => { @@ -36,6 +46,11 @@ export default ( ...state, list: [action.bucket, ...state.list] } + case actionsBuckets.REMOVE: + return { + ...state, + list: removeBucket(state.list, action), + } case actionsBuckets.SET_FILTER: return { ...state, @@ -51,6 +66,16 @@ export default ( ...state, showMakeBucketModal: action.show } + case actionsBuckets.SET_POLICIES: + return { + ...state, + policies: action.policies + } + case actionsBuckets.SHOW_BUCKET_POLICY: + return { + ...state, + showBucketPolicy: action.show + } default: return state } diff --git a/browser/app/js/components/Policy.js b/browser/app/js/components/Policy.js deleted file mode 100644 index cdf95eedf..000000000 --- a/browser/app/js/components/Policy.js +++ /dev/null @@ -1,80 +0,0 @@ -import { READ_ONLY, WRITE_ONLY, READ_WRITE } from '../constants' - -import React, { Component, PropTypes } from 'react' -import connect from 'react-redux/lib/components/connect' -import classnames from 'classnames' -import * as actions from '../actions' - -class Policy extends Component { - constructor(props, context) { - super(props, context) - this.state = {} - } - - handlePolicyChange(e) { - this.setState({ - policy: { - policy: e.target.value - } - }) - } - - removePolicy(e) { - e.preventDefault() - const {dispatch, currentBucket, prefix} = this.props - let newPrefix = prefix.replace(currentBucket + '/', '') - newPrefix = newPrefix.replace('*', '') - web.SetBucketPolicy({ - bucketName: currentBucket, - prefix: newPrefix, - policy: 'none' - }) - .then(() => { - dispatch(actions.setPolicies(this.props.policies.filter(policy => policy.prefix != prefix))) - }) - .catch(e => dispatch(actions.showAlert({ - type: 'danger', - message: e.message, - }))) - } - - render() { - const {policy, prefix, currentBucket} = this.props - let newPrefix = prefix.replace(currentBucket + '/', '') - newPrefix = newPrefix.replace('*', '') - - if (!newPrefix) - newPrefix = '*' - - return ( -
    -
    - { newPrefix } -
    -
    - -
    -
    - -
    -
    - ) - } -} - -export default connect(state => state)(Policy) \ No newline at end of file diff --git a/browser/app/js/components/PolicyInput.js b/browser/app/js/components/PolicyInput.js deleted file mode 100644 index 8508fc0c4..000000000 --- a/browser/app/js/components/PolicyInput.js +++ /dev/null @@ -1,98 +0,0 @@ -import { READ_ONLY, WRITE_ONLY, READ_WRITE } from '../constants' -import React, { Component, PropTypes } from 'react' -import connect from 'react-redux/lib/components/connect' -import classnames from 'classnames' -import * as actions from '../actions' - -class PolicyInput extends Component { - componentDidMount() { - const {web, dispatch} = this.props - this.prefix.focus() - web.ListAllBucketPolicies({ - bucketName: this.props.currentBucket - }).then(res => { - let policies = res.policies - if (policies) dispatch(actions.setPolicies(policies)) - }).catch(err => { - dispatch(actions.showAlert({ - type: 'danger', - message: err.message - })) - }) - } - - componentWillUnmount() { - const {dispatch} = this.props - dispatch(actions.setPolicies([])) - } - - handlePolicySubmit(e) { - e.preventDefault() - const {web, dispatch, currentBucket} = this.props - - let prefix = currentBucket + '/' + this.prefix.value - let policy = this.policy.value - - if (!prefix.endsWith('*')) prefix = prefix + '*' - - let prefixAlreadyExists = this.props.policies.some(elem => prefix === elem.prefix) - - if (prefixAlreadyExists) { - dispatch(actions.showAlert({ - type: 'danger', - message: "Policy for this prefix already exists." - })) - return - } - - web.SetBucketPolicy({ - bucketName: this.props.currentBucket, - prefix: this.prefix.value, - policy: this.policy.value - }) - .then(() => { - dispatch(actions.setPolicies([{ - policy, prefix - }, ...this.props.policies])) - this.prefix.value = '' - }) - .catch(e => dispatch(actions.showAlert({ - type: 'danger', - message: e.message, - }))) - } - - render() { - return ( -
    -
    - this.prefix = prefix } - className="form-control" - placeholder="Prefix" - editable={ true } /> -
    -
    - -
    -
    - -
    -
    - ) - } -} - -export default connect(state => state)(PolicyInput) \ No newline at end of file diff --git a/browser/app/js/objects/actions.js b/browser/app/js/objects/actions.js index 2fe63dc50..e24832dcf 100644 --- a/browser/app/js/objects/actions.js +++ b/browser/app/js/objects/actions.js @@ -58,7 +58,8 @@ export const fetchObjects = append => { buckets: { currentBucket }, objects: { currentPrefix, marker } } = getState() - return web + if (currentBucket) { + return web .ListObjects({ bucketName: currentBucket, prefix: currentPrefix, @@ -87,6 +88,7 @@ export const fetchObjects = append => { dispatch(alertActions.set({ type: "danger", message: err.message })) history.push("/login") }) + } } } diff --git a/cmd/generic-handlers.go b/cmd/generic-handlers.go index fee8cbb20..d4e987e45 100644 --- a/cmd/generic-handlers.go +++ b/cmd/generic-handlers.go @@ -280,7 +280,7 @@ func (h minioReservedBucketHandler) ServeHTTP(w http.ResponseWriter, r *http.Req default: // For all other requests reject access to reserved // buckets - bucketName, _ := urlPath2BucketObjectName(r.URL) + bucketName, _ := urlPath2BucketObjectName(r.URL.Path) if isMinioReservedBucket(bucketName) || isMinioMetaBucket(bucketName) { writeErrorResponse(w, ErrAllAccessDisabled, r.URL) return @@ -439,7 +439,7 @@ var notimplementedObjectResourceNames = map[string]bool{ // Resource handler ServeHTTP() wrapper func (h resourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - bucketName, objectName := urlPath2BucketObjectName(r.URL) + bucketName, objectName := urlPath2BucketObjectName(r.URL.Path) // If bucketName is present and not objectName check for bucket level resource queries. if bucketName != "" && objectName == "" { diff --git a/cmd/utils.go b/cmd/utils.go index 4c9c609ad..bce48a550 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -62,14 +62,9 @@ func cloneHeader(h http.Header) http.Header { } // Convert url path into bucket and object name. -func urlPath2BucketObjectName(u *url.URL) (bucketName, objectName string) { - if u == nil { - // Empty url, return bucket and object names. - return - } - +func urlPath2BucketObjectName(path string) (bucketName, objectName string) { // Trim any preceding slash separator. - urlPath := strings.TrimPrefix(u.Path, slashSeparator) + urlPath := strings.TrimPrefix(path, slashSeparator) // Split urlpath using slash separator into a given number of // expected tokens. diff --git a/cmd/utils_test.go b/cmd/utils_test.go index 8dc61ba5d..01f3fc2a2 100644 --- a/cmd/utils_test.go +++ b/cmd/utils_test.go @@ -205,12 +205,6 @@ func TestURL2BucketObjectName(t *testing.T) { bucket: "bucket", object: "///object////", }, - // Test case 8 url is not allocated. - { - u: nil, - bucket: "", - object: "", - }, // Test case 9 url path is empty. { u: &url.URL{}, @@ -221,7 +215,7 @@ func TestURL2BucketObjectName(t *testing.T) { // Validate all test cases. for i, testCase := range testCases { - bucketName, objectName := urlPath2BucketObjectName(testCase.u) + bucketName, objectName := urlPath2BucketObjectName(testCase.u.Path) if bucketName != testCase.bucket { t.Errorf("Test %d: failed expected bucket name \"%s\", got \"%s\"", i+1, testCase.bucket, bucketName) } diff --git a/cmd/web-handlers.go b/cmd/web-handlers.go index 7726c6619..c2cef603f 100644 --- a/cmd/web-handlers.go +++ b/cmd/web-handlers.go @@ -741,6 +741,7 @@ type ListAllBucketPoliciesArgs struct { // BucketAccessPolicy - Collection of canned bucket policy at a given prefix. type BucketAccessPolicy struct { + Bucket string `json:"bucket"` Prefix string `json:"prefix"` Policy policy.BucketPolicy `json:"policy"` } @@ -770,8 +771,11 @@ func (web *webAPIHandlers) ListAllBucketPolicies(r *http.Request, args *ListAllB } reply.UIVersion = browser.UIVersion for prefix, policy := range policy.GetPolicies(policyInfo.Statements, args.BucketName, "") { + bucketName, objectPrefix := urlPath2BucketObjectName(prefix) + objectPrefix = strings.TrimSuffix(objectPrefix, "*") reply.Policies = append(reply.Policies, BucketAccessPolicy{ - Prefix: prefix, + Bucket: bucketName, + Prefix: objectPrefix, Policy: policy, }) } @@ -822,6 +826,23 @@ func (web *webAPIHandlers) SetBucketPolicy(r *http.Request, args *SetBucketPolic return nil } + _, err = json.Marshal(policyInfo) + if err != nil { + return toJSONError(err) + } + + // Parse check bucket policy. + if s3Error := checkBucketPolicyResources(args.BucketName, policyInfo); s3Error != ErrNone { + apiErr := getAPIError(s3Error) + var err error + if apiErr.Code == "XMinioPolicyNesting" { + err = PolicyNesting{} + } else { + err = fmt.Errorf(apiErr.Description) + } + return toJSONError(err, args.BucketName) + } + // Parse validate and save bucket policy. if err := objectAPI.SetBucketPolicy(context.Background(), args.BucketName, policyInfo); err != nil { return toJSONError(err, args.BucketName) diff --git a/cmd/web-handlers_test.go b/cmd/web-handlers_test.go index 225c39f91..21f0ec0f0 100644 --- a/cmd/web-handlers_test.go +++ b/cmd/web-handlers_test.go @@ -1385,7 +1385,8 @@ func testWebListAllBucketPoliciesHandler(obj ObjectLayer, instanceType string, t } testCaseResult1 := []BucketAccessPolicy{{ - Prefix: bucketName + "/hello*", + Bucket: bucketName, + Prefix: "hello", Policy: policy.BucketPolicyReadWrite, }} testCases := []struct {