> ## Documentation Index
> Fetch the complete documentation index at: https://v0.x-docs.novu.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Object Storage (S3, Blob, GCS)

> Using S3 as a storage backend for your application

For local development we use [LocalStack.cloud](https://localstack.cloud/). It is utilized as a local emulation of S3 during development.

This might be beneficial for testing and development purposes before transitioning to actual S3 service like in a production environment.

## AWS S3

### Configuration

AWS S3 requires the following environment variables to be set:

1. S3\_REGION:
2. S3\_LOCAL\_STACK (optional): S3 endpoint to connect to, leave blank if your connecting directly to AWS
3. S3\_BUCKET\_NAME: Name of the bucket to use
4. STORAGE\_SERVICE: Name of the storage service to use, 'AWS' or 'LOCALSTACK' for S3.

### Bucket Configuration

In general, access to the S3 bucket is very permissive.

The following process is used to manage access to the S3 bucket.

General design:

* public read access (for logo and email attachments—all data is open for read to anyone)
* write access is via pre-signed url (client-side PUT to S3 after server-side request)
* private write access (get, put, delete)
* objects are written with organisationIdTenancy/objectId (at least for brand logos)

Specifics for access:

* CORS access on bucket (this is documented)
* bucket object do not need versioning (as all uploads are deemed unique)
* ACL on buckets is 'public-read' (as part of the pre-signed URLs)
* image uploads (as pre-signed) could also have rules to limit (DENY policy) on extension types (although UI also does this but leaves open a vector)
* AWS (specific): ensure that Allow owenership controls is 'BucketOwnerPreferred' to allow ACL access (rather than role/user) (ie PutObjectAcl)

### Example Terraform

```terraform theme={null}
/**
 * This sets up an S3 bucket for notifications assets (eg photos, documents). The bucket is
 * PUBLIC for read access and needs to be opened up to the API (which is deployed as an ECS task).
 *
 * It also needs to be opened up to worker (to create pre-signed urls for uploads)
 *
 * This bucket is:
 *  * public read access
 *  * private write access to the credentials (added onto the user rather than onto the bucket)
 */

#
# WARNING: this bucket uses IAM Policies and ACL for access
#
#  - @see https://binaryguy.tech/aws/s3/iam-policies-vs-s3-policies-vs-s3-bucket-acls/#:~:text=The%20biggest%20advantage%20of%20using,well%20as%20objects%20in%20it.
#  - @see https://aws.amazon.com/blogs/security/iam-policies-and-bucket-policies-and-acls-oh-my-controlling-access-to-s3-resources/
#  - @see https://stackoverflow.com/questions/47815526/s3-bucket-policy-vs-access-control-list
#
#
# TF: https://www.terraform.io/docs/providers/aws/r/s3_bucket.html
# AWS: http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingBucket.html
# AWS CLI: http://docs.aws.amazon.com/cli/latest/reference/s3api/create-bucket.html
resource "aws_s3_bucket" "notifications" {
  bucket = "${var.environment}-notifications-data"
  # removed for upgrade to aws provider 4.0
  # see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/version-4-upgrade#acl-argument
  # acl    = "private"

  tags = {
    Environment = var.environment
    Terraform   = true
  }
}


# TF: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block
resource "aws_s3_bucket_public_access_block" "notifications" {
  bucket                  = aws_s3_bucket.notifications.id
  block_public_acls       = false
  # This feature protects your bucket from accidentally getting a policy that would enable public access
  # We WANT public access for read
  block_public_policy     = false
  ignore_public_acls      = false
  restrict_public_buckets = false
}


# TF: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_ownership_controls
# this is required when setting the 'public-read' acl—without it it fails on apply
resource "aws_s3_bucket_ownership_controls" "notifications" {
  bucket = aws_s3_bucket.notifications.id

  rule {
    object_ownership = "BucketOwnerPreferred"
  }
}

# TF: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_acl
resource "aws_s3_bucket_acl" "notifications" {
  bucket = aws_s3_bucket.notifications.id
  # Defaults to private
  # ["private" "public-read" "public-read-write" "authenticated-read" "aws-exec-read" "log-delivery-write"]
  acl    = "public-read"

  depends_on = [
    aws_s3_bucket_ownership_controls.notifications,
    aws_s3_bucket_public_access_block.notifications,
  ]
}

#
# Note: no point on having vesrioning enabled
#

#
# Restricted CORS policy but increased headers exposed (that could be further restrictred)
#
# TF: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_cors_configuration
# AWS: https://docs.amplify.aws/lib/storage/getting-started/q/platform/js/#amazon-s3-bucket-cors-policy-setup
resource "aws_s3_bucket_cors_configuration" "notifications" {
  bucket = aws_s3_bucket.notifications.id

  cors_rule {
    allowed_headers = ["*"]
    allowed_methods = [
      "GET",
      "HEAD",
      "PUT",
    ]
    allowed_origins = ["/* OWN FQDN */"]
    expose_headers  = [
      "x-amz-server-side-encryption",
      "x-amz-request-id",
      "x-amz-id-2",
      "x-amz-meta-tag",
      "ETag"
    ]
    max_age_seconds = 3000
  }
}

resource "aws_s3_bucket_policy" "notifications" {
  bucket = aws_s3_bucket.notifications.id
  policy = data.aws_iam_policy_document.notifications-user.json
}

# [Data] IAM policy to define S3 permissions
#
# Restricting users files types to avoid problems (note: hand helds love to upload PDFs)
#
# TF: https://www.terraform.io/docs/providers/aws/d/iam_policy_document.html
# AWS: http://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html
# AWS CLI: http://docs.aws.amazon.com/cli/latest/reference/iam/create-policy.html
data "aws_iam_policy_document" "notifications-user" {
  statement {
    sid    = "DenyNonImage"
    effect = "Deny"
    principals {
      identifiers = [aws_iam_user.notifications.arn]
      type        = "AWS"
    }
    actions = [
      # NOTE: this is an ACL permissions
      "s3:PutObjectAcl",
    ]
    not_resources = [
      "${aws_s3_bucket.notifications.arn}/*.png",
      "${aws_s3_bucket.notifications.arn}/*.jpg",
      "${aws_s3_bucket.notifications.arn}/*.jpeg",
      "${aws_s3_bucket.notifications.arn}/*.gif"
    ]
  }
  # NOTE: these rules exist on the user policy
/*
  statement {
    effect = "Allow"
    principals {
      type        = "AWS"
      identifiers = [aws_iam_user.notifications.arn]
    }
    actions = [
      "s3:GetObject",
      "s3:DeleteObject",
      "s3:ListBucket",
      "s3:PutObject",
      # this is the magic spice, because the perms are created via ACL (rather than roles)
      "s3:PutObjectAcl"
    ]
    resources = [
      "${aws_s3_bucket.notifications.arn}*/
/*",
    ]
  }
*/
}

#######################

# TF: https://www.terraform.io/docs/providers/aws/r/iam_policy.html
# AWS: http://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html
# AWS CLI: http://docs.aws.amazon.com/cli/latest/reference/iam/create-policy.html
resource "aws_iam_policy" "s3" {
  name        = "ecs-app-${var.environment}-s3"
  description = "Access from the ecs tasks to s3 buckets"
  policy      = data.aws_iam_policy_document.s3.json
}

# [Data] IAM policy to define S3 permissions
# TF: https://www.terraform.io/docs/providers/aws/d/iam_policy_document.html
# AWS: http://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html
# AWS CLI: http://docs.aws.amazon.com/cli/latest/reference/iam/create-policy.html
data "aws_iam_policy_document" "s3" {
  statement {
    sid     = ""
    effect  = "Allow"
    actions = [
      "s3:DeleteObject",
      "s3:GetObject",
      "s3:PutObject",
      # this is the magic spice, because the perms are created via ACL (rather than roles)
      "s3:PutObjectAcl",
      "s3:ListBucket"
    ]
    resources = [
      "arn:aws:s3:::${var.assets_bucket}/*"
    ]
  }
}

######################

# Attaches a managed IAM policy to an IAM role for the ecs tasks
# TF: https://www.terraform.io/docs/providers/aws/r/iam_role_policy_attachment.html
# AWS: http://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_managed-vs-inline.html
# AWS CLI: http://docs.aws.amazon.com/cli/latest/reference/iam/attach-role-policy.html
resource "aws_iam_role_policy_attachment" "s3" {
  role       = aws_iam_role.app_task.name  # need your role attached to whatever is doing the execution (eg workload task)
  policy_arn = aws_iam_policy.s3.arn
}
```

## Azure Blob Storage Configuration

### Configuration

Azure Blob Storage requires the following environment variables to be set:

### Required

1. STORAGE\_SERVICE: Name of the storage service to use, 'AZURE' Azure Blob.
2. AZURE\_CONTAINER\_NAME: The name of the container in your Azure Storage account that you wish to use for your blob storage.
3. AZURE\_ACCOUNT\_NAME: The name of your Azure Storage account. This is used to form the URL at which your blob storage is accessible.
4. AZURE\_ACCOUNT\_KEY: The access key for your Azure Storage account. This is used to authenticate requests made against your blob storage.
5. AZURE\_HOST\_NAME: The host name of your Azure Storage account . This is used to form the URL at which your blob storage is accessible. [Ref](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-nodejs?tabs=managed-identity%2Croles-azure-portal%2Csign-in-azure-cli#upload-blobs-to-a-container).

### Bucket Configuration

TBD

## Google Cloud Storage Configuration

### Configuration

Google Cloud Storage requires the following environment variables to be set:

1. `GCS_BUCKET_NAME`: Name of the bucket to use.
2. `GOOGLE_APPLICATION_CREDENTIALS`: Path to the service account key file.
3. `STORAGE_SERVICE`: Name of the storage service to use, 'GCS' for Google Cloud Storage.

### Bucket Configuration

TBD
