diff --git a/Sources/AWSLambdaEvents/Cloudwatch.swift b/Sources/AWSLambdaEvents/Cloudwatch.swift index 36e4010..0895b81 100644 --- a/Sources/AWSLambdaEvents/Cloudwatch.swift +++ b/Sources/AWSLambdaEvents/Cloudwatch.swift @@ -135,3 +135,463 @@ public enum CloudwatchDetails { let type: any CloudwatchDetail.Type } } + +// MARK: - S3 Event Notification + +/// https://docs.aws.amazon.com/AmazonS3/latest/userguide/ev-events.html + +public typealias CloudWatchS3ObjectCreatedNotificationEvent = CloudwatchEvent +public typealias CloudWatchS3ObjectDeletedNotificationEvent = CloudwatchEvent +public typealias CloudWatchS3ObjectRestoreInitiatedNotificationEvent = CloudwatchEvent +public typealias CloudWatchS3ObjectRestoreCompletedNotificationEvent = CloudwatchEvent +public typealias CloudWatchS3ObjectRestoreExpiredNotificationEvent = CloudwatchEvent +public typealias CloudWatchS3ObjectStorageClassChangedNotificationEvent = CloudwatchEvent +public typealias CloudWatchS3ObjectAccessTierChangedNotificationEvent = CloudwatchEvent +public typealias CloudWatchS3ObjectACLUpdatedNotificationEvent = CloudwatchEvent +public typealias CloudWatchS3ObjectTagsAddedNotificationEvent = CloudwatchEvent +public typealias CloudWatchS3ObjectTagsDeletedNotificationEvent = CloudwatchEvent + +extension CloudwatchDetails { + public enum S3: Sendable { + public struct ObjectCreatedNotification: CloudwatchDetail { + public static let name: String = "Object Created" + + public struct Bucket: Codable, Sendable { + public let name: String + } + + public struct Object: Codable, Sendable { + public let key: String + public let size: UInt64 + public let etag: String + public let versionId: String? + public let sequencer: String + + enum CodingKeys: String, CodingKey { + case key + case size + case etag + case versionId = "version-id" + case sequencer + } + } + + public enum Reason: String, Codable, Sendable { + case putObject = "PutObject" + case postObject = "POST Object" + case copyObject = "CopyObject" + case completeMultipartUpload = "CompleteMultipartUpload" + } + + public let version: String + public let bucket: Bucket + public let object: Object + public let requestId: String + public let requester: String + public let sourceIpAddress: String + public let reason: Reason + + enum CodingKeys: String, CodingKey { + case version + case bucket + case object + case requestId = "request-id" + case requester + case sourceIpAddress = "source-ip-address" + case reason + } + } + + public struct ObjectDeletedNotification: CloudwatchDetail { + public static let name: String = "Object Deleted" + + public struct Bucket: Codable, Sendable { + public let name: String + } + + public struct Object: Codable, Sendable { + public let key: String + public let etag: String + public let versionId: String? + public let sequencer: String + + enum CodingKeys: String, CodingKey { + case key + case etag + case versionId = "version-id" + case sequencer + } + } + + public enum Reason: String, Codable, Sendable { + case deleteObject = "DeleteObject" + case lifecycleExpiration = "Lifecycle Expiration" + } + + public enum DeletionType: String, Codable, Sendable { + case permanentlyDeleted = "Permanently Deleted" + case deleteMarkerCreated = "Delete Marker Created" + } + + public let version: String + public let bucket: Bucket + public let object: Object + public let requestId: String + public let requester: String + public let sourceIpAddress: String + public let reason: Reason + public let deletionType: DeletionType + + enum CodingKeys: String, CodingKey { + case version + case bucket + case object + case requestId = "request-id" + case requester + case sourceIpAddress = "source-ip-address" + case reason + case deletionType = "deletion-type" + } + } + + public struct ObjectRestoreInitiatedNotification: CloudwatchDetail { + public static let name: String = "Object Restore Initiated" + + public struct Bucket: Codable, Sendable { + public let name: String + } + + public struct Object: Codable, Sendable { + public let key: String + public let size: UInt64 + public let etag: String + public let versionId: String? + + enum CodingKeys: String, CodingKey { + case key + case size + case etag + case versionId = "version-id" + } + } + + public enum SourceStorageClass: String, Codable, Sendable { + case standard = "STANDARD" + case reducedRedundancy = "REDUCED_REDUNDANCY" + case standardIA = "STANDARD_IA" + case onezoneIA = "ONEZONE_IA" + case intelligentTiering = "INTELLIGENT_TIERING" + case glacier = "GLACIER" + case deepArchive = "DEEP_ARCHIVE" + case outposts = "OUTPOSTS" + case glacierIr = "GLACIER_IR" + } + + public let version: String + public let bucket: Bucket + public let object: Object + public let requestId: String + public let requester: String + public let sourceIpAddress: String + public let sourceStorageClass: SourceStorageClass + + enum CodingKeys: String, CodingKey { + case version + case bucket + case object + case requestId = "request-id" + case requester + case sourceIpAddress = "source-ip-address" + case sourceStorageClass = "source-storage-class" + } + } + + public struct ObjectRestoreCompletedNotification: CloudwatchDetail { + public static let name: String = "Object Restore Completed" + + public struct Bucket: Codable, Sendable { + public let name: String + } + + public struct Object: Codable, Sendable { + public let key: String + public let size: UInt64 + public let etag: String + public let versionId: String? + + enum CodingKeys: String, CodingKey { + case key + case size + case etag + case versionId = "version-id" + } + } + + public enum SourceStorageClass: String, Codable, Sendable { + case standard = "STANDARD" + case reducedRedundancy = "REDUCED_REDUNDANCY" + case standardIA = "STANDARD_IA" + case onezoneIA = "ONEZONE_IA" + case intelligentTiering = "INTELLIGENT_TIERING" + case glacier = "GLACIER" + case deepArchive = "DEEP_ARCHIVE" + case outposts = "OUTPOSTS" + case glacierIr = "GLACIER_IR" + } + + public let version: String + public let bucket: Bucket + public let object: Object + public let requestId: String + public let requester: String + @ISO8601Coding + public var restoreExpiryTime: Date + public let sourceStorageClass: SourceStorageClass + + enum CodingKeys: String, CodingKey { + case version + case bucket + case object + case requestId = "request-id" + case requester + case restoreExpiryTime = "restore-expiry-time" + case sourceStorageClass = "source-storage-class" + } + } + + public struct ObjectRestoreExpiredNotification: CloudwatchDetail { + public static let name: String = "Object Restore Expired" + + public struct Bucket: Codable, Sendable { + public let name: String + } + + public struct Object: Codable, Sendable { + public let key: String + public let etag: String + public let versionId: String? + + enum CodingKeys: String, CodingKey { + case key + case etag + case versionId = "version-id" + } + } + + public let version: String + public let bucket: Bucket + public let object: Object + public let requestId: String + public let requester: String + + enum CodingKeys: String, CodingKey { + case version + case bucket + case object + case requestId = "request-id" + case requester + } + } + + public struct ObjectStorageClassChangedNotification: CloudwatchDetail { + public static let name: String = "Object Storage Class Changed" + + public struct Bucket: Codable, Sendable { + public let name: String + } + + public struct Object: Codable, Sendable { + public let key: String + public let size: UInt64 + public let etag: String + public let versionId: String? + + enum CodingKeys: String, CodingKey { + case key + case size + case etag + case versionId = "version-id" + } + } + + public enum DestinationStorageClass: String, Codable, Sendable { + case standard = "STANDARD" + case reducedRedundancy = "REDUCED_REDUNDANCY" + case standardIA = "STANDARD_IA" + case onezoneIA = "ONEZONE_IA" + case intelligentTiering = "INTELLIGENT_TIERING" + case glacier = "GLACIER" + case deepArchive = "DEEP_ARCHIVE" + case outposts = "OUTPOSTS" + case glacierIr = "GLACIER_IR" + } + + public let version: String + public let bucket: Bucket + public let object: Object + public let requestId: String + public let requester: String + public let destinationStorageClass: DestinationStorageClass + + enum CodingKeys: String, CodingKey { + case version + case bucket + case object + case requestId = "request-id" + case requester + case destinationStorageClass = "destination-storage-class" + } + } + + public struct ObjectAccessTierChangedNotification: CloudwatchDetail { + public static let name: String = "Object Access Tier Changed" + + public struct Bucket: Codable, Sendable { + public let name: String + } + + public struct Object: Codable, Sendable { + public let key: String + public let size: UInt64 + public let etag: String + public let versionId: String? + + enum CodingKeys: String, CodingKey { + case key + case size + case etag + case versionId = "version-id" + } + } + + public enum DestinationAccessTier: String, Codable, Sendable { + case archiveAccess = "ARCHIVE_ACCESS" + case deepArchiveAccess = "DEEP_ARCHIVE_ACCESS" + } + + public let version: String + public let bucket: Bucket + public let object: Object + public let requestId: String + public let requester: String + public let destinationAccessTier: DestinationAccessTier + + enum CodingKeys: String, CodingKey { + case version + case bucket + case object + case requestId = "request-id" + case requester + case destinationAccessTier = "destination-access-tier" + } + } + + public struct ObjectACLUpdatedNotification: CloudwatchDetail { + public static let name: String = "Object ACL Updated" + + public struct Bucket: Codable, Sendable { + public let name: String + } + + public struct Object: Codable, Sendable { + public let key: String + public let etag: String + public let versionId: String? + + enum CodingKeys: String, CodingKey { + case key + case etag + case versionId = "version-id" + } + } + + public let version: String + public let bucket: Bucket + public let object: Object + public let requestId: String + public let requester: String + public let sourceIpAddress: String + + enum CodingKeys: String, CodingKey { + case version + case bucket + case object + case requestId = "request-id" + case requester + case sourceIpAddress = "source-ip-address" + } + } + + public struct ObjectTagsAddedNotification: CloudwatchDetail { + public static let name: String = "Object Tags Added" + + public struct Bucket: Codable, Sendable { + public let name: String + } + + public struct Object: Codable, Sendable { + public let key: String + public let etag: String + public let versionId: String? + + enum CodingKeys: String, CodingKey { + case key + case etag + case versionId = "version-id" + } + } + + public let version: String + public let bucket: Bucket + public let object: Object + public let requestId: String + public let requester: String + public let sourceIpAddress: String + + enum CodingKeys: String, CodingKey { + case version + case bucket + case object + case requestId = "request-id" + case requester + case sourceIpAddress = "source-ip-address" + } + } + + public struct ObjectTagsDeletedNotification: CloudwatchDetail { + public static let name: String = "Object Tags Deleted" + + public struct Bucket: Codable, Sendable { + public let name: String + } + + public struct Object: Codable, Sendable { + public let key: String + public let etag: String + public let versionId: String? + + enum CodingKeys: String, CodingKey { + case key + case etag + case versionId = "version-id" + } + } + + public let version: String + public let bucket: Bucket + public let object: Object + public let requestId: String + public let requester: String + public let sourceIpAddress: String + + enum CodingKeys: String, CodingKey { + case version + case bucket + case object + case requestId = "request-id" + case requester + case sourceIpAddress = "source-ip-address" + } + } + } +} diff --git a/Tests/AWSLambdaEventsTests/CloudwatchTests.swift b/Tests/AWSLambdaEventsTests/CloudwatchTests.swift index 01d63eb..5ebb455 100644 --- a/Tests/AWSLambdaEventsTests/CloudwatchTests.swift +++ b/Tests/AWSLambdaEventsTests/CloudwatchTests.swift @@ -101,6 +101,110 @@ class CloudwatchTests: XCTestCase { XCTAssertEqual(event.detail.instanceId, "0") XCTAssertEqual(event.detail.action, .terminate) } + + func testS3ObjectCreatedEventFromJSON() { + let eventBody = CloudwatchTests.eventBody( + type: CloudwatchDetails.S3.ObjectCreatedNotification.name, + details: "{ \"version\": \"0\", \"bucket\": { \"name\": \"amzn-s3-demo-bucket1\" }, \"object\": { \"key\": \"example-key\", \"size\":5, \"etag\": \"b1946ac92492d2347c6235b4d2611184\", \"version-id\": \"IYV3p45BT0ac8hjHg1houSdS1a.Mro8e\", \"sequencer\": \"617f08299329d189\" }, \"request-id\": \"N4N7GDK58NMKJ12R\", \"requester\": \"123456789012\", \"source-ip-address\": \"1.2.3.4\", \"reason\": \"PutObject\" }" + + ) + let data = eventBody.data(using: .utf8)! + var maybeEvent: CloudWatchS3ObjectCreatedNotificationEvent? + XCTAssertNoThrow( + maybeEvent = try JSONDecoder().decode(CloudWatchS3ObjectCreatedNotificationEvent.self, from: data) + ) + + guard let event = maybeEvent else { + return XCTFail("Expected to have an event") + } + + XCTAssertEqual(event.id, "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c") + XCTAssertEqual(event.source, "aws.events") + XCTAssertEqual(event.accountId, "123456789012") + XCTAssertEqual(event.time, Date(timeIntervalSince1970: 0)) + XCTAssertEqual(event.region, .us_east_1) + XCTAssertEqual(event.resources, ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"]) + XCTAssertEqual(event.detail.version, "0") + XCTAssertEqual(event.detail.bucket.name, "amzn-s3-demo-bucket1") + XCTAssertEqual(event.detail.object.key, "example-key") + XCTAssertEqual(event.detail.object.size, 5) + XCTAssertEqual(event.detail.object.etag, "b1946ac92492d2347c6235b4d2611184") + XCTAssertEqual(event.detail.object.versionId, "IYV3p45BT0ac8hjHg1houSdS1a.Mro8e") + XCTAssertEqual(event.detail.object.sequencer, "617f08299329d189") + XCTAssertEqual(event.detail.requestId, "N4N7GDK58NMKJ12R") + XCTAssertEqual(event.detail.requester, "123456789012") + XCTAssertEqual(event.detail.sourceIpAddress, "1.2.3.4") + XCTAssertEqual(event.detail.reason, .putObject) + } + + func testS3ObjectDeletedEventFromJSON() { + let eventBody = CloudwatchTests.eventBody( + type: CloudwatchDetails.S3.ObjectDeletedNotification.name, + details: "{ \"version\": \"0\", \"bucket\": { \"name\": \"amzn-s3-demo-bucket1\" }, \"object\": { \"key\": \"example-key\", \"etag\": \"d41d8cd98f00b204e9800998ecf8427e\", \"version-id\": \"1QW9g1Z99LUNbvaaYVpW9xDlOLU.qxgF\", \"sequencer\": \"617f0837b476e463\" }, \"request-id\": \"0BH729840619AG5K\", \"requester\": \"123456789012\", \"source-ip-address\": \"1.2.3.4\", \"reason\": \"DeleteObject\", \"deletion-type\": \"Delete Marker Created\" }" + + ) + let data = eventBody.data(using: .utf8)! + var maybeEvent: CloudWatchS3ObjectDeletedNotificationEvent? + XCTAssertNoThrow( + maybeEvent = try JSONDecoder().decode(CloudWatchS3ObjectDeletedNotificationEvent.self, from: data) + ) + + guard let event = maybeEvent else { + return XCTFail("Expected to have an event") + } + + XCTAssertEqual(event.id, "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c") + XCTAssertEqual(event.source, "aws.events") + XCTAssertEqual(event.accountId, "123456789012") + XCTAssertEqual(event.time, Date(timeIntervalSince1970: 0)) + XCTAssertEqual(event.region, .us_east_1) + XCTAssertEqual(event.resources, ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"]) + XCTAssertEqual(event.detail.version, "0") + XCTAssertEqual(event.detail.bucket.name, "amzn-s3-demo-bucket1") + XCTAssertEqual(event.detail.object.key, "example-key") + XCTAssertEqual(event.detail.object.etag, "d41d8cd98f00b204e9800998ecf8427e") + XCTAssertEqual(event.detail.object.versionId, "1QW9g1Z99LUNbvaaYVpW9xDlOLU.qxgF") + XCTAssertEqual(event.detail.object.sequencer, "617f0837b476e463") + XCTAssertEqual(event.detail.requestId, "0BH729840619AG5K") + XCTAssertEqual(event.detail.requester, "123456789012") + XCTAssertEqual(event.detail.sourceIpAddress, "1.2.3.4") + XCTAssertEqual(event.detail.reason, .deleteObject) + XCTAssertEqual(event.detail.deletionType, .deleteMarkerCreated) + } + + func testS3ObjectRestoreCompletedEventFromJSON() { + let eventBody = CloudwatchTests.eventBody( + type: CloudwatchDetails.S3.ObjectRestoreCompletedNotification.name, + details: "{ \"version\": \"0\", \"bucket\": { \"name\": \"amzn-s3-demo-bucket1\" }, \"object\": { \"key\": \"example-key\", \"size\": 5, \"etag\": \"b1946ac92492d2347c6235b4d2611184\", \"version-id\": \"KKsjUC1.6gIjqtvhfg5AdMI0eCePIiT3\" }, \"request-id\": \"189F19CB7FB1B6A4\", \"requester\": \"s3.amazonaws.com\", \"restore-expiry-time\": \"2021-11-13T00:00:00Z\", \"source-storage-class\": \"GLACIER\" }" + + ) + let data = eventBody.data(using: .utf8)! + var maybeEvent: CloudWatchS3ObjectRestoreCompletedNotificationEvent? + XCTAssertNoThrow( + maybeEvent = try JSONDecoder().decode(CloudWatchS3ObjectRestoreCompletedNotificationEvent.self, from: data) + ) + + guard let event = maybeEvent else { + return XCTFail("Expected to have an event") + } + + XCTAssertEqual(event.id, "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c") + XCTAssertEqual(event.source, "aws.events") + XCTAssertEqual(event.accountId, "123456789012") + XCTAssertEqual(event.time, Date(timeIntervalSince1970: 0)) + XCTAssertEqual(event.region, .us_east_1) + XCTAssertEqual(event.resources, ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"]) + XCTAssertEqual(event.detail.version, "0") + XCTAssertEqual(event.detail.bucket.name, "amzn-s3-demo-bucket1") + XCTAssertEqual(event.detail.object.key, "example-key") + XCTAssertEqual(event.detail.object.size, 5) + XCTAssertEqual(event.detail.object.etag, "b1946ac92492d2347c6235b4d2611184") + XCTAssertEqual(event.detail.object.versionId, "KKsjUC1.6gIjqtvhfg5AdMI0eCePIiT3") + XCTAssertEqual(event.detail.requestId, "189F19CB7FB1B6A4") + XCTAssertEqual(event.detail.requester, "s3.amazonaws.com") + XCTAssertEqual(event.detail.restoreExpiryTime.description, "2021-11-13 00:00:00 +0000") + XCTAssertEqual(event.detail.sourceStorageClass, .glacier) + } func testCustomEventFromJSON() { struct Custom: CloudwatchDetail {