allow users to change password through browser (#7683)

Allow IAM users to change the password using
browser UI.
This commit is contained in:
Kanagaraj M 2019-05-30 01:48:46 +05:30 committed by kannappanr
parent 74e2fe0879
commit da8214845a
13 changed files with 509 additions and 304 deletions

View File

@ -18,79 +18,54 @@ import React from "react"
import { connect } from "react-redux" import { connect } from "react-redux"
import web from "../web" import web from "../web"
import * as alertActions from "../alert/actions" import * as alertActions from "../alert/actions"
import { getRandomAccessKey, getRandomSecretKey } from "../utils"
import jwtDecode from "jwt-decode"
import classNames from "classnames"
import { import { Modal, ModalBody, ModalHeader } from "react-bootstrap"
Tooltip,
Modal,
ModalBody,
ModalHeader,
OverlayTrigger
} from "react-bootstrap"
import InputGroup from "./InputGroup" import InputGroup from "./InputGroup"
export class ChangePasswordModal extends React.Component { export class ChangePasswordModal extends React.Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
accessKey: "", currentAccessKey: "",
secretKey: "", currentSecretKey: "",
keysReadOnly: false currentSecretKeyVisible: false,
newAccessKey: "",
newSecretKey: "",
newSecretKeyVisible: false
} }
} }
// When its shown, it loads the access key and secret key. // When its shown, it loads the access key from JWT token
componentWillMount() { componentWillMount() {
const { serverInfo } = this.props const token = jwtDecode(web.GetToken())
// Check environment variables first.
if (serverInfo.info.isEnvCreds || serverInfo.info.isWorm) {
this.setState({ this.setState({
accessKey: "xxxxxxxxx", currentAccessKey: token.sub,
secretKey: "xxxxxxxxx", newAccessKey: token.sub
keysReadOnly: true
})
} else {
web.GetAuth().then(data => {
this.setState({
accessKey: data.accessKey,
secretKey: data.secretKey
})
})
}
}
// Handle field changes from inside the modal.
accessKeyChange(e) {
this.setState({
accessKey: e.target.value
})
}
secretKeyChange(e) {
this.setState({
secretKey: e.target.value
})
}
secretKeyVisible(secretKeyVisible) {
this.setState({
secretKeyVisible
}) })
} }
// Save the auth params and set them. // Save the auth params and set them.
setAuth(e) { setAuth(e) {
const { showAlert } = this.props const { showAlert } = this.props
const accessKey = this.state.accessKey
const secretKey = this.state.secretKey if (this.canUpdateCredentials()) {
const currentAccessKey = this.state.currentAccessKey
const currentSecretKey = this.state.currentSecretKey
const newAccessKey = this.state.newAccessKey
const newSecretKey = this.state.newSecretKey
web web
.SetAuth({ .SetAuth({
accessKey, currentAccessKey,
secretKey currentSecretKey,
newAccessKey,
newSecretKey
}) })
.then(data => { .then(data => {
showAlert({ showAlert({
type: "success", type: "success",
message: "Changed credentials" message: "Credentials updated successfully."
}) })
}) })
.catch(err => { .catch(err => {
@ -100,75 +75,179 @@ export class ChangePasswordModal extends React.Component {
}) })
}) })
} }
}
generateAuth(e) { generateAuth(e) {
web.GenerateAuth().then(data => { const { serverInfo } = this.props
// Generate random access key only for root user
if (!serverInfo.userInfo.isIAMUser) {
this.setState({ this.setState({
accessKey: data.accessKey, newAccessKey: getRandomAccessKey()
secretKey: data.secretKey,
secretKeyVisible: true
})
}) })
} }
this.setState({
newSecretKey: getRandomSecretKey(),
newSecretKeyVisible: true
})
}
canChangePassword() {
const { serverInfo } = this.props
// Password change is not allowed in WORM mode
if (serverInfo.info.isWorm) {
return false
}
// When credentials are set on ENV, password change not allowed for owner
if (serverInfo.info.isEnvCreds && !serverInfo.userInfo.isIAMUser) {
return false
}
return true
}
canUpdateCredentials() {
return (
this.state.currentAccessKey.length > 0 &&
this.state.currentSecretKey.length > 0 &&
this.state.newAccessKey.length > 0 &&
this.state.newSecretKey.length > 0
)
}
render() { render() {
const { hideChangePassword } = this.props const { hideChangePassword, serverInfo } = this.props
const allowChangePassword = this.canChangePassword()
if (!allowChangePassword) {
return (
<Modal bsSize="sm" animation={false} show={true}>
<ModalHeader>Change Password</ModalHeader>
<ModalBody>
Credentials of this user cannot be updated through MinIO Browser.
</ModalBody>
<div className="modal-footer">
<button
id="cancel-change-password"
className="btn btn-link"
onClick={hideChangePassword}
>
Close
</button>
</div>
</Modal>
)
}
return ( return (
<Modal bsSize="sm" animation={false} show={true}> <Modal bsSize="sm" animation={false} show={true}>
<ModalHeader>Change Password</ModalHeader> <ModalHeader>Change Password</ModalHeader>
<ModalBody className="m-t-20"> <ModalBody className="m-t-20">
<div className="has-toggle-password">
<InputGroup <InputGroup
value={this.state.accessKey} value={this.state.currentAccessKey}
onChange={this.accessKeyChange.bind(this)} id="currentAccessKey"
id="accessKey" label="Current Access Key"
label="Access Key" name="currentAccesskey"
name="accesskey"
type="text" type="text"
spellCheck="false" spellCheck="false"
required="required" required="required"
autoComplete="false" autoComplete="false"
align="ig-left" align="ig-left"
readonly={this.state.keysReadOnly} readonly={true}
/> />
<i <i
onClick={this.secretKeyVisible.bind( onClick={() => {
this, this.setState({
!this.state.secretKeyVisible currentSecretKeyVisible: !this.state.currentSecretKeyVisible
)} })
}}
className={ className={
"toggle-password fa fa-eye " + "toggle-password fa fa-eye " +
(this.state.secretKeyVisible ? "toggled" : "") (this.state.currentSecretKeyVisible ? "toggled" : "")
} }
/> />
<InputGroup <InputGroup
value={this.state.secretKey} value={this.state.currentSecretKey}
onChange={this.secretKeyChange.bind(this)} onChange={e => {
id="secretKey" this.setState({ currentSecretKey: e.target.value })
label="Secret Key" }}
name="accesskey" id="currentSecretKey"
type={this.state.secretKeyVisible ? "text" : "password"} label="Current Secret Key"
name="currentSecretKey"
type={this.state.currentSecretKeyVisible ? "text" : "password"}
spellCheck="false" spellCheck="false"
required="required" required="required"
autoComplete="false" autoComplete="false"
align="ig-left" align="ig-left"
readonly={this.state.keysReadOnly}
/> />
</div>
<div className="has-toggle-password m-t-30">
{!serverInfo.userInfo.isIAMUser && (
<InputGroup
value={this.state.newAccessKey}
id="newAccessKey"
label={"New Access Key"}
name="newAccesskey"
type="text"
spellCheck="false"
required="required"
autoComplete="false"
align="ig-left"
onChange={e => {
this.setState({ newAccessKey: e.target.value })
}}
readonly={serverInfo.userInfo.isIAMUser}
/>
)}
<i
onClick={() => {
this.setState({
newSecretKeyVisible: !this.state.newSecretKeyVisible
})
}}
className={
"toggle-password fa fa-eye " +
(this.state.newSecretKeyVisible ? "toggled" : "")
}
/>
<InputGroup
value={this.state.newSecretKey}
onChange={e => {
this.setState({ newSecretKey: e.target.value })
}}
id="newSecretKey"
label="New Secret Key"
name="newSecretKey"
type={this.state.newSecretKeyVisible ? "text" : "password"}
spellCheck="false"
required="required"
autoComplete="false"
align="ig-left"
onChange={e => {
this.setState({ newSecretKey: e.target.value })
}}
/>
</div>
</ModalBody> </ModalBody>
<div className="modal-footer"> <div className="modal-footer">
<button <button
id="generate-keys" id="generate-keys"
className={ className={"btn btn-primary"}
"btn btn-primary " + (this.state.keysReadOnly ? "hidden" : "")
}
onClick={this.generateAuth.bind(this)} onClick={this.generateAuth.bind(this)}
> >
Generate Generate
</button> </button>
<button <button
id="update-keys" id="update-keys"
className={ className={classNames({
"btn btn-success " + (this.state.keysReadOnly ? "hidden" : "") btn: true,
} "btn-success": this.canUpdateCredentials()
})}
disabled={!this.canUpdateCredentials()}
onClick={this.setAuth.bind(this)} onClick={this.setAuth.bind(this)}
> >
Update Update
@ -198,4 +277,7 @@ const mapDispatchToProps = dispatch => {
} }
} }
export default connect(mapStateToProps, mapDispatchToProps)(ChangePasswordModal) export default connect(
mapStateToProps,
mapDispatchToProps
)(ChangePasswordModal)

View File

@ -17,21 +17,38 @@
import React from "react" import React from "react"
import { shallow, mount } from "enzyme" import { shallow, mount } from "enzyme"
import { ChangePasswordModal } from "../ChangePasswordModal" import { ChangePasswordModal } from "../ChangePasswordModal"
import jwtDecode from "jwt-decode"
jest.mock("jwt-decode")
jwtDecode.mockImplementation(() => ({ sub: "minio" }))
jest.mock("../../web", () => ({ jest.mock("../../web", () => ({
GetAuth: jest.fn(() => {
return Promise.resolve({ accessKey: "test1", secretKey: "test2" })
}),
GenerateAuth: jest.fn(() => { GenerateAuth: jest.fn(() => {
return Promise.resolve({ accessKey: "gen1", secretKey: "gen2" }) return Promise.resolve({ accessKey: "gen1", secretKey: "gen2" })
}), }),
SetAuth: jest.fn(({ accessKey, secretKey }) => { SetAuth: jest.fn(
if (accessKey == "test3" && secretKey == "test4") { ({ currentAccessKey, currentSecretKey, newAccessKey, newSecretKey }) => {
if (
currentAccessKey == "minio" &&
currentSecretKey == "minio123" &&
newAccessKey == "test" &&
newSecretKey == "test123"
) {
return Promise.resolve({}) return Promise.resolve({})
} else { } else {
return Promise.reject({ message: "Error" }) return Promise.reject({
} message: "Error"
}) })
}
}
),
GetToken: jest.fn(() => "")
}))
jest.mock("../../utils", () => ({
getRandomAccessKey: () => "raccesskey",
getRandomSecretKey: () => "rsecretkey"
})) }))
describe("ChangePasswordModal", () => { describe("ChangePasswordModal", () => {
@ -40,57 +57,93 @@ describe("ChangePasswordModal", () => {
memory: "test", memory: "test",
platform: "test", platform: "test",
runtime: "test", runtime: "test",
info: { isEnvCreds: false } info: { isEnvCreds: false },
userInfo: { isIAMUser: false }
} }
it("should render without crashing", () => { it("should render without crashing", () => {
shallow(<ChangePasswordModal serverInfo={serverInfo} />) shallow(<ChangePasswordModal serverInfo={serverInfo} />)
}) })
it("should get the keys when its rendered", () => { it("should not allow changing password when isWorm is true", () => {
const wrapper = shallow(<ChangePasswordModal serverInfo={serverInfo} />) const newServerInfo = { ...serverInfo, info: { isWorm: true } }
setImmediate(() => { const wrapper = shallow(<ChangePasswordModal serverInfo={newServerInfo} />)
expect(wrapper.state("accessKey")).toBe("test1") expect(
expect(wrapper.state("secretKey")).toBe("test2") wrapper
}) .find("ModalBody")
.childAt(0)
.text()
).toBe("Credentials of this user cannot be updated through MinIO Browser.")
}) })
it("should show readonly keys when isEnvCreds is true", () => { it("should not allow changing password when isEnvCreds is true and not IAM user", () => {
const newServerInfo = { ...serverInfo, info: { isEnvCreds: true } } const newServerInfo = {
...serverInfo,
info: { isEnvCreds: true },
userInfo: { isIAMUser: false }
}
const wrapper = shallow(<ChangePasswordModal serverInfo={newServerInfo} />) const wrapper = shallow(<ChangePasswordModal serverInfo={newServerInfo} />)
expect(wrapper.state("accessKey")).toBe("xxxxxxxxx") expect(
expect(wrapper.state("secretKey")).toBe("xxxxxxxxx") wrapper
expect(wrapper.find("#accessKey").prop("readonly")).toBeTruthy() .find("ModalBody")
expect(wrapper.find("#secretKey").prop("readonly")).toBeTruthy() .childAt(0)
expect(wrapper.find("#generate-keys").hasClass("hidden")).toBeTruthy() .text()
expect(wrapper.find("#update-keys").hasClass("hidden")).toBeTruthy() ).toBe("Credentials of this user cannot be updated through MinIO Browser.")
}) })
it("should generate accessKey and secretKey when Generate buttons is clicked", () => { it("should generate accessKey and secretKey when Generate buttons is clicked", () => {
const wrapper = shallow(<ChangePasswordModal serverInfo={serverInfo} />) const wrapper = shallow(<ChangePasswordModal serverInfo={serverInfo} />)
wrapper.find("#generate-keys").simulate("click") wrapper.find("#generate-keys").simulate("click")
setImmediate(() => { setImmediate(() => {
expect(wrapper.state("accessKey")).toBe("gen1") expect(wrapper.state("newAccessKey")).toBe("raccesskey")
expect(wrapper.state("secretKey")).toBe("gen2") expect(wrapper.state("newSecretKey")).toBe("rsecretkey")
}) })
}) })
it("should not generate accessKey for IAM User", () => {
const newServerInfo = {
...serverInfo,
userInfo: { isIAMUser: true }
}
const wrapper = shallow(<ChangePasswordModal serverInfo={newServerInfo} />)
wrapper.find("#generate-keys").simulate("click")
setImmediate(() => {
expect(wrapper.state("newAccessKey")).toBe("minio")
expect(wrapper.state("newSecretKey")).toBe("rsecretkey")
})
})
it("should not show new accessKey field for IAM User", () => {
const newServerInfo = {
...serverInfo,
userInfo: { isIAMUser: true }
}
const wrapper = shallow(<ChangePasswordModal serverInfo={newServerInfo} />)
expect(wrapper.find("#newAccesskey").exists()).toBeFalsy()
})
it("should update accessKey and secretKey when Update button is clicked", () => { it("should update accessKey and secretKey when Update button is clicked", () => {
const showAlert = jest.fn() const showAlert = jest.fn()
const wrapper = shallow( const wrapper = shallow(
<ChangePasswordModal serverInfo={serverInfo} showAlert={showAlert} /> <ChangePasswordModal serverInfo={serverInfo} showAlert={showAlert} />
) )
wrapper wrapper
.find("#accessKey") .find("#currentAccessKey")
.simulate("change", { target: { value: "test3" } }) .simulate("change", { target: { value: "minio" } })
wrapper wrapper
.find("#secretKey") .find("#currentSecretKey")
.simulate("change", { target: { value: "test4" } }) .simulate("change", { target: { value: "minio123" } })
wrapper
.find("#newAccessKey")
.simulate("change", { target: { value: "test" } })
wrapper
.find("#newSecretKey")
.simulate("change", { target: { value: "test123" } })
wrapper.find("#update-keys").simulate("click") wrapper.find("#update-keys").simulate("click")
setImmediate(() => { setImmediate(() => {
expect(showAlert).toHaveBeenCalledWith({ expect(showAlert).toHaveBeenCalledWith({
type: "success", type: "success",
message: "Changed credentials" message: "Credentials updated successfully."
}) })
}) })
}) })

View File

@ -54,7 +54,8 @@ export const fetchServerInfo = () => {
memory: res.MinioMemory, memory: res.MinioMemory,
platform: res.MinioPlatform, platform: res.MinioPlatform,
runtime: res.MinioRuntime, runtime: res.MinioRuntime,
info: res.MinioGlobalInfo info: res.MinioGlobalInfo,
userInfo: res.MinioUserInfo
} }
dispatch(setServerInfo(serverInfo)) dispatch(setServerInfo(serverInfo))
}) })

View File

@ -14,11 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
import { minioBrowserPrefix } from './constants.js' import { minioBrowserPrefix } from "./constants.js"
export const sortObjectsByName = (objects, order) => { export const sortObjectsByName = (objects, order) => {
let folders = objects.filter(object => object.name.endsWith('/')) let folders = objects.filter(object => object.name.endsWith("/"))
let files = objects.filter(object => !object.name.endsWith('/')) let files = objects.filter(object => !object.name.endsWith("/"))
folders = folders.sort((a, b) => { folders = folders.sort((a, b) => {
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1 if (a.name.toLowerCase() < b.name.toLowerCase()) return -1
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1 if (a.name.toLowerCase() > b.name.toLowerCase()) return 1
@ -37,32 +37,34 @@ export const sortObjectsByName = (objects, order) => {
} }
export const sortObjectsBySize = (objects, order) => { export const sortObjectsBySize = (objects, order) => {
let folders = objects.filter(object => object.name.endsWith('/')) let folders = objects.filter(object => object.name.endsWith("/"))
let files = objects.filter(object => !object.name.endsWith('/')) let files = objects.filter(object => !object.name.endsWith("/"))
files = files.sort((a, b) => a.size - b.size) files = files.sort((a, b) => a.size - b.size)
if (order) if (order) files = files.reverse()
files = files.reverse()
return [...folders, ...files] return [...folders, ...files]
} }
export const sortObjectsByDate = (objects, order) => { export const sortObjectsByDate = (objects, order) => {
let folders = objects.filter(object => object.name.endsWith('/')) let folders = objects.filter(object => object.name.endsWith("/"))
let files = objects.filter(object => !object.name.endsWith('/')) let files = objects.filter(object => !object.name.endsWith("/"))
files = files.sort((a, b) => new Date(a.lastModified).getTime() - new Date(b.lastModified).getTime()) files = files.sort(
if (order) (a, b) =>
files = files.reverse() new Date(a.lastModified).getTime() - new Date(b.lastModified).getTime()
)
if (order) files = files.reverse()
return [...folders, ...files] return [...folders, ...files]
} }
export const pathSlice = (path) => { export const pathSlice = path => {
path = path.replace(minioBrowserPrefix, '') path = path.replace(minioBrowserPrefix, "")
let prefix = '' let prefix = ""
let bucket = '' let bucket = ""
if (!path) return { if (!path)
return {
bucket, bucket,
prefix prefix
} }
let objectIndex = path.indexOf('/', 1) let objectIndex = path.indexOf("/", 1)
if (objectIndex == -1) { if (objectIndex == -1) {
bucket = path.slice(1) bucket = path.slice(1)
return { return {
@ -79,7 +81,29 @@ export const pathSlice = (path) => {
} }
export const pathJoin = (bucket, prefix) => { export const pathJoin = (bucket, prefix) => {
if (!prefix) if (!prefix) prefix = ""
prefix = '' return minioBrowserPrefix + "/" + bucket + "/" + prefix
return minioBrowserPrefix + '/' + bucket + '/' + prefix }
export const getRandomAccessKey = () => {
const alphaNumericTable = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
let arr = new Uint8Array(20)
window.crypto.getRandomValues(arr)
const random = Array.prototype.map.call(arr, v => {
const i = v % alphaNumericTable.length
return alphaNumericTable.charAt(i)
})
return random.join("")
}
export const getRandomSecretKey = () => {
let arr = new Uint8Array(40)
window.crypto.getRandomValues(arr)
const binStr = Array.prototype.map
.call(arr, v => {
return String.fromCharCode(v)
})
.join("")
const base64Str = btoa(binStr)
return base64Str.replace(/\//g, "+").substr(0, 40)
} }

View File

@ -72,6 +72,9 @@ class Web {
Logout() { Logout() {
storage.removeItem('token') storage.removeItem('token')
} }
GetToken() {
return storage.getItem('token')
}
ServerInfo() { ServerInfo() {
return this.makeCall('ServerInfo') return this.makeCall('ServerInfo')
} }
@ -99,12 +102,6 @@ class Web {
RemoveObject(args) { RemoveObject(args) {
return this.makeCall('RemoveObject', args) return this.makeCall('RemoveObject', args)
} }
GetAuth() {
return this.makeCall('GetAuth')
}
GenerateAuth() {
return this.makeCall('GenerateAuth')
}
SetAuth(args) { SetAuth(args) {
return this.makeCall('SetAuth', args) return this.makeCall('SetAuth', args)
.then(res => { .then(res => {

View File

@ -190,8 +190,8 @@
----------------------------*/ ----------------------------*/
.toggle-password { .toggle-password {
position: absolute; position: absolute;
bottom: 30px; bottom: 0 ;
right: 35px; right: 0;
width: 30px; width: 30px;
height: 30px; height: 30px;
border: 1px solid #eee; border: 1px solid #eee;
@ -206,6 +206,10 @@
background: #eee; background: #eee;
} }
} }
.has-toggle-password {
position: relative;
}
//-------------------------- //--------------------------

View File

@ -68,6 +68,7 @@
"humanize": "0.0.9", "humanize": "0.0.9",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"jwt-decode": "^2.2.0",
"local-storage-fallback": "^4.0.2", "local-storage-fallback": "^4.0.2",
"material-design-iconic-font": "^2.2.0", "material-design-iconic-font": "^2.2.0",
"mime-db": "^1.25.0", "mime-db": "^1.25.0",

File diff suppressed because one or more lines are too long

View File

@ -4907,6 +4907,11 @@ jsprim@^1.2.2:
json-schema "0.2.3" json-schema "0.2.3"
verror "1.10.0" verror "1.10.0"
jwt-decode@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79"
integrity sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=
keycode@^2.1.2: keycode@^2.1.2:
version "2.1.9" version "2.1.9"
resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.1.9.tgz#964a23c54e4889405b4861a5c9f0480d45141dfa" resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.1.9.tgz#964a23c54e4889405b4861a5c9f0480d45141dfa"

View File

@ -491,6 +491,51 @@ func (sys *IAMSys) SetUser(accessKey string, uinfo madmin.UserInfo) error {
return nil return nil
} }
// SetUserSecretKey - sets user secret key
func (sys *IAMSys) SetUserSecretKey(accessKey string, secretKey string) error {
objectAPI := newObjectLayerFn()
if objectAPI == nil {
return errServerNotInitialized
}
sys.Lock()
defer sys.Unlock()
cred, ok := sys.iamUsersMap[accessKey]
if !ok {
return errNoSuchUser
}
uinfo := madmin.UserInfo{
SecretKey: secretKey,
Status: madmin.AccountStatus(cred.Status),
}
configFile := pathJoin(iamConfigUsersPrefix, accessKey, iamIdentityFile)
data, err := json.Marshal(uinfo)
if err != nil {
return err
}
if globalEtcdClient != nil {
err = saveConfigEtcd(context.Background(), globalEtcdClient, configFile, data)
} else {
err = saveConfig(context.Background(), objectAPI, configFile, data)
}
if err != nil {
return err
}
sys.iamUsersMap[accessKey] = auth.Credentials{
AccessKey: accessKey,
SecretKey: secretKey,
Status: string(uinfo.Status),
}
return nil
}
// GetUser - get user credentials // GetUser - get user credentials
func (sys *IAMSys) GetUser(accessKey string) (cred auth.Credentials, ok bool) { func (sys *IAMSys) GetUser(accessKey string) (cred auth.Credentials, ok bool) {
sys.RLock() sys.RLock()

View File

@ -47,6 +47,7 @@ var (
errChangeCredNotAllowed = errors.New("Changing access key and secret key not allowed") errChangeCredNotAllowed = errors.New("Changing access key and secret key not allowed")
errAuthentication = errors.New("Authentication failed, check your access credentials") errAuthentication = errors.New("Authentication failed, check your access credentials")
errNoAuthToken = errors.New("JWT token missing") errNoAuthToken = errors.New("JWT token missing")
errIncorrectCreds = errors.New("Current access key or secret key is incorrect")
) )
func authenticateJWTUsers(accessKey, secretKey string, expiry time.Duration) (string, error) { func authenticateJWTUsers(accessKey, secretKey string, expiry time.Duration) (string, error) {

View File

@ -69,6 +69,7 @@ type ServerInfoRep struct {
MinioPlatform string MinioPlatform string
MinioRuntime string MinioRuntime string
MinioGlobalInfo map[string]interface{} MinioGlobalInfo map[string]interface{}
MinioUserInfo map[string]interface{}
UIVersion string `json:"uiVersion"` UIVersion string `json:"uiVersion"`
} }
@ -97,17 +98,17 @@ func (web *webAPIHandlers) ServerInfo(r *http.Request, args *WebGenericArgs, rep
reply.MinioVersion = Version reply.MinioVersion = Version
reply.MinioGlobalInfo = getGlobalInfo() reply.MinioGlobalInfo = getGlobalInfo()
// If ENV creds are not set and incoming user is not owner
// disable changing credentials. // if etcd is set, disallow changing credentials through UI for owner
v, ok := reply.MinioGlobalInfo["isEnvCreds"].(bool)
if ok && !v {
reply.MinioGlobalInfo["isEnvCreds"] = !owner
}
// if etcd is set disallow changing credentials through UI
if globalEtcdClient != nil { if globalEtcdClient != nil {
reply.MinioGlobalInfo["isEnvCreds"] = true reply.MinioGlobalInfo["isEnvCreds"] = true
} }
// Check if the user is IAM user
reply.MinioUserInfo = map[string]interface{}{
"isIAMUser": !owner,
}
reply.MinioMemory = mem reply.MinioMemory = mem
reply.MinioPlatform = platform reply.MinioPlatform = platform
reply.MinioRuntime = goruntime reply.MinioRuntime = goruntime
@ -768,8 +769,10 @@ func (web webAPIHandlers) GenerateAuth(r *http.Request, args *WebGenericArgs, re
// SetAuthArgs - argument for SetAuth // SetAuthArgs - argument for SetAuth
type SetAuthArgs struct { type SetAuthArgs struct {
AccessKey string `json:"accessKey"` CurrentAccessKey string `json:"currentAccessKey"`
SecretKey string `json:"secretKey"` CurrentSecretKey string `json:"currentSecretKey"`
NewAccessKey string `json:"newAccessKey"`
NewSecretKey string `json:"newSecretKey"`
} }
// SetAuthReply - reply for SetAuth // SetAuthReply - reply for SetAuth
@ -781,17 +784,28 @@ type SetAuthReply struct {
// SetAuth - Set accessKey and secretKey credentials. // SetAuth - Set accessKey and secretKey credentials.
func (web *webAPIHandlers) SetAuth(r *http.Request, args *SetAuthArgs, reply *SetAuthReply) error { func (web *webAPIHandlers) SetAuth(r *http.Request, args *SetAuthArgs, reply *SetAuthReply) error {
_, owner, authErr := webRequestAuthenticate(r) claims, owner, authErr := webRequestAuthenticate(r)
if authErr != nil { if authErr != nil {
return toJSONError(authErr) return toJSONError(authErr)
} }
// If creds are set through ENV disallow changing credentials. // When WORM is enabled, disallow changing credenatials for owner and user
if globalIsEnvCreds || globalWORMEnabled || !owner || globalEtcdClient != nil { if globalWORMEnabled {
return toJSONError(errChangeCredNotAllowed) return toJSONError(errChangeCredNotAllowed)
} }
creds, err := auth.CreateCredentials(args.AccessKey, args.SecretKey) if owner {
if globalIsEnvCreds || globalEtcdClient != nil {
return toJSONError(errChangeCredNotAllowed)
}
// get Current creds and verify
prevCred := globalServerConfig.GetCredential()
if prevCred.AccessKey != args.CurrentAccessKey || prevCred.SecretKey != args.CurrentSecretKey {
return errIncorrectCreds
}
creds, err := auth.CreateCredentials(args.NewAccessKey, args.NewSecretKey)
if err != nil { if err != nil {
return toJSONError(err) return toJSONError(err)
} }
@ -801,7 +815,7 @@ func (web *webAPIHandlers) SetAuth(r *http.Request, args *SetAuthArgs, reply *Se
defer globalServerConfigMu.Unlock() defer globalServerConfigMu.Unlock()
// Update credentials in memory // Update credentials in memory
prevCred := globalServerConfig.SetCredential(creds) prevCred = globalServerConfig.SetCredential(creds)
// Persist updated credentials. // Persist updated credentials.
if err = saveServerConfig(context.Background(), newObjectLayerFn(), globalServerConfig); err != nil { if err = saveServerConfig(context.Background(), newObjectLayerFn(), globalServerConfig); err != nil {
@ -811,38 +825,39 @@ func (web *webAPIHandlers) SetAuth(r *http.Request, args *SetAuthArgs, reply *Se
return toJSONError(err) return toJSONError(err)
} }
reply.Token, err = authenticateWeb(creds.AccessKey, creds.SecretKey) reply.Token, err = authenticateWeb(args.NewAccessKey, args.NewSecretKey)
if err != nil { if err != nil {
return toJSONError(err) return toJSONError(err)
} }
} else {
// for IAM users, access key cannot be updated
// claims.Subject is used instead of accesskey from args
prevCred, ok := globalIAMSys.GetUser(claims.Subject)
if !ok {
return errInvalidAccessKeyID
}
// Throw error when wrong secret key is provided
if prevCred.SecretKey != args.CurrentSecretKey {
return errIncorrectCreds
}
err := globalIAMSys.SetUserSecretKey(claims.Subject, args.NewSecretKey)
if err != nil {
return toJSONError(err)
}
reply.Token, err = authenticateWeb(claims.Subject, args.NewSecretKey)
if err != nil {
return toJSONError(err)
}
}
reply.UIVersion = browser.UIVersion reply.UIVersion = browser.UIVersion
return nil return nil
} }
// GetAuthReply - Reply current credentials.
type GetAuthReply struct {
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
UIVersion string `json:"uiVersion"`
}
// GetAuth - return accessKey and secretKey credentials.
func (web *webAPIHandlers) GetAuth(r *http.Request, args *WebGenericArgs, reply *GetAuthReply) error {
_, owner, authErr := webRequestAuthenticate(r)
if authErr != nil {
return toJSONError(authErr)
}
if !owner {
return toJSONError(errAccessDenied)
}
creds := globalServerConfig.GetCredential()
reply.AccessKey = creds.AccessKey
reply.SecretKey = creds.SecretKey
reply.UIVersion = browser.UIVersion
return nil
}
// URLTokenReply contains the reply for CreateURLToken. // URLTokenReply contains the reply for CreateURLToken.
type URLTokenReply struct { type URLTokenReply struct {
Token string `json:"token"` Token string `json:"token"`

View File

@ -701,18 +701,20 @@ func testSetAuthWebHandler(obj ObjectLayer, instanceType string, t TestErrHandle
} }
testCases := []struct { testCases := []struct {
username string currentAccessKey string
password string currentSecretKey string
newAccessKey string
newSecretKey string
success bool success bool
}{ }{
{"", "", false}, {"", "", "", "", false},
{"1", "1", false}, {"1", "1", "1", "1", false},
{"azerty", "foooooooooooooo", true}, {credentials.AccessKey, credentials.SecretKey, "azerty", "foooooooooooooo", true},
} }
// Iterating over the test cases, calling the function under test and asserting the response. // Iterating over the test cases, calling the function under test and asserting the response.
for i, testCase := range testCases { for i, testCase := range testCases {
setAuthRequest := SetAuthArgs{AccessKey: testCase.username, SecretKey: testCase.password} setAuthRequest := SetAuthArgs{CurrentAccessKey: testCase.currentAccessKey, CurrentSecretKey: testCase.currentSecretKey, NewAccessKey: testCase.newAccessKey, NewSecretKey: testCase.newSecretKey}
setAuthReply := &SetAuthReply{} setAuthReply := &SetAuthReply{}
req, err := newTestWebRPCRequest("Web.SetAuth", authorization, setAuthRequest) req, err := newTestWebRPCRequest("Web.SetAuth", authorization, setAuthRequest)
if err != nil { if err != nil {
@ -735,42 +737,6 @@ func testSetAuthWebHandler(obj ObjectLayer, instanceType string, t TestErrHandle
} }
} }
// Wrapper for calling Get Auth Handler
func TestWebHandlerGetAuth(t *testing.T) {
ExecObjectLayerTest(t, testGetAuthWebHandler)
}
// testGetAuthWebHandler - Test GetAuth web handler
func testGetAuthWebHandler(obj ObjectLayer, instanceType string, t TestErrHandler) {
// Register the API end points with XL/FS object layer.
apiRouter := initTestWebRPCEndPoint(obj)
credentials := globalServerConfig.GetCredential()
rec := httptest.NewRecorder()
authorization, err := getWebRPCToken(apiRouter, credentials.AccessKey, credentials.SecretKey)
if err != nil {
t.Fatal("Cannot authenticate")
}
getAuthRequest := WebGenericArgs{}
getAuthReply := &GetAuthReply{}
req, err := newTestWebRPCRequest("Web.GetAuth", authorization, getAuthRequest)
if err != nil {
t.Fatalf("Failed to create HTTP request: <ERROR> %v", err)
}
apiRouter.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("Expected the response status to be 200, but instead found `%d`", rec.Code)
}
err = getTestWebRPCResponse(rec, &getAuthReply)
if err != nil {
t.Fatalf("Failed, %v", err)
}
if getAuthReply.AccessKey != credentials.AccessKey || getAuthReply.SecretKey != credentials.SecretKey {
t.Fatalf("Failed to get correct auth keys")
}
}
func TestWebCreateURLToken(t *testing.T) { func TestWebCreateURLToken(t *testing.T) {
ExecObjectLayerTest(t, testCreateURLToken) ExecObjectLayerTest(t, testCreateURLToken)
} }
@ -1518,7 +1484,7 @@ func TestWebCheckAuthorization(t *testing.T) {
webRPCs := []string{ webRPCs := []string{
"ServerInfo", "StorageInfo", "MakeBucket", "ServerInfo", "StorageInfo", "MakeBucket",
"ListBuckets", "ListObjects", "RemoveObject", "ListBuckets", "ListObjects", "RemoveObject",
"GenerateAuth", "SetAuth", "GetAuth", "GenerateAuth", "SetAuth",
"GetBucketPolicy", "SetBucketPolicy", "ListAllBucketPolicies", "GetBucketPolicy", "SetBucketPolicy", "ListAllBucketPolicies",
"PresignedGet", "PresignedGet",
} }