diff --git a/models/update_user_groups.go b/models/update_user_groups.go new file mode 100644 index 000000000..d27245947 --- /dev/null +++ b/models/update_user_groups.go @@ -0,0 +1,81 @@ +// 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 . +// + +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" +) + +// UpdateUserGroups update user groups +// +// swagger:model updateUserGroups +type UpdateUserGroups struct { + + // groups + // Required: true + Groups []string `json:"groups"` +} + +// Validate validates this update user groups +func (m *UpdateUserGroups) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateGroups(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *UpdateUserGroups) validateGroups(formats strfmt.Registry) error { + + if err := validate.Required("groups", "body", m.Groups); err != nil { + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *UpdateUserGroups) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *UpdateUserGroups) UnmarshalBinary(b []byte) error { + var res UpdateUserGroups + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/portal-ui/bindata_assetfs.go b/portal-ui/bindata_assetfs.go index c4cc19dc0..32cfff90e 100644 --- a/portal-ui/bindata_assetfs.go +++ b/portal-ui/bindata_assetfs.go @@ -36,10 +36,10 @@ package portal import ( - "github.com/elazarl/go-bindata-assetfs" "bytes" "compress/gzip" "fmt" + "github.com/elazarl/go-bindata-assetfs" "io" "io/ioutil" "os" @@ -914,14 +914,14 @@ var _bintree = &bintree{nil, map[string]*bintree{ "images": &bintree{nil, map[string]*bintree{ "BG_Illustration.svg": &bintree{buildImagesBg_illustrationSvg, map[string]*bintree{}}, }}, - "index.html": &bintree{buildIndexHtml, map[string]*bintree{}}, - "logo192.png": &bintree{buildLogo192Png, map[string]*bintree{}}, - "logo512.png": &bintree{buildLogo512Png, map[string]*bintree{}}, - "manifest.json": &bintree{buildManifestJson, map[string]*bintree{}}, + "index.html": &bintree{buildIndexHtml, map[string]*bintree{}}, + "logo192.png": &bintree{buildLogo192Png, map[string]*bintree{}}, + "logo512.png": &bintree{buildLogo512Png, map[string]*bintree{}}, + "manifest.json": &bintree{buildManifestJson, map[string]*bintree{}}, "precache-manifest.bef0dbe8fb8fcdf4145f7d058e838793.js": &bintree{buildPrecacheManifestBef0dbe8fb8fcdf4145f7d058e838793Js, map[string]*bintree{}}, - "robots.txt": &bintree{buildRobotsTxt, map[string]*bintree{}}, - "safari-pinned-tab.svg": &bintree{buildSafariPinnedTabSvg, map[string]*bintree{}}, - "service-worker.js": &bintree{buildServiceWorkerJs, map[string]*bintree{}}, + "robots.txt": &bintree{buildRobotsTxt, map[string]*bintree{}}, + "safari-pinned-tab.svg": &bintree{buildSafariPinnedTabSvg, map[string]*bintree{}}, + "service-worker.js": &bintree{buildServiceWorkerJs, map[string]*bintree{}}, "static": &bintree{nil, map[string]*bintree{ "css": &bintree{nil, map[string]*bintree{ "2.57146246.chunk.css": &bintree{buildStaticCss257146246ChunkCss, map[string]*bintree{}}, diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/AddBucket.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/AddBucket.tsx index 0d629b128..75ad3f8ea 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/AddBucket.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/AddBucket.tsx @@ -49,7 +49,7 @@ interface IAddBucketProps { interface IAddBucketState { addLoading: boolean; addError: string; - bucketName: string; + bucketName: string; } class AddBucket extends React.Component { @@ -92,7 +92,7 @@ class AddBucket extends React.Component { render() { const { classes, open } = this.props; - const { addLoading, addError} = this.state; + const { addLoading, addError } = this.state; return ( { storage.removeItem("token"); this.props.userLoggedIn(false); history.push("/"); - } + }; api .invoke("POST", `/api/v1/logout`) .then(() => { diff --git a/restapi/admin_users.go b/restapi/admin_users.go index 5bd9e5539..224756897 100644 --- a/restapi/admin_users.go +++ b/restapi/admin_users.go @@ -17,9 +17,6 @@ package restapi import ( - "context" - "log" - "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/swag" "github.com/minio/mcs/restapi/operations" @@ -27,6 +24,11 @@ import ( "github.com/minio/mcs/restapi/operations/admin_api" "github.com/minio/mcs/models" + "github.com/minio/minio/pkg/madmin" + + "context" + "errors" + "log" ) func registerUsersHandlers(api *operations.McsAPI) { @@ -54,6 +56,15 @@ func registerUsersHandlers(api *operations.McsAPI) { } return admin_api.NewRemoveUserNoContent() }) + // Update User-Groups + api.AdminAPIUpdateUserGroupsHandler = admin_api.UpdateUserGroupsHandlerFunc(func(params admin_api.UpdateUserGroupsParams, principal *models.Principal) middleware.Responder { + userUpdateResponse, err := getUpdateUserGroupsResponse(params) + if err != nil { + return admin_api.NewUpdateUserGroupsDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())}) + } + + return admin_api.NewUpdateUserGroupsOK().WithPayload(userUpdateResponse) + }) } func listUsers(ctx context.Context, client MinioAdmin) ([]*models.User, error) { @@ -167,3 +178,122 @@ func getRemoveUserResponse(params admin_api.RemoveUserParams) error { log.Println("User removed successfully:", params.Name) return nil } + +// getUserInfo calls MinIO server get the User Information +func getUserInfo(ctx context.Context, client MinioAdmin, accessKey string) (*madmin.UserInfo, error) { + userInfo, err := client.getUserInfo(ctx, accessKey) + + if err != nil { + return nil, err + } + return &userInfo, nil +} + +// updateUserGroups invokes getUserInfo() to get the old groups from the user, +// then we merge the list with the new groups list to have a shorter iteration between groups and we do a comparison between the current and old groups. +// We delete or update the groups according the location in each list and send the user with the new groups from `MinioAdmin` to the client +func updateUserGroups(ctx context.Context, client MinioAdmin, user string, groupsToAssign []string) (*models.User, error) { + parallelUserUpdate := func(groupName string, originGroups []string) chan error { + chProcess := make(chan error) + + go func() error { + defer close(chProcess) + + //Compare if groupName is in the arrays + isGroupPersistent := IsElementInArray(groupsToAssign, groupName) + isInOriginGroups := IsElementInArray(originGroups, groupName) + + if isGroupPersistent && isInOriginGroups { // Group is already assigned and doesn't need to be updated + chProcess <- nil + + return nil + } + + isRemove := false // User is added by default + + // User is deleted from the group + if !isGroupPersistent { + isRemove = true + } + + userToAddRemove := []string{user} + + updateReturn := updateGroupMembers(ctx, client, groupName, userToAddRemove, isRemove) + + chProcess <- updateReturn + + return updateReturn + }() + + return chProcess + } + + userInfoOr, err := getUserInfo(ctx, client, user) + if err != nil { + return nil, err + } + + memberOf := userInfoOr.MemberOf + mergedGroupArray := UniqueKeys(append(memberOf, groupsToAssign...)) + + var listOfUpdates []chan error + + // Each group must be updated individually because there is no way to update all the groups at once for a user, + // we are using the same logic as 'mc admin group add' command + for _, groupN := range mergedGroupArray { + proc := parallelUserUpdate(groupN, memberOf) + listOfUpdates = append(listOfUpdates, proc) + } + + channelHasError := false + + for _, chanRet := range listOfUpdates { + locError := <-chanRet + + if locError != nil { + channelHasError = true + } + } + + if channelHasError { + errRt := errors.New("there was an error updating the groups") + return nil, errRt + } + + userInfo, err := getUserInfo(ctx, client, user) + if err != nil { + return nil, err + } + + userReturn := &models.User{ + AccessKey: user, + MemberOf: userInfo.MemberOf, + Policy: userInfo.PolicyName, + Status: string(userInfo.Status), + } + + return userReturn, nil +} + +func getUpdateUserGroupsResponse(params admin_api.UpdateUserGroupsParams) (*models.User, error) { + ctx := context.Background() + + mAdmin, err := newMAdminClient() + if err != nil { + log.Println("error creating Madmin Client:", err) + return nil, err + } + + // create a minioClient interface implementation + // defining the client to be used + adminClient := adminClient{client: mAdmin} + + user, err := updateUserGroups(ctx, adminClient, params.Name, params.Body.Groups) + + if err != nil { + log.Println("error updating users's groups:", params.Body.Groups) + return nil, err + } + + return user, nil +} diff --git a/restapi/admin_users_test.go b/restapi/admin_users_test.go index 32f06ab5e..917b45f61 100644 --- a/restapi/admin_users_test.go +++ b/restapi/admin_users_test.go @@ -32,6 +32,7 @@ import ( var minioListUsersMock func() (map[string]madmin.UserInfo, error) var minioAddUserMock func(accessKey, secreyKey string) error var minioRemoveUserMock func(accessKey string) error +var minioGetUserInfoMock func(accessKey string) (madmin.UserInfo, error) // mock function of listUsers() func (ac adminClientMock) listUsers(ctx context.Context) (map[string]madmin.UserInfo, error) { @@ -48,6 +49,11 @@ func (ac adminClientMock) removeUser(ctx context.Context, accessKey string) erro return minioRemoveUserMock(accessKey) } +//mock function of getUserInfo() +func (ac adminClientMock) getUserInfo(ctx context.Context, accessKey string) (madmin.UserInfo, error) { + return minioGetUserInfoMock(accessKey) +} + func TestListUsers(t *testing.T) { assert := asrt.New(t) adminClient := adminClientMock{} @@ -169,3 +175,110 @@ func TestRemoveUser(t *testing.T) { assert.Equal("error", err.Error()) } } + +func TestUserGroups(t *testing.T) { + assert := asrt.New(t) + // mock minIO client + adminClient := adminClientMock{} + ctx := context.Background() + + function := "updateUserGroups()" + mockUserGroups := []string{"group1", "group2", "group3"} + mockUserName := "testUser" + mockResponse := &madmin.UserInfo{ + MemberOf: []string{"group1", "group2", "gropup3"}, + PolicyName: "", + Status: "enabled", + SecretKey: mockUserName, + } + mockEmptyResponse := &madmin.UserInfo{ + MemberOf: nil, + PolicyName: "", + Status: "", + SecretKey: "", + } + + // Test-1: updateUserGroups() updates the groups for a user + // mock function response from updateUserGroups(accessKey, groupsToAssign) + + minioGetUserInfoMock = func(accessKey string) (madmin.UserInfo, error) { + return *mockResponse, nil + } + + minioUpdateGroupMembersMock = func(remove madmin.GroupAddRemove) error { + return nil + } + + if _, err := updateUserGroups(ctx, adminClient, mockUserName, mockUserGroups); err != nil { + t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) + } + + // Test-2: updateUserGroups() make sure errors are handled correctly when error on DeleteUser() + // mock function response from removeUser(accessKey) + + minioUpdateGroupMembersMock = func(remove madmin.GroupAddRemove) error { + return errors.New("error") + } + + if _, err := updateUserGroups(ctx, adminClient, mockUserName, mockUserGroups); assert.Error(err) { + assert.Equal("there was an error updating the groups", err.Error()) + } + + // Test-3: updateUserGroups() make sure we return the correct error when getUserInfo returns error + minioGetUserInfoMock = func(accessKey string) (madmin.UserInfo, error) { + return *mockEmptyResponse, errors.New("error getting user ") + } + + minioUpdateGroupMembersMock = func(remove madmin.GroupAddRemove) error { + return nil + } + + if _, err := updateUserGroups(ctx, adminClient, mockUserName, mockUserGroups); assert.Error(err) { + assert.Equal("error getting user ", err.Error()) + } +} + +func TestGetUserInfo(t *testing.T) { + assert := asrt.New(t) + adminClient := adminClientMock{} + ctx := context.Background() + + // Test-1 : getUserInfo() get user info + userName := "userNameTest" + mockResponse := &madmin.UserInfo{ + SecretKey: userName, + PolicyName: "", + MemberOf: []string{"group1", "group2", "group3"}, + Status: "enabled", + } + emptyMockResponse := &madmin.UserInfo{ + SecretKey: "", + PolicyName: "", + Status: "", + MemberOf: nil, + } + + // mock function response from getUserInfo() + minioGetUserInfoMock = func(username string) (madmin.UserInfo, error) { + return *mockResponse, nil + } + function := "getUserInfo()" + info, err := getUserInfo(ctx, adminClient, userName) + if err != nil { + t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) + } + + assert.Equal(userName, info.SecretKey) + assert.Equal("", info.PolicyName) + assert.ElementsMatch([]string{"group1", "group2", "group3"}, info.MemberOf) + assert.Equal(mockResponse.Status, info.Status) + + // Test-2 : getUserInfo() Return error and see that the error is handled correctly and returned + minioGetUserInfoMock = func(username string) (madmin.UserInfo, error) { + return *emptyMockResponse, errors.New("error") + } + _, err = getUserInfo(ctx, adminClient, userName) + if assert.Error(err) { + assert.Equal("error", err.Error()) + } +} diff --git a/restapi/client-admin.go b/restapi/client-admin.go index 19b14775d..9908324eb 100644 --- a/restapi/client-admin.go +++ b/restapi/client-admin.go @@ -58,6 +58,7 @@ type MinioAdmin interface { listUsers(ctx context.Context) (map[string]madmin.UserInfo, error) addUser(ctx context.Context, acessKey, SecretKey string) error removeUser(ctx context.Context, accessKey string) error + getUserInfo(ctx context.Context, accessKey string) (madmin.UserInfo, error) listGroups(ctx context.Context) ([]string, error) updateGroupMembers(ctx context.Context, greq madmin.GroupAddRemove) error getGroupDescription(ctx context.Context, group string) (*madmin.GroupDesc, error) @@ -99,6 +100,11 @@ func (ac adminClient) removeUser(ctx context.Context, accessKey string) error { return ac.client.RemoveUser(ctx, accessKey) } +//implements madmin.GetUserInfo() +func (ac adminClient) getUserInfo(ctx context.Context, accessKey string) (madmin.UserInfo, error) { + return ac.client.GetUserInfo(ctx, accessKey) +} + // implements madmin.ListGroups() func (ac adminClient) listGroups(ctx context.Context) ([]string, error) { return ac.client.ListGroups(ctx) diff --git a/restapi/embedded_spec.go b/restapi/embedded_spec.go index a7977fd20..693b07afe 100644 --- a/restapi/embedded_spec.go +++ b/restapi/embedded_spec.go @@ -1069,6 +1069,45 @@ func init() { } } } + }, + "/users/{name}/groups": { + "put": { + "tags": [ + "AdminAPI" + ], + "summary": "Update Groups for a user", + "operationId": "UpdateUserGroups", + "parameters": [ + { + "type": "string", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/updateUserGroups" + } + } + ], + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/user" + } + }, + "default": { + "description": "Generic error response.", + "schema": { + "$ref": "#/definitions/error" + } + } + } + } } }, "definitions": { @@ -1591,6 +1630,20 @@ func init() { } } }, + "updateUserGroups": { + "type": "object", + "required": [ + "groups" + ], + "properties": { + "groups": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "user": { "type": "object", "properties": { @@ -2661,6 +2714,45 @@ func init() { } } } + }, + "/users/{name}/groups": { + "put": { + "tags": [ + "AdminAPI" + ], + "summary": "Update Groups for a user", + "operationId": "UpdateUserGroups", + "parameters": [ + { + "type": "string", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/updateUserGroups" + } + } + ], + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/user" + } + }, + "default": { + "description": "Generic error response.", + "schema": { + "$ref": "#/definitions/error" + } + } + } + } } }, "definitions": { @@ -3183,6 +3275,20 @@ func init() { } } }, + "updateUserGroups": { + "type": "object", + "required": [ + "groups" + ], + "properties": { + "groups": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "user": { "type": "object", "properties": { diff --git a/restapi/operations/admin_api/update_user_groups.go b/restapi/operations/admin_api/update_user_groups.go new file mode 100644 index 000000000..df021380b --- /dev/null +++ b/restapi/operations/admin_api/update_user_groups.go @@ -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 . +// + +package admin_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/mcs/models" +) + +// UpdateUserGroupsHandlerFunc turns a function with the right signature into a update user groups handler +type UpdateUserGroupsHandlerFunc func(UpdateUserGroupsParams, *models.Principal) middleware.Responder + +// Handle executing the request and returning a response +func (fn UpdateUserGroupsHandlerFunc) Handle(params UpdateUserGroupsParams, principal *models.Principal) middleware.Responder { + return fn(params, principal) +} + +// UpdateUserGroupsHandler interface for that can handle valid update user groups params +type UpdateUserGroupsHandler interface { + Handle(UpdateUserGroupsParams, *models.Principal) middleware.Responder +} + +// NewUpdateUserGroups creates a new http.Handler for the update user groups operation +func NewUpdateUserGroups(ctx *middleware.Context, handler UpdateUserGroupsHandler) *UpdateUserGroups { + return &UpdateUserGroups{Context: ctx, Handler: handler} +} + +/*UpdateUserGroups swagger:route PUT /users/{name}/groups AdminAPI updateUserGroups + +Update Groups for a user + +*/ +type UpdateUserGroups struct { + Context *middleware.Context + Handler UpdateUserGroupsHandler +} + +func (o *UpdateUserGroups) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + r = rCtx + } + var Params = NewUpdateUserGroupsParams() + + 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) + +} diff --git a/restapi/operations/admin_api/update_user_groups_parameters.go b/restapi/operations/admin_api/update_user_groups_parameters.go new file mode 100644 index 000000000..4cafd86c9 --- /dev/null +++ b/restapi/operations/admin_api/update_user_groups_parameters.go @@ -0,0 +1,120 @@ +// 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 . +// + +package admin_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/go-openapi/strfmt" + + "github.com/minio/mcs/models" +) + +// NewUpdateUserGroupsParams creates a new UpdateUserGroupsParams object +// no default values defined in spec. +func NewUpdateUserGroupsParams() UpdateUserGroupsParams { + + return UpdateUserGroupsParams{} +} + +// UpdateUserGroupsParams contains all the bound params for the update user groups operation +// typically these are obtained from a http.Request +// +// swagger:parameters UpdateUserGroups +type UpdateUserGroupsParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + Required: true + In: body + */ + Body *models.UpdateUserGroups + /* + Required: true + In: path + */ + Name string +} + +// 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 NewUpdateUserGroupsParams() beforehand. +func (o *UpdateUserGroupsParams) 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.UpdateUserGroups + 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")) + } + rName, rhkName, _ := route.Params.GetOK("name") + if err := o.bindName(rName, rhkName, route.Formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindName binds and validates parameter Name from path. +func (o *UpdateUserGroupsParams) bindName(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + + o.Name = raw + + return nil +} diff --git a/restapi/operations/admin_api/update_user_groups_responses.go b/restapi/operations/admin_api/update_user_groups_responses.go new file mode 100644 index 000000000..03b5045ce --- /dev/null +++ b/restapi/operations/admin_api/update_user_groups_responses.go @@ -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 . +// + +package admin_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/mcs/models" +) + +// UpdateUserGroupsOKCode is the HTTP code returned for type UpdateUserGroupsOK +const UpdateUserGroupsOKCode int = 200 + +/*UpdateUserGroupsOK A successful response. + +swagger:response updateUserGroupsOK +*/ +type UpdateUserGroupsOK struct { + + /* + In: Body + */ + Payload *models.User `json:"body,omitempty"` +} + +// NewUpdateUserGroupsOK creates UpdateUserGroupsOK with default headers values +func NewUpdateUserGroupsOK() *UpdateUserGroupsOK { + + return &UpdateUserGroupsOK{} +} + +// WithPayload adds the payload to the update user groups o k response +func (o *UpdateUserGroupsOK) WithPayload(payload *models.User) *UpdateUserGroupsOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the update user groups o k response +func (o *UpdateUserGroupsOK) SetPayload(payload *models.User) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *UpdateUserGroupsOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +/*UpdateUserGroupsDefault Generic error response. + +swagger:response updateUserGroupsDefault +*/ +type UpdateUserGroupsDefault struct { + _statusCode int + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewUpdateUserGroupsDefault creates UpdateUserGroupsDefault with default headers values +func NewUpdateUserGroupsDefault(code int) *UpdateUserGroupsDefault { + if code <= 0 { + code = 500 + } + + return &UpdateUserGroupsDefault{ + _statusCode: code, + } +} + +// WithStatusCode adds the status to the update user groups default response +func (o *UpdateUserGroupsDefault) WithStatusCode(code int) *UpdateUserGroupsDefault { + o._statusCode = code + return o +} + +// SetStatusCode sets the status to the update user groups default response +func (o *UpdateUserGroupsDefault) SetStatusCode(code int) { + o._statusCode = code +} + +// WithPayload adds the payload to the update user groups default response +func (o *UpdateUserGroupsDefault) WithPayload(payload *models.Error) *UpdateUserGroupsDefault { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the update user groups default response +func (o *UpdateUserGroupsDefault) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *UpdateUserGroupsDefault) 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 + } + } +} diff --git a/restapi/operations/admin_api/update_user_groups_urlbuilder.go b/restapi/operations/admin_api/update_user_groups_urlbuilder.go new file mode 100644 index 000000000..ca99f82f8 --- /dev/null +++ b/restapi/operations/admin_api/update_user_groups_urlbuilder.go @@ -0,0 +1,116 @@ +// 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 . +// + +package admin_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" + "strings" +) + +// UpdateUserGroupsURL generates an URL for the update user groups operation +type UpdateUserGroupsURL struct { + Name string + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// 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 *UpdateUserGroupsURL) WithBasePath(bp string) *UpdateUserGroupsURL { + 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 *UpdateUserGroupsURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *UpdateUserGroupsURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/users/{name}/groups" + + name := o.Name + if name != "" { + _path = strings.Replace(_path, "{name}", name, -1) + } else { + return nil, errors.New("name is required on UpdateUserGroupsURL") + } + + _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 *UpdateUserGroupsURL) 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 *UpdateUserGroupsURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *UpdateUserGroupsURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on UpdateUserGroupsURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on UpdateUserGroupsURL") + } + + 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 *UpdateUserGroupsURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/restapi/operations/mcs_api.go b/restapi/operations/mcs_api.go index 0679559fa..294f26fea 100644 --- a/restapi/operations/mcs_api.go +++ b/restapi/operations/mcs_api.go @@ -162,6 +162,9 @@ func NewMcsAPI(spec *loads.Document) *McsAPI { AdminAPIUpdateGroupHandler: admin_api.UpdateGroupHandlerFunc(func(params admin_api.UpdateGroupParams, principal *models.Principal) middleware.Responder { return middleware.NotImplemented("operation admin_api.UpdateGroup has not yet been implemented") }), + AdminAPIUpdateUserGroupsHandler: admin_api.UpdateUserGroupsHandlerFunc(func(params admin_api.UpdateUserGroupsParams, principal *models.Principal) middleware.Responder { + return middleware.NotImplemented("operation admin_api.UpdateUserGroups has not yet been implemented") + }), KeyAuth: func(token string, scopes []string) (*models.Principal, error) { return nil, errors.NotImplemented("oauth2 bearer auth (key) has not yet been implemented") @@ -277,6 +280,8 @@ type McsAPI struct { AdminAPISetPolicyHandler admin_api.SetPolicyHandler // AdminAPIUpdateGroupHandler sets the operation handler for the update group operation AdminAPIUpdateGroupHandler admin_api.UpdateGroupHandler + // AdminAPIUpdateUserGroupsHandler sets the operation handler for the update user groups operation + AdminAPIUpdateUserGroupsHandler admin_api.UpdateUserGroupsHandler // ServeError is called when an error is received, there is a default handler // but you can set your own with this ServeError func(http.ResponseWriter, *http.Request, error) @@ -449,6 +454,9 @@ func (o *McsAPI) Validate() error { if o.AdminAPIUpdateGroupHandler == nil { unregistered = append(unregistered, "admin_api.UpdateGroupHandler") } + if o.AdminAPIUpdateUserGroupsHandler == nil { + unregistered = append(unregistered, "admin_api.UpdateUserGroupsHandler") + } if len(unregistered) > 0 { return fmt.Errorf("missing registration: %s", strings.Join(unregistered, ", ")) @@ -681,6 +689,10 @@ func (o *McsAPI) initHandlerCache() { o.handlers["PUT"] = make(map[string]http.Handler) } o.handlers["PUT"]["/groups/{name}"] = admin_api.NewUpdateGroup(o.context, o.AdminAPIUpdateGroupHandler) + if o.handlers["PUT"] == nil { + o.handlers["PUT"] = make(map[string]http.Handler) + } + o.handlers["PUT"]["/users/{name}/groups"] = admin_api.NewUpdateUserGroups(o.context, o.AdminAPIUpdateUserGroupsHandler) } // Serve creates a http handler to serve the API over HTTP diff --git a/restapi/utils.go b/restapi/utils.go index 313e5fde0..7bcf98a7b 100644 --- a/restapi/utils.go +++ b/restapi/utils.go @@ -30,3 +30,27 @@ func DifferenceArrays(a, b []string) []string { } return diff } + +// IsElementInSlice returns true if the string belongs to the slice +func IsElementInArray(a []string, b string) bool { + for _, e := range a { + if e == b { + return true + } + } + + return false +} + +// UniqueKeys returns an array without duplicated keys +func UniqueKeys(a []string) []string { + keys := make(map[string]bool) + list := []string{} + for _, entry := range a { + if _, value := keys[entry]; !value { + keys[entry] = true + list = append(list, entry) + } + } + return list +} diff --git a/restapi/utils_test.go b/restapi/utils_test.go new file mode 100644 index 000000000..591eb0cda --- /dev/null +++ b/restapi/utils_test.go @@ -0,0 +1,64 @@ +// 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 . + +package restapi + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDifferenceArrays(t *testing.T) { + assert := assert.New(t) + exampleArrayAMock := []string{"a", "b", "c"} + exampleArrayBMock := []string{"b", "d"} + resultABArrayMock := []string{"a", "c"} + resultBAArrayMock := []string{"d"} + + // Test-1: test DifferenceArrays() with array a vs array b + diffArray := DifferenceArrays(exampleArrayAMock, exampleArrayBMock) + assert.ElementsMatchf(diffArray, resultABArrayMock, "return array AB doesn't match %s") + + // Test-2: test DifferenceArrays() with array b vs array a + diffArray2 := DifferenceArrays(exampleArrayBMock, exampleArrayAMock) + assert.ElementsMatchf(diffArray2, resultBAArrayMock, "return array BA doesn't match %s") +} + +func TestIsElementInArray(t *testing.T) { + assert := assert.New(t) + + exampleElementsArray := []string{"c", "a", "d", "b"} + + // Test-1: test IsElementInArray() with element that is in the list + responseArray := IsElementInArray(exampleElementsArray, "a") + assert.Equal(true, responseArray) + + // Test-2: test IsElementInArray() with element that is not in the list + responseArray2 := IsElementInArray(exampleElementsArray, "e") + assert.Equal(false, responseArray2) +} + +func TestUniqueKeys(t *testing.T) { + assert := assert.New(t) + + exampleMixedArray := []string{"a", "b", "c", "e", "d", "b", "c", "h", "f", "g"} + exampleUniqueArray := []string{"a", "b", "c", "e", "d", "h", "f", "g"} + + // Test-1 test UniqueKeys returns an array with unique elements + responseArray := UniqueKeys(exampleMixedArray) + assert.ElementsMatchf(responseArray, exampleUniqueArray, "returned array doesn't contain the correct elements %s") +} diff --git a/swagger.yml b/swagger.yml index 13598a209..16173648d 100644 --- a/swagger.yml +++ b/swagger.yml @@ -268,6 +268,31 @@ paths: $ref: "#/definitions/error" tags: - AdminAPI + /users/{name}/groups: + put: + summary: Update Groups for a user + operationId: UpdateUserGroups + parameters: + - name: name + in: path + required: true + type: string + - name: body + in: body + required: true + schema: + $ref: "#/definitions/updateUserGroups" + responses: + 200: + description: A successful response. + schema: + $ref: "#/definitions/user" + default: + description: Generic error response. + schema: + $ref: "#/definitions/error" + tags: + - AdminAPI /groups: get: summary: List Groups @@ -1057,3 +1082,12 @@ definitions: type: array items: type: string + updateUserGroups: + type: object + required: + - groups + properties: + groups: + type: array + items: + type: string