mirror of
https://github.com/versity/versitygw.git
synced 2026-08-18 13:16:31 +00:00
Closes #1640 Add a standalone AWS IAM Query API implementation for managing IAM users through standard AWS SDKs and the AWS CLI. Server usage Start the IAM server with internal file-backed storage: mkdir -p /tmp/versitygw-iam ./versitygw --port 127.0.0.1:7070 --access user --secret pass iam --dir /tmp/versitygw-iam Start the IAM server with Vault KV v2 storage using AppRole: VGW_IAM_VAULT_ROLE_SECRET=<role-secret> ./versitygw --port 127.0.0.1:7070 --access user --secret pass iam --vault-endpoint-url http://127.0.0.1:8200 --vault-auth-method approle --vault-role-id <role-id> --vault-mount-path kv --vault-secret-storage-path iam Vault authentication also supports root tokens, separate authentication and secret-storage namespaces, custom mount paths, server certificate validation, and mutual TLS client certificates. Configure the AWS CLI credentials used by the IAM server: export AWS_ACCESS_KEY_ID=user export AWS_SECRET_ACCESS_KEY=pass export AWS_DEFAULT_REGION=us-east-1 Implemented IAM actions CreateUser creates an IAM user with an AWS-compatible ARN, generated AIDA user ID, creation timestamp, optional path, and tags. It validates usernames, paths, tag limits, reserved tag prefixes, duplicate tag keys, and existing users. aws --endpoint-url http://127.0.0.1:7070 iam create-user --user-name bob aws --endpoint-url http://127.0.0.1:7070 iam create-user --user-name bob --path /engineering/ --tags Key=team,Value=storage GetUser returns a stored user or the root identity when requested without a username through the IAM Query API. aws --endpoint-url http://127.0.0.1:7070 iam get-user --user-name bob ListUsers returns users in deterministic username order and supports path filtering, marker-based pagination, and MaxItems limits. aws --endpoint-url http://127.0.0.1:7070 iam list-users aws --endpoint-url http://127.0.0.1:7070 iam list-users --path-prefix /engineering/ --max-items 100 UpdateUser updates the username and/or path, recalculates the user ARN, and rejects conflicts with existing users. aws --endpoint-url http://127.0.0.1:7070 iam update-user --user-name bob --new-user-name robert --new-path /platform/ DeleteUser permanently removes an IAM user and returns AWS-compatible errors for missing users. aws --endpoint-url http://127.0.0.1:7070 iam delete-user --user-name robert IAM protocol and authentication - Support the AWS IAM Query protocol version 2010-05-08 over GET and POST form requests. - Return AWS-compatible XML responses, error documents, status codes, request IDs, user metadata, and pagination fields. - Authenticate root credentials with AWS Signature Version 4 for the IAM service in us-east-1. - Support both Authorization-header and query-string SigV4 authentication. - Validate credential scope, signed headers, timestamps, clock skew, content length, signatures, and unsupported signature or session-token modes. - Add IAM-specific validation and error mapping for malformed requests, invalid actions, duplicate entities, missing users, throttling, and internal failures. Storage implementations - Add an internal JSON-backed store using iam.json and iam.json.backup with atomic temporary-file replacement, concurrent access protection, stable ordering, pagination, and persistence across restarts. - Add a Vault KV v2 store with one secret per user, CAS-based duplicate protection, permanent deletion, AppRole reauthentication, namespace support, configurable authentication and KV mounts, root-token authentication, and TLS/mTLS configuration. - Introduce a common Storer interface and require exactly one storage backend to be configured. Server and embedding support - Register the new `versitygw iam` command with environment-variable and CLI configuration for both storage backends. - Add `embedgw.RunIAMAPI` and `IAMConfig` for embedding the IAM service in Go applications. Gateway-level internal packages - Add `internal/iamstore` as a reusable generic file-backed IAM persistence engine and migrate the existing gateway internal IAM service to it. - Add `internal/sigv4auth` for shared SigV4 header and presigned-query parsing, canonical request generation, signature verification, and structured authentication errors. - Refactor the S3 authentication paths to use the shared SigV4 implementation while preserving S3-specific error responses. - Add `internal/httpctx` for shared Fiber context keys and AWS-style request ID handling. - Add `internal/routekit` for shared query, form, and header route matchers. - Add `internal/netutil` for reusable certificate storage, hostname-aware listeners, multi-address serving, TLS listeners, and UNIX socket handling. - Update the custom SigV4 signer to honor an explicitly supplied signed-header list so unrelated headers do not alter IAM signatures. Testing and CI - Add AWS IAM SDK-based integration coverage for all supported user actions, header authentication, query authentication, validation, errors, filtering, and pagination. - Split standalone IAM tests into `versitygw test iam` and retain existing gateway IAM tests under `versitygw test gw-iam`. - Add unit coverage for controllers, authentication, routing, storage, embedding, listeners, request matching, persistence, and signing behavior. - Add `runiamtests.sh` to exercise internal storage over HTTP and HTTPS plus Vault storage through AppRole. - Add a dedicated IAM functional-test workflow with a Vault service and merged runtime coverage reporting. - Include the IAM test runner in shellcheck and add the AWS IAM SDK dependency.
264 lines
6.4 KiB
Go
264 lines
6.4 KiB
Go
// 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 integration
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
|
|
"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/transfermanager"
|
|
"github.com/aws/aws-sdk-go-v2/service/iam"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
"github.com/aws/smithy-go/middleware"
|
|
)
|
|
|
|
type S3Conf struct {
|
|
awsID string
|
|
awsSecret string
|
|
awsRegion string
|
|
endpoint string
|
|
websiteScheme string
|
|
websiteDomain string
|
|
websitePort string
|
|
hostStyle bool
|
|
checksumDisable bool
|
|
PartSize int64
|
|
Concurrency int
|
|
debug bool
|
|
versioningEnabled bool
|
|
azureTests bool
|
|
windowsTests bool
|
|
sidecarTests bool
|
|
tlsStatus bool
|
|
httpClient *http.Client
|
|
}
|
|
|
|
func NewS3Conf(opts ...Option) *S3Conf {
|
|
s := &S3Conf{}
|
|
|
|
for _, opt := range opts {
|
|
opt(s)
|
|
}
|
|
|
|
customTransport := &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
InsecureSkipVerify: s.tlsStatus,
|
|
},
|
|
}
|
|
|
|
customHTTPClient := &http.Client{
|
|
Transport: customTransport,
|
|
Timeout: shortTimeout,
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
},
|
|
}
|
|
|
|
s.httpClient = customHTTPClient
|
|
|
|
return s
|
|
}
|
|
|
|
type Option func(*S3Conf)
|
|
|
|
func WithAccess(ak string) Option {
|
|
return func(s *S3Conf) { s.awsID = ak }
|
|
}
|
|
func WithSecret(sk string) Option {
|
|
return func(s *S3Conf) { s.awsSecret = sk }
|
|
}
|
|
func WithRegion(r string) Option {
|
|
return func(s *S3Conf) { s.awsRegion = r }
|
|
}
|
|
func WithEndpoint(e string) Option {
|
|
return func(s *S3Conf) { s.endpoint = e }
|
|
}
|
|
func WithWebsiteScheme(scheme string) Option {
|
|
return func(s *S3Conf) { s.websiteScheme = scheme }
|
|
}
|
|
func WithWebsiteDomain(d string) Option {
|
|
return func(s *S3Conf) { s.websiteDomain = d }
|
|
}
|
|
func WithWebsitePort(p string) Option {
|
|
return func(s *S3Conf) { s.websitePort = p }
|
|
}
|
|
func WithDisableChecksum() Option {
|
|
return func(s *S3Conf) { s.checksumDisable = true }
|
|
}
|
|
func WithHostStyle() Option {
|
|
return func(s *S3Conf) { s.hostStyle = true }
|
|
}
|
|
func WithPartSize(p int64) Option {
|
|
return func(s *S3Conf) { s.PartSize = p }
|
|
}
|
|
func WithConcurrency(c int) Option {
|
|
return func(s *S3Conf) { s.Concurrency = c }
|
|
}
|
|
func WithDebug() Option {
|
|
return func(s *S3Conf) { s.debug = true }
|
|
}
|
|
func WithVersioningEnabled() Option {
|
|
return func(s *S3Conf) { s.versioningEnabled = true }
|
|
}
|
|
func WithAzureMode() Option {
|
|
return func(s *S3Conf) { s.azureTests = true }
|
|
}
|
|
func WithWindowsMode() Option {
|
|
return func(s *S3Conf) { s.windowsTests = true }
|
|
}
|
|
func WithSidecarMode() Option {
|
|
return func(s *S3Conf) { s.sidecarTests = true }
|
|
}
|
|
func WithTLSStatus(ts bool) Option {
|
|
return func(s *S3Conf) { s.tlsStatus = ts }
|
|
}
|
|
|
|
func (c *S3Conf) getCreds() credentials.StaticCredentialsProvider {
|
|
// TODO support token/IAM
|
|
if c.awsSecret == "" {
|
|
c.awsSecret = os.Getenv("AWS_SECRET_ACCESS_KEY")
|
|
}
|
|
if c.awsSecret == "" {
|
|
log.Fatal("no AWS_SECRET_ACCESS_KEY found")
|
|
}
|
|
|
|
return credentials.NewStaticCredentialsProvider(c.awsID, c.awsSecret, "")
|
|
}
|
|
|
|
func (c *S3Conf) GetClient() *s3.Client {
|
|
return s3.NewFromConfig(c.Config(), func(o *s3.Options) {
|
|
if c.hostStyle {
|
|
o.BaseEndpoint = &c.endpoint
|
|
o.UsePathStyle = false
|
|
}
|
|
})
|
|
}
|
|
|
|
func (c *S3Conf) GetIAMClient() *iam.Client {
|
|
return iam.NewFromConfig(c.Config())
|
|
}
|
|
|
|
func (c *S3Conf) GetPresignClient() *s3.PresignClient {
|
|
return s3.NewPresignClient(c.GetClient())
|
|
}
|
|
|
|
func (c *S3Conf) GetAnonymousClient() *s3.Client {
|
|
cfg := c.Config()
|
|
cfg.Credentials = aws.AnonymousCredentials{}
|
|
return s3.NewFromConfig(cfg, func(o *s3.Options) {
|
|
if c.hostStyle {
|
|
o.BaseEndpoint = &c.endpoint
|
|
o.UsePathStyle = false
|
|
}
|
|
})
|
|
}
|
|
|
|
func (cfg *S3Conf) getUserClient(usr user) *s3.Client {
|
|
config := *cfg
|
|
config.awsID = usr.access
|
|
config.awsSecret = usr.secret
|
|
|
|
return config.GetClient()
|
|
}
|
|
|
|
func (c *S3Conf) Config() aws.Config {
|
|
creds := c.getCreds()
|
|
|
|
opts := []func(*config.LoadOptions) error{
|
|
config.WithRegion(c.awsRegion),
|
|
config.WithCredentialsProvider(creds),
|
|
config.WithHTTPClient(c.httpClient),
|
|
config.WithRetryMaxAttempts(1),
|
|
}
|
|
|
|
opts = append(opts, config.WithHTTPClient(c.httpClient))
|
|
|
|
if c.checksumDisable {
|
|
opts = append(opts,
|
|
config.WithAPIOptions([]func(*middleware.Stack) error{v4.SwapComputePayloadSHA256ForUnsignedPayloadMiddleware}))
|
|
}
|
|
|
|
if c.debug {
|
|
opts = append(opts,
|
|
config.WithClientLogMode(aws.LogSigning|aws.LogRetries|aws.LogRequest|aws.LogResponse|aws.LogRequestEventMessage|aws.LogResponseEventMessage))
|
|
}
|
|
|
|
cfg, err := config.LoadDefaultConfig(
|
|
context.TODO(), opts...)
|
|
if err != nil {
|
|
log.Fatalln("error:", err)
|
|
}
|
|
|
|
if c.endpoint != "" && c.endpoint != "aws" {
|
|
cfg.BaseEndpoint = &c.endpoint
|
|
}
|
|
|
|
return cfg
|
|
}
|
|
|
|
func (c *S3Conf) UploadData(r io.Reader, bucket, object string) error {
|
|
uploader := transfermanager.New(c.GetClient(),
|
|
func(options *transfermanager.Options) {
|
|
options.PartSizeBytes = c.PartSize
|
|
options.Concurrency = c.Concurrency
|
|
})
|
|
|
|
upinfo := &transfermanager.UploadObjectInput{
|
|
Body: r,
|
|
Bucket: &bucket,
|
|
Key: &object,
|
|
}
|
|
|
|
_, err := uploader.UploadObject(context.Background(), upinfo)
|
|
return err
|
|
}
|
|
|
|
func (c *S3Conf) DownloadData(w io.WriterAt, bucket, object string) (int64, error) {
|
|
downloader := transfermanager.New(c.GetClient(),
|
|
func(options *transfermanager.Options) {
|
|
options.PartSizeBytes = c.PartSize
|
|
options.Concurrency = c.Concurrency
|
|
})
|
|
|
|
downinfo := &transfermanager.DownloadObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &object,
|
|
WriterAt: w,
|
|
}
|
|
|
|
out, err := downloader.DownloadObject(context.Background(), downinfo)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return *out.ContentLength, nil
|
|
}
|
|
|
|
func (c *S3Conf) getAdminCommand(args ...string) []string {
|
|
if c.tlsStatus {
|
|
return append([]string{"admin", "--allow-insecure"}, args...)
|
|
}
|
|
|
|
return append([]string{"admin"}, args...)
|
|
}
|