From 1808335381243de4f8231f291fb4d95c5cdbf4a5 Mon Sep 17 00:00:00 2001 From: jonaustin09 Date: Wed, 19 Jun 2024 16:06:35 -0400 Subject: [PATCH] feat: Added admin api and CLI command for updating gateway users attributes --- auth/iam.go | 26 ++++++- auth/iam_cache.go | 25 +++++++ auth/iam_internal.go | 29 ++++++++ auth/iam_ldap.go | 74 ++++++++++++++++--- auth/iam_s3_object.go | 20 ++++++ auth/iam_single.go | 5 ++ auth/iam_vault.go | 22 ++++++ cmd/versitygw/admin.go | 85 ++++++++++++++++++++++ cmd/versitygw/main.go | 15 ++++ s3api/admin-router.go | 3 + s3api/controllers/admin.go | 31 ++++++++ s3api/controllers/admin_test.go | 116 ++++++++++++++++++++++++++++++ s3api/controllers/iam_moq_test.go | 50 +++++++++++++ s3api/router.go | 3 + 14 files changed, 491 insertions(+), 13 deletions(-) diff --git a/auth/iam.go b/auth/iam.go index a5027c3..11a40c9 100644 --- a/auth/iam.go +++ b/auth/iam.go @@ -37,12 +37,32 @@ type Account struct { GroupID int `json:"groupID"` } +// Mutable props, which could be changed when updating an IAM account +type MutableProps struct { + Secret *string `json:"secret"` + UserID *int `json:"userID"` + GroupID *int `json:"groupID"` +} + +func updateAcc(acc *Account, props MutableProps) { + if props.Secret != nil { + acc.Secret = *props.Secret + } + if props.GroupID != nil { + acc.GroupID = *props.GroupID + } + if props.UserID != nil { + acc.UserID = *props.UserID + } +} + // IAMService is the interface for all IAM service implementations // //go:generate moq -out ../s3api/controllers/iam_moq_test.go -pkg controllers . IAMService type IAMService interface { CreateAccount(account Account) error GetUserAccount(access string) (Account, error) + UpdateUserAccount(access string, props MutableProps) error DeleteUserAccount(access string) error ListUserAccounts() ([]Account, error) Shutdown() error @@ -65,6 +85,8 @@ type Opts struct { LDAPAccessAtr string LDAPSecretAtr string LDAPRoleAtr string + LDAPUserIdAtr string + LDAPGroupIdAtr string VaultEndpointURL string VaultSecretStoragePath string VaultMountPath string @@ -96,8 +118,8 @@ func New(o *Opts) (IAMService, error) { fmt.Printf("initializing internal IAM with %q\n", o.Dir) case o.LDAPServerURL != "": svc, err = NewLDAPService(o.LDAPServerURL, o.LDAPBindDN, o.LDAPPassword, - o.LDAPQueryBase, o.LDAPAccessAtr, o.LDAPSecretAtr, o.LDAPRoleAtr, - o.LDAPObjClasses) + o.LDAPQueryBase, o.LDAPAccessAtr, o.LDAPSecretAtr, o.LDAPRoleAtr, o.LDAPUserIdAtr, + o.LDAPGroupIdAtr, o.LDAPObjClasses) fmt.Printf("initializing LDAP IAM with %q\n", o.LDAPServerURL) case o.S3Endpoint != "": svc, err = NewS3(o.S3Access, o.S3Secret, o.S3Region, o.S3Bucket, diff --git a/auth/iam_cache.go b/auth/iam_cache.go index b674fca..2eea1ba 100644 --- a/auth/iam_cache.go +++ b/auth/iam_cache.go @@ -66,6 +66,21 @@ func (i *icache) get(k string) (Account, bool) { return v.value, true } +func (i *icache) update(k string, props MutableProps) { + i.Lock() + defer i.Unlock() + + item, found := i.items[k] + if found { + updateAcc(&item.value, props) + + // refresh the expiration date + item.exp = time.Now().Add(i.expire) + + i.items[k] = item + } +} + func (i *icache) Delete(k string) { i.Lock() delete(i.items, k) @@ -166,6 +181,16 @@ func (c *IAMCache) DeleteUserAccount(access string) error { return nil } +func (c *IAMCache) UpdateUserAccount(access string, props MutableProps) error { + err := c.service.UpdateUserAccount(access, props) + if err != nil { + return err + } + + c.iamcache.update(access, props) + return nil +} + // ListUserAccounts is a passthrough to the underlying service and // does not make use of the cache func (c *IAMCache) ListUserAccounts() ([]Account, error) { diff --git a/auth/iam_internal.go b/auth/iam_internal.go index 6588b64..facfd02 100644 --- a/auth/iam_internal.go +++ b/auth/iam_internal.go @@ -113,6 +113,35 @@ func (s *IAMServiceInternal) GetUserAccount(access string) (Account, error) { return acct, nil } +// UpdateUserAccount updates the specified user account fields. Returns +// ErrNoSuchUser if the account does not exist. +func (s *IAMServiceInternal) UpdateUserAccount(access string, props MutableProps) error { + s.Lock() + defer s.Unlock() + + return s.storeIAM(func(data []byte) ([]byte, error) { + conf, err := parseIAM(data) + if err != nil { + return nil, fmt.Errorf("get iam data: %w", err) + } + + acc, found := conf.AccessAccounts[access] + if !found { + return nil, ErrNoSuchUser + } + + updateAcc(&acc, props) + conf.AccessAccounts[access] = acc + + b, err := json.Marshal(conf) + if err != nil { + return nil, fmt.Errorf("failed to serialize iam: %w", err) + } + + return b, nil + }) +} + // DeleteUserAccount deletes the specified user account. Does not check if // account exists. func (s *IAMServiceInternal) DeleteUserAccount(access string) error { diff --git a/auth/iam_ldap.go b/auth/iam_ldap.go index a115229..962d9ab 100644 --- a/auth/iam_ldap.go +++ b/auth/iam_ldap.go @@ -16,6 +16,7 @@ package auth import ( "fmt" + "strconv" "strings" "github.com/go-ldap/ldap/v3" @@ -28,12 +29,15 @@ type LdapIAMService struct { accessAtr string secretAtr string roleAtr string + groupIdAtr string + userIdAtr string } var _ IAMService = &LdapIAMService{} -func NewLDAPService(url, bindDN, pass, queryBase, accAtr, secAtr, roleAtr, objClasses string) (IAMService, error) { - if url == "" || bindDN == "" || pass == "" || queryBase == "" || accAtr == "" || secAtr == "" || roleAtr == "" || objClasses == "" { +func NewLDAPService(url, bindDN, pass, queryBase, accAtr, secAtr, roleAtr, userIdAtr, groupIdAtr, objClasses string) (IAMService, error) { + if url == "" || bindDN == "" || pass == "" || queryBase == "" || accAtr == "" || + secAtr == "" || roleAtr == "" || userIdAtr == "" || groupIdAtr == "" || objClasses == "" { return nil, fmt.Errorf("required parameters list not fully provided") } conn, err := ldap.DialURL(url) @@ -52,15 +56,19 @@ func NewLDAPService(url, bindDN, pass, queryBase, accAtr, secAtr, roleAtr, objCl accessAtr: accAtr, secretAtr: secAtr, roleAtr: roleAtr, + userIdAtr: userIdAtr, + groupIdAtr: groupIdAtr, }, nil } func (ld *LdapIAMService) CreateAccount(account Account) error { - userEntry := ldap.NewAddRequest(fmt.Sprintf("%v=%v, %v", ld.accessAtr, account.Access, ld.queryBase), nil) + userEntry := ldap.NewAddRequest(fmt.Sprintf("%v=%v,%v", ld.accessAtr, account.Access, ld.queryBase), nil) userEntry.Attribute("objectClass", ld.objClasses) userEntry.Attribute(ld.accessAtr, []string{account.Access}) userEntry.Attribute(ld.secretAtr, []string{account.Secret}) userEntry.Attribute(ld.roleAtr, []string{string(account.Role)}) + userEntry.Attribute(ld.groupIdAtr, []string{fmt.Sprint(account.GroupID)}) + userEntry.Attribute(ld.userIdAtr, []string{fmt.Sprint(account.UserID)}) err := ld.conn.Add(userEntry) if err != nil { @@ -79,7 +87,7 @@ func (ld *LdapIAMService) GetUserAccount(access string) (Account, error) { 0, false, fmt.Sprintf("(%v=%v)", ld.accessAtr, access), - []string{ld.accessAtr, ld.secretAtr, ld.roleAtr}, + []string{ld.accessAtr, ld.secretAtr, ld.roleAtr, ld.userIdAtr, ld.groupIdAtr}, nil, ) @@ -88,14 +96,48 @@ func (ld *LdapIAMService) GetUserAccount(access string) (Account, error) { return Account{}, err } + if len(result.Entries) == 0 { + return Account{}, ErrNoSuchUser + } + entry := result.Entries[0] + groupId, err := strconv.Atoi(entry.GetAttributeValue(ld.groupIdAtr)) + if err != nil { + return Account{}, fmt.Errorf("invalid entry value for group-id: %v", entry.GetAttributeValue(ld.groupIdAtr)) + } + userId, err := strconv.Atoi(entry.GetAttributeValue(ld.userIdAtr)) + if err != nil { + return Account{}, fmt.Errorf("invalid entry value for group-id: %v", entry.GetAttributeValue(ld.userIdAtr)) + } return Account{ - Access: entry.GetAttributeValue(ld.accessAtr), - Secret: entry.GetAttributeValue(ld.secretAtr), - Role: Role(entry.GetAttributeValue(ld.roleAtr)), + Access: entry.GetAttributeValue(ld.accessAtr), + Secret: entry.GetAttributeValue(ld.secretAtr), + Role: Role(entry.GetAttributeValue(ld.roleAtr)), + GroupID: groupId, + UserID: userId, }, nil } +func (ld *LdapIAMService) UpdateUserAccount(access string, props MutableProps) error { + req := ldap.NewModifyRequest(fmt.Sprintf("%v=%v, %v", ld.accessAtr, access, ld.queryBase), nil) + if props.Secret != nil { + req.Replace(ld.secretAtr, []string{*props.Secret}) + } + if props.GroupID != nil { + req.Replace(ld.groupIdAtr, []string{fmt.Sprint(*props.GroupID)}) + } + if props.UserID != nil { + req.Replace(ld.userIdAtr, []string{fmt.Sprint(*props.UserID)}) + } + + err := ld.conn.Modify(req) + //TODO: Handle non existing user case + if err != nil { + return err + } + return nil +} + func (ld *LdapIAMService) DeleteUserAccount(access string) error { delReq := ldap.NewDelRequest(fmt.Sprintf("%v=%v, %v", ld.accessAtr, access, ld.queryBase), nil) @@ -120,7 +162,7 @@ func (ld *LdapIAMService) ListUserAccounts() ([]Account, error) { 0, false, fmt.Sprintf("(&%v)", searchFilter), - []string{ld.accessAtr, ld.secretAtr, ld.roleAtr}, + []string{ld.accessAtr, ld.secretAtr, ld.roleAtr, ld.groupIdAtr, ld.userIdAtr}, nil, ) @@ -131,10 +173,20 @@ func (ld *LdapIAMService) ListUserAccounts() ([]Account, error) { result := []Account{} for _, el := range resp.Entries { + groupId, err := strconv.Atoi(el.GetAttributeValue(ld.groupIdAtr)) + if err != nil { + return nil, fmt.Errorf("invalid entry value for group-id: %v", el.GetAttributeValue(ld.groupIdAtr)) + } + userId, err := strconv.Atoi(el.GetAttributeValue(ld.userIdAtr)) + if err != nil { + return nil, fmt.Errorf("invalid entry value for group-id: %v", el.GetAttributeValue(ld.userIdAtr)) + } result = append(result, Account{ - Access: el.GetAttributeValue(ld.accessAtr), - Secret: el.GetAttributeValue(ld.secretAtr), - Role: Role(el.GetAttributeValue(ld.roleAtr)), + Access: el.GetAttributeValue(ld.accessAtr), + Secret: el.GetAttributeValue(ld.secretAtr), + Role: Role(el.GetAttributeValue(ld.roleAtr)), + GroupID: groupId, + UserID: userId, }) } diff --git a/auth/iam_s3_object.go b/auth/iam_s3_object.go index 0b7637c..d6eed43 100644 --- a/auth/iam_s3_object.go +++ b/auth/iam_s3_object.go @@ -140,6 +140,26 @@ func (s *IAMServiceS3) GetUserAccount(access string) (Account, error) { return acct, nil } +func (s *IAMServiceS3) UpdateUserAccount(access string, props MutableProps) error { + s.Lock() + defer s.Unlock() + + conf, err := s.getAccounts() + if err != nil { + return err + } + + acc, ok := conf.AccessAccounts[access] + if !ok { + return ErrNoSuchUser + } + + updateAcc(&acc, props) + conf.AccessAccounts[access] = acc + + return s.storeAccts(conf) +} + func (s *IAMServiceS3) DeleteUserAccount(access string) error { s.Lock() defer s.Unlock() diff --git a/auth/iam_single.go b/auth/iam_single.go index af8b765..a946b3b 100644 --- a/auth/iam_single.go +++ b/auth/iam_single.go @@ -35,6 +35,11 @@ func (IAMServiceSingle) GetUserAccount(access string) (Account, error) { return Account{}, ErrNoSuchUser } +// UpdateUserAccount no accounts in single tenant mode +func (IAMServiceSingle) UpdateUserAccount(access string, props MutableProps) error { + return ErrNotSupported +} + // DeleteUserAccount no accounts in single tenant mode func (IAMServiceSingle) DeleteUserAccount(access string) error { return ErrNotSupported diff --git a/auth/iam_vault.go b/auth/iam_vault.go index fe2d6be..b4f6fc2 100644 --- a/auth/iam_vault.go +++ b/auth/iam_vault.go @@ -140,6 +140,28 @@ func (vt *VaultIAMService) GetUserAccount(access string) (Account, error) { return acc, nil } +func (vt *VaultIAMService) UpdateUserAccount(access string, props MutableProps) error { + //TODO: We need something like a transaction here ? + acc, err := vt.GetUserAccount(access) + if err != nil { + return err + } + + updateAcc(&acc, props) + + err = vt.DeleteUserAccount(access) + if err != nil { + return err + } + + err = vt.CreateAccount(acc) + if err != nil { + return err + } + + return nil +} + func (vt *VaultIAMService) DeleteUserAccount(access string) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) _, err := vt.client.Secrets.KvV2DeleteMetadataAndAllVersions(ctx, vt.secretStoragePath+"/"+access, vt.reqOpts...) diff --git a/cmd/versitygw/admin.go b/cmd/versitygw/admin.go index 46fefae..f9e9a51 100644 --- a/cmd/versitygw/admin.go +++ b/cmd/versitygw/admin.go @@ -82,6 +82,34 @@ func adminCommand() *cli.Command { }, }, }, + { + Name: "update-user", + Usage: "Updates a user account", + Action: updateUser, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "access", + Usage: "user access key id to be updated", + Required: true, + Aliases: []string{"a"}, + }, + &cli.StringFlag{ + Name: "secret", + Usage: "secret access key for the new user", + Aliases: []string{"s"}, + }, + &cli.IntFlag{ + Name: "user-id", + Usage: "userID for the new user", + Aliases: []string{"ui"}, + }, + &cli.IntFlag{ + Name: "group-id", + Usage: "groupID for the new user", + Aliases: []string{"gi"}, + }, + }, + }, { Name: "delete-user", Usage: "Delete a user", @@ -276,6 +304,63 @@ func deleteUser(ctx *cli.Context) error { return nil } +func updateUser(ctx *cli.Context) error { + access, secret, userId, groupId := ctx.String("access"), ctx.String("secret"), ctx.Int("user-id"), ctx.Int("group-id") + props := auth.MutableProps{} + if ctx.IsSet("secret") { + props.Secret = &secret + } + if ctx.IsSet("user-id") { + props.UserID = &userId + } + if ctx.IsSet("group-id") { + props.GroupID = &groupId + } + + propsJSON, err := json.Marshal(props) + if err != nil { + return fmt.Errorf("failed to parse user attributes: %w", err) + } + + req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%v/update-user?access=%v", adminEndpoint, access), bytes.NewBuffer(propsJSON)) + if err != nil { + return fmt.Errorf("failed to send the request: %w", err) + } + + signer := v4.NewSigner() + + hashedPayload := sha256.Sum256(propsJSON) + hexPayload := hex.EncodeToString(hashedPayload[:]) + + req.Header.Set("X-Amz-Content-Sha256", hexPayload) + + signErr := signer.SignHTTP(req.Context(), aws.Credentials{AccessKeyID: adminAccess, SecretAccessKey: adminSecret}, req, hexPayload, "s3", region, time.Now()) + if signErr != nil { + return fmt.Errorf("failed to sign the request: %w", err) + } + + client := initHTTPClient() + + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to send the request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + if resp.StatusCode >= 400 { + return fmt.Errorf("%s", body) + } + + fmt.Printf("%s\n", body) + + return nil +} + func listUsers(ctx *cli.Context) error { req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%v/list-users", adminEndpoint), nil) if err != nil { diff --git a/cmd/versitygw/main.go b/cmd/versitygw/main.go index c60cb34..ac60571 100644 --- a/cmd/versitygw/main.go +++ b/cmd/versitygw/main.go @@ -56,6 +56,7 @@ var ( ldapURL, ldapBindDN, ldapPassword string ldapQueryBase, ldapObjClasses string ldapAccessAtr, ldapSecAtr, ldapRoleAtr string + ldapUserIdAtr, ldapGroupIdAtr string vaultEndpointURL, vaultSecretStoragePath string vaultMountPath, vaultRootToken string vaultRoleId, vaultRoleSecret string @@ -331,6 +332,18 @@ func initFlags() []cli.Flag { EnvVars: []string{"VGW_IAM_LDAP_ROLE_ATR"}, Destination: &ldapRoleAtr, }, + &cli.StringFlag{ + Name: "iam-ldap-user-id-atr", + Usage: "ldap server user id attribute name", + EnvVars: []string{"VGW_IAM_LDAP_USER_ID_ATR"}, + Destination: &ldapUserIdAtr, + }, + &cli.StringFlag{ + Name: "iam-ldap-group-id-atr", + Usage: "ldap server user group id attribute name", + EnvVars: []string{"VGW_IAM_LDAP_GROUP_ID_ATR"}, + Destination: &ldapGroupIdAtr, + }, &cli.StringFlag{ Name: "iam-vault-endpoint-url", Usage: "vault server url", @@ -569,6 +582,8 @@ func runGateway(ctx context.Context, be backend.Backend) error { LDAPAccessAtr: ldapAccessAtr, LDAPSecretAtr: ldapSecAtr, LDAPRoleAtr: ldapRoleAtr, + LDAPUserIdAtr: ldapUserIdAtr, + LDAPGroupIdAtr: ldapGroupIdAtr, VaultEndpointURL: vaultEndpointURL, VaultSecretStoragePath: vaultSecretStoragePath, VaultMountPath: vaultMountPath, diff --git a/s3api/admin-router.go b/s3api/admin-router.go index 8aef22d..0abec3d 100644 --- a/s3api/admin-router.go +++ b/s3api/admin-router.go @@ -32,6 +32,9 @@ func (ar *S3AdminRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMSe // DeleteUsers admin api app.Patch("/delete-user", controller.DeleteUser) + // UpdateUser admin api + app.Patch("/update-user", controller.UpdateUser) + // ListUsers admin api app.Patch("/list-users", controller.ListUsers) diff --git a/s3api/controllers/admin.go b/s3api/controllers/admin.go index 085b014..2305999 100644 --- a/s3api/controllers/admin.go +++ b/s3api/controllers/admin.go @@ -63,6 +63,37 @@ func (c AdminController) CreateUser(ctx *fiber.Ctx) error { return ctx.Status(fiber.StatusCreated).SendString("The user has been created successfully") } +func (c AdminController) UpdateUser(ctx *fiber.Ctx) error { + acct := ctx.Locals("account").(auth.Account) + if acct.Role != "admin" { + return ctx.Status(fiber.StatusForbidden).SendString("access denied: only admin users have access to this resource") + } + + access := ctx.Query("access") + if access == "" { + return ctx.Status(fiber.StatusBadRequest).SendString("missing user access parameter") + } + + var props auth.MutableProps + if err := json.Unmarshal(ctx.Body(), &props); err != nil { + return ctx.Status(fiber.StatusBadRequest).SendString(fmt.Errorf("invalid request body %w", err).Error()) + } + + err := c.iam.UpdateUserAccount(access, props) + if err != nil { + status := fiber.StatusInternalServerError + msg := fmt.Errorf("failed to update user account: %w", err).Error() + + if strings.Contains(msg, "user not found") { + status = fiber.StatusNotFound + } + + return ctx.Status(status).SendString(msg) + } + + return ctx.SendString("the user has been updated successfully") +} + func (c AdminController) DeleteUser(ctx *fiber.Ctx) error { access := ctx.Query("access") acct := ctx.Locals("account").(auth.Account) diff --git a/s3api/controllers/admin_test.go b/s3api/controllers/admin_test.go index f26f247..aa871c4 100644 --- a/s3api/controllers/admin_test.go +++ b/s3api/controllers/admin_test.go @@ -119,6 +119,122 @@ func TestAdminController_CreateUser(t *testing.T) { } } +func TestAdminController_UpdateUser(t *testing.T) { + type args struct { + req *http.Request + } + + adminController := AdminController{ + iam: &IAMServiceMock{ + UpdateUserAccountFunc: func(access string, props auth.MutableProps) error { + return nil + }, + }, + } + + app := fiber.New() + + app.Use(func(ctx *fiber.Ctx) error { + ctx.Locals("account", auth.Account{Access: "admin1", Secret: "secret", Role: "admin"}) + return ctx.Next() + }) + + app.Patch("/update-user", adminController.UpdateUser) + + appErr := fiber.New() + + appErr.Use(func(ctx *fiber.Ctx) error { + ctx.Locals("account", auth.Account{Access: "user1", Secret: "secret", Role: "user"}) + return ctx.Next() + }) + + appErr.Patch("/update-user", adminController.UpdateUser) + + successBody, _ := json.Marshal(auth.MutableProps{Secret: getPtr("hello")}) + + adminControllerErr := AdminController{ + iam: &IAMServiceMock{ + UpdateUserAccountFunc: func(access string, props auth.MutableProps) error { + return auth.ErrNoSuchUser + }, + }, + } + + appNotFound := fiber.New() + + appNotFound.Use(func(ctx *fiber.Ctx) error { + ctx.Locals("account", auth.Account{Access: "admin1", Secret: "secret", Role: "admin"}) + return ctx.Next() + }) + + appNotFound.Patch("/update-user", adminControllerErr.UpdateUser) + + tests := []struct { + name string + app *fiber.App + args args + wantErr bool + statusCode int + }{ + { + name: "Admin-update-user-success", + app: app, + args: args{ + req: httptest.NewRequest(http.MethodPatch, "/update-user?access=access", bytes.NewBuffer(successBody)), + }, + wantErr: false, + statusCode: 200, + }, + { + name: "Admin-update-user-missing-access", + app: app, + args: args{ + req: httptest.NewRequest(http.MethodPatch, "/update-user", bytes.NewBuffer(successBody)), + }, + wantErr: false, + statusCode: 400, + }, + { + name: "Admin-update-user-invalid-request-body", + app: app, + args: args{ + req: httptest.NewRequest(http.MethodPatch, "/update-user?access=access", nil), + }, + wantErr: false, + statusCode: 400, + }, + { + name: "Admin-update-user-invalid-requester-role", + app: appErr, + args: args{ + req: httptest.NewRequest(http.MethodPatch, "/update-user?access=access", nil), + }, + wantErr: false, + statusCode: 403, + }, + { + name: "Admin-update-user-not-found", + app: appNotFound, + args: args{ + req: httptest.NewRequest(http.MethodPatch, "/update-user?access=access", bytes.NewBuffer(successBody)), + }, + wantErr: false, + statusCode: 404, + }, + } + for _, tt := range tests { + resp, err := tt.app.Test(tt.args.req) + + if (err != nil) != tt.wantErr { + t.Errorf("AdminController.UpdateUser() error = %v, wantErr %v", err, tt.wantErr) + } + + if resp.StatusCode != tt.statusCode { + t.Errorf("AdminController.UpdateUser() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode) + } + } +} + func TestAdminController_DeleteUser(t *testing.T) { type args struct { req *http.Request diff --git a/s3api/controllers/iam_moq_test.go b/s3api/controllers/iam_moq_test.go index 7349947..b131ec2 100644 --- a/s3api/controllers/iam_moq_test.go +++ b/s3api/controllers/iam_moq_test.go @@ -33,6 +33,9 @@ var _ auth.IAMService = &IAMServiceMock{} // ShutdownFunc: func() error { // panic("mock out the Shutdown method") // }, +// UpdateUserAccountFunc: func(access string, props auth.MutableProps) error { +// panic("mock out the UpdateUserAccount method") +// }, // } // // // use mockedIAMService in code that requires auth.IAMService @@ -55,6 +58,9 @@ type IAMServiceMock struct { // ShutdownFunc mocks the Shutdown method. ShutdownFunc func() error + // UpdateUserAccountFunc mocks the UpdateUserAccount method. + UpdateUserAccountFunc func(access string, props auth.MutableProps) error + // calls tracks calls to the methods. calls struct { // CreateAccount holds details about calls to the CreateAccount method. @@ -78,12 +84,20 @@ type IAMServiceMock struct { // Shutdown holds details about calls to the Shutdown method. Shutdown []struct { } + // UpdateUserAccount holds details about calls to the UpdateUserAccount method. + UpdateUserAccount []struct { + // Access is the access argument value. + Access string + // Props is the props argument value. + Props auth.MutableProps + } } lockCreateAccount sync.RWMutex lockDeleteUserAccount sync.RWMutex lockGetUserAccount sync.RWMutex lockListUserAccounts sync.RWMutex lockShutdown sync.RWMutex + lockUpdateUserAccount sync.RWMutex } // CreateAccount calls CreateAccountFunc. @@ -235,3 +249,39 @@ func (mock *IAMServiceMock) ShutdownCalls() []struct { mock.lockShutdown.RUnlock() return calls } + +// UpdateUserAccount calls UpdateUserAccountFunc. +func (mock *IAMServiceMock) UpdateUserAccount(access string, props auth.MutableProps) error { + if mock.UpdateUserAccountFunc == nil { + panic("IAMServiceMock.UpdateUserAccountFunc: method is nil but IAMService.UpdateUserAccount was just called") + } + callInfo := struct { + Access string + Props auth.MutableProps + }{ + Access: access, + Props: props, + } + mock.lockUpdateUserAccount.Lock() + mock.calls.UpdateUserAccount = append(mock.calls.UpdateUserAccount, callInfo) + mock.lockUpdateUserAccount.Unlock() + return mock.UpdateUserAccountFunc(access, props) +} + +// UpdateUserAccountCalls gets all the calls that were made to UpdateUserAccount. +// Check the length with: +// +// len(mockedIAMService.UpdateUserAccountCalls()) +func (mock *IAMServiceMock) UpdateUserAccountCalls() []struct { + Access string + Props auth.MutableProps +} { + var calls []struct { + Access string + Props auth.MutableProps + } + mock.lockUpdateUserAccount.RLock() + calls = mock.calls.UpdateUserAccount + mock.lockUpdateUserAccount.RUnlock() + return calls +} diff --git a/s3api/router.go b/s3api/router.go index 9e85ea2..c8d2279 100644 --- a/s3api/router.go +++ b/s3api/router.go @@ -40,6 +40,9 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ // DeleteUsers admin api app.Patch("/delete-user", adminController.DeleteUser) + // UpdateUser admin api + app.Patch("update-user", adminController.UpdateUser) + // ListUsers admin api app.Patch("/list-users", adminController.ListUsers)