entc/integration: codegen and test

This commit is contained in:
Giau. Tran Minh
2026-05-18 17:13:35 +00:00
parent 2d33420c0c
commit 8cd2285eb3
42 changed files with 8932 additions and 101 deletions

View File

@@ -0,0 +1,85 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
// Package blob provides a gocloud.dev/blob adapter for ent's generated Blob
// interface. It is used by the integration tests and serves as a reference
// implementation for wiring blob storage into ent-generated code.
package blob
import (
"context"
"io"
"io/fs"
goblob "gocloud.dev/blob"
"gocloud.dev/gcerrors"
)
// GoBucket wraps a gocloud.dev/blob.Bucket and implements the generated Blob interface.
type GoBucket struct {
b *goblob.Bucket
readerOpts *goblob.ReaderOptions
writerOpts *goblob.WriterOptions
}
// OpenBucket opens a gocloud.dev/blob bucket by URL and returns it as a [*GoBucket].
func OpenBucket(ctx context.Context, url string) (*GoBucket, error) {
b, err := goblob.OpenBucket(ctx, url)
if err != nil {
return nil, err
}
return &GoBucket{b: b}, nil
}
// Prefixed returns a new GoBucket scoped to the given key prefix.
// The original bucket is consumed and must not be used after this call.
func (b *GoBucket) Prefixed(prefix string) *GoBucket {
return &GoBucket{
b: goblob.PrefixedBucket(b.b, prefix),
readerOpts: b.readerOpts,
writerOpts: b.writerOpts,
}
}
// WithReaderOptions returns a new GoBucket that uses the given reader options.
func (b *GoBucket) WithReaderOptions(opts *goblob.ReaderOptions) *GoBucket {
return &GoBucket{b: b.b, writerOpts: b.writerOpts, readerOpts: opts}
}
// WithWriterOptions returns a new GoBucket that uses the given writer options.
func (b *GoBucket) WithWriterOptions(opts *goblob.WriterOptions) *GoBucket {
return &GoBucket{b: b.b, writerOpts: opts, readerOpts: b.readerOpts}
}
// NewReader opens a reader for the blob stored at key.
// It returns [fs.ErrNotExist] if the key does not exist.
func (b *GoBucket) NewReader(ctx context.Context, key string) (io.ReadCloser, error) {
switch r, err := b.b.NewReader(ctx, key, b.readerOpts); {
case gcerrors.Code(err) == gcerrors.NotFound:
return nil, fs.ErrNotExist
case err != nil:
return nil, err
default:
return r, nil
}
}
// NewWriter opens a writer for the blob stored at key.
func (b *GoBucket) NewWriter(ctx context.Context, key string) (io.WriteCloser, error) {
return b.b.NewWriter(ctx, key, b.writerOpts)
}
// Delete removes the blob at key. Returns nil if the key does not exist.
func (b *GoBucket) Delete(ctx context.Context, key string) error {
err := b.b.Delete(ctx, key)
if gcerrors.Code(err) == gcerrors.NotFound {
return nil
}
return err
}
// Close releases the underlying gocloud bucket resources.
func (b *GoBucket) Close() error {
return b.b.Close()
}

View File

@@ -0,0 +1,145 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package blob
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"errors"
"fmt"
"io"
)
// tenantKey is the context key for the tenant name.
type tenantKey struct{}
// WithTenant returns a context carrying the given tenant name.
func WithTenant(ctx context.Context, tenant string) context.Context {
return context.WithValue(ctx, tenantKey{}, tenant)
}
// TenantFrom extracts the tenant name from the context.
// Returns "" if no tenant is set.
func TenantFrom(ctx context.Context) string {
v, _ := ctx.Value(tenantKey{}).(string)
return v
}
// Encrypted wraps a GoBucket and transparently encrypts/decrypts blob data
// using AES-CTR with a per-tenant derived key.
//
// The encryption key is derived from the tenant name (from context) combined
// with a master seed using SHA-256. This ensures that data written by one
// tenant cannot be decrypted by another tenant.
//
// A random IV is prepended to the ciphertext on write and read back on open.
//
// NOTE: This is a test-only implementation. AES-CTR provides confidentiality
// but no integrity/authentication — ciphertext can be tampered with undetected.
// Production systems should use an AEAD mode such as AES-GCM or add a MAC
// over the IV and ciphertext to detect tampering on read.
//
// Usage:
//
// bucket, _ := blob.OpenBucket(ctx, url)
// enc := blob.NewEncrypted(bucket, masterSeed)
// // Use enc as the Blob implementation in BlobOpeners.
// // Ensure the context carries the tenant: blob.WithTenant(ctx, "acme")
type Encrypted struct {
inner *GoBucket
seed []byte
}
// NewEncrypted creates an encrypting wrapper around the given bucket.
// The seed is combined with the tenant name (from context) at each operation
// to derive a per-tenant AES-256 key via SHA-256.
func NewEncrypted(bucket *GoBucket, seed []byte) *Encrypted {
return &Encrypted{inner: bucket, seed: seed}
}
// deriveKey produces a 32-byte AES-256 key from the tenant + seed.
func (e *Encrypted) deriveKey(tenant string) (cipher.Block, error) {
if tenant == "" {
return nil, errors.New("blob: encryption requires a tenant in context (use blob.WithTenant)")
}
h := sha256.New()
h.Write([]byte(tenant))
h.Write(e.seed)
b, err := aes.NewCipher(h.Sum(nil)) // 32 bytes → AES-256
if err != nil {
return nil, fmt.Errorf("blob: deriving encryption key: %w", err)
}
return b, nil
}
// NewReader opens and decrypts the blob at key. The first [aes.BlockSize] bytes
// are treated as the IV; the rest is decrypted with AES-CTR using the
// tenant-derived key.
// Returns [fs.ErrNotExist] if the key does not exist.
func (e *Encrypted) NewReader(ctx context.Context, key string) (io.ReadCloser, error) {
block, err := e.deriveKey(TenantFrom(ctx))
if err != nil {
return nil, err
}
rc, err := e.inner.NewReader(ctx, key)
if err != nil {
return nil, err
}
iv := make([]byte, aes.BlockSize)
switch _, err := io.ReadFull(rc, iv); {
case err == io.EOF, err == io.ErrUnexpectedEOF:
return nil, errors.Join(fmt.Errorf("blob: ciphertext too short for key %q", key), rc.Close())
case err != nil:
return nil, errors.Join(err, rc.Close())
}
return &decryptionReader{
Reader: cipher.StreamReader{S: cipher.NewCTR(block, iv), R: rc},
closer: rc,
}, nil
}
// NewWriter opens an encrypting writer for the blob at key. A random IV is
// written first, followed by AES-CTR encrypted data using the tenant-derived key.
func (e *Encrypted) NewWriter(ctx context.Context, key string) (io.WriteCloser, error) {
block, err := e.deriveKey(TenantFrom(ctx))
if err != nil {
return nil, err
}
wc, err := e.inner.NewWriter(ctx, key)
if err != nil {
return nil, err
}
iv := make([]byte, aes.BlockSize)
if _, err := rand.Read(iv); err != nil {
return nil, errors.Join(fmt.Errorf("blob: generating IV: %w", err), wc.Close())
}
if _, err := wc.Write(iv); err != nil {
return nil, errors.Join(fmt.Errorf("blob: writing IV: %w", err), wc.Close())
}
return cipher.StreamWriter{S: cipher.NewCTR(block, iv), W: wc}, nil
}
// Close releases the underlying bucket resources.
func (e *Encrypted) Close() error {
return e.inner.Close()
}
// Delete removes the blob at key. Encryption is irrelevant for deletion.
func (e *Encrypted) Delete(ctx context.Context, key string) error {
return e.inner.Delete(ctx, key)
}
// decryptionReader decrypts on Read and closes the underlying storage decryptionReader.
type decryptionReader struct {
io.Reader
closer io.Closer
}
func (r *decryptionReader) Close() error {
return r.closer.Close()
}