Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions documentation/BucketInfoModelVersion.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,16 @@ this._quotaMax = quotaMax || 0;
### Usage

Used to store bucket quota

## Model version 18

### Properties Added

```javascript
private _bucketLoggingStatus?: BucketLoggingStatus;
```

### Usage

Used for bucket logging
https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerLogs.html
40 changes: 36 additions & 4 deletions lib/models/BucketInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import { ACL as OACL } from './ObjectMD';
import { areTagsValid, BucketTag } from '../s3middleware/tagging';
import { VeeamCapability, VeeamSOSApiSchema, VeeamSOSApiSerializable } from './Veeam';
import { AzureInfoMetadata } from './BucketAzureInfo';
import BucketLoggingStatus from './BucketLoggingStatus';

// WHEN UPDATING THIS NUMBER, UPDATE BucketInfoModelVersion.md CHANGELOG
// BucketInfoModelVersion.md can be found in documentation/ at the root
// of this repository
const modelVersion = 17;
const modelVersion = 18;

export type CORS = {
id: string;
Expand Down Expand Up @@ -78,6 +79,7 @@ export type BucketMetadata = {
tags: Array<BucketTag>,
capabilities?: Capabilities,
quotaMax: bigint | number,
bucketLoggingStatus?: BucketLoggingStatus,
};

export type BucketMetadataJSON = Omit<BucketMetadata, 'quotaMax' | 'capabilities'> & {
Expand Down Expand Up @@ -115,6 +117,7 @@ export default class BucketInfo implements BucketMetadata {
private _ingestion?: { status: 'enabled' | 'disabled' };
private _capabilities?: Capabilities;
private _quotaMax: bigint;
private _bucketLoggingStatus?: BucketLoggingStatus;

/**
* Represents all bucket information.
Expand Down Expand Up @@ -201,6 +204,7 @@ export default class BucketInfo implements BucketMetadata {
tags?: Array<BucketTag> | [],
capabilities?: Capabilities,
quotaMax?: bigint | number,
bucketLoggingStatus?: BucketLoggingStatus,
) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof owner, 'string');
Expand Down Expand Up @@ -327,6 +331,10 @@ export default class BucketInfo implements BucketMetadata {
}
assert.strictEqual(areTagsValid(tags), true);

if (bucketLoggingStatus) {
assert(bucketLoggingStatus instanceof BucketLoggingStatus);
}

// IF UPDATING PROPERTIES, INCREMENT MODELVERSION NUMBER ABOVE
this._acl = aclInstance;
this._name = name;
Expand All @@ -353,6 +361,7 @@ export default class BucketInfo implements BucketMetadata {
this._objectLockConfiguration = objectLockConfiguration;
this._notificationConfiguration = notificationConfiguration;
this._tags = tags;
this._bucketLoggingStatus = bucketLoggingStatus;

this._capabilities = capabilities && {
...capabilities,
Expand Down Expand Up @@ -401,6 +410,7 @@ export default class BucketInfo implements BucketMetadata {
VeeamCapability.serialize(this._capabilities.VeeamSOSApi),
},
quotaMax: this._quotaMax.toString(),
bucketLoggingStatus: this._bucketLoggingStatus,
};
const final = this._websiteConfiguration
? {
Expand Down Expand Up @@ -433,6 +443,8 @@ export default class BucketInfo implements BucketMetadata {
};
const websiteConfig = obj.websiteConfiguration ?
new WebsiteConfiguration(obj.websiteConfiguration) : undefined;
const bucketLoggingStatus = obj.bucketLoggingStatus ?
new BucketLoggingStatus((obj.bucketLoggingStatus as any)._loggingEnabled) : undefined;
return new BucketInfo(obj.name, obj.owner, obj.ownerDisplayName,
obj.creationDate, obj.mdBucketModelVersion, obj.acl,
obj.transient, obj.deleted, obj.serverSideEncryption,
Expand All @@ -441,7 +453,7 @@ export default class BucketInfo implements BucketMetadata {
obj.bucketPolicy, obj.uid, obj.readLocationConstraint, obj.isNFS,
obj.ingestion, obj.azureInfo, obj.objectLockEnabled,
obj.objectLockConfiguration, obj.notificationConfiguration, obj.tags,
capabilities, BigInt(obj.quotaMax || 0n));
capabilities, BigInt(obj.quotaMax || 0n), bucketLoggingStatus);
}

/**
Expand Down Expand Up @@ -474,7 +486,7 @@ export default class BucketInfo implements BucketMetadata {
data._isNFS, data._ingestion, data._azureInfo,
data._objectLockEnabled, data._objectLockConfiguration,
data._notificationConfiguration, data._tags, capabilities,
BigInt(data._quotaMax || 0n));
BigInt(data._quotaMax || 0n), data._bucketLoggingStatus);
}

/**
Expand All @@ -484,6 +496,8 @@ export default class BucketInfo implements BucketMetadata {
* @return Return an BucketInfo
*/
static fromJson(data: BucketMetadataJSON) {
const bucketLoggingStatus = data.bucketLoggingStatus ?
new BucketLoggingStatus((data.bucketLoggingStatus as any)._loggingEnabled) : undefined;
return new BucketInfo(data.name, data.owner, data.ownerDisplayName,
data.creationDate, data.mdBucketModelVersion, data.acl,
data.transient, data.deleted, data.serverSideEncryption,
Expand All @@ -497,7 +511,7 @@ export default class BucketInfo implements BucketMetadata {
...data.capabilities,
VeeamSOSApi: data.capabilities?.VeeamSOSApi &&
VeeamCapability.parse(data.capabilities?.VeeamSOSApi),
}, BigInt(data.quotaMax || 0n));
}, BigInt(data.quotaMax || 0n), bucketLoggingStatus);
}

/**
Expand Down Expand Up @@ -1070,4 +1084,22 @@ export default class BucketInfo implements BucketMetadata {
this._quotaMax = BigInt(quota || 0n);
return this;
}

/**
* Get bucket logging status
* @returns - bucket logging status
*/
getBucketLoggingStatus() : BucketLoggingStatus | undefined {
return this._bucketLoggingStatus;
}

/**
* Set bucket logging status
* @param bucketLoggingStatus - bucket logging status
* @returns - this
*/
setBucketLoggingStatus(bucketLoggingStatus : BucketLoggingStatus) {
this._bucketLoggingStatus = bucketLoggingStatus;
return this;
}
}
193 changes: 193 additions & 0 deletions lib/models/BucketLoggingStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { parseString } from 'xml2js';
import errors, { ArsenalError, errorInstances } from '../errors';

/** BucketLoggingStatus constants, not documented by AWS but found via testing */
const TARGET_BUCKET_MIN_LENGTH = 3;
const TARGET_BUCKET_MAX_LENGTH = 255;
const TARGET_PREFIX_MAX_LENGTH = 800;

/**
* Format of xml request:
* https://docs.aws.amazon.com/AmazonS3/latest/API/API_LoggingEnabled.html
* https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketLogging.html
*
<?xml version="1.0" encoding="UTF-8"?>
<BucketLoggingStatus xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<LoggingEnabled>
<TargetBucket>string</TargetBucket>
<TargetGrants>
<Grant>
<Grantee>
<DisplayName>string</DisplayName>
<EmailAddress>string</EmailAddress>
<ID>string</ID>
<xsi:type>string</xsi:type>
<URI>string</URI>
</Grantee>
<Permission>string</Permission>
</Grant>
</TargetGrants>
<TargetObjectKeyFormat>
<PartitionedPrefix>
<PartitionDateSource>string</PartitionDateSource>
</PartitionedPrefix>
<SimplePrefix>
</SimplePrefix>
</TargetObjectKeyFormat>
<TargetPrefix>string</TargetPrefix>
</LoggingEnabled>
</BucketLoggingStatus>
*/

export type LoggingEnabled = {
TargetBucket: string;
TargetPrefix: string;
// TargetGrants and TargetObjectKeyFormat are not implemented.
};

export default class BucketLoggingStatus {
private _loggingEnabled?: LoggingEnabled;

constructor(loggingEnabled?: LoggingEnabled) {
this._loggingEnabled = loggingEnabled;
}

getLoggingEnabled(): LoggingEnabled | undefined {
return this._loggingEnabled;
}

toXML(): string {
let loggingEnabledXML = "";
if (this._loggingEnabled) {
loggingEnabledXML = `<LoggingEnabled>
<TargetBucket>${this._loggingEnabled.TargetBucket}</TargetBucket>
<TargetPrefix>${this._loggingEnabled.TargetPrefix}</TargetPrefix>
</LoggingEnabled>
`;
}

return `<?xml version="1.0" encoding="UTF-8"?>
<BucketLoggingStatus xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
${loggingEnabledXML}
</BucketLoggingStatus>
`;
}

static fromXML(
data: string,
): { error?: { arsenalError: ArsenalError, details: any }; res?: BucketLoggingStatus; } {
let parsed, parseError;

try {
parseString(data, (err: any, res: any) => {
parseError = err;
parsed = res;
});

if (parseError) {
return {
error: { arsenalError: errors.MalformedXML, details: parseError },
};
}
} catch (err) {
return {
error: { arsenalError: errors.MalformedXML, details: err },
};
}

if (!parsed) {
return {
error: { arsenalError: errors.MalformedXML, details: 'request xml is undefined or empty' },
};
}

if (!parsed.BucketLoggingStatus) {
return {
error: { arsenalError: errors.MalformedXML, details: 'missing BucketLoggingStatus root' },
};
}

let loggingEnabled: LoggingEnabled | undefined = undefined;
if (parsed.BucketLoggingStatus.LoggingEnabled) {
const loggingEnabledData = parsed.BucketLoggingStatus.LoggingEnabled[0];

if (
!Object.prototype.hasOwnProperty.call(loggingEnabledData, 'TargetBucket') ||
loggingEnabledData.TargetBucket === null ||
loggingEnabledData.TargetBucket === undefined
) {
return {
error: {
arsenalError: errors.MalformedXML,
details: 'missing TargetBucket field in LoggingEnabled',
},
};
} else if (loggingEnabledData.TargetBucket[0].length < TARGET_BUCKET_MIN_LENGTH) {
return {
error: {
arsenalError: errors.InvalidBucketName,
details: `TargetBucket field length < ${TARGET_BUCKET_MIN_LENGTH}`,
},
};
} else if (loggingEnabledData.TargetBucket[0].length > TARGET_BUCKET_MAX_LENGTH) {
return {
error: {
arsenalError: errors.InvalidBucketName,
details: `TargetBucket field length > ${TARGET_BUCKET_MAX_LENGTH}`,
},
};
}

if (
!Object.prototype.hasOwnProperty.call(loggingEnabledData, 'TargetPrefix') ||
loggingEnabledData.TargetPrefix === null ||
loggingEnabledData.TargetPrefix === undefined
) {
return {
error: {
arsenalError: errors.MalformedXML,
details: 'missing TargetPrefix field in LoggingEnabled',
},
};
} else if (loggingEnabledData.TargetPrefix[0].length > TARGET_PREFIX_MAX_LENGTH) {
return {
error: {
arsenalError: errorInstances.InvalidArgument
.customizeDescription(`Field exceeds ${TARGET_PREFIX_MAX_LENGTH} bytes`)
.addMetadataEntry('invalidArguments',
[{ ArgumentName: 'TargetPrefix', ArgumentValue: loggingEnabledData.TargetPrefix[0] }]),
details: `TargetPrefix field length > ${TARGET_PREFIX_MAX_LENGTH}`,
},
};
}

if (loggingEnabledData.TargetGrants) {
return {
error: {
arsenalError: errors.NotImplemented,
details: 'TargetGrants field in LoggingEnabled is not implemented',
},
};
}

if (loggingEnabledData.TargetObjectKeyFormat) {
return {
error: {
arsenalError: errors.NotImplemented,
details: 'TargetObjectKeyFormat field in LoggingEnabled is not implemented',
},
};
}

loggingEnabled = {
TargetBucket: loggingEnabledData.TargetBucket[0],
TargetPrefix: loggingEnabledData.TargetPrefix[0],
};
}

return {
error: undefined,
res: new BucketLoggingStatus(loggingEnabled),
};
}
};
1 change: 1 addition & 0 deletions lib/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export { default as ObjectMDArchive } from './ObjectMDArchive';
export { default as ObjectMDAzureInfo } from './ObjectMDAzureInfo';
export { default as ObjectMDLocation } from './ObjectMDLocation';
export { default as ReplicationConfiguration } from './ReplicationConfiguration';
export { default as BucketLoggingStatus } from './BucketLoggingStatus';
export * as WebsiteConfiguration from './WebsiteConfiguration';
4 changes: 4 additions & 0 deletions lib/policyEvaluator/utils/actionMaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const sharedActionMap = {
bucketGetVersioning: 's3:GetBucketVersioning',
bucketGetWebsite: 's3:GetBucketWebsite',
bucketGetTagging: 's3:GetBucketTagging',
bucketGetLogging: 's3:GetBucketLogging',
bucketHead: 's3:ListBucket',
bucketPutACL: 's3:PutBucketAcl',
bucketPutCors: 's3:PutBucketCORS',
Expand All @@ -30,6 +31,7 @@ const sharedActionMap = {
bucketPutVersioning: 's3:PutBucketVersioning',
bucketPutWebsite: 's3:PutBucketWebsite',
bucketPutTagging: 's3:PutBucketTagging',
bucketPutLogging: 's3:PutBucketLogging',
bypassGovernanceRetention: 's3:BypassGovernanceRetention',
listMultipartUploads: 's3:ListBucketMultipartUploads',
listParts: 's3:ListMultipartUploadParts',
Expand Down Expand Up @@ -121,6 +123,7 @@ const actionMonitoringMapS3 = {
bucketGetEncryption: 'GetBucketEncryption',
bucketGetWebsite: 'GetBucketWebsite',
bucketGetTagging: 'GetBucketTagging',
bucketGetLogging: 'GetBucketLogging',
bucketHead: 'HeadBucket',
bucketPut: 'CreateBucket',
bucketPutACL: 'PutBucketAcl',
Expand All @@ -134,6 +137,7 @@ const actionMonitoringMapS3 = {
bucketPutEncryption: 'PutBucketEncryption',
bucketPutWebsite: 'PutBucketWebsite',
bucketPutTagging: 'PutBucketTagging',
bucketPutLogging: 'PutBucketLogging',
completeMultipartUpload: 'CompleteMultipartUpload',
initiateMultipartUpload: 'CreateMultipartUpload',
listMultipartUploads: 'ListMultipartUploads',
Expand Down
2 changes: 2 additions & 0 deletions lib/s3routes/routes/routeGET.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export default function routerGET(
call('metadataSearch');
} else if (query.quota !== undefined) {
call('bucketGetQuota');
} else if (query.logging !== undefined) {
call('bucketGetLogging');
} else {
// GET bucket
call('bucketGet');
Expand Down
Loading
Loading