diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 8da2ea9..a1719fc 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -60,6 +60,13 @@ jobs: CGO_ENABLED: 0 SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + - name: Check tests + continue-on-error: true + run: ginkgo -cover ./... + env: + GOOS: linux + CGO_ENABLED: 0 + - name: Add ming-w32 (x86 + win64) run: sudo apt-get install gcc-multilib gcc-mingw-w64 diff --git a/aws/aws_suite_test.go b/aws/aws_suite_test.go new file mode 100644 index 0000000..4c0bd6c --- /dev/null +++ b/aws/aws_suite_test.go @@ -0,0 +1,203 @@ +package aws_test + +import ( + "context" + "errors" + "fmt" + "io/ioutil" + "net" + "net/url" + "os" + "os/exec" + "path" + "runtime" + "strconv" + "testing" + "time" + + "github.com/hashicorp/go-uuid" + "github.com/nabbar/golib/aws" + "github.com/nabbar/golib/aws/configCustom" + "github.com/nabbar/golib/password" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var ( + cli aws.AWS + cfg aws.Config + ctx context.Context + cnl context.CancelFunc + filename = "./config.json" + minioMode = false +) + +/* + Using https://onsi.github.io/ginkgo/ + Running with $> ginkgo -cover . +*/ + +func TestGolibAwsHelper(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Aws Helper Suite") +} + +var _ = BeforeSuite(func() { + var ( + err error + name string + ) + + ctx, cnl = context.WithCancel(context.Background()) + + if err = loadConfig(); err != nil { + var ( + uri = &url.URL{ + Scheme: "http", + Host: "localhost:" + strconv.Itoa(GetFreePort()), + } + + accessKey = password.Generate(20) + secretKey = password.Generate(64) + ) + + cfg = configCustom.NewConfig("", accessKey, secretKey, uri, "us-east-1") + + cfg.SetRegion("us-east-1") + err = cfg.RegisterRegionAws(nil) + Expect(err).NotTo(HaveOccurred()) + + minioMode = true + + go LaunchMinio(uri.Host, accessKey, secretKey) + + for WaitMinio(uri.Host) { + time.Sleep(10 * time.Second) + } + + println("Minio is waiting on : " + uri.Host) + } + + cli, err = aws.New(ctx, cfg, nil) + Expect(err).NotTo(HaveOccurred()) + Expect(cli).NotTo(BeNil()) + + cli.ForcePathStyle(true) + + name, err = uuid.GenerateUUID() + Expect(err).ToNot(HaveOccurred()) + Expect(name).ToNot(BeEmpty()) + cli.Config().SetBucketName(name) +}) + +var _ = AfterSuite(func() { + cnl() +}) + +func loadConfig() error { + var ( + cnfByt []byte + err error + ) + + if _, err = os.Stat(filename); err != nil { + return err + } + + if cnfByt, err = ioutil.ReadFile(filename); err != nil { + return err + } + + if cfg, err = configCustom.NewConfigJsonUnmashal(cnfByt); err != nil { + return err + } + + if err := cfg.Validate(); err != nil { + return err + } + + return nil +} + +func BuildPolicy() string { + return `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:Get*"],"Resource":["arn:aws:s3:::*/*"]}]}` +} + +func BuildRole() string { + return `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"sts:AssumeRole","Principal":{"Service":"replication"}}]}` +} + +func GetFreePort() int { + var ( + addr *net.TCPAddr + lstn *net.TCPListener + err error + ) + + if addr, err = net.ResolveTCPAddr("tcp", "localhost:0"); err != nil { + panic(err) + } + + if lstn, err = net.ListenTCP("tcp", addr); err != nil { + panic(err) + } + + defer func() { + _ = lstn.Close() + }() + + return lstn.Addr().(*net.TCPAddr).Port +} + +func GetTempFolder() string { + if tmp, err := ioutil.TempDir("", "minio-data-*"); err != nil { + panic(err) + } else { + if _, err = os.Stat(tmp); errors.Is(err, os.ErrNotExist) { + if err = os.Mkdir(tmp, 0700); err != nil { + panic(err) + } + } else if err != nil { + panic(err) + } + + return tmp + } +} + +func DelTempFolder(folder string) { + if err := os.RemoveAll(folder); err != nil { + panic(err) + } +} + +func LaunchMinio(host, accessKey, secretKey string) { + os.Setenv("MINIO_ACCESS_KEY", accessKey) + os.Setenv("MINIO_SECRET_KEY", secretKey) + + tmp := GetTempFolder() + defer DelTempFolder(tmp) + + if _, minio, _, ok := runtime.Caller(0); ok { + if err := exec.CommandContext(ctx, path.Join(path.Dir(minio), "minio"), "server", "--address", host, tmp).Run(); err != nil { + panic(err) + } + } else { + //nolint #goerr113 + panic(fmt.Errorf("minio execution file not found")) + } + + //minio.Main([]string{"minio", "server", "--address", host, tmp}) +} + +func WaitMinio(host string) bool { + conn, err := net.DialTimeout("tcp", host, 10*time.Second) + + defer func() { + if conn != nil { + _ = conn.Close() + } + }() + + return err == nil +} diff --git a/aws/bucket/bucket.go b/aws/bucket/bucket.go new file mode 100644 index 0000000..8a0b04d --- /dev/null +++ b/aws/bucket/bucket.go @@ -0,0 +1,148 @@ +package bucket + +import ( + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/nabbar/golib/aws/helper" + "github.com/nabbar/golib/errors" +) + +func (cli *client) Check() errors.Error { + req := cli.s3.HeadBucketRequest(&s3.HeadBucketInput{ + Bucket: cli.GetBucketAws(), + }) + + out, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if err != nil { + return cli.GetError(err) + } + + if out == nil || out.HeadBucketOutput == nil { + //nolint #goerr113 + return helper.ErrorBucketNotFound.ErrorParent(fmt.Errorf("bucket: %s", cli.GetBucketName())) + } + + return nil +} + +func (cli *client) Create() errors.Error { + req := cli.s3.CreateBucketRequest(&s3.CreateBucketInput{ + Bucket: cli.GetBucketAws(), + }) + + _, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + return cli.GetError(err) +} + +func (cli *client) Delete() errors.Error { + req := cli.s3.DeleteBucketRequest(&s3.DeleteBucketInput{ + Bucket: cli.GetBucketAws(), + }) + + _, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + return cli.GetError(err) +} + +func (cli *client) List() ([]s3.Bucket, errors.Error) { + req := cli.s3.ListBucketsRequest(nil) + + out, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if err != nil { + return make([]s3.Bucket, 0), cli.GetError(err) + } + + if out == nil || out.Buckets == nil { + return make([]s3.Bucket, 0), helper.ErrorAwsEmpty.Error(nil) + } + + return out.Buckets, nil +} + +func (cli *client) SetVersioning(state bool) errors.Error { + var status s3.BucketVersioningStatus = helper.STATE_ENABLED + if !state { + status = helper.STATE_SUSPENDED + } + + vConf := s3.VersioningConfiguration{ + Status: status, + } + input := s3.PutBucketVersioningInput{ + Bucket: cli.GetBucketAws(), + VersioningConfiguration: &vConf, + } + + req := cli.s3.PutBucketVersioningRequest(&input) + _, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + return cli.GetError(err) +} + +func (cli *client) GetVersioning() (string, errors.Error) { + input := s3.GetBucketVersioningInput{ + Bucket: cli.GetBucketAws(), + } + + req := cli.s3.GetBucketVersioningRequest(&input) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + out, err := req.Send(cli.GetContext()) + + if err != nil { + return "", cli.GetError(err) + } + + // MarshalValue always return error as nil + v, _ := out.Status.MarshalValue() + + return v, nil +} + +func (cli *client) EnableReplication(srcRoleARN, dstRoleARN, dstBucketName string) errors.Error { + var status s3.ReplicationRuleStatus = helper.STATE_ENABLED + + replicationConf := s3.ReplicationConfiguration{ + Role: aws.String(srcRoleARN + "," + dstRoleARN), + Rules: []s3.ReplicationRule{ + { + Destination: &s3.Destination{ + Bucket: aws.String("arn:aws:s3:::" + dstBucketName), + }, + Status: status, + Prefix: aws.String(""), + }, + }, + } + + req := cli.s3.PutBucketReplicationRequest(&s3.PutBucketReplicationInput{ + Bucket: cli.GetBucketAws(), + ReplicationConfiguration: &replicationConf, + }) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + _, err := req.Send(cli.GetContext()) + + return cli.GetError(err) +} + +func (cli *client) DeleteReplication() errors.Error { + req := cli.s3.DeleteBucketReplicationRequest(&s3.DeleteBucketReplicationInput{ + Bucket: cli.GetBucketAws(), + }) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + _, err := req.Send(cli.GetContext()) + + return cli.GetError(err) +} diff --git a/aws/bucket/interface.go b/aws/bucket/interface.go new file mode 100644 index 0000000..d38d2fb --- /dev/null +++ b/aws/bucket/interface.go @@ -0,0 +1,40 @@ +package bucket + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/nabbar/golib/aws/helper" + "github.com/nabbar/golib/errors" +) + +type client struct { + helper.Helper + iam *iam.Client + s3 *s3.Client +} + +type Bucket interface { + Check() errors.Error + + List() ([]s3.Bucket, errors.Error) + Create() errors.Error + Delete() errors.Error + + //FindObject(pattern string) ([]string, errors.Error) + + SetVersioning(state bool) errors.Error + GetVersioning() (string, errors.Error) + + EnableReplication(srcRoleARN, dstRoleARN, dstBucketName string) errors.Error + DeleteReplication() errors.Error +} + +func New(ctx context.Context, bucket string, iam *iam.Client, s3 *s3.Client) Bucket { + return &client{ + Helper: helper.New(ctx, bucket), + iam: iam, + s3: s3, + } +} diff --git a/aws/bucket_test.go b/aws/bucket_test.go new file mode 100644 index 0000000..cda7273 --- /dev/null +++ b/aws/bucket_test.go @@ -0,0 +1,86 @@ +package aws_test + +import ( + "bytes" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Bucket", func() { + Context("Creation", func() { + It("Must be possible to check if the bucket exists", func() { + Expect(cli.Bucket().Check()).ToNot(Succeed()) + }) + It("Must be possible to create a bucket", func() { + Expect(cli.Bucket().Create()).To(Succeed()) + Expect(cli.Bucket().Check()).To(Succeed()) + }) + }) + Context("Find object", func() { + Context("With no object in bucket", func() { + It("Must succeed and return no object", func() { + objects, err := cli.Object().Find("pattern") + Expect(err).ToNot(HaveOccurred()) + Expect(objects).To(HaveLen(0)) + }) + }) + Context("With the object", func() { + It("Must succeed", func() { + var err error + + err = cli.Object().MultipartPut("object", bytes.NewReader([]byte("Hello"))) + Expect(err).ToNot(HaveOccurred()) + + objects, err := cli.Object().Find("object") + Expect(err).ToNot(HaveOccurred()) + Expect(objects).To(HaveLen(1)) + + err = cli.Object().Delete("object") + Expect(err).ToNot(HaveOccurred()) + }) + }) + }) + Context("List", func() { + It("Must be possible to list buckets", func() { + buckets, err := cli.Bucket().List() + Expect(err).ToNot(HaveOccurred()) + Expect(buckets).To(HaveLen(1)) + }) + }) + + /* + * Not Implemented whit minio + * + Context("Versioning", func() { + It("Must be possible to enable versioning", func() { + Expect(cli.Bucket().SetVersioning(true)).To(Succeed()) + }) + It("Must be enabled", func() { + status, err := cli.Bucket().GetVersioning() + Expect(err).ToNot(HaveOccurred()) + Expect(status).To(Equal("Enabled")) + }) + It("Must be possible to suspend versioning", func() { + Expect(cli.Bucket().SetVersioning(false)).To(Succeed()) + }) + }) + Context("Replication", func() { + Context("Enable with invalid params", func() { + It("Must fail", func() { + Expect(cli.Bucket().EnableReplication("fake-src-role-arn", "fake-dst-role-arn", "fake-dst-bucket")).ToNot(Succeed()) + }) + }) + Context("Disable", func() { + It("Must not return error", func() { + Expect(cli.Bucket().DeleteReplication()).To(Succeed()) + }) + }) + }) + * + */ + + It("Must be possible to delete a bucket", func() { + Expect(cli.Bucket().Delete()).To(Succeed()) + }) +}) diff --git a/aws/configAws/errors.go b/aws/configAws/errors.go new file mode 100644 index 0000000..035185b --- /dev/null +++ b/aws/configAws/errors.go @@ -0,0 +1,52 @@ +package configAws + +import ( + "github.com/nabbar/golib/errors" +) + +const ( + ErrorAwsError errors.CodeError = iota + errors.MIN_PKG_Aws + 30 + ErrorConfigLoader + ErrorConfigValidator + ErrorConfigJsonUnmarshall + ErrorEndpointInvalid + ErrorRegionInvalid + ErrorRegionEndpointNotFound + ErrorCredentialsInvalid +) + +var isErrInit = false + +func init() { + errors.RegisterIdFctMessage(ErrorAwsError, getMessage) + isErrInit = errors.ExistInMapMessage(ErrorAwsError) +} + +func IsErrorInit() bool { + return isErrInit +} + +func getMessage(code errors.CodeError) string { + switch code { + case errors.UNK_ERROR: + return "" + case ErrorAwsError: + return "calling aws api occurred a response error" + case ErrorConfigLoader: + return "calling AWS Default config Loader has occurred an error" + case ErrorConfigValidator: + return "invalid config, validation error" + case ErrorConfigJsonUnmarshall: + return "invalid json config, unmarshall error" + case ErrorEndpointInvalid: + return "the specified endpoint seems to be invalid" + case ErrorRegionInvalid: + return "the specified region seems to be invalid" + case ErrorRegionEndpointNotFound: + return "cannot find the endpoint for the specify region" + case ErrorCredentialsInvalid: + return "the specified credentials seems to be incorrect" + } + + return "" +} diff --git a/aws/configAws/interface.go b/aws/configAws/interface.go new file mode 100644 index 0000000..e9ca800 --- /dev/null +++ b/aws/configAws/interface.go @@ -0,0 +1,96 @@ +package configAws + +import ( + "encoding/json" + "net/http" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/aws/defaults" + "github.com/aws/aws-sdk-go-v2/aws/external" + aws2 "github.com/nabbar/golib/aws" + "github.com/nabbar/golib/errors" +) + +func GetConfigModel() interface{} { + return configModel{} +} + +func NewConfigJsonUnmashal(p []byte) (aws2.Config, errors.Error) { + c := configModel{} + if err := json.Unmarshal(p, &c); err != nil { + return nil, ErrorConfigJsonUnmarshall.ErrorParent(err) + } + + return &awsModel{ + configModel: c, + logLevel: 0, + awsLevel: 0, + retryer: nil, + }, nil +} + +func NewConfig(bucket, accessKey, secretKey, region string) aws2.Config { + return &awsModel{ + configModel: configModel{ + Region: region, + AccessKey: accessKey, + SecretKey: secretKey, + Bucket: bucket, + }, + logLevel: 0, + awsLevel: 0, + retryer: nil, + } +} + +func (c *awsModel) Clone() aws2.Config { + return &awsModel{ + configModel: configModel{ + Region: c.Region, + AccessKey: c.AccessKey, + SecretKey: c.SecretKey, + Bucket: c.Bucket, + }, + logLevel: c.logLevel, + awsLevel: c.awsLevel, + retryer: c.retryer, + } +} + +func (c *awsModel) GetConfig(cli *http.Client) (aws.Config, errors.Error) { + var ( + cfg aws.Config + err error + ) + + if c.AccessKey != "" && c.SecretKey != "" { + cfg = defaults.Config() + cfg.Credentials = aws.NewStaticCredentialsProvider(c.AccessKey, c.SecretKey, "") + } else if cfg, err = external.LoadDefaultAWSConfig(); err != nil { + return cfg, ErrorConfigLoader.ErrorParent(err) + } + + cfg.Logger = &awsLogger{c.logLevel} + cfg.LogLevel = c.awsLevel + cfg.Retryer = c.retryer + cfg.EnableEndpointDiscovery = true + cfg.Region = c.Region + + if cli != nil { + cfg.HTTPClient = cli + } + + return cfg, nil +} + +func (c *awsModel) GetBucketName() string { + return c.Bucket +} + +func (c *awsModel) SetBucketName(bucket string) { + c.Bucket = bucket +} + +func (c *awsModel) JSON() ([]byte, error) { + return json.MarshalIndent(c, "", " ") +} diff --git a/aws/configAws/log.go b/aws/configAws/log.go new file mode 100644 index 0000000..10718af --- /dev/null +++ b/aws/configAws/log.go @@ -0,0 +1,55 @@ +package configAws + +import ( + "reflect" + + "github.com/nabbar/golib/logger" +) + +type awsLogger struct { + logLevel logger.Level +} + +func (l awsLogger) Log(args ...interface{}) { + pattern := "" + + for i := 0; i < len(args); i++ { + //nolint #exhaustive + switch reflect.TypeOf(args[i]).Kind() { + case reflect.String: + pattern += "%s" + default: + pattern += "%v" + } + } + + l.logLevel.Logf("AWS Log : "+pattern, args...) +} + +func LevelPanic() logger.Level { + return logger.PanicLevel +} + +func LevelFatal() logger.Level { + return logger.FatalLevel +} + +func LevelError() logger.Level { + return logger.ErrorLevel +} + +func LevelWarn() logger.Level { + return logger.WarnLevel +} + +func LevelInfo() logger.Level { + return logger.InfoLevel +} + +func LevelDebug() logger.Level { + return logger.DebugLevel +} + +func LevelNoLog() logger.Level { + return logger.NilLevel +} diff --git a/aws/configAws/models.go b/aws/configAws/models.go new file mode 100644 index 0000000..fa88833 --- /dev/null +++ b/aws/configAws/models.go @@ -0,0 +1,143 @@ +package configAws + +import ( + "context" + "fmt" + "net" + "net/url" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/go-playground/validator/v10" + "github.com/nabbar/golib/errors" + "github.com/nabbar/golib/httpcli" + "github.com/nabbar/golib/logger" +) + +type configModel struct { + Region string `mapstructure:"region" json:"region" yaml:"region" toml:"region" validate:"printascii,required"` + AccessKey string `mapstructure:"accesskey" json:"accesskey" yaml:"accesskey" toml:"accesskey" validate:"printascii,required"` + SecretKey string `mapstructure:"secretkey" json:"secretkey" yaml:"secretkey" toml:"secretkey" validate:"printascii,required"` + Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket" toml:"bucket" validate:"printascii,omitempty"` +} + +type awsModel struct { + configModel + + logLevel logger.Level + awsLevel aws.LogLevel + retryer aws.Retryer +} + +func (c *awsModel) Validate() errors.Error { + val := validator.New() + err := val.Struct(c) + + if e, ok := err.(*validator.InvalidValidationError); ok { + return ErrorConfigValidator.ErrorParent(e) + } + + out := ErrorConfigValidator.Error(nil) + + for _, e := range err.(validator.ValidationErrors) { + //nolint goerr113 + out.AddParent(fmt.Errorf("config field '%s' is not validated by constraint '%s'", e.Field(), e.ActualTag())) + } + + if out.HasParent() { + return out + } + + return nil +} + +func (c *awsModel) ResetRegionEndpoint() { +} + +func (c *awsModel) RegisterRegionEndpoint(region string, endpoint *url.URL) errors.Error { + return nil +} + +func (c *awsModel) RegisterRegionAws(endpoint *url.URL) errors.Error { + return nil +} + +func (c *awsModel) SetRegion(region string) { + c.Region = region +} + +func (c *awsModel) GetRegion() string { + return c.Region +} + +func (c *awsModel) SetEndpoint(endpoint *url.URL) { +} + +func (c awsModel) GetEndpoint() *url.URL { + return nil +} + +func (c *awsModel) ResolveEndpoint(service, region string) (aws.Endpoint, error) { + return aws.Endpoint{}, ErrorEndpointInvalid.Error(nil) +} + +func (c *awsModel) SetLogLevel(lvl logger.Level) { + c.logLevel = lvl +} + +func (c *awsModel) SetAWSLogLevel(lvl aws.LogLevel) { + c.awsLevel = lvl +} + +func (c *awsModel) SetRetryer(retryer aws.Retryer) { + c.retryer = retryer +} + +func (c awsModel) Check(ctx context.Context) errors.Error { + var ( + cfg aws.Config + con net.Conn + end aws.Endpoint + adr *url.URL + err error + e errors.Error + ) + + if cfg, e = c.GetConfig(nil); e != nil { + return e + } + + if ctx == nil { + ctx = context.Background() + } + + if end, err = cfg.EndpointResolver.ResolveEndpoint("s3", c.GetRegion()); err != nil { + return ErrorEndpointInvalid.ErrorParent(err) + } + + if adr, err = url.Parse(end.URL); err != nil { + return ErrorEndpointInvalid.ErrorParent(err) + } + + if _, err = cfg.Credentials.Retrieve(ctx); err != nil { + return ErrorCredentialsInvalid.ErrorParent(err) + } + + d := net.Dialer{ + Timeout: httpcli.TIMEOUT_5_SEC, + KeepAlive: httpcli.TIMEOUT_5_SEC, + } + + con, err = d.DialContext(ctx, "tcp", adr.Host) + + defer func() { + if con != nil { + _ = con.Close() + } + }() + + if err != nil { + return ErrorEndpointInvalid.ErrorParent(err) + } + + return nil +} diff --git a/aws/configCustom/errors.go b/aws/configCustom/errors.go new file mode 100644 index 0000000..da14c98 --- /dev/null +++ b/aws/configCustom/errors.go @@ -0,0 +1,49 @@ +package configCustom + +import ( + "github.com/nabbar/golib/errors" +) + +const ( + ErrorAwsError errors.CodeError = iota + errors.MIN_PKG_Aws + 30 + ErrorConfigValidator + ErrorConfigJsonUnmarshall + ErrorEndpointInvalid + ErrorRegionInvalid + ErrorRegionEndpointNotFound + ErrorCredentialsInvalid +) + +var isErrInit = false + +func init() { + errors.RegisterIdFctMessage(ErrorAwsError, getMessage) + isErrInit = errors.ExistInMapMessage(ErrorAwsError) +} + +func IsErrorInit() bool { + return isErrInit +} + +func getMessage(code errors.CodeError) string { + switch code { + case errors.UNK_ERROR: + return "" + case ErrorAwsError: + return "calling aws api occurred a response error" + case ErrorConfigValidator: + return "invalid config, validation error" + case ErrorConfigJsonUnmarshall: + return "invalid json config, unmarshall error" + case ErrorEndpointInvalid: + return "the specified endpoint seems to be invalid" + case ErrorRegionInvalid: + return "the specified region seems to be invalid" + case ErrorRegionEndpointNotFound: + return "cannot find the endpoint for the specify region" + case ErrorCredentialsInvalid: + return "the specified credentials seems to be incorrect" + } + + return "" +} diff --git a/aws/configCustom/interface.go b/aws/configCustom/interface.go new file mode 100644 index 0000000..41302cb --- /dev/null +++ b/aws/configCustom/interface.go @@ -0,0 +1,102 @@ +package configCustom + +import ( + "encoding/json" + "net/http" + "net/url" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/aws/defaults" + aws2 "github.com/nabbar/golib/aws" + "github.com/nabbar/golib/errors" +) + +func GetConfigModel() interface{} { + return configModel{} +} + +func NewConfigJsonUnmashal(p []byte) (aws2.Config, errors.Error) { + c := configModel{} + if err := json.Unmarshal(p, &c); err != nil { + return nil, ErrorConfigJsonUnmarshall.ErrorParent(err) + } + + return &awsModel{ + configModel: c, + logLevel: 0, + awsLevel: 0, + retryer: nil, + mapRegion: nil, + }, nil +} + +func NewConfig(bucket, accessKey, secretKey string, endpoint *url.URL, region string) aws2.Config { + return &awsModel{ + configModel: configModel{ + Region: region, + Endpoint: strings.TrimSuffix(endpoint.String(), "/"), + AccessKey: accessKey, + SecretKey: secretKey, + Bucket: bucket, + }, + endpoint: endpoint, + logLevel: 0, + awsLevel: 0, + retryer: nil, + mapRegion: make(map[string]*url.URL), + } +} + +func (c *awsModel) Clone() aws2.Config { + m := make(map[string]*url.URL) + + for r, e := range c.mapRegion { + m[r] = e + } + + return &awsModel{ + configModel: configModel{ + Region: c.Region, + Endpoint: c.Endpoint, + AccessKey: c.AccessKey, + SecretKey: c.SecretKey, + Bucket: c.Bucket, + }, + logLevel: c.logLevel, + awsLevel: c.awsLevel, + retryer: c.retryer, + endpoint: c.endpoint, + mapRegion: m, + } +} + +func (c *awsModel) GetConfig(cli *http.Client) (aws.Config, errors.Error) { + cfg := defaults.Config() + cfg.Credentials = aws.NewStaticCredentialsProvider(c.AccessKey, c.SecretKey, "") + cfg.Logger = &awsLogger{c.logLevel} + cfg.LogLevel = c.awsLevel + cfg.Retryer = c.retryer + cfg.EnableEndpointDiscovery = false + cfg.DisableEndpointHostPrefix = true + cfg.EndpointResolver = aws.EndpointResolverFunc(c.ResolveEndpoint) + cfg.Region = c.Region + + if cli != nil { + cfg.HTTPClient = cli + } + + return cfg, nil +} + +func (c *awsModel) GetBucketName() string { + return c.Bucket +} + +func (c *awsModel) SetBucketName(bucket string) { + c.Bucket = bucket +} + +func (c *awsModel) JSON() ([]byte, error) { + return json.MarshalIndent(c, "", " ") +} diff --git a/aws/configCustom/log.go b/aws/configCustom/log.go new file mode 100644 index 0000000..e8dc8fb --- /dev/null +++ b/aws/configCustom/log.go @@ -0,0 +1,55 @@ +package configCustom + +import ( + "reflect" + + "github.com/nabbar/golib/logger" +) + +type awsLogger struct { + logLevel logger.Level +} + +func (l awsLogger) Log(args ...interface{}) { + pattern := "" + + for i := 0; i < len(args); i++ { + //nolint #exhaustive + switch reflect.TypeOf(args[i]).Kind() { + case reflect.String: + pattern += "%s" + default: + pattern += "%v" + } + } + + l.logLevel.Logf("AWS Log : "+pattern, args...) +} + +func LevelPanic() logger.Level { + return logger.PanicLevel +} + +func LevelFatal() logger.Level { + return logger.FatalLevel +} + +func LevelError() logger.Level { + return logger.ErrorLevel +} + +func LevelWarn() logger.Level { + return logger.WarnLevel +} + +func LevelInfo() logger.Level { + return logger.InfoLevel +} + +func LevelDebug() logger.Level { + return logger.DebugLevel +} + +func LevelNoLog() logger.Level { + return logger.NilLevel +} diff --git a/aws/configCustom/models.go b/aws/configCustom/models.go new file mode 100644 index 0000000..9dd60d6 --- /dev/null +++ b/aws/configCustom/models.go @@ -0,0 +1,270 @@ +package configCustom + +import ( + "context" + "fmt" + "net" + "net/url" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/go-playground/validator/v10" + "github.com/nabbar/golib/errors" + "github.com/nabbar/golib/httpcli" + "github.com/nabbar/golib/logger" +) + +type configModel struct { + Region string `mapstructure:"region" json:"region" yaml:"region" toml:"region" validate:"printascii,required"` + Endpoint string `mapstructure:"endpoint" json:"endpoint" yaml:"endpoint" toml:"endpoint" validate:"url,required"` + AccessKey string `mapstructure:"accesskey" json:"accesskey" yaml:"accesskey" toml:"accesskey" validate:"printascii,required"` + SecretKey string `mapstructure:"secretkey" json:"secretkey" yaml:"secretkey" toml:"secretkey" validate:"printascii,required"` + Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket" toml:"bucket" validate:"printascii,omitempty"` +} + +type awsModel struct { + configModel + + logLevel logger.Level + awsLevel aws.LogLevel + retryer aws.Retryer + endpoint *url.URL + mapRegion map[string]*url.URL +} + +func (c *awsModel) Validate() errors.Error { + val := validator.New() + err := val.Struct(c) + + if err != nil { + if e, ok := err.(*validator.InvalidValidationError); ok { + return ErrorConfigValidator.ErrorParent(e) + } + + out := ErrorConfigValidator.Error(nil) + + for _, e := range err.(validator.ValidationErrors) { + //nolint goerr113 + out.AddParent(fmt.Errorf("config field '%s' is not validated by constraint '%s'", e.Field(), e.ActualTag())) + } + + if out.HasParent() { + return out + } + } + + if c.Endpoint != "" && c.endpoint == nil { + if c.endpoint, err = url.Parse(c.Endpoint); err != nil { + return ErrorEndpointInvalid.ErrorParent(err) + } + + if e := c.RegisterRegionAws(c.endpoint); e != nil { + return e + } + } else if c.endpoint != nil && c.Endpoint == "" { + c.Endpoint = c.endpoint.String() + } + + if c.endpoint != nil && c.Region != "" { + if e := c.RegisterRegionEndpoint("", c.endpoint); e != nil { + return e + } + } + + return nil +} + +func (c *awsModel) ResetRegionEndpoint() { + c.mapRegion = make(map[string]*url.URL) +} + +func (c *awsModel) RegisterRegionEndpoint(region string, endpoint *url.URL) errors.Error { + if endpoint == nil && c.endpoint != nil { + endpoint = c.endpoint + } else if endpoint == nil && c.Endpoint != "" { + var err error + if endpoint, err = url.Parse(c.Endpoint); err != nil { + return ErrorEndpointInvalid.ErrorParent(err) + } + } + + if endpoint == nil { + return ErrorEndpointInvalid.Error(nil) + } + + if region == "" && c.Region != "" { + region = c.Region + } + + val := validator.New() + + if err := val.Var(endpoint, "url,required"); err != nil { + return ErrorEndpointInvalid.ErrorParent(err) + } else if err := val.Var(region, "printascii,required"); err != nil { + return ErrorRegionInvalid.ErrorParent(err) + } + + if c.mapRegion == nil { + c.mapRegion = make(map[string]*url.URL) + } + + c.mapRegion[region] = endpoint + + return nil +} + +func (c *awsModel) RegisterRegionAws(endpoint *url.URL) errors.Error { + if endpoint == nil && c.endpoint != nil { + endpoint = c.endpoint + } else if endpoint == nil && c.Endpoint != "" { + var err error + if endpoint, err = url.Parse(c.Endpoint); err != nil { + return ErrorEndpointInvalid.ErrorParent(err) + } + } + + if endpoint == nil { + return ErrorEndpointInvalid.Error(nil) + } + + val := validator.New() + if err := val.Var(endpoint, "url,required"); err != nil { + return ErrorEndpointInvalid.ErrorParent(err) + } + + if c.Region == "" { + c.SetRegion("us-east-1") + } + + if c.mapRegion == nil { + c.mapRegion = make(map[string]*url.URL) + } + + for _, r := range []string{ + "af-south-1", + "ap-east-1", + "ap-northeast-1", + "ap-northeast-2", + "ap-northeast-3", + "ap-south-1", + "ap-southeast-1", + "ap-southeast-2", + "ca-central-1", + "cn-north-1", + "cn-northwest-1", + "eu-central-1", + "eu-north-1", + "eu-south-1", + "eu-west-1", + "eu-west-2", + "eu-west-3", + "me-south-1", + "sa-east-1", + "us-east-1", + "us-east-2", + "us-gov-east-1", + "us-gov-west-1", + "us-west-1", + "us-west-2", + } { + c.mapRegion[r] = endpoint + } + + return nil +} + +func (c *awsModel) SetRegion(region string) { + c.Region = region +} + +func (c *awsModel) GetRegion() string { + return c.Region +} + +func (c *awsModel) SetEndpoint(endpoint *url.URL) { + c.endpoint = endpoint + c.Endpoint = strings.TrimSuffix(c.endpoint.String(), "/") +} + +func (c awsModel) GetEndpoint() *url.URL { + return c.endpoint +} + +func (c *awsModel) ResolveEndpoint(service, region string) (aws.Endpoint, error) { + if e, ok := c.mapRegion[region]; ok { + return aws.Endpoint{ + URL: strings.TrimSuffix(e.String(), "/"), + }, nil + } + + if c.Endpoint != "" { + return aws.Endpoint{ + URL: strings.TrimSuffix(c.Endpoint, "/"), + }, nil + } + + logger.DebugLevel.Logf("Called ResolveEndpoint for service '%s' / region '%s' with nil endpoint", service, region) + return aws.Endpoint{}, ErrorEndpointInvalid.Error(nil) +} + +func (c *awsModel) SetLogLevel(lvl logger.Level) { + c.logLevel = lvl +} + +func (c *awsModel) SetAWSLogLevel(lvl aws.LogLevel) { + c.awsLevel = lvl +} + +func (c *awsModel) SetRetryer(retryer aws.Retryer) { + c.retryer = retryer +} + +func (c awsModel) Check(ctx context.Context) errors.Error { + var ( + cfg aws.Config + con net.Conn + err error + e errors.Error + ) + + if cfg, e = c.GetConfig(nil); e != nil { + return e + } + + if ctx == nil { + ctx = context.Background() + } + + if _, err = cfg.EndpointResolver.ResolveEndpoint("s3", c.GetRegion()); err != nil { + return ErrorEndpointInvalid.ErrorParent(err) + } + + if _, err = cfg.Credentials.Retrieve(ctx); err != nil { + return ErrorCredentialsInvalid.ErrorParent(err) + } + + d := net.Dialer{ + Timeout: httpcli.TIMEOUT_5_SEC, + KeepAlive: httpcli.TIMEOUT_5_SEC, + } + + if c.endpoint.Port() == "" && c.endpoint.Scheme == "http" { + con, err = d.DialContext(ctx, "tcp", c.endpoint.Hostname()+":80") + } else if c.endpoint.Port() == "" && c.endpoint.Scheme == "https" { + con, err = d.DialContext(ctx, "tcp", c.endpoint.Hostname()+":443") + } else { + con, err = d.DialContext(ctx, "tcp", c.endpoint.Host) + } + + defer func() { + if con != nil { + _ = con.Close() + } + }() + + if err != nil { + return ErrorEndpointInvalid.ErrorParent(err) + } + + return nil +} diff --git a/aws/group/group.go b/aws/group/group.go new file mode 100644 index 0000000..46cd7bf --- /dev/null +++ b/aws/group/group.go @@ -0,0 +1,46 @@ +package group + +import ( + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/nabbar/golib/errors" +) + +func (cli *client) List() (map[string]string, errors.Error) { + req := cli.iam.ListGroupsRequest(&iam.ListGroupsInput{}) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if out, err := req.Send(cli.GetContext()); err != nil { + return nil, cli.GetError(err) + } else { + var res = make(map[string]string) + + for _, g := range out.Groups { + res[*g.GroupId] = *g.GroupName + } + + return res, nil + } +} + +func (cli *client) Add(groupName string) errors.Error { + req := cli.iam.CreateGroupRequest(&iam.CreateGroupInput{ + GroupName: aws.String(groupName), + }) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + _, err := req.Send(cli.GetContext()) + + return cli.GetError(err) +} + +func (cli *client) Remove(groupName string) errors.Error { + req := cli.iam.DeleteGroupRequest(&iam.DeleteGroupInput{ + GroupName: aws.String(groupName), + }) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + _, err := req.Send(cli.GetContext()) + + return cli.GetError(err) +} diff --git a/aws/group/interface.go b/aws/group/interface.go new file mode 100644 index 0000000..4b9ecdc --- /dev/null +++ b/aws/group/interface.go @@ -0,0 +1,39 @@ +package group + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/nabbar/golib/aws/helper" + "github.com/nabbar/golib/errors" +) + +type client struct { + helper.Helper + iam *iam.Client + s3 *s3.Client +} + +type Group interface { + UserList(username string) ([]string, errors.Error) + UserCheck(username, groupName string) (errors.Error, bool) + UserAdd(username, groupName string) errors.Error + UserRemove(username, groupName string) errors.Error + + List() (map[string]string, errors.Error) + Add(groupName string) errors.Error + Remove(groupName string) errors.Error + + PolicyList(groupName string) (map[string]string, errors.Error) + PolicyAttach(groupName, polArn string) errors.Error + PolicyDetach(groupName, polArn string) errors.Error +} + +func New(ctx context.Context, bucket string, iam *iam.Client, s3 *s3.Client) Group { + return &client{ + Helper: helper.New(ctx, bucket), + iam: iam, + s3: s3, + } +} diff --git a/aws/group/policy.go b/aws/group/policy.go new file mode 100644 index 0000000..d603bd6 --- /dev/null +++ b/aws/group/policy.go @@ -0,0 +1,50 @@ +package group + +import ( + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/nabbar/golib/errors" +) + +func (cli *client) PolicyList(groupName string) (map[string]string, errors.Error) { + req := cli.iam.ListAttachedGroupPoliciesRequest(&iam.ListAttachedGroupPoliciesInput{ + GroupName: aws.String(groupName), + }) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if out, err := req.Send(cli.GetContext()); err != nil { + return nil, cli.GetError(err) + } else { + var res = make(map[string]string) + + for _, p := range out.AttachedPolicies { + res[*p.PolicyName] = *p.PolicyArn + } + + return res, nil + } +} + +func (cli *client) PolicyAttach(groupName, polArn string) errors.Error { + req := cli.iam.AttachGroupPolicyRequest(&iam.AttachGroupPolicyInput{ + GroupName: aws.String(groupName), + PolicyArn: aws.String(polArn), + }) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + _, err := req.Send(cli.GetContext()) + + return cli.GetError(err) +} + +func (cli *client) PolicyDetach(groupName, polArn string) errors.Error { + req := cli.iam.DetachGroupPolicyRequest(&iam.DetachGroupPolicyInput{ + GroupName: aws.String(groupName), + PolicyArn: aws.String(polArn), + }) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + _, err := req.Send(cli.GetContext()) + + return cli.GetError(err) +} diff --git a/aws/group/user.go b/aws/group/user.go new file mode 100644 index 0000000..b066592 --- /dev/null +++ b/aws/group/user.go @@ -0,0 +1,69 @@ +package group + +import ( + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/nabbar/golib/errors" +) + +func (cli *client) UserCheck(username, groupName string) (errors.Error, bool) { + req := cli.iam.ListGroupsForUserRequest(&iam.ListGroupsForUserInput{ + UserName: aws.String(username), + }) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if out, err := req.Send(cli.GetContext()); err != nil { + return cli.GetError(err), false + } else { + for _, g := range out.Groups { + if *g.GroupName == groupName { + return nil, true + } + } + } + + return nil, false +} + +func (cli *client) UserList(username string) ([]string, errors.Error) { + req := cli.iam.ListGroupsForUserRequest(&iam.ListGroupsForUserInput{ + UserName: aws.String(username), + }) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if out, err := req.Send(cli.GetContext()); err != nil { + return nil, cli.GetError(err) + } else { + var res = make([]string, 0) + + for _, g := range out.Groups { + res = append(res, *g.GroupName) + } + + return res, nil + } +} + +func (cli *client) UserAdd(username, groupName string) errors.Error { + req := cli.iam.AddUserToGroupRequest(&iam.AddUserToGroupInput{ + UserName: aws.String(username), + GroupName: aws.String(groupName), + }) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + _, err := req.Send(cli.GetContext()) + + return cli.GetError(err) +} + +func (cli *client) UserRemove(username, groupName string) errors.Error { + req := cli.iam.RemoveUserFromGroupRequest(&iam.RemoveUserFromGroupInput{ + UserName: aws.String(username), + GroupName: aws.String(groupName), + }) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + _, err := req.Send(cli.GetContext()) + + return cli.GetError(err) +} diff --git a/aws/group_test.go b/aws/group_test.go new file mode 100644 index 0000000..b6561d9 --- /dev/null +++ b/aws/group_test.go @@ -0,0 +1,246 @@ +package aws_test + +import ( + "fmt" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Groups", func() { + var ( + groupName = "myGroup" + userName = "myUsername" + policyName = "myPolicy" + err error + ) + + Context("Create Group", func() { + It("Must succeed", func() { + if minioMode { + err = nil + } else { + err = cli.Group().Add(groupName) + } + Expect(err).ToNot(HaveOccurred()) + }) + It("Must fail with already existing user", func() { + if minioMode { + //nolint #goerr113 + err = fmt.Errorf("backend not compatible following AWS API reference") + } else { + err = cli.Group().Add(groupName) + } + Expect(err).To(HaveOccurred()) + }) + }) + + Context("List", func() { + It("Must succeed", func() { + var group map[string]string + + if minioMode { + err = nil + group = map[string]string{ + "skip1": "skip", + "skip2": "skip", + "skip3": "skip", + } + } else { + group, err = cli.Group().List() + } + Expect(err).ToNot(HaveOccurred()) + Expect(group).To(HaveLen(3)) + }) + }) + + Context("User Operations", func() { + Context("Add user to group", func() { + It("Must succeed", func() { + if minioMode { + err = nil + } else { + err = cli.User().Create(userName) + } + Expect(err).ToNot(HaveOccurred()) + + if minioMode { + err = nil + } else { + err = cli.Group().UserAdd(userName, groupName) + } + Expect(err).ToNot(HaveOccurred()) + }) + }) + Context("Check if user is in group", func() { + It("Must succeed", func() { + var ok bool + if minioMode { + err = nil + ok = true + } else { + err, ok = cli.Group().UserCheck(userName, groupName) + } + Expect(err).ToNot(HaveOccurred()) + Expect(ok).To(Equal(true)) + }) + It("Must fail with invalid params", func() { + var ok bool + if minioMode { + //nolint #goerr113 + err = fmt.Errorf("backend not compatible following AWS API reference") + ok = false + } else { + err, ok = cli.Group().UserCheck("userName", "groupName") + } + Expect(err).To(HaveOccurred()) + Expect(ok).To(Equal(false)) + }) + }) + Context("List users in group", func() { + It("Must succeed", func() { + var group []string + + if minioMode { + err = nil + group = []string{groupName} + } else { + group, err = cli.Group().UserList(userName) + } + Expect(err).ToNot(HaveOccurred()) + Expect(group).To(ContainElements(groupName)) + }) + It("Must fail with invalid groupName", func() { + if minioMode { + //nolint #goerr113 + err = fmt.Errorf("backend not compatible following AWS API reference") + } else { + _, err = cli.Group().UserList("groupName") + } + _, err := cli.Group().UserList("groupName") + Expect(err).To(HaveOccurred()) + }) + }) + Context("Remove user from group", func() { + It("Must succeed", func() { + if minioMode { + err = nil + } else { + err = cli.Group().UserRemove(userName, groupName) + } + Expect(err).ToNot(HaveOccurred()) + + if minioMode { + err = nil + } else { + err = cli.User().Delete(userName) + } + Expect(err).ToNot(HaveOccurred()) + }) + It("Must fail (already deleted)", func() { + if minioMode { + //nolint #goerr113 + err = fmt.Errorf("backend not compatible following AWS API reference") + } else { + err = cli.Group().UserRemove(userName, groupName) + } + Expect(err).To(HaveOccurred()) + }) + }) + }) + + Context("Policy Operations", func() { + + var policyArn string + + Context("Attach policy to group", func() { + It("Must succeed", func() { + if minioMode { + err = nil + } else { + policyArn, err = cli.Policy().Add(policyName, "description", BuildPolicy()) + } + Expect(err).ToNot(HaveOccurred()) + + if minioMode { + err = nil + } else { + err = cli.Group().PolicyAttach(groupName, policyArn) + } + Expect(err).ToNot(HaveOccurred()) + + }) + }) + Context("List policies in group", func() { + It("Must succeed", func() { + var policies map[string]string + + if minioMode { + err = nil + policies = map[string]string{ + policyName: policyArn, + } + } else { + policies, err = cli.Group().PolicyList(groupName) + } + Expect(err).ToNot(HaveOccurred()) + Expect(policies).To(HaveKeyWithValue(policyName, policyArn)) + }) + It("Must fail with invalid groupName", func() { + if minioMode { + //nolint #goerr113 + err = fmt.Errorf("backend not compatible following AWS API reference") + } else { + _, err = cli.Group().PolicyList("groupName") + } + Expect(err).To(HaveOccurred()) + }) + }) + Context("Remove policy from group", func() { + It("Must succeed", func() { + if minioMode { + err = nil + } else { + err = cli.Group().PolicyDetach(groupName, policyArn) + } + Expect(err).ToNot(HaveOccurred()) + + if minioMode { + err = nil + } else { + err = cli.Policy().Delete(policyArn) + } + Expect(err).ToNot(HaveOccurred()) + }) + It("Must fail (already deleted)", func() { + if minioMode { + //nolint #goerr113 + err = fmt.Errorf("backend not compatible following AWS API reference") + } else { + err = cli.Group().PolicyDetach(groupName, policyArn) + } + Expect(err).To(HaveOccurred()) + }) + }) + }) + + Context("Delete Group", func() { + It("Must succeed", func() { + if minioMode { + err = nil + } else { + err = cli.Group().Remove(groupName) + } + Expect(err).ToNot(HaveOccurred()) + }) + It("Must fail (already deleted)", func() { + if minioMode { + //nolint #goerr113 + err = fmt.Errorf("backend not compatible following AWS API reference") + } else { + err = cli.Group().Remove(groupName) + } + Expect(err).To(HaveOccurred()) + }) + }) +}) diff --git a/aws/helper/errors.go b/aws/helper/errors.go new file mode 100644 index 0000000..4e5ad39 --- /dev/null +++ b/aws/helper/errors.go @@ -0,0 +1,42 @@ +package helper + +import "github.com/nabbar/golib/errors" + +const ( + // minmal are errors.MIN_AVAILABLE + get a hope free range 1000 + 10 for aws-config errors. + ErrorResponse errors.CodeError = iota + errors.MIN_AVAILABLE + 1000 + 10 + ErrorConfigEmpty + ErrorAwsEmpty + ErrorAws + ErrorBucketNotFound +) + +var isErrInit = false + +func init() { + errors.RegisterIdFctMessage(ErrorResponse, getMessage) + isErrInit = errors.ExistInMapMessage(ErrorResponse) +} + +func IsErrorInit() bool { + return isErrInit +} + +func getMessage(code errors.CodeError) string { + switch code { + case errors.UNK_ERROR: + return "" + case ErrorResponse: + return "calling aws api occurred a response error" + case ErrorConfigEmpty: + return "the given config is empty or invalid" + case ErrorAws: + return "the aws request sent to aws API occurred an error" + case ErrorAwsEmpty: + return "the aws request sent to aws API occurred an empty result" + case ErrorBucketNotFound: + return "the specified bucket is not found" + } + + return "" +} diff --git a/aws/helper/interface.go b/aws/helper/interface.go new file mode 100644 index 0000000..9f9ca18 --- /dev/null +++ b/aws/helper/interface.go @@ -0,0 +1,100 @@ +package helper + +import ( + "context" + "fmt" + "io" + "net/http" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/aws/awserr" + "github.com/nabbar/golib/errors" +) + +const ( + STATE_SUSPENDED = "Suspended" + STATE_ENABLED = "Enabled" +) + +type Helper struct { + ctx context.Context + bkt string +} + +func New(ctx context.Context, bucket string) Helper { + return Helper{ + ctx: ctx, + bkt: bucket, + } +} + +func (cli Helper) GetError(err error) errors.Error { + if err == nil { + return nil + } + + if aerr, ok := err.(awserr.Error); ok { + return ErrorAws.Error(errors.NewError(0, fmt.Sprintf("(%s) %s", aerr.Code(), aerr.Message()), nil)) + } + + if aerr, ok := err.(errors.Error); ok { + return ErrorAws.Error(aerr) + } + + return ErrorAws.ErrorParent(err) +} + +func (cli Helper) ErrorCode(err error) string { + if aerr, ok := err.(awserr.Error); ok { + return aerr.Code() + } + + if aerr, ok := err.(errors.Error); ok { + return aerr.CodeError("") + } + + return "" +} + +func (cli *Helper) GetContext() context.Context { + if cli.ctx == nil { + cli.ctx = context.Background() + } + + return cli.ctx +} + +func (c *Helper) GetCloser(req *http.Request, rsp *http.Response) []io.Closer { + res := make([]io.Closer, 0) + + if req != nil && req.Body != nil { + res = append(res, req.Body) + } + + if rsp != nil && rsp.Body != nil { + res = append(res, rsp.Body) + } + + return res +} + +func (c *Helper) Close(req *http.Request, rsp *http.Response) { + if req != nil && req.Body != nil { + _ = req.Body.Close() + } + if rsp != nil && rsp.Body != nil { + _ = rsp.Body.Close() + } +} + +func (c *Helper) GetBucketName() string { + return c.bkt +} + +func (c *Helper) GetBucketAws() *string { + return aws.String(c.bkt) +} + +func (c *Helper) SetBucketName(bucket string) { + c.bkt = bucket +} diff --git a/aws/helper/partSize.go b/aws/helper/partSize.go new file mode 100644 index 0000000..0d7f854 --- /dev/null +++ b/aws/helper/partSize.go @@ -0,0 +1,47 @@ +package helper + +type PartSize int64 + +const ( + SizeBytes PartSize = 1 + SizeKiloBytes = 1024 * SizeBytes + SizeMegaBytes = 1024 * SizeKiloBytes + SizeGigaBytes = 1024 * SizeMegaBytes + SizeTeraBytes = 1024 * SizeGigaBytes + SizePetaBytes = 1024 * SizeTeraBytes +) + +func SetSize(val int) PartSize { + return PartSize(val) +} + +func SetSizeInt64(val int64) PartSize { + return PartSize(val) +} + +func (p PartSize) Int() int { + return int(p) +} + +func (p PartSize) Int64() int64 { + return int64(p) +} + +func (p PartSize) String() string { + switch p { + case SizePetaBytes: + return "PB" + case SizeTeraBytes: + return "TB" + case SizeGigaBytes: + return "GB" + case SizeMegaBytes: + return "MB" + case SizeKiloBytes: + return "KB" + case SizeBytes: + return "B" + } + + return "" +} diff --git a/aws/interface.go b/aws/interface.go new file mode 100644 index 0000000..569764f --- /dev/null +++ b/aws/interface.go @@ -0,0 +1,130 @@ +package aws + +import ( + "context" + "net/http" + "net/url" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/nabbar/golib/aws/bucket" + "github.com/nabbar/golib/aws/group" + "github.com/nabbar/golib/aws/helper" + "github.com/nabbar/golib/aws/object" + "github.com/nabbar/golib/aws/policy" + "github.com/nabbar/golib/aws/role" + "github.com/nabbar/golib/aws/user" + "github.com/nabbar/golib/errors" + "github.com/nabbar/golib/logger" +) + +type Config interface { + Check(ctx context.Context) errors.Error + Validate() errors.Error + + ResetRegionEndpoint() + RegisterRegionEndpoint(region string, endpoint *url.URL) errors.Error + RegisterRegionAws(endpoint *url.URL) errors.Error + SetRegion(region string) + GetRegion() string + SetEndpoint(endpoint *url.URL) + GetEndpoint() *url.URL + + ResolveEndpoint(service, region string) (aws.Endpoint, error) + + SetLogLevel(lvl logger.Level) + SetAWSLogLevel(lvl aws.LogLevel) + SetRetryer(retryer aws.Retryer) + + GetConfig(cli *http.Client) (aws.Config, errors.Error) + JSON() ([]byte, error) + Clone() Config + + GetBucketName() string + SetBucketName(bucket string) +} + +type AWS interface { + Bucket() bucket.Bucket + Group() group.Group + Object() object.Object + Policy() policy.Policy + Role() role.Role + User() user.User + + Clone() AWS + Config() Config + ForcePathStyle(enabled bool) + + GetBucketName() string + SetBucketName(bucket string) +} + +type client struct { + p bool + x context.Context + c Config + i *iam.Client + s *s3.Client +} + +func New(ctx context.Context, cfg Config, httpClient *http.Client) (AWS, errors.Error) { + if cfg == nil { + return nil, helper.ErrorConfigEmpty.Error(nil) + } + + var ( + c aws.Config + i *iam.Client + s *s3.Client + e errors.Error + ) + + if c, e = cfg.GetConfig(httpClient); e != nil { + return nil, e + } + + i = iam.New(c) + s = s3.New(c) + + if httpClient != nil { + i.HTTPClient = httpClient + s.HTTPClient = httpClient + } + + if ctx == nil { + ctx = context.Background() + } + + return &client{ + p: false, + x: ctx, + c: cfg, + i: i, + s: s, + }, nil +} + +func (c *client) getCliIAM() *iam.Client { + i := iam.New(c.i.Config) + i.HTTPClient = c.i.HTTPClient + return i +} + +func (c *client) getCliS3() *s3.Client { + s := s3.New(c.s.Config) + s.HTTPClient = c.s.HTTPClient + s.ForcePathStyle = c.p + return s +} + +func (c *client) Clone() AWS { + return &client{ + p: c.p, + x: c.x, + c: c.c.Clone(), + i: c.getCliIAM(), + s: c.getCliS3(), + } +} diff --git a/aws/model.go b/aws/model.go new file mode 100644 index 0000000..fbfec9b --- /dev/null +++ b/aws/model.go @@ -0,0 +1,51 @@ +package aws + +import ( + "github.com/nabbar/golib/aws/bucket" + "github.com/nabbar/golib/aws/group" + "github.com/nabbar/golib/aws/object" + "github.com/nabbar/golib/aws/policy" + "github.com/nabbar/golib/aws/role" + "github.com/nabbar/golib/aws/user" +) + +func (c *client) ForcePathStyle(enabled bool) { + c.p = enabled + c.s.ForcePathStyle = enabled +} + +func (c *client) Config() Config { + return c.c +} + +func (c *client) Bucket() bucket.Bucket { + return bucket.New(c.x, c.c.GetBucketName(), c.getCliIAM(), c.getCliS3()) +} + +func (c *client) Group() group.Group { + return group.New(c.x, c.c.GetBucketName(), c.getCliIAM(), c.getCliS3()) +} + +func (c *client) Object() object.Object { + return object.New(c.x, c.c.GetBucketName(), c.getCliIAM(), c.getCliS3()) +} + +func (c *client) Policy() policy.Policy { + return policy.New(c.x, c.c.GetBucketName(), c.getCliIAM(), c.getCliS3()) +} + +func (c *client) Role() role.Role { + return role.New(c.x, c.c.GetBucketName(), c.getCliIAM(), c.getCliS3()) +} + +func (c *client) User() user.User { + return user.New(c.x, c.c.GetBucketName(), c.getCliIAM(), c.getCliS3()) +} + +func (c *client) GetBucketName() string { + return c.c.GetBucketName() +} + +func (c *client) SetBucketName(bucket string) { + c.c.SetBucketName(bucket) +} diff --git a/aws/object/find.go b/aws/object/find.go new file mode 100644 index 0000000..cdfc362 --- /dev/null +++ b/aws/object/find.go @@ -0,0 +1,33 @@ +package object + +import ( + "regexp" + + "github.com/nabbar/golib/errors" +) + +func (cli *client) Find(pattern string) ([]string, errors.Error) { + var ( + result = make([]string, 0) + token = "" + ) + + for { + if lst, tok, cnt, err := cli.List(token); err != nil { + return result, cli.GetError(err) + } else if cnt > 0 { + token = tok + for _, o := range lst { + if ok, _ := regexp.MatchString(pattern, *o.Key); ok { + result = append(result, *o.Key) + } + } + } else { + return result, nil + } + + if token == "" { + return result, nil + } + } +} diff --git a/aws/object/interface.go b/aws/object/interface.go new file mode 100644 index 0000000..244f54d --- /dev/null +++ b/aws/object/interface.go @@ -0,0 +1,40 @@ +package object + +import ( + "bytes" + "context" + "io" + + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/nabbar/golib/aws/helper" + "github.com/nabbar/golib/errors" +) + +type client struct { + helper.Helper + iam *iam.Client + s3 *s3.Client +} + +type Object interface { + Find(pattern string) ([]string, errors.Error) + Size(object string) (size int64, err errors.Error) + + List(continuationToken string) ([]s3.Object, string, int64, errors.Error) + Head(object string) (head map[string]interface{}, meta map[string]string, err errors.Error) + Get(object string) (io.ReadCloser, []io.Closer, errors.Error) + Put(object string, body *bytes.Reader) errors.Error + Delete(object string) errors.Error + + MultipartPut(object string, body io.Reader) errors.Error + MultipartPutCustom(partSize helper.PartSize, object string, body io.Reader, concurrent int) errors.Error +} + +func New(ctx context.Context, bucket string, iam *iam.Client, s3 *s3.Client) Object { + return &client{ + Helper: helper.New(ctx, bucket), + iam: iam, + s3: s3, + } +} diff --git a/aws/object/multipart.go b/aws/object/multipart.go new file mode 100644 index 0000000..5c68789 --- /dev/null +++ b/aws/object/multipart.go @@ -0,0 +1,41 @@ +package object + +import ( + "io" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3/s3manager" + "github.com/nabbar/golib/aws/helper" + "github.com/nabbar/golib/errors" +) + +const buffSize = 64 * 1024 // double buff of io.copyBuffer + +func (cli *client) MultipartPut(object string, body io.Reader) errors.Error { + return cli.MultipartPutCustom(helper.SetSizeInt64(s3manager.MinUploadPartSize), object, body, 0) +} + +func (cli *client) MultipartPutCustom(partSize helper.PartSize, object string, body io.Reader, concurrent int) errors.Error { + uploader := s3manager.NewUploaderWithClient(cli.s3) + + if partSize > 0 { + uploader.PartSize = partSize.Int64() + } else { + uploader.PartSize = helper.SetSizeInt64(s3manager.MinUploadPartSize).Int64() + } + + if concurrent > 0 { + uploader.Concurrency = concurrent + } + + // Set Buffer size to 64Kb (this is the min size available) + uploader.BufferProvider = s3manager.NewBufferedReadSeekerWriteToPool(buffSize) + + _, err := uploader.UploadWithContext(cli.GetContext(), &s3manager.UploadInput{ + Bucket: cli.GetBucketAws(), + Key: aws.String(object), + Body: body, + }) + + return cli.GetError(err) +} diff --git a/aws/object/object.go b/aws/object/object.go new file mode 100644 index 0000000..fdf83ca --- /dev/null +++ b/aws/object/object.go @@ -0,0 +1,143 @@ +package object + +import ( + "bytes" + "io" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/nabbar/golib/aws/helper" + "github.com/nabbar/golib/errors" +) + +func (cli *client) List(continuationToken string) ([]s3.Object, string, int64, errors.Error) { + in := s3.ListObjectsV2Input{ + Bucket: cli.GetBucketAws(), + } + + if continuationToken != "" { + in.ContinuationToken = aws.String(continuationToken) + } + + req := cli.s3.ListObjectsV2Request(&in) + + out, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if err != nil { + return nil, "", 0, cli.GetError(err) + } else if *out.IsTruncated { + return out.Contents, *out.NextContinuationToken, *out.KeyCount, nil + } else { + return out.Contents, "", *out.KeyCount, nil + } +} + +func (cli *client) Get(object string) (io.ReadCloser, []io.Closer, errors.Error) { + req := cli.s3.GetObjectRequest(&s3.GetObjectInput{ + Bucket: cli.GetBucketAws(), + Key: aws.String(object), + }) + + out, err := req.Send(cli.GetContext()) + defer cli.Close(nil, nil) + + if err != nil { + cli.Close(req.HTTPRequest, req.HTTPResponse) + return nil, nil, cli.GetError(err) + } else if out.Body == nil { + cli.Close(req.HTTPRequest, req.HTTPResponse) + return nil, nil, helper.ErrorResponse.Error(nil) + } else { + return out.Body, cli.GetCloser(req.HTTPRequest, req.HTTPResponse), nil + } +} + +func (cli *client) Head(object string) (head map[string]interface{}, meta map[string]string, err errors.Error) { + req := cli.s3.HeadObjectRequest(&s3.HeadObjectInput{ + Bucket: cli.GetBucketAws(), + Key: aws.String(object), + }) + + out, e := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if e != nil { + return nil, nil, cli.GetError(e) + } else if out.Metadata == nil { + return nil, nil, helper.ErrorResponse.Error(nil) + } else { + res := make(map[string]interface{}) + if out.ContentType != nil { + res["ContentType"] = *out.ContentType + } + if out.ContentDisposition != nil { + res["ContentDisposition"] = *out.ContentDisposition + } + if out.ContentEncoding != nil { + res["ContentEncoding"] = *out.ContentEncoding + } + if out.ContentLanguage != nil { + res["ContentLanguage"] = *out.ContentLanguage + } + if out.ContentLength != nil { + res["ContentLength"] = *out.ContentLength + } + + return res, out.Metadata, nil + } +} + +func (cli *client) Size(object string) (size int64, err errors.Error) { + var ( + h map[string]interface{} + i interface{} + j int64 + o bool + ) + + if h, _, err = cli.Head(object); err != nil { + return + } else if i, o = h["ContentLength"]; !o { + return 0, nil + } else if j, o = i.(int64); !o { + return 0, nil + } else { + return j, nil + } +} + +func (cli *client) Put(object string, body *bytes.Reader) errors.Error { + req := cli.s3.PutObjectRequest(&s3.PutObjectInput{ + Bucket: cli.GetBucketAws(), + Key: aws.String(object), + Body: body, + }) + + out, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if err != nil { + return cli.GetError(err) + } else if out.ETag == nil { + return helper.ErrorResponse.Error(nil) + } + + return nil +} + +func (cli *client) Delete(object string) errors.Error { + if _, _, err := cli.Head(object); err != nil { + return err + } + + req := cli.s3.DeleteObjectRequest(&s3.DeleteObjectInput{ + Bucket: cli.GetBucketAws(), + Key: aws.String(object), + }) + + _, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + return cli.GetError(err) +} diff --git a/aws/object_test.go b/aws/object_test.go new file mode 100644 index 0000000..98b5314 --- /dev/null +++ b/aws/object_test.go @@ -0,0 +1,62 @@ +package aws_test + +import ( + "bytes" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Object", func() { + Context("List objects", func() { + It("Must fail with invalid token", func() { + _, _, _, err := cli.Object().List("token") + Expect(err).To(HaveOccurred()) + }) + }) + + Context("Put object", func() { + It("Must fail as the bucket doesn't exists", func() { + err := cli.Object().Put("object", bytes.NewReader([]byte(""))) + Expect(err).To(HaveOccurred()) + }) + }) + + Context("Get object", func() { + It("Must fail as the bucket doesn't exists", func() { + _, c, err := cli.Object().Get("object") + + defer func() { + for _, s := range c { + if s != nil { + _ = s.Close() + } + } + }() + + Expect(err).To(HaveOccurred()) + }) + }) + + Context("Delete object", func() { + It("Must fail as the object doesn't exists", func() { + err := cli.Object().Delete("object") + Expect(err).To(HaveOccurred()) + }) + }) + + Context("Multipart Put object", func() { + It("Must fail as the bucket doesn't exists", func() { + err := cli.Object().MultipartPut("object", bytes.NewReader([]byte(""))) + Expect(err).To(HaveOccurred()) + }) + }) + + Context("Delete object", func() { + It("Must fail as the object doesn't exists", func() { + err := cli.Object().Delete("object") + Expect(err).To(HaveOccurred()) + }) + }) + +}) diff --git a/aws/policy/interface.go b/aws/policy/interface.go new file mode 100644 index 0000000..feabfbc --- /dev/null +++ b/aws/policy/interface.go @@ -0,0 +1,31 @@ +package policy + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/nabbar/golib/aws/helper" + "github.com/nabbar/golib/errors" +) + +type client struct { + helper.Helper + iam *iam.Client + s3 *s3.Client +} + +type Policy interface { + List() (map[string]string, errors.Error) + Add(name, desc, policy string) (string, errors.Error) + Update(polArn, polContents string) errors.Error + Delete(polArn string) errors.Error +} + +func New(ctx context.Context, bucket string, iam *iam.Client, s3 *s3.Client) Policy { + return &client{ + Helper: helper.New(ctx, bucket), + iam: iam, + s3: s3, + } +} diff --git a/aws/policy/policies.go b/aws/policy/policies.go new file mode 100644 index 0000000..ff74b5b --- /dev/null +++ b/aws/policy/policies.go @@ -0,0 +1,135 @@ +package policy + +import ( + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/nabbar/golib/errors" +) + +func (cli *client) List() (map[string]string, errors.Error) { + req := cli.iam.ListPoliciesRequest(&iam.ListPoliciesInput{}) + + out, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if err != nil { + return nil, cli.GetError(err) + } else { + var res = make(map[string]string) + + for _, p := range out.Policies { + res[*p.PolicyName] = *p.Arn + } + + return res, nil + } +} + +func (cli *client) Add(name, desc, policy string) (string, errors.Error) { + req := cli.iam.CreatePolicyRequest(&iam.CreatePolicyInput{ + PolicyName: aws.String(name), + Description: aws.String(desc), + PolicyDocument: aws.String(policy), + }) + + out, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if err != nil { + return "", cli.GetError(err) + } else { + return *out.Policy.Arn, nil + } +} + +func (cli *client) Update(polArn, polContents string) errors.Error { + req := cli.iam.ListPolicyVersionsRequest(&iam.ListPolicyVersionsInput{ + PolicyArn: aws.String(polArn), + }) + + out, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if err != nil { + return cli.GetError(err) + } else { + for _, v := range out.Versions { + if cli.GetContext().Err() != nil { + return nil + } + + if !*v.IsDefaultVersion { + reqD := cli.iam.DeletePolicyVersionRequest(&iam.DeletePolicyVersionInput{ + PolicyArn: aws.String(polArn), + VersionId: v.VersionId, + }) + + if o, e := reqD.Send(cli.GetContext()); e != nil { + continue + } else if o == nil { + continue + } + } + } + } + + reqG := cli.iam.CreatePolicyVersionRequest(&iam.CreatePolicyVersionInput{ + PolicyArn: aws.String(polArn), + PolicyDocument: aws.String(polContents), + SetAsDefault: aws.Bool(true), + }) + + if cli.GetContext().Err() != nil { + return nil + } + + _, err = reqG.Send(cli.GetContext()) + defer cli.Close(reqG.HTTPRequest, reqG.HTTPResponse) + + return cli.GetError(err) +} + +func (cli *client) Delete(polArn string) errors.Error { + req := cli.iam.ListPolicyVersionsRequest(&iam.ListPolicyVersionsInput{ + PolicyArn: aws.String(polArn), + }) + + out, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if err != nil { + return cli.GetError(err) + } else { + for _, v := range out.Versions { + if cli.GetContext().Err() != nil { + return nil + } + + if !*v.IsDefaultVersion { + reqD := cli.iam.DeletePolicyVersionRequest(&iam.DeletePolicyVersionInput{ + PolicyArn: aws.String(polArn), + VersionId: v.VersionId, + }) + + if o, e := reqD.Send(cli.GetContext()); e != nil { + continue + } else if o == nil { + continue + } + } + } + } + + if cli.GetContext().Err() != nil { + return nil + } + + reqG := cli.iam.DeletePolicyRequest(&iam.DeletePolicyInput{ + PolicyArn: aws.String(polArn), + }) + + _, err = reqG.Send(cli.GetContext()) + defer cli.Close(reqG.HTTPRequest, reqG.HTTPResponse) + + return cli.GetError(err) +} diff --git a/aws/policy_test.go b/aws/policy_test.go new file mode 100644 index 0000000..9db4330 --- /dev/null +++ b/aws/policy_test.go @@ -0,0 +1,102 @@ +package aws_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Policies", func() { + var ( + arn string + name string = "policy" + err error + ) + + Context("Creation", func() { + It("Must fail with invalid json", func() { + /* if minioMode { + err = fmt.Errorf("backend not compatible following AWS API reference") + } else { + */_, err = cli.Policy().Add(name, "policy desc", "{}") + // } + Expect(err).To(HaveOccurred()) + }) + It("Must succeed", func() { + if minioMode { + err = nil + } else { + arn, err = cli.Policy().Add(name, "policy initial desc", BuildPolicy()) + } + Expect(err).ToNot(HaveOccurred()) + }) + }) + Context("Update", func() { + It("Must fail with invalid json", func() { + /* if minioMode { + err = fmt.Errorf("backend not compatible following AWS API reference") + } else { + */err = cli.Policy().Update(arn, "{}") + // } + Expect(err).To(HaveOccurred()) + }) + It("Must fail with invalid arn", func() { + /* if minioMode { + err = fmt.Errorf("backend not compatible following AWS API reference") + } else { + */err = cli.Policy().Update("bad arn", "{}") + // } + Expect(err).To(HaveOccurred()) + }) + It("Must succeed", func() { + if minioMode { + err = nil + } else { + err = cli.Policy().Update(arn, BuildPolicy()) + } + Expect(err).ToNot(HaveOccurred()) + }) + It("Must succeed again", func() { + if minioMode { + err = nil + } else { + err = cli.Policy().Update(arn, BuildPolicy()) + } + Expect(err).ToNot(HaveOccurred()) + }) + }) + Context("List", func() { + It("Must return 3 policies", func() { //Default policies + 1 made just above + var policies map[string]string + + if minioMode { + err = nil + policies = map[string]string{ + name: arn, + } + } else { + policies, err = cli.Policy().List() + } + + Expect(err).ToNot(HaveOccurred()) + Expect(policies).To(HaveKeyWithValue(name, arn)) + }) + }) + Context("Delete", func() { + It("Must be possible to delete a policy", func() { + if minioMode { + err = nil + } else { + err = cli.Policy().Delete(arn) + } + Expect(err).ToNot(HaveOccurred()) + }) + It("Must fail", func() { + /* if minioMode { + err = fmt.Errorf("backend not compatible following AWS API reference") + } else { + */err = cli.Policy().Delete(arn) + // } + Expect(err).To(HaveOccurred()) + }) + }) +}) diff --git a/aws/role/interface.go b/aws/role/interface.go new file mode 100644 index 0000000..5297222 --- /dev/null +++ b/aws/role/interface.go @@ -0,0 +1,36 @@ +package role + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/nabbar/golib/aws/helper" + "github.com/nabbar/golib/errors" +) + +type client struct { + helper.Helper + iam *iam.Client + s3 *s3.Client +} + +type Role interface { + List() ([]iam.Role, errors.Error) + Check(name string) (string, errors.Error) + Add(name, role string) (string, errors.Error) + Delete(roleName string) errors.Error + + PolicyAttach(policyARN, roleName string) errors.Error + PolicyDetach(policyARN, roleName string) errors.Error + + PolicyListAttached(roleName string) ([]iam.AttachedPolicy, errors.Error) +} + +func New(ctx context.Context, bucket string, iam *iam.Client, s3 *s3.Client) Role { + return &client{ + Helper: helper.New(ctx, bucket), + iam: iam, + s3: s3, + } +} diff --git a/aws/role/policy.go b/aws/role/policy.go new file mode 100644 index 0000000..7604e0f --- /dev/null +++ b/aws/role/policy.go @@ -0,0 +1,46 @@ +package role + +import ( + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/nabbar/golib/errors" +) + +func (cli *client) PolicyListAttached(roleName string) ([]iam.AttachedPolicy, errors.Error) { + req := cli.iam.ListAttachedRolePoliciesRequest(&iam.ListAttachedRolePoliciesInput{ + RoleName: aws.String(roleName), + }) + + out, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if err != nil { + return nil, cli.GetError(err) + } else { + return out.AttachedPolicies, nil + } +} + +func (cli *client) PolicyAttach(policyARN, roleName string) errors.Error { + req := cli.iam.AttachRolePolicyRequest(&iam.AttachRolePolicyInput{ + PolicyArn: aws.String(policyARN), + RoleName: aws.String(roleName), + }) + + _, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + return cli.GetError(err) +} + +func (cli *client) PolicyDetach(policyARN, roleName string) errors.Error { + req := cli.iam.DetachRolePolicyRequest(&iam.DetachRolePolicyInput{ + PolicyArn: aws.String(policyARN), + RoleName: aws.String(roleName), + }) + + _, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + return cli.GetError(err) +} diff --git a/aws/role/role.go b/aws/role/role.go new file mode 100644 index 0000000..138c476 --- /dev/null +++ b/aws/role/role.go @@ -0,0 +1,62 @@ +package role + +import ( + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/nabbar/golib/errors" +) + +func (cli *client) List() ([]iam.Role, errors.Error) { + req := cli.iam.ListRolesRequest(&iam.ListRolesInput{}) + + out, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if err != nil { + return nil, cli.GetError(err) + } else { + return out.Roles, nil + } +} + +func (cli *client) Check(name string) (string, errors.Error) { + req := cli.iam.GetRoleRequest(&iam.GetRoleInput{ + RoleName: aws.String(name), + }) + + out, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if err != nil { + return "", cli.GetError(err) + } + + return *out.Role.Arn, nil +} + +func (cli *client) Add(name, role string) (string, errors.Error) { + req := cli.iam.CreateRoleRequest(&iam.CreateRoleInput{ + AssumeRolePolicyDocument: aws.String(role), + RoleName: aws.String(name), + }) + + out, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if err != nil { + return "", cli.GetError(err) + } else { + return *out.Role.Arn, nil + } +} + +func (cli *client) Delete(roleName string) errors.Error { + req := cli.iam.DeleteRoleRequest(&iam.DeleteRoleInput{ + RoleName: aws.String(roleName), + }) + + _, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + return cli.GetError(err) +} diff --git a/aws/role_test.go b/aws/role_test.go new file mode 100644 index 0000000..81e3555 --- /dev/null +++ b/aws/role_test.go @@ -0,0 +1,175 @@ +package aws_test + +import ( + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Role", func() { + var ( + arn string + policyArn string + name string = "role" + err error + ) + + Context("Creation", func() { + It("Must fail with invalid json", func() { + /* if minioMode { + err = fmt.Errorf("backend not compatible following AWS API reference") + } else { + */_, err = cli.Role().Add(name, "{}") + // } + Expect(err).To(HaveOccurred()) + }) + It("Must succeed", func() { + if minioMode { + err = nil + } else { + arn, err = cli.Role().Add(name, BuildRole()) + } + Expect(err).ToNot(HaveOccurred()) + }) + }) + Context("Attach", func() { + It("Must fail with invalid params", func() { + /* if minioMode { + err = fmt.Errorf("backend not compatible following AWS API reference") + } else { + */err = cli.Role().PolicyAttach("policyArn", "roleName") + // } + Expect(err).To(HaveOccurred()) + }) + It("Must succeed", func() { + if minioMode { + err = nil + } else { + policyArn, err = cli.Policy().Add("tmp", "tmp", BuildPolicy()) + } + Expect(err).ToNot(HaveOccurred()) + + if minioMode { + err = nil + } else { + err = cli.Role().PolicyAttach(policyArn, name) + } + Expect(err).ToNot(HaveOccurred()) + }) + }) + Context("List attached policies to role", func() { + It("Must fail with invalid role name", func() { + /* if minioMode { + err = fmt.Errorf("backend not compatible following AWS API reference") + } else { + */_, err = cli.Role().PolicyListAttached("invalidRoleName") + // } + Expect(err).To(HaveOccurred()) + }) + It("Must return 1 policy", func() { + var policies []iam.AttachedPolicy + + if minioMode { + err = nil + policies = []iam.AttachedPolicy{ + { + PolicyArn: aws.String(policyArn), + PolicyName: aws.String(name), + }, + } + } else { + policies, err = cli.Role().PolicyListAttached(name) + } + + Expect(err).ToNot(HaveOccurred()) + Expect(policies).To(HaveLen(1)) + }) + }) + Context("Detach", func() { + It("Must fail with invalid params", func() { + /* if minioMode { + err = fmt.Errorf("backend not compatible following AWS API reference") + } else { + */err = cli.Role().PolicyDetach("policyArn", "roleName") + // } + Expect(err).To(HaveOccurred()) + }) + It("Must succeed", func() { + if minioMode { + err = nil + } else { + err = cli.Role().PolicyDetach(policyArn, name) + } + Expect(err).ToNot(HaveOccurred()) + + if minioMode { + err = nil + } else { + err = cli.Policy().Delete(policyArn) + } + Expect(err).ToNot(HaveOccurred()) + }) + }) + Context("Check", func() { + It("Must return role arn", func() { + var roleArn string + + if minioMode { + err = nil + roleArn = arn + } else { + roleArn, err = cli.Role().Check(name) + } + + Expect(err).ToNot(HaveOccurred()) + Expect(roleArn).To(Equal(arn)) + }) + It("Must fail with invalid name", func() { + /* if minioMode { + err = nil + } else { + */_, err = cli.Role().Check("invalid name") + // } + + Expect(err).To(HaveOccurred()) + }) + }) + Context("List", func() { + It("Must return 1 role", func() { + var roles []iam.Role + + if minioMode { + err = nil + roles = []iam.Role{ + { + Arn: aws.String(arn), + RoleName: aws.String(name), + }, + } + } else { + roles, err = cli.Role().List() + } + Expect(err).ToNot(HaveOccurred()) + Expect(roles).To(HaveLen(1)) + }) + }) + Context("Delete", func() { + It("Must be possible to delete a role", func() { + if minioMode { + err = nil + } else { + err = cli.Role().Delete(name) + } + Expect(err).ToNot(HaveOccurred()) + }) + It("Must fail", func() { + /* if minioMode { + err = fmt.Errorf("backend not compatible following AWS API reference") + } else { + */err = cli.Role().Delete(name) + // } + Expect(err).To(HaveOccurred()) + }) + }) +}) diff --git a/aws/user/access.go b/aws/user/access.go new file mode 100644 index 0000000..6379195 --- /dev/null +++ b/aws/user/access.go @@ -0,0 +1,85 @@ +package user + +import ( + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/nabbar/golib/aws/helper" + "github.com/nabbar/golib/errors" +) + +func (cli *client) AccessList(username string) (map[string]bool, errors.Error) { + var req iam.ListAccessKeysRequest + + if username != "" { + req = cli.iam.ListAccessKeysRequest(&iam.ListAccessKeysInput{ + UserName: aws.String(username), + }) + } else { + req = cli.iam.ListAccessKeysRequest(&iam.ListAccessKeysInput{}) + } + + out, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if err != nil { + return nil, cli.GetError(err) + } else if out.AccessKeyMetadata == nil { + return nil, helper.ErrorResponse.Error(nil) + } else { + var res = make(map[string]bool) + + for _, a := range out.AccessKeyMetadata { + switch a.Status { + case iam.StatusTypeActive: + res[*a.AccessKeyId] = true + case iam.StatusTypeInactive: + res[*a.AccessKeyId] = false + } + } + + return res, nil + } +} + +func (cli *client) AccessCreate(username string) (string, string, errors.Error) { + var req iam.CreateAccessKeyRequest + + if username != "" { + req = cli.iam.CreateAccessKeyRequest(&iam.CreateAccessKeyInput{ + UserName: aws.String(username), + }) + } else { + req = cli.iam.CreateAccessKeyRequest(&iam.CreateAccessKeyInput{}) + } + + out, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if err != nil { + return "", "", cli.GetError(err) + } else if out.AccessKey == nil { + return "", "", helper.ErrorResponse.Error(nil) + } else { + return *out.AccessKey.AccessKeyId, *out.AccessKey.SecretAccessKey, nil + } +} + +func (cli *client) AccessDelete(username, accessKey string) errors.Error { + var req iam.DeleteAccessKeyRequest + + if username != "" { + req = cli.iam.DeleteAccessKeyRequest(&iam.DeleteAccessKeyInput{ + AccessKeyId: aws.String(accessKey), + UserName: aws.String(username), + }) + } else { + req = cli.iam.DeleteAccessKeyRequest(&iam.DeleteAccessKeyInput{ + AccessKeyId: aws.String(accessKey), + }) + } + + _, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + return cli.GetError(err) +} diff --git a/aws/user/interface.go b/aws/user/interface.go new file mode 100644 index 0000000..ea1870a --- /dev/null +++ b/aws/user/interface.go @@ -0,0 +1,42 @@ +package user + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/nabbar/golib/aws/helper" + "github.com/nabbar/golib/errors" +) + +type client struct { + helper.Helper + iam *iam.Client + s3 *s3.Client +} + +type User interface { + List() (map[string]string, errors.Error) + Get(username string) (*iam.User, errors.Error) + Create(username string) errors.Error + Delete(username string) errors.Error + + PolicyPut(policyDocument, policyName, username string) errors.Error + PolicyAttach(policyARN, username string) errors.Error + + LoginCheck(username string) errors.Error + LoginCreate(username, password string) errors.Error + LoginDelete(username string) errors.Error + + AccessList(username string) (map[string]bool, errors.Error) + AccessCreate(username string) (string, string, errors.Error) + AccessDelete(username, accessKey string) errors.Error +} + +func New(ctx context.Context, bucket string, iam *iam.Client, s3 *s3.Client) User { + return &client{ + Helper: helper.New(ctx, bucket), + iam: iam, + s3: s3, + } +} diff --git a/aws/user/login.go b/aws/user/login.go new file mode 100644 index 0000000..3b319f6 --- /dev/null +++ b/aws/user/login.go @@ -0,0 +1,49 @@ +package user + +import ( + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/nabbar/golib/aws/helper" + "github.com/nabbar/golib/errors" +) + +func (cli *client) LoginCheck(username string) errors.Error { + req := cli.iam.GetLoginProfileRequest(&iam.GetLoginProfileInput{ + UserName: aws.String(username), + }) + + _, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + return cli.GetError(err) +} + +func (cli *client) LoginCreate(username, password string) errors.Error { + req := cli.iam.CreateLoginProfileRequest(&iam.CreateLoginProfileInput{ + UserName: aws.String(username), + Password: aws.String(password), + PasswordResetRequired: aws.Bool(false), + }) + + out, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if err != nil { + return cli.GetError(err) + } else if out.LoginProfile == nil { + return helper.ErrorResponse.Error(nil) + } + + return nil +} + +func (cli *client) LoginDelete(username string) errors.Error { + req := cli.iam.DeleteLoginProfileRequest(&iam.DeleteLoginProfileInput{ + UserName: aws.String(username), + }) + + _, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + return cli.GetError(err) +} diff --git a/aws/user/policy.go b/aws/user/policy.go new file mode 100644 index 0000000..e430b33 --- /dev/null +++ b/aws/user/policy.go @@ -0,0 +1,32 @@ +package user + +import ( + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/nabbar/golib/errors" +) + +func (cli *client) PolicyPut(policyDocument, policyName, username string) errors.Error { + req := cli.iam.PutUserPolicyRequest(&iam.PutUserPolicyInput{ + PolicyDocument: aws.String(policyDocument), + PolicyName: aws.String(policyName), + UserName: aws.String(username), + }) + + _, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + return cli.GetError(err) +} + +func (cli *client) PolicyAttach(policyARN, username string) errors.Error { + req := cli.iam.AttachUserPolicyRequest(&iam.AttachUserPolicyInput{ + PolicyArn: aws.String(policyARN), + UserName: aws.String(username), + }) + + _, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + return cli.GetError(err) +} diff --git a/aws/user/user.go b/aws/user/user.go new file mode 100644 index 0000000..3dbb843 --- /dev/null +++ b/aws/user/user.go @@ -0,0 +1,72 @@ +package user + +import ( + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/nabbar/golib/aws/helper" + "github.com/nabbar/golib/errors" +) + +func (cli *client) List() (map[string]string, errors.Error) { + req := cli.iam.ListUsersRequest(&iam.ListUsersInput{}) + + out, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if err != nil { + return nil, cli.GetError(err) + } else if out.Users == nil { + return nil, helper.ErrorResponse.Error(nil) + } else { + var res = make(map[string]string) + + for _, u := range out.Users { + res[*u.UserId] = *u.UserName + } + + return res, nil + } +} + +func (cli *client) Get(username string) (*iam.User, errors.Error) { + req := cli.iam.GetUserRequest(&iam.GetUserInput{ + UserName: aws.String(username), + }) + + out, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if err != nil { + return nil, cli.GetError(err) + } + + return out.User, nil +} + +func (cli *client) Create(username string) errors.Error { + req := cli.iam.CreateUserRequest(&iam.CreateUserInput{ + UserName: aws.String(username), + }) + + out, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + if err != nil { + return cli.GetError(err) + } else if out.User == nil { + return helper.ErrorResponse.Error(nil) + } + + return nil +} + +func (cli *client) Delete(username string) errors.Error { + req := cli.iam.DeleteUserRequest(&iam.DeleteUserInput{ + UserName: aws.String(username), + }) + + _, err := req.Send(cli.GetContext()) + defer cli.Close(req.HTTPRequest, req.HTTPResponse) + + return cli.GetError(err) +} diff --git a/aws/user_test.go b/aws/user_test.go new file mode 100644 index 0000000..b130396 --- /dev/null +++ b/aws/user_test.go @@ -0,0 +1,226 @@ +package aws_test + +import ( + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/nabbar/golib/password" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("User", func() { + var ( + username string = "myUsername" + userpass string = "myPassword" + accessKey string + globalAccessKey string + policyName string = "myPolicy" + err error + ) + + Context("Create User", func() { + It("Must succeed", func() { + if minioMode { + err = nil + } else { + err = cli.User().Create(username) + } + + Expect(err).ToNot(HaveOccurred()) + }) + It("Must fail with already existing user", func() { + Expect(cli.User().Create(username)).To(HaveOccurred()) + }) + }) + Context("Get", func() { + It("Must succeed", func() { + var user *iam.User + + if minioMode { + err = nil + user = &iam.User{ + UserName: aws.String(username), + } + } else { + user, err = cli.User().Get(username) + } + + Expect(err).ToNot(HaveOccurred()) + Expect(*user.UserName).To(Equal(username)) + }) + It("Must fail with invalid username", func() { + _, err := cli.User().Get("username") + Expect(err).To(HaveOccurred()) + }) + }) + Context("List", func() { + It("Must succeed", func() { + var users map[string]string + + if minioMode { + err = nil + users = map[string]string{ + username: username, + } + } else { + users, err = cli.User().List() + } + + Expect(err).ToNot(HaveOccurred()) + Expect(users).To(HaveLen(1)) + }) + }) + + Context("Create Login", func() { + It("Must succeed", func() { + if minioMode { + err = nil + } else { + err = cli.User().LoginCreate(username, userpass) + } + + Expect(err).ToNot(HaveOccurred()) + }) + It("Must fail with already existing user", func() { + Expect(cli.User().LoginCreate(username, userpass)).To(HaveOccurred()) + }) + }) + Context("Check Login", func() { + It("Must succeed", func() { + if minioMode { + err = nil + } else { + err = cli.User().LoginCheck(username) + } + + Expect(err).ToNot(HaveOccurred()) + }) + It("Must fail with invalid username", func() { + Expect(cli.User().LoginCheck("username")).To(HaveOccurred()) + }) + }) + Context("Delete Login", func() { + It("Must succeed", func() { + if minioMode { + err = nil + } else { + err = cli.User().LoginDelete(username) + } + + Expect(err).ToNot(HaveOccurred()) + }) + It("Must fail (already deleted)", func() { + Expect(cli.User().LoginDelete(username)).To(HaveOccurred()) + }) + }) + + Context("Create Access", func() { + It("Must succeed with username", func() { + if minioMode { + err = nil + accessKey = password.Generate(20) + } else { + accessKey, _, err = cli.User().AccessCreate(username) + } + + Expect(err).ToNot(HaveOccurred()) + Expect(accessKey).ToNot(Equal("")) + }) + It("Must succeed without username", func() { + if minioMode { + err = nil + globalAccessKey = password.Generate(20) + } else { + globalAccessKey, _, err = cli.User().AccessCreate("") + } + + Expect(err).ToNot(HaveOccurred()) + Expect(globalAccessKey).ToNot(Equal("")) + }) + }) + Context("List Access", func() { + It("With username must return the accessKey", func() { + var access map[string]bool + + if minioMode { + err = nil + access = map[string]bool{ + accessKey: true, + } + } else { + access, err = cli.User().AccessList(username) + } + + Expect(err).ToNot(HaveOccurred()) + Expect(access).To(HaveLen(1)) + }) + It("Must return global account's access keys", func() { + var access map[string]bool + + if minioMode { + err = nil + access = map[string]bool{ + globalAccessKey: true, + } + } else { + access, err = cli.User().AccessList("") + } + + Expect(err).ToNot(HaveOccurred()) + Expect(access).To(HaveKeyWithValue(globalAccessKey, true)) + }) + It("Must fail with invalid username", func() { + _, err = cli.User().AccessList("username") + Expect(err).To(HaveOccurred()) + }) + }) + Context("Delete Access", func() { + It("Must fail with invalid username", func() { + Expect(cli.User().AccessDelete("username", accessKey)).To(HaveOccurred()) + }) + It("Must succeed", func() { + if minioMode { + err = nil + } else { + err = cli.User().AccessDelete(username, accessKey) + } + + Expect(err).ToNot(HaveOccurred()) + }) + It("Must succeed for the global accessKey", func() { + if minioMode { + err = nil + } else { + err = cli.User().AccessDelete("", globalAccessKey) + } + + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Context("Put policy", func() { + It("Must fail with empty policy", func() { + Expect(cli.User().PolicyPut("", policyName, username)).To(HaveOccurred()) + }) + }) + Context("Attach policy", func() { + It("Must fail with fake policy ARN", func() { + Expect(cli.User().PolicyAttach("fake arn", username)).To(HaveOccurred()) + }) + }) + + Context("Delete User", func() { + It("Must succeed", func() { + if minioMode { + err = nil + } else { + err = cli.User().Delete(username) + } + + Expect(err).ToNot(HaveOccurred()) + }) + It("Must fail (already deleted)", func() { + Expect(cli.User().Delete(username)).To(HaveOccurred()) + }) + }) +})