Merge pull request #209 from versity/feat/issue-205-list-accs

Issue 205 list users in admin CLI
This commit is contained in:
Ben McClelland
2023-09-05 15:23:32 -07:00
committed by GitHub
8 changed files with 268 additions and 16 deletions

View File

@@ -31,6 +31,7 @@ type IAMService interface {
CreateAccount(access string, account Account) error
GetUserAccount(access string) (Account, error)
DeleteUserAccount(access string) error
ListUserAccounts() ([]UserAcc, error)
}
var ErrNoSuchUser = errors.New("user not found")

View File

@@ -41,6 +41,12 @@ type Storer interface {
StoreIAM(UpdateAcctFunc) error
}
type UserAcc struct {
Access string `json:"access"`
Secret string `json:"secret"`
Role string `json:"role"`
}
// IAMConfig stores all internal IAM accounts
type IAMConfig struct {
AccessAccounts map[string]Account `json:"accessAccounts"`
@@ -179,3 +185,34 @@ func (s *IAMServiceInternal) DeleteUserAccount(access string) error {
return b, nil
})
}
// ListUserAccounts lists all the user accounts stored.
func (s *IAMServiceInternal) ListUserAccounts() (accs []UserAcc, err error) {
s.mu.RLock()
defer s.mu.RUnlock()
data, err := s.storer.GetIAM()
if err != nil {
return []UserAcc{}, fmt.Errorf("get iam data: %w", err)
}
serial := crc32.ChecksumIEEE(data)
if serial != s.serial {
s.mu.RUnlock()
err := s.updateCache()
s.mu.RLock()
if err != nil {
return []UserAcc{}, fmt.Errorf("refresh iam cache: %w", err)
}
}
for access, usr := range s.accts.AccessAccounts {
accs = append(accs, UserAcc{
Access: access,
Secret: usr.Secret,
Role: usr.Role,
})
}
return accs, nil
}

View File

@@ -17,6 +17,7 @@ package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
@@ -25,21 +26,20 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"github.com/urfave/cli/v2"
"github.com/versity/versitygw/auth"
)
var (
adminAccess string
adminSecret string
adminAccess string
adminSecret string
adminEndpoint string
)
func adminCommand() *cli.Command {
return &cli.Command{
Name: "admin",
Usage: "admin CLI tool",
Description: `admin CLI tool for interacting with admin api.
Here is the available api list:
create-user
`,
Name: "admin",
Usage: "admin CLI tool",
Description: `Admin CLI tool for interacting with admin APIs.`,
Subcommands: []*cli.Command{
{
Name: "create-user",
@@ -48,13 +48,13 @@ func adminCommand() *cli.Command {
Flags: []cli.Flag{
&cli.StringFlag{
Name: "access",
Usage: "access value for the new user",
Usage: "access key id for the new user",
Required: true,
Aliases: []string{"a"},
},
&cli.StringFlag{
Name: "secret",
Usage: "secret value for the new user",
Usage: "secret access key for the new user",
Required: true,
Aliases: []string{"s"},
},
@@ -73,20 +73,26 @@ func adminCommand() *cli.Command {
Flags: []cli.Flag{
&cli.StringFlag{
Name: "access",
Usage: "access value for the user to be deleted",
Usage: "access key id of the user to be deleted",
Required: true,
Aliases: []string{"a"},
},
},
},
{
Name: "list-users",
Usage: "List all the gateway users",
Action: listUsers,
},
},
Flags: []cli.Flag{
// TODO: create a configuration file for this
&cli.StringFlag{
Name: "access",
Usage: "admin access account",
Usage: "admin access key id",
EnvVars: []string{"ADMIN_ACCESS_KEY_ID", "ADMIN_ACCESS_KEY"},
Aliases: []string{"a"},
Required: true,
Destination: &adminAccess,
},
&cli.StringFlag{
@@ -94,8 +100,16 @@ func adminCommand() *cli.Command {
Usage: "admin secret access key",
EnvVars: []string{"ADMIN_SECRET_ACCESS_KEY", "ADMIN_SECRET_KEY"},
Aliases: []string{"s"},
Required: true,
Destination: &adminSecret,
},
&cli.StringFlag{
Name: "endpoint-url",
Usage: "admin apis endpoint url",
Aliases: []string{"er"},
Required: true,
Destination: &adminEndpoint,
},
},
}
}
@@ -109,7 +123,7 @@ func createUser(ctx *cli.Context) error {
return fmt.Errorf("invalid input parameter for role")
}
req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("http://localhost:7070/create-user?access=%v&secret=%v&role=%v", access, secret, role), nil)
req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%v/create-user?access=%v&secret=%v&role=%v", adminEndpoint, access, secret, role), nil)
if err != nil {
return fmt.Errorf("failed to send the request: %w", err)
}
@@ -137,6 +151,7 @@ func createUser(ctx *cli.Context) error {
if err != nil {
return err
}
defer resp.Body.Close()
fmt.Printf("%s\n", body)
@@ -149,7 +164,7 @@ func deleteUser(ctx *cli.Context) error {
return fmt.Errorf("invalid input parameter for the new user")
}
req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("http://localhost:7070/delete-user?access=%v", access), nil)
req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%v/delete-user?access=%v", adminEndpoint, access), nil)
if err != nil {
return fmt.Errorf("failed to send the request: %w", err)
}
@@ -177,8 +192,55 @@ func deleteUser(ctx *cli.Context) error {
if err != nil {
return err
}
defer resp.Body.Close()
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 {
return fmt.Errorf("failed to send the request: %w", err)
}
signer := v4.NewSigner()
hashedPayload := sha256.Sum256([]byte{})
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 := http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to send the request: %w", err)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
defer resp.Body.Close()
var accs []auth.UserAcc
if err := json.Unmarshal(body, &accs); err != nil {
return err
}
jsonData, err := json.MarshalIndent(accs, "", " ")
if err != nil {
return err
}
fmt.Println(string(jsonData))
return nil
}

View File

@@ -461,7 +461,7 @@ type user struct {
func createUsers(s *S3Conf, users []user) error {
for _, usr := range users {
out, err := execCommand("admin", "-a", s.awsID, "-s", s.awsSecret, "create-user", "-a", usr.access, "-s", usr.secret, "-r", usr.role)
out, err := execCommand("admin", "-a", s.awsID, "-s", s.awsSecret, "-er", s.endpoint, "create-user", "-a", usr.access, "-s", usr.secret, "-r", usr.role)
if err != nil {
return err
}

View File

@@ -60,3 +60,16 @@ func (c AdminController) DeleteUser(ctx *fiber.Ctx) error {
return ctx.SendString("The user has been deleted successfully")
}
func (c AdminController) ListUsers(ctx *fiber.Ctx) error {
role := ctx.Locals("role").(string)
if role != "admin" {
return fmt.Errorf("access denied: only admin users have access to this resource")
}
accs, err := c.IAMService.ListUserAccounts()
if err != nil {
return err
}
return ctx.JSON(accs)
}

View File

@@ -15,6 +15,7 @@
package controllers
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
@@ -171,3 +172,99 @@ func TestAdminController_DeleteUser(t *testing.T) {
}
}
}
func TestAdminController_ListUsers(t *testing.T) {
type args struct {
req *http.Request
}
adminController := AdminController{
IAMService: &IAMServiceMock{
ListUserAccountsFunc: func() ([]auth.UserAcc, error) {
return []auth.UserAcc{}, nil
},
},
}
adminControllerErr := AdminController{
IAMService: &IAMServiceMock{
ListUserAccountsFunc: func() ([]auth.UserAcc, error) {
return []auth.UserAcc{}, fmt.Errorf("server error")
},
},
}
appErr := fiber.New()
appErr.Use(func(ctx *fiber.Ctx) error {
ctx.Locals("role", "admin")
return ctx.Next()
})
appErr.Patch("/list-users", adminControllerErr.ListUsers)
appRoleErr := fiber.New()
appRoleErr.Use(func(ctx *fiber.Ctx) error {
ctx.Locals("role", "user")
return ctx.Next()
})
appRoleErr.Patch("/list-users", adminController.ListUsers)
appSucc := fiber.New()
appSucc.Use(func(ctx *fiber.Ctx) error {
ctx.Locals("role", "admin")
return ctx.Next()
})
appSucc.Patch("/list-users", adminController.ListUsers)
tests := []struct {
name string
app *fiber.App
args args
wantErr bool
statusCode int
}{
{
name: "Admin-list-users-access-denied",
app: appRoleErr,
args: args{
req: httptest.NewRequest(http.MethodPatch, "/list-users", nil),
},
wantErr: false,
statusCode: 500,
},
{
name: "Admin-list-users-iam-error",
app: appErr,
args: args{
req: httptest.NewRequest(http.MethodPatch, "/list-users", nil),
},
wantErr: false,
statusCode: 500,
},
{
name: "Admin-list-users-success",
app: appSucc,
args: args{
req: httptest.NewRequest(http.MethodPatch, "/list-users", nil),
},
wantErr: false,
statusCode: 200,
},
}
for _, tt := range tests {
resp, err := tt.app.Test(tt.args.req)
if (err != nil) != tt.wantErr {
t.Errorf("AdminController.ListUsers() error = %v, wantErr %v", err, tt.wantErr)
}
if resp.StatusCode != tt.statusCode {
t.Errorf("AdminController.ListUsers() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
}
}
}

View File

@@ -27,6 +27,9 @@ var _ auth.IAMService = &IAMServiceMock{}
// GetUserAccountFunc: func(access string) (auth.Account, error) {
// panic("mock out the GetUserAccount method")
// },
// ListUserAccountsFunc: func() ([]auth.UserAcc, error) {
// panic("mock out the ListUserAccounts method")
// },
// }
//
// // use mockedIAMService in code that requires auth.IAMService
@@ -43,6 +46,9 @@ type IAMServiceMock struct {
// GetUserAccountFunc mocks the GetUserAccount method.
GetUserAccountFunc func(access string) (auth.Account, error)
// ListUserAccountsFunc mocks the ListUserAccounts method.
ListUserAccountsFunc func() ([]auth.UserAcc, error)
// calls tracks calls to the methods.
calls struct {
// CreateAccount holds details about calls to the CreateAccount method.
@@ -62,10 +68,14 @@ type IAMServiceMock struct {
// Access is the access argument value.
Access string
}
// ListUserAccounts holds details about calls to the ListUserAccounts method.
ListUserAccounts []struct {
}
}
lockCreateAccount sync.RWMutex
lockDeleteUserAccount sync.RWMutex
lockGetUserAccount sync.RWMutex
lockListUserAccounts sync.RWMutex
}
// CreateAccount calls CreateAccountFunc.
@@ -167,3 +177,30 @@ func (mock *IAMServiceMock) GetUserAccountCalls() []struct {
mock.lockGetUserAccount.RUnlock()
return calls
}
// ListUserAccounts calls ListUserAccountsFunc.
func (mock *IAMServiceMock) ListUserAccounts() ([]auth.UserAcc, error) {
if mock.ListUserAccountsFunc == nil {
panic("IAMServiceMock.ListUserAccountsFunc: method is nil but IAMService.ListUserAccounts was just called")
}
callInfo := struct {
}{}
mock.lockListUserAccounts.Lock()
mock.calls.ListUserAccounts = append(mock.calls.ListUserAccounts, callInfo)
mock.lockListUserAccounts.Unlock()
return mock.ListUserAccountsFunc()
}
// ListUserAccountsCalls gets all the calls that were made to ListUserAccounts.
// Check the length with:
//
// len(mockedIAMService.ListUserAccountsCalls())
func (mock *IAMServiceMock) ListUserAccountsCalls() []struct {
} {
var calls []struct {
}
mock.lockListUserAccounts.RLock()
calls = mock.calls.ListUserAccounts
mock.lockListUserAccounts.RUnlock()
return calls
}

View File

@@ -29,10 +29,15 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
s3ApiController := controllers.New(be, iam, logger, evs)
adminController := controllers.AdminController{IAMService: iam}
//CreateUser admin api
app.Patch("/create-user", adminController.CreateUser)
// Admin Delete api
// DeleteUsers admin api
app.Patch("/delete-user", adminController.DeleteUser)
// ListUsers admin api
app.Patch("/list-users", adminController.ListUsers)
// ListBuckets action
app.Get("/", s3ApiController.ListBuckets)