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
26 changes: 26 additions & 0 deletions handwritten/storage/src/bucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,10 @@ export interface RestoreOptions {
generation: string;
projection?: 'full' | 'noAcl';
}
export interface EncryptionEnforcementConfig {
restrictionMode?: 'NotRestricted' | 'FullyRestricted';
readonly effectiveTime?: string;
}
export interface BucketMetadata extends BaseMetadata {
acl?: AclMetadata[] | null;
autoclass?: {
Expand All @@ -316,6 +320,9 @@ export interface BucketMetadata extends BaseMetadata {
defaultObjectAcl?: AclMetadata[];
encryption?: {
defaultKmsKeyName?: string;
googleManagedEncryptionEnforcementConfig?: EncryptionEnforcementConfig;
customerManagedEncryptionEnforcementConfig?: EncryptionEnforcementConfig;
customerSuppliedEncryptionEnforcementConfig?: EncryptionEnforcementConfig;
} | null;
hierarchicalNamespace?: {
enabled?: boolean;
Expand Down Expand Up @@ -1193,6 +1200,25 @@ class Bucket extends ServiceObject<Bucket, BucketMetadata> {
* }, function(err, apiResponse) {});
*
* //-
* // Enforce CMEK-only encryption for new objects.
* // This blocks Google-Managed and Customer-Supplied keys.
* //-
* bucket.setMetadata({
* encryption: {
* defaultKmsKeyName: 'projects/grape-spaceship-123/...',
* googleManagedEncryptionEnforcementConfig: {
* restrictionMode: 'FullyRestricted'
* },
* customerSuppliedEncryptionEnforcementConfig: {
* restrictionMode: 'FullyRestricted'
* },
* customerManagedEncryptionEnforcementConfig: {
* restrictionMode: 'NotRestricted'
* }
* }
* }, function(err, apiResponse) {});
*
* //-
* // Set the default event-based hold value for new objects in this
* // bucket.
* //-
Expand Down
83 changes: 83 additions & 0 deletions handwritten/storage/system-test/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2992,6 +2992,89 @@ describe('storage', function () {
`${metadata!.encryption!.defaultKmsKeyName}/cryptoKeyVersions/1`,
);
});

describe('encryption enforcement', () => {
it('should enforce FullyRestricted CSEK policy', async () => {
await bucket.setMetadata({
encryption: {
defaultKmsKeyName: kmsKeyName,
customerSuppliedEncryptionEnforcementConfig: {
restrictionMode: 'FullyRestricted',
},
},
});

await new Promise(res =>
setTimeout(res, BUCKET_METADATA_UPDATE_WAIT_TIME)
);

const encryptionKey = crypto.randomBytes(32);
const file = bucket.file('csek-attempt', {encryptionKey});

await assert.rejects(
file.save(FILE_CONTENTS, {resumable: false}),
(err: ApiError) => {
const failureMessage =
"Requested encryption type for object is not compliant with the bucket's encryption enforcement configuration.";
assert.strictEqual(err.code, 412);
assert.ok(err.message.includes(failureMessage));
return true;
}
);
});

it('should allow uploads that comply with enforcement', async () => {
await bucket.setMetadata({
encryption: {
googleManagedEncryptionEnforcementConfig: {
restrictionMode: 'NotRestricted',
},
},
});

const file = bucket.file('compliant-file');
await file.save(FILE_CONTENTS);

const [metadata] = await file.getMetadata();
assert.ok(metadata.customerEncryption);
});

it('should retain defaultKmsKeyName when updating enforcement settings independently', async () => {
await bucket.setMetadata({
encryption: {
defaultKmsKeyName: kmsKeyName,
},
});

await new Promise(res =>
setTimeout(res, BUCKET_METADATA_UPDATE_WAIT_TIME)
);

await bucket.setMetadata({
encryption: {
googleManagedEncryptionEnforcementConfig: {
restrictionMode: 'FullyRestricted',
},
},
});

await new Promise(res =>
setTimeout(res, BUCKET_METADATA_UPDATE_WAIT_TIME)
);

const [metadata] = await bucket.getMetadata();
assert.strictEqual(
metadata.encryption?.defaultKmsKeyName,
kmsKeyName
);

assert.strictEqual(
metadata.encryption?.googleManagedEncryptionEnforcementConfig
?.restrictionMode,
'FullyRestricted'
);
});
});
});
});

Expand Down
141 changes: 141 additions & 0 deletions handwritten/storage/test/bucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3300,4 +3300,145 @@ describe('Bucket', () => {
done();
});
});

describe('setMetadata', () => {
describe('encryption enforcement', () => {
it('should correctly format restrictionMode for all enforcement types', () => {
const effectiveTime = '2026-02-02T12:00:00Z';
const encryptionMetadata = {
encryption: {
defaultKmsKeyName: 'kms-key-name',
googleManagedEncryptionEnforcementConfig: {
restrictionMode: 'FullyRestricted',
effectiveTime: effectiveTime,
},
customerManagedEncryptionEnforcementConfig: {
restrictionMode: 'NotRestricted',
effectiveTime: effectiveTime,
},
customerSuppliedEncryptionEnforcementConfig: {
restrictionMode: 'FullyRestricted',
effectiveTime: effectiveTime,
},
},
};

bucket.setMetadata = (metadata: BucketMetadata) => {
assert.strictEqual(
metadata.encryption?.defaultKmsKeyName,
encryptionMetadata.encryption.defaultKmsKeyName
);

assert.deepStrictEqual(
metadata.encryption?.googleManagedEncryptionEnforcementConfig,
{restrictionMode: 'FullyRestricted', effectiveTime: effectiveTime}
);

assert.deepStrictEqual(
metadata.encryption?.customerManagedEncryptionEnforcementConfig,
{restrictionMode: 'NotRestricted', effectiveTime: effectiveTime}
);

assert.deepStrictEqual(
metadata.encryption?.customerSuppliedEncryptionEnforcementConfig,
{restrictionMode: 'FullyRestricted', effectiveTime: effectiveTime}
);
};
bucket.setMetadata(encryptionMetadata, assert.ifError);
});

it('should preserve existing encryption fields during a partial update', done => {
bucket.metadata = {
encryption: {
defaultKmsKeyName: 'kms-key-name',
googleManagedEncryptionEnforcementConfig: {
restrictionMode: 'FullyRestricted',
},
},
};

const patch = {
encryption: {
customerSuppliedEncryptionEnforcementConfig: {
restrictionMode: 'FullyRestricted',
},
},
};

bucket.setMetadata = (metadata: BucketMetadata) => {
assert.strictEqual(
metadata.encryption?.customerSuppliedEncryptionEnforcementConfig
?.restrictionMode,
'FullyRestricted'
);
done();
};

bucket.setMetadata(patch, assert.ifError);
});

it('should reject or handle invalid restrictionMode values', done => {
const invalidMetadata = {
encryption: {
googleManagedEncryptionEnforcementConfig: {
restrictionMode: 'fully_restricted',
},
},
};

bucket.setMetadata = (metadata: BucketMetadata) => {
assert.strictEqual(
metadata.encryption?.googleManagedEncryptionEnforcementConfig
?.restrictionMode,
'fully_restricted'
);
done();
};

bucket.setMetadata(invalidMetadata, assert.ifError);
});

it('should not include enforcement configs that are not provided', done => {
const partialMetadata = {
encryption: {
defaultKmsKeyName: 'test-key',
googleManagedEncryptionEnforcementConfig: {
restrictionMode: 'FullyRestricted',
},
},
};

bucket.setMetadata = (metadata: BucketMetadata) => {
assert.ok(metadata.encryption?.defaultKmsKeyName);
assert.ok(
metadata.encryption?.googleManagedEncryptionEnforcementConfig
);
assert.strictEqual(
metadata.encryption?.customerManagedEncryptionEnforcementConfig,
undefined
);
assert.strictEqual(
metadata.encryption?.customerSuppliedEncryptionEnforcementConfig,
undefined
);
done();
};

bucket.setMetadata(partialMetadata, assert.ifError);
});

it('should allow nullifying encryption enforcement', done => {
const clearMetadata = {
encryption: null,
};

bucket.setMetadata = (metadata: BucketMetadata) => {
assert.strictEqual(metadata.encryption, null);
done();
};

bucket.setMetadata(clearMetadata, assert.ifError);
});
});
});
});