Change Password support for Console (#457)
- Account change password endpoints - Change account password modal - Grouped account settings and service accounts - Removed the SuperAdmin credentials from almost all places, only missing place is Oauth login - Renamed service-accounts UI labels to account in Menu Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
This commit is contained in:
98
models/account_change_password_request.go
Normal file
98
models/account_change_password_request.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package models
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
|
||||
import (
|
||||
"github.com/go-openapi/errors"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/go-openapi/swag"
|
||||
"github.com/go-openapi/validate"
|
||||
)
|
||||
|
||||
// AccountChangePasswordRequest account change password request
|
||||
//
|
||||
// swagger:model accountChangePasswordRequest
|
||||
type AccountChangePasswordRequest struct {
|
||||
|
||||
// current secret key
|
||||
// Required: true
|
||||
CurrentSecretKey *string `json:"current_secret_key"`
|
||||
|
||||
// new secret key
|
||||
// Required: true
|
||||
NewSecretKey *string `json:"new_secret_key"`
|
||||
}
|
||||
|
||||
// Validate validates this account change password request
|
||||
func (m *AccountChangePasswordRequest) Validate(formats strfmt.Registry) error {
|
||||
var res []error
|
||||
|
||||
if err := m.validateCurrentSecretKey(formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if err := m.validateNewSecretKey(formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if len(res) > 0 {
|
||||
return errors.CompositeValidationError(res...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *AccountChangePasswordRequest) validateCurrentSecretKey(formats strfmt.Registry) error {
|
||||
|
||||
if err := validate.Required("current_secret_key", "body", m.CurrentSecretKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *AccountChangePasswordRequest) validateNewSecretKey(formats strfmt.Registry) error {
|
||||
|
||||
if err := validate.Required("new_secret_key", "body", m.NewSecretKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalBinary interface implementation
|
||||
func (m *AccountChangePasswordRequest) MarshalBinary() ([]byte, error) {
|
||||
if m == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return swag.WriteJSON(m)
|
||||
}
|
||||
|
||||
// UnmarshalBinary interface implementation
|
||||
func (m *AccountChangePasswordRequest) UnmarshalBinary(b []byte) error {
|
||||
var res AccountChangePasswordRequest
|
||||
if err := swag.ReadJSON(b, &res); err != nil {
|
||||
return err
|
||||
}
|
||||
*m = res
|
||||
return nil
|
||||
}
|
||||
@@ -32,17 +32,23 @@ import (
|
||||
// swagger:model principal
|
||||
type Principal struct {
|
||||
|
||||
// access key ID
|
||||
AccessKeyID string `json:"accessKeyID,omitempty"`
|
||||
// s t s access key ID
|
||||
STSAccessKeyID string `json:"STSAccessKeyID,omitempty"`
|
||||
|
||||
// s t s secret access key
|
||||
STSSecretAccessKey string `json:"STSSecretAccessKey,omitempty"`
|
||||
|
||||
// s t s session token
|
||||
STSSessionToken string `json:"STSSessionToken,omitempty"`
|
||||
|
||||
// account access key
|
||||
AccountAccessKey string `json:"accountAccessKey,omitempty"`
|
||||
|
||||
// account secret key
|
||||
AccountSecretKey string `json:"accountSecretKey,omitempty"`
|
||||
|
||||
// actions
|
||||
Actions []string `json:"actions"`
|
||||
|
||||
// secret access key
|
||||
SecretAccessKey string `json:"secretAccessKey,omitempty"`
|
||||
|
||||
// session token
|
||||
SessionToken string `json:"sessionToken,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates this principal
|
||||
|
||||
@@ -31,7 +31,7 @@ var (
|
||||
notifications = "/notification-endpoints"
|
||||
buckets = "/buckets"
|
||||
bucketsDetail = "/buckets/:bucketName"
|
||||
serviceAccounts = "/service-accounts"
|
||||
serviceAccounts = "/account"
|
||||
tenants = "/tenants"
|
||||
tenantsDetail = "/namespaces/:tenantNamespace/tenants/:tenantName"
|
||||
remoteBuckets = "/remote-buckets"
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -58,12 +59,14 @@ func IsSessionTokenValid(token string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// DecryptedClaims claims struct for decrypted credentials
|
||||
type DecryptedClaims struct {
|
||||
AccessKeyID string
|
||||
SecretAccessKey string
|
||||
SessionToken string
|
||||
Actions []string
|
||||
// TokenClaims claims struct for decrypted credentials
|
||||
type TokenClaims struct {
|
||||
STSAccessKeyID string `json:"stsAccessKeyID,omitempty"`
|
||||
STSSecretAccessKey string `json:"stsSecretAccessKey,omitempty"`
|
||||
STSSessionToken string `json:"stsSessionToken,omitempty"`
|
||||
AccountAccessKey string `json:"accountAccessKey,omitempty"`
|
||||
AccountSecretKey string `json:"accountSecretKey,omitempty"`
|
||||
Actions []string `json:"actions,omitempty"`
|
||||
}
|
||||
|
||||
// SessionTokenAuthenticate takes a session token, decode it, extract claims and validate the signature
|
||||
@@ -71,12 +74,15 @@ type DecryptedClaims struct {
|
||||
//
|
||||
// returns claims after validation in the following format:
|
||||
//
|
||||
// type DecryptedClaims struct {
|
||||
// AccessKeyID
|
||||
// SecretAccessKey
|
||||
// SessionToken
|
||||
// type TokenClaims struct {
|
||||
// STSAccessKeyID
|
||||
// STSSecretAccessKey
|
||||
// STSSessionToken
|
||||
// AccountAccessKey
|
||||
// AccountSecretKey
|
||||
// Actions
|
||||
// }
|
||||
func SessionTokenAuthenticate(token string) (*DecryptedClaims, error) {
|
||||
func SessionTokenAuthenticate(token string) (*TokenClaims, error) {
|
||||
if token == "" {
|
||||
return nil, errNoAuthToken
|
||||
}
|
||||
@@ -94,9 +100,16 @@ func SessionTokenAuthenticate(token string) (*DecryptedClaims, error) {
|
||||
|
||||
// NewEncryptedTokenForClient generates a new session token with claims based on the provided STS credentials, first
|
||||
// encrypts the claims and the sign them
|
||||
func NewEncryptedTokenForClient(credentials *credentials.Value, actions []string) (string, error) {
|
||||
func NewEncryptedTokenForClient(credentials *credentials.Value, accountAccessKey, accountSecretKey string, actions []string) (string, error) {
|
||||
if credentials != nil {
|
||||
encryptedClaims, err := encryptClaims(credentials.AccessKeyID, credentials.SecretAccessKey, credentials.SessionToken, actions)
|
||||
encryptedClaims, err := encryptClaims(&TokenClaims{
|
||||
STSAccessKeyID: credentials.AccessKeyID,
|
||||
STSSecretAccessKey: credentials.SecretAccessKey,
|
||||
STSSessionToken: credentials.SessionToken,
|
||||
AccountAccessKey: accountAccessKey,
|
||||
AccountSecretKey: accountSecretKey,
|
||||
Actions: actions,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -107,8 +120,11 @@ func NewEncryptedTokenForClient(credentials *credentials.Value, actions []string
|
||||
|
||||
// encryptClaims() receives the STS claims, concatenate them and encrypt them using AES-GCM
|
||||
// returns a base64 encoded ciphertext
|
||||
func encryptClaims(accessKeyID, secretAccessKey, sessionToken string, actions []string) (string, error) {
|
||||
payload := []byte(fmt.Sprintf("%s#%s#%s#%s", accessKeyID, secretAccessKey, sessionToken, strings.Join(actions, ",")))
|
||||
func encryptClaims(credentials *TokenClaims) (string, error) {
|
||||
payload, err := json.Marshal(credentials)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ciphertext, err := encrypt(payload, []byte{})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
@@ -117,8 +133,8 @@ func encryptClaims(accessKeyID, secretAccessKey, sessionToken string, actions []
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// decryptClaims() receives base64 encoded ciphertext, decode it, decrypt it (AES-GCM) and produces a *DecryptedClaims object
|
||||
func decryptClaims(ciphertext string) (*DecryptedClaims, error) {
|
||||
// decryptClaims() receives base64 encoded ciphertext, decode it, decrypt it (AES-GCM) and produces a *TokenClaims object
|
||||
func decryptClaims(ciphertext string) (*TokenClaims, error) {
|
||||
decoded, err := base64.StdEncoding.DecodeString(ciphertext)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
@@ -129,19 +145,13 @@ func decryptClaims(ciphertext string) (*DecryptedClaims, error) {
|
||||
log.Println(err)
|
||||
return nil, errClaimsFormat
|
||||
}
|
||||
s := strings.Split(string(plaintext), "#")
|
||||
// Validate that the decrypted string has the right format "accessKeyID:secretAccessKey:sessionToken"
|
||||
if len(s) != 4 {
|
||||
tokenClaims := &TokenClaims{}
|
||||
err = json.Unmarshal(plaintext, tokenClaims)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil, errClaimsFormat
|
||||
}
|
||||
accessKeyID, secretAccessKey, sessionToken, actions := s[0], s[1], s[2], s[3]
|
||||
actionsList := strings.Split(actions, ",")
|
||||
return &DecryptedClaims{
|
||||
AccessKeyID: accessKeyID,
|
||||
SecretAccessKey: secretAccessKey,
|
||||
SessionToken: sessionToken,
|
||||
Actions: actionsList,
|
||||
}, nil
|
||||
return tokenClaims, nil
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -315,9 +325,11 @@ func GetClaimsFromTokenInRequest(req *http.Request) (*models.Principal, error) {
|
||||
return nil, err
|
||||
}
|
||||
return &models.Principal{
|
||||
AccessKeyID: claims.AccessKeyID,
|
||||
Actions: claims.Actions,
|
||||
SecretAccessKey: claims.SecretAccessKey,
|
||||
SessionToken: claims.SessionToken,
|
||||
STSAccessKeyID: claims.STSAccessKeyID,
|
||||
Actions: claims.Actions,
|
||||
STSSecretAccessKey: claims.STSSecretAccessKey,
|
||||
STSSessionToken: claims.STSSessionToken,
|
||||
AccountAccessKey: claims.AccountAccessKey,
|
||||
AccountSecretKey: claims.AccountSecretKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -36,14 +36,14 @@ func TestNewJWTWithClaimsForClient(t *testing.T) {
|
||||
funcAssert := assert.New(t)
|
||||
// Test-1 : NewEncryptedTokenForClient() is generated correctly without errors
|
||||
function := "NewEncryptedTokenForClient()"
|
||||
token, err := NewEncryptedTokenForClient(creds, []string{""})
|
||||
token, err := NewEncryptedTokenForClient(creds, "", "", []string{""})
|
||||
if err != nil || token == "" {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err)
|
||||
}
|
||||
// saving token for future tests
|
||||
goodToken = token
|
||||
// Test-2 : NewEncryptedTokenForClient() throws error because of empty credentials
|
||||
if _, err = NewEncryptedTokenForClient(nil, []string{""}); err != nil {
|
||||
if _, err = NewEncryptedTokenForClient(nil, "", "", []string{""}); err != nil {
|
||||
funcAssert.Equal("provided credentials are empty", err.Error())
|
||||
}
|
||||
}
|
||||
@@ -56,9 +56,9 @@ func TestJWTAuthenticate(t *testing.T) {
|
||||
if err != nil || claims == nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err)
|
||||
} else {
|
||||
funcAssert.Equal(claims.AccessKeyID, creds.AccessKeyID)
|
||||
funcAssert.Equal(claims.SecretAccessKey, creds.SecretAccessKey)
|
||||
funcAssert.Equal(claims.SessionToken, creds.SessionToken)
|
||||
funcAssert.Equal(claims.STSAccessKeyID, creds.AccessKeyID)
|
||||
funcAssert.Equal(claims.STSSecretAccessKey, creds.SecretAccessKey)
|
||||
funcAssert.Equal(claims.STSSessionToken, creds.SessionToken)
|
||||
}
|
||||
// Test-2 : SessionTokenAuthenticate() return an error because of a tampered token
|
||||
if _, err := SessionTokenAuthenticate(badToken); err != nil {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -36,6 +36,9 @@ import {
|
||||
containerForHeader,
|
||||
searchField,
|
||||
} from "../Common/FormComponents/common/styleLibrary";
|
||||
import Divider from "@material-ui/core/Divider";
|
||||
import LockIcon from "@material-ui/icons/Lock";
|
||||
import ChangePasswordModal from "./ChangePasswordModal";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
@@ -87,7 +90,7 @@ interface IServiceAccountsProps {
|
||||
classes: any;
|
||||
}
|
||||
|
||||
const ServiceAccounts = ({ classes }: IServiceAccountsProps) => {
|
||||
const Account = ({ classes }: IServiceAccountsProps) => {
|
||||
const [records, setRecords] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>("");
|
||||
@@ -102,6 +105,10 @@ const ServiceAccounts = ({ classes }: IServiceAccountsProps) => {
|
||||
newServiceAccount,
|
||||
setNewServiceAccount,
|
||||
] = useState<NewServiceAccount | null>(null);
|
||||
const [
|
||||
changePasswordModalOpen,
|
||||
setChangePasswordModalOpen,
|
||||
] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchRecords();
|
||||
@@ -194,9 +201,44 @@ const ServiceAccounts = ({ classes }: IServiceAccountsProps) => {
|
||||
entity="Service Account"
|
||||
/>
|
||||
)}
|
||||
<PageHeader label="Service Accounts" />
|
||||
<PageHeader label="Account" />
|
||||
<Grid container>
|
||||
<Grid item xs={12} className={classes.container}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h5" component="h5">
|
||||
Settings
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<ChangePasswordModal
|
||||
open={changePasswordModalOpen}
|
||||
closeModal={() => setChangePasswordModalOpen(false)}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<LockIcon />}
|
||||
onClick={() => setChangePasswordModalOpen(true)}
|
||||
>
|
||||
Change Password
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
<Divider />
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h5" component="h5">
|
||||
Service Accounts
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.actionsTray}>
|
||||
<TextField
|
||||
placeholder="Search Service Accounts"
|
||||
@@ -257,4 +299,4 @@ const ServiceAccounts = ({ classes }: IServiceAccountsProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default withStyles(styles)(ServiceAccounts);
|
||||
export default withStyles(styles)(Account);
|
||||
197
portal-ui/src/screens/Console/Account/ChangePasswordModal.tsx
Normal file
197
portal-ui/src/screens/Console/Account/ChangePasswordModal.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
|
||||
import { Button, LinearProgress } from "@material-ui/core";
|
||||
import {
|
||||
actionsTray,
|
||||
containerForHeader,
|
||||
modalBasic,
|
||||
} from "../Common/FormComponents/common/styleLibrary";
|
||||
import { ChangePasswordRequest } from "../Buckets/types";
|
||||
import api from "../../../common/api";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
errorBlock: {
|
||||
color: "red",
|
||||
},
|
||||
buttonContainer: {
|
||||
textAlign: "right",
|
||||
},
|
||||
...actionsTray,
|
||||
...modalBasic,
|
||||
...containerForHeader(theme.spacing(4)),
|
||||
});
|
||||
|
||||
interface IChangePasswordProps {
|
||||
classes: any;
|
||||
open: boolean;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
const ChangePassword = ({
|
||||
classes,
|
||||
open,
|
||||
closeModal,
|
||||
}: IChangePasswordProps) => {
|
||||
const [currentPassword, setCurrentPassword] = useState<string>("");
|
||||
const [newPassword, setNewPassword] = useState<string>("");
|
||||
const [reNewPassword, setReNewPassword] = useState<string>("");
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const changePassword = (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (newPassword != reNewPassword) {
|
||||
setError("New passwords don't match");
|
||||
return;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
|
||||
let request: ChangePasswordRequest = {
|
||||
current_secret_key: currentPassword,
|
||||
new_secret_key: newPassword,
|
||||
};
|
||||
|
||||
api
|
||||
.invoke("POST", "/api/v1/account/change-password", request)
|
||||
.then((res) => {
|
||||
setLoading(false);
|
||||
setError("");
|
||||
setNewPassword("");
|
||||
setReNewPassword("");
|
||||
setCurrentPassword("");
|
||||
closeModal();
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
setNewPassword("");
|
||||
setReNewPassword("");
|
||||
setCurrentPassword("");
|
||||
setError(err);
|
||||
});
|
||||
};
|
||||
|
||||
return open ? (
|
||||
<ModalWrapper
|
||||
title="Change Password"
|
||||
modalOpen={open}
|
||||
onClose={() => {
|
||||
setNewPassword("");
|
||||
setReNewPassword("");
|
||||
setCurrentPassword("");
|
||||
closeModal();
|
||||
}}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<form
|
||||
noValidate
|
||||
autoComplete="off"
|
||||
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
|
||||
changePassword(e);
|
||||
}}
|
||||
>
|
||||
<Grid container>
|
||||
<Grid item xs={12} className={classes.formScrollable}>
|
||||
{error !== "" && (
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="current-password"
|
||||
name="current-password"
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setCurrentPassword(event.target.value);
|
||||
}}
|
||||
label="Current Password"
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="new-password"
|
||||
name="new-password"
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNewPassword(event.target.value);
|
||||
}}
|
||||
label="New Password"
|
||||
type="password"
|
||||
value={newPassword}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="re-new-password"
|
||||
name="re-new-password"
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setReNewPassword(event.target.value);
|
||||
}}
|
||||
label="Type New Password Again"
|
||||
type="password"
|
||||
value={reNewPassword}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.buttonContainer}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={
|
||||
loading ||
|
||||
!(
|
||||
currentPassword.length > 0 &&
|
||||
newPassword.length > 0 &&
|
||||
reNewPassword.length > 0
|
||||
)
|
||||
}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Grid>
|
||||
{loading && (
|
||||
<Grid item xs={12}>
|
||||
<LinearProgress />
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</form>
|
||||
</ModalWrapper>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default withStyles(styles)(ChangePassword);
|
||||
@@ -85,6 +85,11 @@ export interface MakeBucketRequest {
|
||||
quota?: QuotaRequest;
|
||||
}
|
||||
|
||||
export interface ChangePasswordRequest {
|
||||
current_secret_key: string;
|
||||
new_secret_key: string;
|
||||
}
|
||||
|
||||
export interface IRemoteBucket {
|
||||
name: string;
|
||||
accessKey: string;
|
||||
|
||||
@@ -34,7 +34,7 @@ import Policies from "./Policies/Policies";
|
||||
import Dashboard from "./Dashboard/Dashboard";
|
||||
import Menu from "./Menu/Menu";
|
||||
import api from "../../common/api";
|
||||
import ServiceAccounts from "./ServiceAccounts/ServiceAccounts";
|
||||
import Account from "./Account/Account";
|
||||
import Users from "./Users/Users";
|
||||
import Groups from "./Groups/Groups";
|
||||
import ListNotificationEndpoints from "./NotificationEndopoints/ListNotificationEndpoints";
|
||||
@@ -237,8 +237,12 @@ const Console = ({
|
||||
path: "/configurations-list",
|
||||
},
|
||||
{
|
||||
component: ServiceAccounts,
|
||||
path: "/service-accounts",
|
||||
component: Permissions,
|
||||
path: "/permissions",
|
||||
},
|
||||
{
|
||||
component: Account,
|
||||
path: "/account",
|
||||
},
|
||||
{
|
||||
component: WebhookPanel,
|
||||
@@ -290,20 +294,20 @@ const Console = ({
|
||||
<LinearProgress />
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
The instance needs to be restarted for configuration changes
|
||||
<React.Fragment>
|
||||
The instance needs to be restarted for configuration changes
|
||||
to take effect.{" "}
|
||||
<Button
|
||||
color="secondary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
restartServer();
|
||||
}}
|
||||
>
|
||||
Restart
|
||||
<Button
|
||||
color="secondary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
restartServer();
|
||||
}}
|
||||
>
|
||||
Restart
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Container className={classes.container}>
|
||||
|
||||
@@ -202,8 +202,8 @@ const Menu = ({ userLoggedIn, classes, pages }: IMenuProps) => {
|
||||
group: "User",
|
||||
type: "item",
|
||||
component: NavLink,
|
||||
to: "/service-accounts",
|
||||
name: "Service Accounts",
|
||||
to: "/account",
|
||||
name: "Account",
|
||||
icon: <ServiceAccountsIcon />,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -123,7 +123,7 @@ func min(x, y int64) int64 {
|
||||
|
||||
func getMaxAllocatableMemoryResponse(session *models.Principal, numNodes int32) (*models.MaxAllocatableMemResponse, *models.Error) {
|
||||
ctx := context.Background()
|
||||
client, err := cluster.K8sClient(session.SessionToken)
|
||||
client, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
}
|
||||
|
||||
@@ -163,12 +163,12 @@ func registerTenantHandlers(api *operations.ConsoleAPI) {
|
||||
|
||||
// getDeleteTenantResponse gets the output of deleting a minio instance
|
||||
func getDeleteTenantResponse(session *models.Principal, params admin_api.DeleteTenantParams) *models.Error {
|
||||
opClientClientSet, err := cluster.OperatorClient(session.SessionToken)
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err)
|
||||
}
|
||||
// get Kubernetes Client
|
||||
clientset, err := cluster.K8sClient(session.SessionToken)
|
||||
clientset, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err)
|
||||
}
|
||||
@@ -336,7 +336,7 @@ func getTenantInfoResponse(session *models.Principal, params admin_api.TenantInf
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
opClientClientSet, err := cluster.OperatorClient(session.SessionToken)
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
}
|
||||
@@ -409,7 +409,7 @@ func listTenants(ctx context.Context, operatorClient OperatorClientI, namespace
|
||||
|
||||
func getListAllTenantsResponse(session *models.Principal, params admin_api.ListAllTenantsParams) (*models.ListTenantsResponse, *models.Error) {
|
||||
ctx := context.Background()
|
||||
opClientClientSet, err := cluster.OperatorClient(session.SessionToken)
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
}
|
||||
@@ -426,7 +426,7 @@ func getListAllTenantsResponse(session *models.Principal, params admin_api.ListA
|
||||
// getListTenantsResponse list tenants by namespace
|
||||
func getListTenantsResponse(session *models.Principal, params admin_api.ListTenantsParams) (*models.ListTenantsResponse, *models.Error) {
|
||||
ctx := context.Background()
|
||||
opClientClientSet, err := cluster.OperatorClient(session.SessionToken)
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
}
|
||||
@@ -454,7 +454,7 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create
|
||||
}
|
||||
}
|
||||
// get Kubernetes Client
|
||||
clientSet, err := cluster.K8sClient(session.SessionToken)
|
||||
clientSet, err := cluster.K8sClient(session.STSSessionToken)
|
||||
k8sClient := k8sClient{
|
||||
client: clientSet,
|
||||
}
|
||||
@@ -779,7 +779,7 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create
|
||||
minInst.Spec.Console.Image = tenantReq.ConsoleImage
|
||||
}
|
||||
|
||||
opClient, err := cluster.OperatorClient(session.SessionToken)
|
||||
opClient, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
}
|
||||
@@ -792,7 +792,7 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create
|
||||
|
||||
// Integratrions
|
||||
if os.Getenv("GKE_INTEGRATION") != "" {
|
||||
err := gkeIntegration(clientSet, tenantName, ns, session.SessionToken)
|
||||
err := gkeIntegration(clientSet, tenantName, ns, session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
}
|
||||
@@ -967,12 +967,12 @@ func removeAnnotations(annotationsOne, annotationsTwo map[string]string) map[str
|
||||
|
||||
func getUpdateTenantResponse(session *models.Principal, params admin_api.UpdateTenantParams) *models.Error {
|
||||
ctx := context.Background()
|
||||
opClientClientSet, err := cluster.OperatorClient(session.SessionToken)
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err)
|
||||
}
|
||||
// get Kubernetes Client
|
||||
clientSet, err := cluster.K8sClient(session.SessionToken)
|
||||
clientSet, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err)
|
||||
}
|
||||
@@ -1017,7 +1017,7 @@ func addTenantPool(ctx context.Context, operatorClient OperatorClientI, params a
|
||||
|
||||
func getTenantAddPoolResponse(session *models.Principal, params admin_api.TenantAddPoolParams) *models.Error {
|
||||
ctx := context.Background()
|
||||
opClientClientSet, err := cluster.OperatorClient(session.SessionToken)
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err)
|
||||
}
|
||||
@@ -1036,11 +1036,11 @@ func getTenantUsageResponse(session *models.Principal, params admin_api.GetTenan
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
opClientClientSet, err := cluster.OperatorClient(session.SessionToken)
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err, errorUnableToGetTenantUsage)
|
||||
}
|
||||
clientSet, err := cluster.K8sClient(session.SessionToken)
|
||||
clientSet, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err, errorUnableToGetTenantUsage)
|
||||
}
|
||||
@@ -1496,7 +1496,7 @@ func parseNodeSelectorTerm(term *corev1.NodeSelectorTerm) *models.NodeSelectorTe
|
||||
|
||||
func getTenantUpdatePoolResponse(session *models.Principal, params admin_api.TenantUpdatePoolsParams) (*models.Tenant, *models.Error) {
|
||||
ctx := context.Background()
|
||||
opClientClientSet, err := cluster.OperatorClient(session.SessionToken)
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
}
|
||||
|
||||
@@ -85,14 +85,14 @@ func tenantUpdateCertificates(ctx context.Context, operatorClient OperatorClient
|
||||
func getTenantUpdateCertificatesResponse(session *models.Principal, params admin_api.TenantUpdateCertificateParams) *models.Error {
|
||||
ctx := context.Background()
|
||||
// get Kubernetes Client
|
||||
clientSet, err := cluster.K8sClient(session.SessionToken)
|
||||
clientSet, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err, errorUnableToUpdateTenantCertificates)
|
||||
}
|
||||
k8sClient := k8sClient{
|
||||
client: clientSet,
|
||||
}
|
||||
opClientClientSet, err := cluster.OperatorClient(session.SessionToken)
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err, errorUnableToUpdateTenantCertificates)
|
||||
}
|
||||
@@ -163,14 +163,14 @@ func tenantUpdateEncryption(ctx context.Context, operatorClient OperatorClientI,
|
||||
func getTenantUpdateEncryptionResponse(session *models.Principal, params admin_api.TenantUpdateEncryptionParams) *models.Error {
|
||||
ctx := context.Background()
|
||||
// get Kubernetes Client
|
||||
clientSet, err := cluster.K8sClient(session.SessionToken)
|
||||
clientSet, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err, errorUpdatingEncryptionConfig)
|
||||
}
|
||||
k8sClient := k8sClient{
|
||||
client: clientSet,
|
||||
}
|
||||
opClientClientSet, err := cluster.OperatorClient(session.SessionToken)
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err, errorUpdatingEncryptionConfig)
|
||||
}
|
||||
|
||||
@@ -47,3 +47,7 @@ func (ac adminClientMock) removeRemoteBucket(ctx context.Context, bucket, arn st
|
||||
func (ac adminClientMock) addRemoteBucket(ctx context.Context, bucket string, target *madmin.BucketTarget) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (ac adminClientMock) changePassword(ctx context.Context, accessKey, secretKey string) error {
|
||||
return minioChangePasswordMock(ctx, accessKey, secretKey)
|
||||
}
|
||||
|
||||
@@ -105,6 +105,8 @@ type MinioAdmin interface {
|
||||
getRemoteBucket(ctx context.Context, bucket, arnType string) (targets *madmin.BucketTarget, err error)
|
||||
removeRemoteBucket(ctx context.Context, bucket, arn string) error
|
||||
addRemoteBucket(ctx context.Context, bucket string, target *madmin.BucketTarget) (string, error)
|
||||
// Account password management
|
||||
changePassword(ctx context.Context, accessKey, secretKey string) error
|
||||
}
|
||||
|
||||
// Interface implementation
|
||||
@@ -115,14 +117,18 @@ type adminClient struct {
|
||||
client *madmin.AdminClient
|
||||
}
|
||||
|
||||
func (ac adminClient) changePassword(ctx context.Context, accessKey, secretKey string) error {
|
||||
return ac.client.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled)
|
||||
}
|
||||
|
||||
// implements madmin.ListUsers()
|
||||
func (ac adminClient) listUsers(ctx context.Context) (map[string]madmin.UserInfo, error) {
|
||||
return ac.client.ListUsers(ctx)
|
||||
}
|
||||
|
||||
// implements madmin.AddUser()
|
||||
func (ac adminClient) addUser(ctx context.Context, acessKey, secretKey string) error {
|
||||
return ac.client.AddUser(ctx, acessKey, secretKey)
|
||||
func (ac adminClient) addUser(ctx context.Context, accessKey, secretKey string) error {
|
||||
return ac.client.AddUser(ctx, accessKey, secretKey)
|
||||
}
|
||||
|
||||
// implements madmin.RemoveUser()
|
||||
@@ -301,7 +307,7 @@ func newAdminFromClaims(claims *models.Principal) (*madmin.AdminClient, error) {
|
||||
endpoint := getMinIOEndpoint()
|
||||
|
||||
adminClient, err := madmin.NewWithOptions(endpoint, &madmin.Options{
|
||||
Creds: credentials.NewStaticV4(claims.AccessKeyID, claims.SecretAccessKey, claims.SessionToken),
|
||||
Creds: credentials.NewStaticV4(claims.STSAccessKeyID, claims.STSSecretAccessKey, claims.STSSessionToken),
|
||||
Secure: tlsEnabled,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -229,17 +229,35 @@ func (c mcClient) shareDownload(ctx context.Context, versionID string, expires t
|
||||
return c.client.ShareDownload(ctx, versionID, expires)
|
||||
}
|
||||
|
||||
// ConsoleCredentials interface with all functions to be implemented
|
||||
// ConsoleCredentialsI interface with all functions to be implemented
|
||||
// by mock when testing, it should include all needed consoleCredentials.Login api calls
|
||||
// that are used within this project.
|
||||
type ConsoleCredentials interface {
|
||||
type ConsoleCredentialsI interface {
|
||||
Get() (credentials.Value, error)
|
||||
Expire()
|
||||
GetAccountAccessKey() string
|
||||
GetAccountSecretKey() string
|
||||
GetActions() []string
|
||||
}
|
||||
|
||||
// Interface implementation
|
||||
type consoleCredentials struct {
|
||||
consoleCredentials *credentials.Credentials
|
||||
accountAccessKey string
|
||||
accountSecretKey string
|
||||
actions []string
|
||||
}
|
||||
|
||||
func (c consoleCredentials) GetActions() []string {
|
||||
return c.actions
|
||||
}
|
||||
|
||||
func (c consoleCredentials) GetAccountAccessKey() string {
|
||||
return c.accountAccessKey
|
||||
}
|
||||
|
||||
func (c consoleCredentials) GetAccountSecretKey() string {
|
||||
return c.accountSecretKey
|
||||
}
|
||||
|
||||
// implements *Login.Get()
|
||||
@@ -269,6 +287,7 @@ func (s consoleSTSAssumeRole) IsExpired() bool {
|
||||
|
||||
var (
|
||||
MinioEndpoint = getMinIOServer()
|
||||
MinioRegion = getMinIORegion()
|
||||
)
|
||||
|
||||
func newConsoleCredentials(accessKey, secretKey, location string) (*credentials.Credentials, error) {
|
||||
@@ -321,7 +340,7 @@ func newConsoleCredentials(accessKey, secretKey, location string) (*credentials.
|
||||
// getConsoleCredentialsFromSession returns the *consoleCredentials.Login associated to the
|
||||
// provided session token, this is useful for running the Expire() or IsExpired() operations
|
||||
func getConsoleCredentialsFromSession(claims *models.Principal) *credentials.Credentials {
|
||||
return credentials.NewStaticV4(claims.AccessKeyID, claims.SecretAccessKey, claims.SessionToken)
|
||||
return credentials.NewStaticV4(claims.STSAccessKeyID, claims.STSSecretAccessKey, claims.STSSessionToken)
|
||||
}
|
||||
|
||||
// newMinioClient creates a new MinIO client based on the consoleCredentials extracted
|
||||
@@ -355,7 +374,7 @@ func newS3BucketClient(claims *models.Principal, bucketName string, prefix strin
|
||||
return nil, fmt.Errorf("the provided credentials are invalid")
|
||||
}
|
||||
|
||||
s3Config := newS3Config(endpoint, claims.AccessKeyID, claims.SecretAccessKey, claims.SessionToken, false)
|
||||
s3Config := newS3Config(endpoint, claims.STSAccessKeyID, claims.STSSecretAccessKey, claims.STSSessionToken, false)
|
||||
client, pErr := mc.S3New(s3Config)
|
||||
if pErr != nil {
|
||||
return nil, pErr.Cause
|
||||
@@ -378,7 +397,7 @@ func newTenantS3BucketClient(claims *models.Principal, tenantEndpoint, bucketNam
|
||||
return nil, fmt.Errorf("the provided credentials are invalid")
|
||||
}
|
||||
|
||||
s3Config := newS3Config(tenantEndpoint, claims.AccessKeyID, claims.SecretAccessKey, claims.SessionToken, false)
|
||||
s3Config := newS3Config(tenantEndpoint, claims.STSAccessKeyID, claims.STSSecretAccessKey, claims.STSSessionToken, false)
|
||||
client, pErr := mc.S3New(s3Config)
|
||||
if pErr != nil {
|
||||
return nil, pErr.Cause
|
||||
|
||||
@@ -56,6 +56,10 @@ func getMinIOServer() string {
|
||||
return strings.TrimSpace(env.Get(ConsoleMinIOServer, "http://localhost:9000"))
|
||||
}
|
||||
|
||||
func getMinIORegion() string {
|
||||
return strings.TrimSpace(env.Get(ConsoleMinIORegion, ""))
|
||||
}
|
||||
|
||||
func getMinIOEndpoint() string {
|
||||
server := getMinIOServer()
|
||||
if strings.Contains(server, "://") {
|
||||
|
||||
@@ -80,10 +80,12 @@ func configureAPI(api *operations.ConsoleAPI) http.Handler {
|
||||
return nil, errors.New(401, "incorrect api key auth")
|
||||
}
|
||||
return &models.Principal{
|
||||
AccessKeyID: claims.AccessKeyID,
|
||||
Actions: claims.Actions,
|
||||
SecretAccessKey: claims.SecretAccessKey,
|
||||
SessionToken: claims.SessionToken,
|
||||
STSAccessKeyID: claims.STSAccessKeyID,
|
||||
Actions: claims.Actions,
|
||||
STSSecretAccessKey: claims.STSSecretAccessKey,
|
||||
STSSessionToken: claims.STSSessionToken,
|
||||
AccountAccessKey: claims.AccountAccessKey,
|
||||
AccountSecretKey: claims.AccountSecretKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -135,6 +137,8 @@ func configureAPI(api *operations.ConsoleAPI) http.Handler {
|
||||
registerBucketQuotaHandlers(api)
|
||||
// List buckets
|
||||
registerOperatorBucketsHandlers(api)
|
||||
// Register Account handlers
|
||||
registerAccountHandlers(api)
|
||||
|
||||
api.PreServerShutdown = func() {}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ const (
|
||||
ConsoleAccessKey = "CONSOLE_ACCESS_KEY"
|
||||
ConsoleSecretKey = "CONSOLE_SECRET_KEY"
|
||||
ConsoleMinIOServer = "CONSOLE_MINIO_SERVER"
|
||||
ConsoleMinIORegion = "CONSOLE_MINIO_REGION"
|
||||
ConsoleProductionMode = "CONSOLE_PRODUCTION_MODE"
|
||||
ConsoleHostname = "CONSOLE_HOSTNAME"
|
||||
ConsolePort = "CONSOLE_PORT"
|
||||
|
||||
@@ -52,6 +52,39 @@ func init() {
|
||||
},
|
||||
"basePath": "/api/v1",
|
||||
"paths": {
|
||||
"/account/change-password": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"UserAPI"
|
||||
],
|
||||
"summary": "Change password of currently logged in user.",
|
||||
"operationId": "AccountChangePassword",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/accountChangePasswordRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "A successful login.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/loginResponse"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Generic error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/admin/arns": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -2909,6 +2942,21 @@ func init() {
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"accountChangePasswordRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"current_secret_key",
|
||||
"new_secret_key"
|
||||
],
|
||||
"properties": {
|
||||
"current_secret_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"new_secret_key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"addBucketReplication": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -4490,7 +4538,19 @@ func init() {
|
||||
"principal": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"accessKeyID": {
|
||||
"STSAccessKeyID": {
|
||||
"type": "string"
|
||||
},
|
||||
"STSSecretAccessKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"STSSessionToken": {
|
||||
"type": "string"
|
||||
},
|
||||
"accountAccessKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"accountSecretKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"actions": {
|
||||
@@ -4498,12 +4558,6 @@ func init() {
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"secretAccessKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"sessionToken": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -5112,6 +5166,39 @@ func init() {
|
||||
},
|
||||
"basePath": "/api/v1",
|
||||
"paths": {
|
||||
"/account/change-password": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"UserAPI"
|
||||
],
|
||||
"summary": "Change password of currently logged in user.",
|
||||
"operationId": "AccountChangePassword",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/accountChangePasswordRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "A successful login.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/loginResponse"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Generic error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/admin/arns": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -8492,6 +8579,21 @@ func init() {
|
||||
}
|
||||
}
|
||||
},
|
||||
"accountChangePasswordRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"current_secret_key",
|
||||
"new_secret_key"
|
||||
],
|
||||
"properties": {
|
||||
"current_secret_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"new_secret_key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"addBucketReplication": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -9938,7 +10040,19 @@ func init() {
|
||||
"principal": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"accessKeyID": {
|
||||
"STSAccessKeyID": {
|
||||
"type": "string"
|
||||
},
|
||||
"STSSecretAccessKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"STSSessionToken": {
|
||||
"type": "string"
|
||||
},
|
||||
"accountAccessKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"accountSecretKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"actions": {
|
||||
@@ -9946,12 +10060,6 @@ func init() {
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"secretAccessKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"sessionToken": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -18,7 +18,6 @@ var (
|
||||
errorGenericUnauthorized = errors.New("unauthorized")
|
||||
errorGenericForbidden = errors.New("forbidden")
|
||||
errorGenericNotFound = errors.New("not found")
|
||||
errConnectingToMinio = errors.New("unable to connect to MinIO instance")
|
||||
// Explicit error messages
|
||||
errorInvalidErasureCodingValue = errors.New("invalid Erasure Coding Value")
|
||||
errorUnableToGetTenantUsage = errors.New("unable to get tenant usage")
|
||||
@@ -32,6 +31,7 @@ var (
|
||||
errPolicyBodyNotInRequest = errors.New("error policy body not in request")
|
||||
errInvalidEncryptionAlgorithm = errors.New("error invalid encryption algorithm")
|
||||
errSSENotConfigured = errors.New("error server side encryption configuration was not found")
|
||||
errChangePassword = errors.New("unable to update password, please check your current password")
|
||||
)
|
||||
|
||||
// prepareError receives an error object and parse it against k8sErrors, returns the right error code paired with a generic error message
|
||||
@@ -90,6 +90,11 @@ func prepareError(err ...error) *models.Error {
|
||||
errorCode = 401
|
||||
errorMessage = errorGenericInvalidSession.Error()
|
||||
}
|
||||
// account change password
|
||||
if errors.Is(err[0], errChangePassword) {
|
||||
errorCode = 403
|
||||
errorMessage = errChangePassword.Error()
|
||||
}
|
||||
if madmin.ToErrorResponse(err[0]).Code == "InvalidAccessKeyId" {
|
||||
errorCode = 401
|
||||
errorMessage = errorGenericInvalidSession.Error()
|
||||
|
||||
@@ -65,6 +65,9 @@ func NewConsoleAPI(spec *loads.Document) *ConsoleAPI {
|
||||
BinProducer: runtime.ByteStreamProducer(),
|
||||
JSONProducer: runtime.JSONProducer(),
|
||||
|
||||
UserAPIAccountChangePasswordHandler: user_api.AccountChangePasswordHandlerFunc(func(params user_api.AccountChangePasswordParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation user_api.AccountChangePassword has not yet been implemented")
|
||||
}),
|
||||
UserAPIAddBucketReplicationHandler: user_api.AddBucketReplicationHandlerFunc(func(params user_api.AddBucketReplicationParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation user_api.AddBucketReplication has not yet been implemented")
|
||||
}),
|
||||
@@ -366,6 +369,8 @@ type ConsoleAPI struct {
|
||||
// APIAuthorizer provides access control (ACL/RBAC/ABAC) by providing access to the request and authenticated principal
|
||||
APIAuthorizer runtime.Authorizer
|
||||
|
||||
// UserAPIAccountChangePasswordHandler sets the operation handler for the account change password operation
|
||||
UserAPIAccountChangePasswordHandler user_api.AccountChangePasswordHandler
|
||||
// UserAPIAddBucketReplicationHandler sets the operation handler for the add bucket replication operation
|
||||
UserAPIAddBucketReplicationHandler user_api.AddBucketReplicationHandler
|
||||
// AdminAPIAddGroupHandler sets the operation handler for the add group operation
|
||||
@@ -608,6 +613,9 @@ func (o *ConsoleAPI) Validate() error {
|
||||
unregistered = append(unregistered, "KeyAuth")
|
||||
}
|
||||
|
||||
if o.UserAPIAccountChangePasswordHandler == nil {
|
||||
unregistered = append(unregistered, "user_api.AccountChangePasswordHandler")
|
||||
}
|
||||
if o.UserAPIAddBucketReplicationHandler == nil {
|
||||
unregistered = append(unregistered, "user_api.AddBucketReplicationHandler")
|
||||
}
|
||||
@@ -959,6 +967,10 @@ func (o *ConsoleAPI) initHandlerCache() {
|
||||
o.handlers = make(map[string]map[string]http.Handler)
|
||||
}
|
||||
|
||||
if o.handlers["POST"] == nil {
|
||||
o.handlers["POST"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["POST"]["/account/change-password"] = user_api.NewAccountChangePassword(o.context, o.UserAPIAccountChangePasswordHandler)
|
||||
if o.handlers["POST"] == nil {
|
||||
o.handlers["POST"] = make(map[string]http.Handler)
|
||||
}
|
||||
|
||||
90
restapi/operations/user_api/account_change_password.go
Normal file
90
restapi/operations/user_api/account_change_password.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the generate command
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
)
|
||||
|
||||
// AccountChangePasswordHandlerFunc turns a function with the right signature into a account change password handler
|
||||
type AccountChangePasswordHandlerFunc func(AccountChangePasswordParams, *models.Principal) middleware.Responder
|
||||
|
||||
// Handle executing the request and returning a response
|
||||
func (fn AccountChangePasswordHandlerFunc) Handle(params AccountChangePasswordParams, principal *models.Principal) middleware.Responder {
|
||||
return fn(params, principal)
|
||||
}
|
||||
|
||||
// AccountChangePasswordHandler interface for that can handle valid account change password params
|
||||
type AccountChangePasswordHandler interface {
|
||||
Handle(AccountChangePasswordParams, *models.Principal) middleware.Responder
|
||||
}
|
||||
|
||||
// NewAccountChangePassword creates a new http.Handler for the account change password operation
|
||||
func NewAccountChangePassword(ctx *middleware.Context, handler AccountChangePasswordHandler) *AccountChangePassword {
|
||||
return &AccountChangePassword{Context: ctx, Handler: handler}
|
||||
}
|
||||
|
||||
/*AccountChangePassword swagger:route POST /account/change-password UserAPI accountChangePassword
|
||||
|
||||
Change password of currently logged in user.
|
||||
|
||||
*/
|
||||
type AccountChangePassword struct {
|
||||
Context *middleware.Context
|
||||
Handler AccountChangePasswordHandler
|
||||
}
|
||||
|
||||
func (o *AccountChangePassword) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
route, rCtx, _ := o.Context.RouteInfo(r)
|
||||
if rCtx != nil {
|
||||
r = rCtx
|
||||
}
|
||||
var Params = NewAccountChangePasswordParams()
|
||||
|
||||
uprinc, aCtx, err := o.Context.Authorize(r, route)
|
||||
if err != nil {
|
||||
o.Context.Respond(rw, r, route.Produces, route, err)
|
||||
return
|
||||
}
|
||||
if aCtx != nil {
|
||||
r = aCtx
|
||||
}
|
||||
var principal *models.Principal
|
||||
if uprinc != nil {
|
||||
principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise
|
||||
}
|
||||
|
||||
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
|
||||
o.Context.Respond(rw, r, route.Produces, route, err)
|
||||
return
|
||||
}
|
||||
|
||||
res := o.Handler.Handle(Params, principal) // actually handle the request
|
||||
|
||||
o.Context.Respond(rw, r, route.Produces, route, res)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-openapi/errors"
|
||||
"github.com/go-openapi/runtime"
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
)
|
||||
|
||||
// NewAccountChangePasswordParams creates a new AccountChangePasswordParams object
|
||||
// no default values defined in spec.
|
||||
func NewAccountChangePasswordParams() AccountChangePasswordParams {
|
||||
|
||||
return AccountChangePasswordParams{}
|
||||
}
|
||||
|
||||
// AccountChangePasswordParams contains all the bound params for the account change password operation
|
||||
// typically these are obtained from a http.Request
|
||||
//
|
||||
// swagger:parameters AccountChangePassword
|
||||
type AccountChangePasswordParams struct {
|
||||
|
||||
// HTTP Request Object
|
||||
HTTPRequest *http.Request `json:"-"`
|
||||
|
||||
/*
|
||||
Required: true
|
||||
In: body
|
||||
*/
|
||||
Body *models.AccountChangePasswordRequest
|
||||
}
|
||||
|
||||
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
|
||||
// for simple values it will use straight method calls.
|
||||
//
|
||||
// To ensure default values, the struct must have been initialized with NewAccountChangePasswordParams() beforehand.
|
||||
func (o *AccountChangePasswordParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
|
||||
var res []error
|
||||
|
||||
o.HTTPRequest = r
|
||||
|
||||
if runtime.HasBody(r) {
|
||||
defer r.Body.Close()
|
||||
var body models.AccountChangePasswordRequest
|
||||
if err := route.Consumer.Consume(r.Body, &body); err != nil {
|
||||
if err == io.EOF {
|
||||
res = append(res, errors.Required("body", "body", ""))
|
||||
} else {
|
||||
res = append(res, errors.NewParseError("body", "body", "", err))
|
||||
}
|
||||
} else {
|
||||
// validate body object
|
||||
if err := body.Validate(route.Formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if len(res) == 0 {
|
||||
o.Body = &body
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res = append(res, errors.Required("body", "body", ""))
|
||||
}
|
||||
if len(res) > 0 {
|
||||
return errors.CompositeValidationError(res...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
133
restapi/operations/user_api/account_change_password_responses.go
Normal file
133
restapi/operations/user_api/account_change_password_responses.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-openapi/runtime"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
)
|
||||
|
||||
// AccountChangePasswordCreatedCode is the HTTP code returned for type AccountChangePasswordCreated
|
||||
const AccountChangePasswordCreatedCode int = 201
|
||||
|
||||
/*AccountChangePasswordCreated A successful login.
|
||||
|
||||
swagger:response accountChangePasswordCreated
|
||||
*/
|
||||
type AccountChangePasswordCreated struct {
|
||||
|
||||
/*
|
||||
In: Body
|
||||
*/
|
||||
Payload *models.LoginResponse `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
// NewAccountChangePasswordCreated creates AccountChangePasswordCreated with default headers values
|
||||
func NewAccountChangePasswordCreated() *AccountChangePasswordCreated {
|
||||
|
||||
return &AccountChangePasswordCreated{}
|
||||
}
|
||||
|
||||
// WithPayload adds the payload to the account change password created response
|
||||
func (o *AccountChangePasswordCreated) WithPayload(payload *models.LoginResponse) *AccountChangePasswordCreated {
|
||||
o.Payload = payload
|
||||
return o
|
||||
}
|
||||
|
||||
// SetPayload sets the payload to the account change password created response
|
||||
func (o *AccountChangePasswordCreated) SetPayload(payload *models.LoginResponse) {
|
||||
o.Payload = payload
|
||||
}
|
||||
|
||||
// WriteResponse to the client
|
||||
func (o *AccountChangePasswordCreated) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
|
||||
|
||||
rw.WriteHeader(201)
|
||||
if o.Payload != nil {
|
||||
payload := o.Payload
|
||||
if err := producer.Produce(rw, payload); err != nil {
|
||||
panic(err) // let the recovery middleware deal with this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*AccountChangePasswordDefault Generic error response.
|
||||
|
||||
swagger:response accountChangePasswordDefault
|
||||
*/
|
||||
type AccountChangePasswordDefault struct {
|
||||
_statusCode int
|
||||
|
||||
/*
|
||||
In: Body
|
||||
*/
|
||||
Payload *models.Error `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
// NewAccountChangePasswordDefault creates AccountChangePasswordDefault with default headers values
|
||||
func NewAccountChangePasswordDefault(code int) *AccountChangePasswordDefault {
|
||||
if code <= 0 {
|
||||
code = 500
|
||||
}
|
||||
|
||||
return &AccountChangePasswordDefault{
|
||||
_statusCode: code,
|
||||
}
|
||||
}
|
||||
|
||||
// WithStatusCode adds the status to the account change password default response
|
||||
func (o *AccountChangePasswordDefault) WithStatusCode(code int) *AccountChangePasswordDefault {
|
||||
o._statusCode = code
|
||||
return o
|
||||
}
|
||||
|
||||
// SetStatusCode sets the status to the account change password default response
|
||||
func (o *AccountChangePasswordDefault) SetStatusCode(code int) {
|
||||
o._statusCode = code
|
||||
}
|
||||
|
||||
// WithPayload adds the payload to the account change password default response
|
||||
func (o *AccountChangePasswordDefault) WithPayload(payload *models.Error) *AccountChangePasswordDefault {
|
||||
o.Payload = payload
|
||||
return o
|
||||
}
|
||||
|
||||
// SetPayload sets the payload to the account change password default response
|
||||
func (o *AccountChangePasswordDefault) SetPayload(payload *models.Error) {
|
||||
o.Payload = payload
|
||||
}
|
||||
|
||||
// WriteResponse to the client
|
||||
func (o *AccountChangePasswordDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
|
||||
|
||||
rw.WriteHeader(o._statusCode)
|
||||
if o.Payload != nil {
|
||||
payload := o.Payload
|
||||
if err := producer.Produce(rw, payload); err != nil {
|
||||
panic(err) // let the recovery middleware deal with this
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the generate command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
golangswaggerpaths "path"
|
||||
)
|
||||
|
||||
// AccountChangePasswordURL generates an URL for the account change password operation
|
||||
type AccountChangePasswordURL struct {
|
||||
_basePath string
|
||||
}
|
||||
|
||||
// WithBasePath sets the base path for this url builder, only required when it's different from the
|
||||
// base path specified in the swagger spec.
|
||||
// When the value of the base path is an empty string
|
||||
func (o *AccountChangePasswordURL) WithBasePath(bp string) *AccountChangePasswordURL {
|
||||
o.SetBasePath(bp)
|
||||
return o
|
||||
}
|
||||
|
||||
// SetBasePath sets the base path for this url builder, only required when it's different from the
|
||||
// base path specified in the swagger spec.
|
||||
// When the value of the base path is an empty string
|
||||
func (o *AccountChangePasswordURL) SetBasePath(bp string) {
|
||||
o._basePath = bp
|
||||
}
|
||||
|
||||
// Build a url path and query string
|
||||
func (o *AccountChangePasswordURL) Build() (*url.URL, error) {
|
||||
var _result url.URL
|
||||
|
||||
var _path = "/account/change-password"
|
||||
|
||||
_basePath := o._basePath
|
||||
if _basePath == "" {
|
||||
_basePath = "/api/v1"
|
||||
}
|
||||
_result.Path = golangswaggerpaths.Join(_basePath, _path)
|
||||
|
||||
return &_result, nil
|
||||
}
|
||||
|
||||
// Must is a helper function to panic when the url builder returns an error
|
||||
func (o *AccountChangePasswordURL) Must(u *url.URL, err error) *url.URL {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if u == nil {
|
||||
panic("url can't be nil")
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
// String returns the string representation of the path with query string
|
||||
func (o *AccountChangePasswordURL) String() string {
|
||||
return o.Must(o.Build()).String()
|
||||
}
|
||||
|
||||
// BuildFull builds a full url with scheme, host, path and query string
|
||||
func (o *AccountChangePasswordURL) BuildFull(scheme, host string) (*url.URL, error) {
|
||||
if scheme == "" {
|
||||
return nil, errors.New("scheme is required for a full url on AccountChangePasswordURL")
|
||||
}
|
||||
if host == "" {
|
||||
return nil, errors.New("host is required for a full url on AccountChangePasswordURL")
|
||||
}
|
||||
|
||||
base, err := o.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
base.Scheme = scheme
|
||||
base.Host = host
|
||||
return base, nil
|
||||
}
|
||||
|
||||
// StringFull returns the string representation of a complete url
|
||||
func (o *AccountChangePasswordURL) StringFull(scheme, host string) string {
|
||||
return o.Must(o.BuildFull(scheme, host)).String()
|
||||
}
|
||||
@@ -43,11 +43,11 @@ func getOperatorListBucketsResponse(session *models.Principal, namespace, tenant
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
opClientClientSet, err := cluster.OperatorClient(session.SessionToken)
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
}
|
||||
clientSet, err := cluster.K8sClient(session.SessionToken)
|
||||
clientSet, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ func getResourceQuota(ctx context.Context, client K8sClientI, namespace, resourc
|
||||
|
||||
func getResourceQuotaResponse(session *models.Principal, params admin_api.GetResourceQuotaParams) (*models.ResourceQuota, *models.Error) {
|
||||
ctx := context.Background()
|
||||
client, err := cluster.K8sClient(session.SessionToken)
|
||||
client, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
}
|
||||
|
||||
97
restapi/user_account.go
Normal file
97
restapi/user_account.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package restapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/runtime"
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/console/restapi/operations"
|
||||
"github.com/minio/console/restapi/operations/user_api"
|
||||
)
|
||||
|
||||
func registerAccountHandlers(api *operations.ConsoleAPI) {
|
||||
// change user password
|
||||
api.UserAPIAccountChangePasswordHandler = user_api.AccountChangePasswordHandlerFunc(func(params user_api.AccountChangePasswordParams, session *models.Principal) middleware.Responder {
|
||||
changePasswordResponse, err := getChangePasswordResponse(session, params)
|
||||
if err != nil {
|
||||
return user_api.NewAccountChangePasswordDefault(int(err.Code)).WithPayload(err)
|
||||
}
|
||||
// Custom response writer to update the session cookies
|
||||
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
|
||||
cookie := NewSessionCookieForConsole(changePasswordResponse.SessionID)
|
||||
http.SetCookie(w, &cookie)
|
||||
user_api.NewLoginCreated().WithPayload(changePasswordResponse).WriteResponse(w, p)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// changePassword validate current current user password and if it's correct set the new password
|
||||
func changePassword(ctx context.Context, client MinioAdmin, session *models.Principal, currentSecretKey, newSecretKey string) error {
|
||||
if session.AccountSecretKey != currentSecretKey {
|
||||
return errChangePassword
|
||||
}
|
||||
if err := client.changePassword(ctx, session.AccountAccessKey, newSecretKey); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getChangePasswordResponse will validate user knows what is the current password (avoid account hijacking), update user account password
|
||||
// and authenticate the user generating a new session token/cookie
|
||||
func getChangePasswordResponse(session *models.Principal, params user_api.AccountChangePasswordParams) (*models.LoginResponse, *models.Error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
// changePassword operations requires an AdminClient initialized with parent account credentials not
|
||||
// STS credentials
|
||||
parentAccountClient, err := newMAdminClient(&models.Principal{
|
||||
STSAccessKeyID: session.AccountAccessKey,
|
||||
STSSecretAccessKey: session.AccountSecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
}
|
||||
// parentAccountClient will contain access and secret key credentials for the user
|
||||
userClient := adminClient{client: parentAccountClient}
|
||||
accessKey := session.AccountAccessKey
|
||||
currentSecretKey := *params.Body.CurrentSecretKey
|
||||
newSecretKey := *params.Body.NewSecretKey
|
||||
// currentSecretKey will compare currentSecretKey against the stored secret key inside the encrypted session
|
||||
if err := changePassword(ctx, userClient, session, currentSecretKey, newSecretKey); err != nil {
|
||||
return nil, prepareError(errChangePassword, nil, err)
|
||||
}
|
||||
// user credentials are updated at this point, we need to generate a new admin client and authenticate using
|
||||
// the new credentials
|
||||
credentials, err := getConsoleCredentials(ctx, accessKey, newSecretKey)
|
||||
if err != nil {
|
||||
return nil, prepareError(errInvalidCredentials, nil, err)
|
||||
}
|
||||
// authenticate user and generate new session token
|
||||
sessionID, err := login(credentials)
|
||||
if err != nil {
|
||||
return nil, prepareError(errInvalidCredentials, nil, err)
|
||||
}
|
||||
// serialize output
|
||||
loginResponse := &models.LoginResponse{
|
||||
SessionID: *sessionID,
|
||||
}
|
||||
return loginResponse, nil
|
||||
}
|
||||
111
restapi/user_account_test.go
Normal file
111
restapi/user_account_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package restapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
)
|
||||
|
||||
var minioChangePasswordMock func(ctx context.Context, accessKey, secretKey string) error
|
||||
|
||||
func Test_changePassword(t *testing.T) {
|
||||
client := adminClientMock{}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
client adminClientMock
|
||||
session *models.Principal
|
||||
currentSecretKey string
|
||||
newSecretKey string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
mock func()
|
||||
}{
|
||||
{
|
||||
name: "password changed successfully",
|
||||
args: args{
|
||||
client: client,
|
||||
ctx: context.Background(),
|
||||
session: &models.Principal{
|
||||
AccountAccessKey: "TESTTEST",
|
||||
AccountSecretKey: "TESTTEST",
|
||||
},
|
||||
currentSecretKey: "TESTTEST",
|
||||
newSecretKey: "TESTTEST2",
|
||||
},
|
||||
mock: func() {
|
||||
minioChangePasswordMock = func(ctx context.Context, accessKey, secretKey string) error {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error when changing password",
|
||||
args: args{
|
||||
client: client,
|
||||
ctx: context.Background(),
|
||||
session: &models.Principal{
|
||||
AccountAccessKey: "TESTTEST",
|
||||
AccountSecretKey: "TESTTEST",
|
||||
},
|
||||
currentSecretKey: "TESTTEST",
|
||||
newSecretKey: "TESTTEST2",
|
||||
},
|
||||
mock: func() {
|
||||
minioChangePasswordMock = func(ctx context.Context, accessKey, secretKey string) error {
|
||||
return errors.New("there was an error, please try again")
|
||||
}
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "error because current password doesn't match",
|
||||
args: args{
|
||||
client: client,
|
||||
ctx: context.Background(),
|
||||
session: &models.Principal{
|
||||
AccountAccessKey: "TESTTEST",
|
||||
AccountSecretKey: "TESTTEST123",
|
||||
},
|
||||
currentSecretKey: "TESTTEST",
|
||||
newSecretKey: "TESTTEST2",
|
||||
},
|
||||
mock: func() {
|
||||
minioChangePasswordMock = func(ctx context.Context, accessKey, secretKey string) error {
|
||||
return errors.New("there was an error, please try again")
|
||||
}
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.mock != nil {
|
||||
tt.mock()
|
||||
}
|
||||
if err := changePassword(tt.args.ctx, tt.args.client, tt.args.session, tt.args.currentSecretKey, tt.args.newSecretKey); (err != nil) != tt.wantErr {
|
||||
t.Errorf("changePassword() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -83,14 +83,14 @@ func registerLoginHandlers(api *operations.ConsoleAPI) {
|
||||
|
||||
// login performs a check of consoleCredentials against MinIO, generates some claims and returns the jwt
|
||||
// for subsequent authentication
|
||||
func login(credentials ConsoleCredentials, actions []string) (*string, error) {
|
||||
func login(credentials ConsoleCredentialsI) (*string, error) {
|
||||
// try to obtain consoleCredentials,
|
||||
tokens, err := credentials.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// if we made it here, the consoleCredentials work, generate a jwt with claims
|
||||
token, err := auth.NewEncryptedTokenForClient(&tokens, actions)
|
||||
token, err := auth.NewEncryptedTokenForClient(&tokens, credentials.GetAccountAccessKey(), credentials.GetAccountSecretKey(), credentials.GetActions())
|
||||
if err != nil {
|
||||
log.Println("error authenticating user", err)
|
||||
return nil, errInvalidCredentials
|
||||
@@ -112,33 +112,22 @@ func getConfiguredRegionForLogin(ctx context.Context, client MinioAdmin) (string
|
||||
return location, nil
|
||||
}
|
||||
|
||||
// getLoginResponse performs login() and serializes it to the handler's output
|
||||
func getLoginResponse(lr *models.LoginRequest) (*models.LoginResponse, *models.Error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
mAdmin, err := newSuperMAdminClient()
|
||||
func getConsoleCredentials(ctx context.Context, accessKey, secretKey string) (*consoleCredentials, error) {
|
||||
mAdminClient, err := newMAdminClient(&models.Principal{
|
||||
STSAccessKeyID: accessKey,
|
||||
STSSecretAccessKey: secretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, prepareError(err, errorGeneric)
|
||||
return nil, err
|
||||
}
|
||||
adminClient := adminClient{client: mAdmin}
|
||||
// obtain the configured MinIO region
|
||||
// need it for user authentication
|
||||
location, err := getConfiguredRegionForLogin(ctx, adminClient)
|
||||
if err != nil {
|
||||
return nil, prepareError(err, errConnectingToMinio)
|
||||
}
|
||||
creds, err := newConsoleCredentials(*lr.AccessKey, *lr.SecretKey, location)
|
||||
if err != nil {
|
||||
return nil, prepareError(err, errInvalidCredentials)
|
||||
}
|
||||
credentials := consoleCredentials{consoleCredentials: creds}
|
||||
userAdminClient := adminClient{client: mAdminClient}
|
||||
// obtain the current policy assigned to this user
|
||||
// necessary for generating the list of allowed endpoints
|
||||
userInfo, err := adminClient.getUserInfo(ctx, *lr.AccessKey)
|
||||
userInfo, err := userAdminClient.getUserInfo(ctx, accessKey)
|
||||
if err != nil {
|
||||
return nil, prepareError(err, errorGeneric)
|
||||
return nil, err
|
||||
}
|
||||
policy, _ := adminClient.getPolicy(ctx, userInfo.PolicyName)
|
||||
policy, _ := userAdminClient.getPolicy(ctx, userInfo.PolicyName)
|
||||
// by default every user starts with an empty array of available actions
|
||||
// therefore we would have access only to pages that doesn't require any privilege
|
||||
// ie: service-account page
|
||||
@@ -147,7 +136,29 @@ func getLoginResponse(lr *models.LoginRequest) (*models.LoginResponse, *models.E
|
||||
if policy != nil {
|
||||
actions = acl.GetActionsStringFromPolicy(policy)
|
||||
}
|
||||
sessionID, err := login(credentials, actions)
|
||||
creds, err := newConsoleCredentials(accessKey, secretKey, MinioRegion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// consoleCredentials will be sts credentials, account credentials will be need it in the scenario the user wish
|
||||
return &consoleCredentials{
|
||||
consoleCredentials: creds,
|
||||
accountAccessKey: accessKey,
|
||||
accountSecretKey: secretKey,
|
||||
actions: actions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getLoginResponse performs login() and serializes it to the handler's output
|
||||
func getLoginResponse(lr *models.LoginRequest) (*models.LoginResponse, *models.Error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
// prepare console credentials
|
||||
credentials, err := getConsoleCredentials(ctx, *lr.AccessKey, *lr.SecretKey)
|
||||
if err != nil {
|
||||
return nil, prepareError(errInvalidCredentials, nil, err)
|
||||
}
|
||||
sessionID, err := login(credentials)
|
||||
if err != nil {
|
||||
return nil, prepareError(errInvalidCredentials, nil, err)
|
||||
}
|
||||
@@ -250,8 +261,8 @@ func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.Logi
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
}
|
||||
credentials := consoleCredentials{consoleCredentials: creds}
|
||||
token, err := login(credentials, actions)
|
||||
credentials := consoleCredentials{consoleCredentials: creds, actions: actions}
|
||||
token, err := login(credentials)
|
||||
if err != nil {
|
||||
return nil, prepareError(errInvalidCredentials, nil, err)
|
||||
}
|
||||
@@ -270,9 +281,8 @@ func getLoginOperatorResponse(lmr *models.LoginOperatorRequest) (*models.LoginRe
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
}
|
||||
credentials := consoleCredentials{consoleCredentials: creds}
|
||||
var actions []string
|
||||
token, err := login(credentials, actions)
|
||||
credentials := consoleCredentials{consoleCredentials: creds, actions: []string{}}
|
||||
token, err := login(credentials)
|
||||
if err != nil {
|
||||
return nil, prepareError(errInvalidCredentials, nil, err)
|
||||
}
|
||||
|
||||
@@ -29,9 +29,21 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Define a mock struct of ConsoleCredentials interface implementation
|
||||
// Define a mock struct of ConsoleCredentialsI interface implementation
|
||||
type consoleCredentialsMock struct{}
|
||||
|
||||
func (ac consoleCredentialsMock) GetActions() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (ac consoleCredentialsMock) GetAccountAccessKey() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (ac consoleCredentialsMock) GetAccountSecretKey() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Common mocks
|
||||
var consoleCredentialsGetMock func() (credentials.Value, error)
|
||||
|
||||
@@ -52,7 +64,7 @@ func TestLogin(t *testing.T) {
|
||||
SignerType: 0,
|
||||
}, nil
|
||||
}
|
||||
token, err := login(consoleCredentials, []string{""})
|
||||
token, err := login(consoleCredentials)
|
||||
funcAssert.NotEmpty(token, "Token was returned empty")
|
||||
funcAssert.Nil(err, "error creating a session")
|
||||
|
||||
@@ -60,7 +72,7 @@ func TestLogin(t *testing.T) {
|
||||
consoleCredentialsGetMock = func() (credentials.Value, error) {
|
||||
return credentials.Value{}, errors.New("")
|
||||
}
|
||||
_, err = login(consoleCredentials, []string{""})
|
||||
_, err = login(consoleCredentials)
|
||||
funcAssert.NotNil(err, "not error returned creating a session")
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ func registerLogoutHandlers(api *operations.ConsoleAPI) {
|
||||
}
|
||||
|
||||
// logout() call Expire() on the provided consoleCredentials
|
||||
func logout(credentials ConsoleCredentials) {
|
||||
func logout(credentials ConsoleCredentialsI) {
|
||||
credentials.Expire()
|
||||
}
|
||||
|
||||
|
||||
@@ -187,11 +187,11 @@ func newWebSocketTenantAdminClient(conn *websocket.Conn, session *models.Princip
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
opClientClientSet, err := cluster.OperatorClient(session.SessionToken)
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clientSet, err := cluster.K8sClient(session.SessionToken)
|
||||
clientSet, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -238,12 +238,12 @@ func newWebSocketS3Client(conn *websocket.Conn, claims *models.Principal, namesp
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
opClientClientSet, err := cluster.OperatorClient(claims.SessionToken)
|
||||
opClientClientSet, err := cluster.OperatorClient(claims.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clientSet, err := cluster.K8sClient(claims.SessionToken)
|
||||
clientSet, err := cluster.K8sClient(claims.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -268,8 +268,8 @@ func newWebSocketS3Client(conn *websocket.Conn, claims *models.Principal, namesp
|
||||
}
|
||||
|
||||
tenantClaims := &models.Principal{
|
||||
AccessKeyID: tenantCreds.accessKey,
|
||||
SecretAccessKey: tenantCreds.secretKey,
|
||||
STSAccessKeyID: tenantCreds.accessKey,
|
||||
STSSecretAccessKey: tenantCreds.secretKey,
|
||||
}
|
||||
|
||||
svcURL := GetTenantServiceURL(minTenant)
|
||||
|
||||
44
swagger.yml
44
swagger.yml
@@ -136,6 +136,28 @@ paths:
|
||||
tags:
|
||||
- UserAPI
|
||||
|
||||
/account/change-password:
|
||||
post:
|
||||
summary: Change password of currently logged in user.
|
||||
operationId: AccountChangePassword
|
||||
parameters:
|
||||
- name: body
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/accountChangePasswordRequest"
|
||||
responses:
|
||||
201:
|
||||
description: A successful login.
|
||||
schema:
|
||||
$ref: "#/definitions/loginResponse"
|
||||
default:
|
||||
description: Generic error response.
|
||||
schema:
|
||||
$ref: "#/definitions/error"
|
||||
tags:
|
||||
- UserAPI
|
||||
|
||||
/buckets:
|
||||
get:
|
||||
summary: List Buckets
|
||||
@@ -1953,6 +1975,18 @@ paths:
|
||||
- OperatorAPI
|
||||
|
||||
definitions:
|
||||
|
||||
accountChangePasswordRequest:
|
||||
type: object
|
||||
required:
|
||||
- current_secret_key
|
||||
- new_secret_key
|
||||
properties:
|
||||
current_secret_key:
|
||||
type: string
|
||||
new_secret_key:
|
||||
type: string
|
||||
|
||||
bucketEncryptionType:
|
||||
type: string
|
||||
enum:
|
||||
@@ -2460,16 +2494,20 @@ definitions:
|
||||
principal:
|
||||
type: object
|
||||
properties:
|
||||
accessKeyID:
|
||||
STSAccessKeyID:
|
||||
type: string
|
||||
secretAccessKey:
|
||||
STSSecretAccessKey:
|
||||
type: string
|
||||
sessionToken:
|
||||
STSSessionToken:
|
||||
type: string
|
||||
actions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
accountAccessKey:
|
||||
type: string
|
||||
accountSecretKey:
|
||||
type: string
|
||||
startProfilingItem:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
Reference in New Issue
Block a user