mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
tenant.go → org_isolation.go tenant_db.go → org_db.go tenant_storage.go → org_storage.go Content was already renamed in previous commit. This adds the new filenames after the old ones were removed.
141 lines
4.4 KiB
Go
141 lines
4.4 KiB
Go
package platform
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
)
|
|
|
|
// OrgStorage manages per-org and per-user S3 bucket isolation.
|
|
//
|
|
// Bucket layout on Hanzo S3 (s3.hanzo.space):
|
|
//
|
|
// orgs/{orgSlug}/ ← org bucket prefix
|
|
// orgs/{orgSlug}/org/ ← org-level shared data
|
|
// orgs/{orgSlug}/users/{userId}/ ← per-user isolated storage
|
|
//
|
|
// Each org gets its own SSE-KMS key derived from the master key.
|
|
// Each user gets their own SSE-C key for client-side encryption.
|
|
//
|
|
// IAM policy ensures:
|
|
// - Org admins can access orgs/{orgSlug}/*
|
|
// - Users can only access orgs/{orgSlug}/users/{userId}/*
|
|
// - No cross-org or cross-user access possible
|
|
type OrgStorage struct {
|
|
// Endpoint is the S3 endpoint (e.g., "s3.hanzo.space:9000" or "s3.hanzo.ai").
|
|
Endpoint string
|
|
|
|
// Bucket is the root bucket name (e.g., "orgs").
|
|
Bucket string
|
|
|
|
// MasterKey for deriving per-org and per-user SSE keys.
|
|
MasterKey string
|
|
|
|
// UseSSL enables TLS for the S3 connection.
|
|
UseSSL bool
|
|
|
|
// Region for the S3 bucket (default: "us-east-1").
|
|
Region string
|
|
}
|
|
|
|
// OrgPrefix returns the S3 key prefix for an org.
|
|
func (s *OrgStorage) OrgPrefix(orgSlug string) string {
|
|
return fmt.Sprintf("orgs/%s/", orgSlug)
|
|
}
|
|
|
|
// OrgDataPrefix returns the S3 key prefix for org-level shared data.
|
|
func (s *OrgStorage) OrgDataPrefix(orgSlug string) string {
|
|
return fmt.Sprintf("orgs/%s/org/", orgSlug)
|
|
}
|
|
|
|
// UserPrefix returns the S3 key prefix for a specific user.
|
|
func (s *OrgStorage) UserPrefix(orgSlug, userId string) string {
|
|
return fmt.Sprintf("orgs/%s/users/%s/", orgSlug, userId)
|
|
}
|
|
|
|
// OrgSSEKey derives a per-org SSE-C encryption key (32 bytes, base64-encoded).
|
|
// Used as the SSE-C CustomerKey header for server-side encryption.
|
|
func (s *OrgStorage) OrgSSEKey(orgSlug string) string {
|
|
if s.MasterKey == "" {
|
|
return ""
|
|
}
|
|
mac := hmac.New(sha256.New, []byte(s.MasterKey))
|
|
mac.Write([]byte("s3:org:" + orgSlug))
|
|
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
|
}
|
|
|
|
// UserSSEKey derives a per-user SSE-C encryption key (32 bytes, base64-encoded).
|
|
// Each user's objects are encrypted with a unique key — even if the bucket is shared,
|
|
// objects cannot be decrypted without the user-specific key.
|
|
func (s *OrgStorage) UserSSEKey(orgSlug, userId string) string {
|
|
if s.MasterKey == "" {
|
|
return ""
|
|
}
|
|
mac := hmac.New(sha256.New, []byte(s.MasterKey))
|
|
mac.Write([]byte("s3:user:" + orgSlug + ":" + userId))
|
|
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
|
}
|
|
|
|
// BucketPolicy returns a MinIO/S3 bucket policy JSON that enforces per-org
|
|
// path isolation. Uses encoding/json to prevent injection via orgSlug/iamUser.
|
|
func (s *OrgStorage) BucketPolicy(orgSlug, iamUser string) string {
|
|
if err := validateSlug(orgSlug); err != nil {
|
|
return "{}"
|
|
}
|
|
policy := map[string]any{
|
|
"Version": "2012-10-17",
|
|
"Statement": []map[string]any{
|
|
{
|
|
"Effect": "Allow",
|
|
"Principal": map[string]any{"AWS": []string{iamUser}},
|
|
"Action": []string{"s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket"},
|
|
"Resource": []string{fmt.Sprintf("arn:aws:s3:::%s/orgs/%s/*", s.Bucket, orgSlug)},
|
|
"Condition": map[string]any{
|
|
"StringLike": map[string]any{
|
|
"s3:prefix": []string{fmt.Sprintf("orgs/%s/*", orgSlug)},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
b, _ := json.MarshalIndent(policy, "", " ")
|
|
return string(b)
|
|
}
|
|
|
|
// UserBucketPolicy returns a policy restricting a user to their own prefix only.
|
|
func (s *OrgStorage) UserBucketPolicy(orgSlug, userId, iamUser string) string {
|
|
if err := validateSlug(orgSlug); err != nil {
|
|
return "{}"
|
|
}
|
|
if err := validateSlug(userId); err != nil {
|
|
return "{}"
|
|
}
|
|
prefix := fmt.Sprintf("orgs/%s/users/%s/*", orgSlug, userId)
|
|
policy := map[string]any{
|
|
"Version": "2012-10-17",
|
|
"Statement": []map[string]any{
|
|
{
|
|
"Effect": "Allow",
|
|
"Principal": map[string]any{"AWS": []string{iamUser}},
|
|
"Action": []string{"s3:GetObject", "s3:PutObject", "s3:DeleteObject"},
|
|
"Resource": []string{fmt.Sprintf("arn:aws:s3:::%s/%s", s.Bucket, prefix)},
|
|
},
|
|
{
|
|
"Effect": "Allow",
|
|
"Principal": map[string]any{"AWS": []string{iamUser}},
|
|
"Action": []string{"s3:ListBucket"},
|
|
"Resource": []string{fmt.Sprintf("arn:aws:s3:::%s", s.Bucket)},
|
|
"Condition": map[string]any{
|
|
"StringLike": map[string]any{
|
|
"s3:prefix": []string{prefix},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
b, _ := json.MarshalIndent(policy, "", " ")
|
|
return string(b)
|
|
}
|