diff --git a/auth/iam.go b/auth/iam.go index 9b786a7..5c6d49a 100644 --- a/auth/iam.go +++ b/auth/iam.go @@ -44,23 +44,25 @@ type IAMService interface { var ErrNoSuchUser = errors.New("user not found") type Opts struct { - Dir string - LDAPServerURL string - LDAPBindDN string - LDAPPassword string - LDAPQueryBase string - LDAPObjClasses string - LDAPAccessAtr string - LDAPSecretAtr string - LDAPRoleAtr string - S3Access string - S3Secret string - S3Region string - S3Bucket string - S3Endpoint string - CacheDisable bool - CacheTTL int - CachePrune int + Dir string + LDAPServerURL string + LDAPBindDN string + LDAPPassword string + LDAPQueryBase string + LDAPObjClasses string + LDAPAccessAtr string + LDAPSecretAtr string + LDAPRoleAtr string + S3Access string + S3Secret string + S3Region string + S3Bucket string + S3Endpoint string + S3DisableSSlVerfiy bool + S3Debug bool + CacheDisable bool + CacheTTL int + CachePrune int } func New(o *Opts) (IAMService, error) { @@ -78,7 +80,7 @@ func New(o *Opts) (IAMService, error) { fmt.Printf("initializing LDAP IAM with %q\n", o.LDAPServerURL) case o.S3Endpoint != "": svc, err = NewS3(o.S3Access, o.S3Secret, o.S3Region, o.S3Bucket, - o.S3Endpoint) + o.S3Endpoint, o.S3DisableSSlVerfiy, o.S3Debug) fmt.Printf("initializing S3 IAM with '%v/%v'\n", o.S3Endpoint, o.S3Bucket) default: diff --git a/auth/iam_proxy.go b/auth/iam_proxy.go deleted file mode 100644 index 5cf0eec..0000000 --- a/auth/iam_proxy.go +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright 2023 Versity Software -// This file is licensed under the Apache License, Version 2.0 -// (the "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package auth - -import ( - "bytes" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "net/http" - "time" - - "github.com/aws/aws-sdk-go-v2/aws" - v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" -) - -type IAMServiceS3 struct { - access string - secret string - region string - bucket string - endpoint string -} - -var _ IAMService = &IAMServiceS3{} - -func NewS3(access, secret, region, bucket, endpoint string) (IAMService, error) { - if access == "" { - return nil, fmt.Errorf("must provide s3 IAM service access key") - } - if secret == "" { - return nil, fmt.Errorf("must provide s3 IAM service secret key") - } - if region == "" { - return nil, fmt.Errorf("must provide s3 IAM service region") - } - if bucket == "" { - return nil, fmt.Errorf("must provide s3 IAM service bucket") - } - if endpoint == "" { - return nil, fmt.Errorf("must provide s3 IAM service endpoint") - } - - return &IAMServiceS3{ - access: access, - secret: secret, - region: region, - bucket: bucket, - endpoint: endpoint, - }, nil -} - -func (s *IAMServiceS3) CreateAccount(account Account) error { - accJson, err := json.Marshal(account) - if err != nil { - return fmt.Errorf("failed to parse user data: %w", err) - } - - req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%v/create-user", s.endpoint), bytes.NewBuffer(accJson)) - if err != nil { - return fmt.Errorf("failed to send the request: %w", err) - } - - signer := v4.NewSigner() - - hashedPayload := sha256.Sum256(accJson) - hexPayload := hex.EncodeToString(hashedPayload[:]) - - req.Header.Set("X-Amz-Content-Sha256", hexPayload) - - signErr := signer.SignHTTP(req.Context(), aws.Credentials{AccessKeyID: s.access, SecretAccessKey: s.secret}, req, hexPayload, "s3", s.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) - } - - if resp.StatusCode > 300 { - body, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - defer resp.Body.Close() - return fmt.Errorf(string(body)) - } - - return nil -} - -func (s *IAMServiceS3) GetUserAccount(access string) (Account, error) { - return Account{ - Access: s.access, - Secret: s.secret, - Role: "admin", - }, nil -} - -func (s *IAMServiceS3) DeleteUserAccount(access string) error { - req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%v/delete-user?access=%v", s.endpoint, access), 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: s.access, SecretAccessKey: s.secret}, req, hexPayload, "s3", s.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) - } - - if resp.StatusCode > 300 { - body, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - defer resp.Body.Close() - return fmt.Errorf(string(body)) - } - - return nil -} - -func (s *IAMServiceS3) ListUserAccounts() ([]Account, error) { - req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%v/list-users", s.endpoint), nil) - if err != nil { - return []Account{}, 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: s.access, SecretAccessKey: s.secret}, req, hexPayload, "s3", s.region, time.Now()) - if signErr != nil { - return []Account{}, fmt.Errorf("failed to sign the request: %w", err) - } - - client := http.Client{} - - resp, err := client.Do(req) - if err != nil { - return []Account{}, fmt.Errorf("failed to send the request: %w", err) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return []Account{}, err - } - defer resp.Body.Close() - - var accs []Account - if err := json.Unmarshal(body, &accs); err != nil { - return []Account{}, err - } - - return accs, nil -} - -func (IAMServiceS3) Shutdown() error { - return nil -} diff --git a/auth/iam_s3_object.go b/auth/iam_s3_object.go new file mode 100644 index 0000000..1590c82 --- /dev/null +++ b/auth/iam_s3_object.go @@ -0,0 +1,263 @@ +// Copyright 2023 Versity Software +// This file is licensed under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package auth + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "sort" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/aws/smithy-go" +) + +// IAMServiceS3 stores user accounts in an S3 object +// The endpoint, credentials, bucket, and region are provided +// from cli configuration. +// The object format and name is the same as the internal IAM service: +// coming from iAMConfig and iamFile in iam_internal. + +type IAMServiceS3 struct { + access string + secret string + region string + bucket string + endpoint string + sslSkipVerify bool + debug bool + client *s3.Client +} + +var _ IAMService = &IAMServiceS3{} + +func NewS3(access, secret, region, bucket, endpoint string, sslSkipVerify, debug bool) (*IAMServiceS3, error) { + if access == "" { + return nil, fmt.Errorf("must provide s3 IAM service access key") + } + if secret == "" { + return nil, fmt.Errorf("must provide s3 IAM service secret key") + } + if region == "" { + return nil, fmt.Errorf("must provide s3 IAM service region") + } + if bucket == "" { + return nil, fmt.Errorf("must provide s3 IAM service bucket") + } + if endpoint == "" { + return nil, fmt.Errorf("must provide s3 IAM service endpoint") + } + + i := &IAMServiceS3{ + access: access, + secret: secret, + region: region, + bucket: bucket, + endpoint: endpoint, + sslSkipVerify: sslSkipVerify, + debug: debug, + } + + cfg, err := i.getConfig() + if err != nil { + return nil, fmt.Errorf("init s3 IAM: %v", err) + } + + i.client = s3.NewFromConfig(cfg) + return i, nil +} + +func (s *IAMServiceS3) CreateAccount(account Account) error { + conf, err := s.getAccounts() + if err != nil { + return err + } + + _, ok := conf.AccessAccounts[account.Access] + if ok { + return fmt.Errorf("account already exists") + } + conf.AccessAccounts[account.Access] = account + + return s.storeAccts(conf) +} + +func (s *IAMServiceS3) GetUserAccount(access string) (Account, error) { + conf, err := s.getAccounts() + if err != nil { + return Account{}, err + } + + acct, ok := conf.AccessAccounts[access] + if !ok { + return Account{}, ErrNoSuchUser + } + + return acct, nil +} + +func (s *IAMServiceS3) DeleteUserAccount(access string) error { + conf, err := s.getAccounts() + if err != nil { + return err + } + + _, ok := conf.AccessAccounts[access] + if !ok { + return fmt.Errorf("account does not exist") + } + delete(conf.AccessAccounts, access) + + return s.storeAccts(conf) +} + +func (s *IAMServiceS3) ListUserAccounts() ([]Account, error) { + conf, err := s.getAccounts() + if err != nil { + return nil, err + } + + keys := make([]string, 0, len(conf.AccessAccounts)) + for k := range conf.AccessAccounts { + keys = append(keys, k) + } + sort.Strings(keys) + + var accs []Account + for _, k := range keys { + accs = append(accs, Account{ + Access: k, + Secret: conf.AccessAccounts[k].Secret, + Role: conf.AccessAccounts[k].Role, + UserID: conf.AccessAccounts[k].UserID, + GroupID: conf.AccessAccounts[k].GroupID, + ProjectID: conf.AccessAccounts[k].ProjectID, + }) + } + + return accs, nil +} + +// ResolveEndpoint is used for on prem or non-aws endpoints +func (s *IAMServiceS3) ResolveEndpoint(service, region string, options ...interface{}) (aws.Endpoint, error) { + return aws.Endpoint{ + PartitionID: "aws", + URL: s.endpoint, + SigningRegion: s.region, + HostnameImmutable: true, + }, nil +} + +func (s *IAMServiceS3) Shutdown() error { + return nil +} + +func (s *IAMServiceS3) getConfig() (aws.Config, error) { + creds := credentials.NewStaticCredentialsProvider(s.access, s.secret, "") + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: s.sslSkipVerify}, + } + client := &http.Client{Transport: tr} + + opts := []func(*config.LoadOptions) error{ + config.WithRegion(s.region), + config.WithCredentialsProvider(creds), + config.WithHTTPClient(client), + } + + if s.endpoint != "" { + opts = append(opts, + config.WithEndpointResolverWithOptions(s)) + } + + if s.debug { + opts = append(opts, + config.WithClientLogMode(aws.LogSigning|aws.LogRetries|aws.LogRequest|aws.LogResponse|aws.LogRequestEventMessage|aws.LogResponseEventMessage)) + } + + return config.LoadDefaultConfig(context.Background(), opts...) +} + +func (s *IAMServiceS3) getAccounts() (iAMConfig, error) { + obj := iamFile + + out, err := s.client.GetObject(context.Background(), &s3.GetObjectInput{ + Bucket: &s.bucket, + Key: &obj, + }) + if err != nil { + // if the error is object not exists, + // init empty accounts stuct and return that + var nsk *types.NoSuchKey + if errors.As(err, &nsk) { + return iAMConfig{}, nil + } + var apiErr smithy.APIError + if errors.As(err, &apiErr) { + if apiErr.ErrorCode() == "NotFound" { + return iAMConfig{}, nil + } + } + + // all other errors, return the error + return iAMConfig{}, fmt.Errorf("get %v: %w", obj, err) + } + + defer out.Body.Close() + + b, err := io.ReadAll(out.Body) + if err != nil { + return iAMConfig{}, fmt.Errorf("read %v: %w", obj, err) + } + + conf, err := parseIAM(b) + if err != nil { + return iAMConfig{}, fmt.Errorf("parse iam data: %w", err) + } + + return conf, nil +} + +func (s *IAMServiceS3) storeAccts(conf iAMConfig) error { + b, err := json.Marshal(conf) + if err != nil { + return fmt.Errorf("failed to serialize iam: %w", err) + } + + obj := iamFile + uploader := manager.NewUploader(s.client) + upinfo := &s3.PutObjectInput{ + Body: bytes.NewReader(b), + Bucket: &s.bucket, + Key: &obj, + } + _, err = uploader.Upload(context.Background(), upinfo) + if err != nil { + return fmt.Errorf("store accounts in %v: %w", iamFile, err) + } + + return nil +} diff --git a/cmd/versitygw/main.go b/cmd/versitygw/main.go index 6b5a71b..df2092c 100644 --- a/cmd/versitygw/main.go +++ b/cmd/versitygw/main.go @@ -51,6 +51,7 @@ var ( s3IamAccess, s3IamSecret string s3IamRegion, s3IamBucket string s3IamEndpoint string + s3IamSslNoVerify, s3IamDebug bool iamCacheDisable bool iamCacheTTL int iamCachePrune int @@ -289,6 +290,16 @@ func initFlags() []cli.Flag { Usage: "s3 IAM endpoint", Destination: &s3IamEndpoint, }, + &cli.BoolFlag{ + Name: "s3-iam-noverify", + Usage: "s3 IAM disable ssl verification", + Destination: &s3IamSslNoVerify, + }, + &cli.BoolFlag{ + Name: "s3-iam-debug", + Usage: "s3 IAM debug output", + Destination: &s3IamDebug, + }, &cli.BoolFlag{ Name: "iam-cache-disable", Usage: "disable local iam cache", @@ -370,23 +381,25 @@ func runGateway(ctx context.Context, be backend.Backend) error { } iam, err := auth.New(&auth.Opts{ - Dir: iamDir, - LDAPServerURL: ldapURL, - LDAPBindDN: ldapBindDN, - LDAPPassword: ldapPassword, - LDAPQueryBase: ldapQueryBase, - LDAPObjClasses: ldapObjClasses, - LDAPAccessAtr: ldapAccessAtr, - LDAPSecretAtr: ldapSecAtr, - LDAPRoleAtr: ldapRoleAtr, - S3Access: s3IamAccess, - S3Secret: s3IamSecret, - S3Region: s3IamRegion, - S3Bucket: s3IamBucket, - S3Endpoint: s3IamEndpoint, - CacheDisable: iamCacheDisable, - CacheTTL: iamCacheTTL, - CachePrune: iamCachePrune, + Dir: iamDir, + LDAPServerURL: ldapURL, + LDAPBindDN: ldapBindDN, + LDAPPassword: ldapPassword, + LDAPQueryBase: ldapQueryBase, + LDAPObjClasses: ldapObjClasses, + LDAPAccessAtr: ldapAccessAtr, + LDAPSecretAtr: ldapSecAtr, + LDAPRoleAtr: ldapRoleAtr, + S3Access: s3IamAccess, + S3Secret: s3IamSecret, + S3Region: s3IamRegion, + S3Bucket: s3IamBucket, + S3Endpoint: s3IamEndpoint, + S3DisableSSlVerfiy: s3IamSslNoVerify, + S3Debug: s3IamDebug, + CacheDisable: iamCacheDisable, + CacheTTL: iamCacheTTL, + CachePrune: iamCachePrune, }) if err != nil { return fmt.Errorf("setup iam: %w", err)