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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user