Skip to content

Commit f228801

Browse files
committed
feat: add getBucketPolicy, putBucketPolicy & deleteBucketPolicy
1 parent 1dafea7 commit f228801

File tree

4 files changed

+356
-2
lines changed

4 files changed

+356
-2
lines changed

deps.ts

+5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ export function sha256Hex(data: string | Uint8Array): string {
1313
hasher.update(data);
1414
return hasher.toString("hex");
1515
}
16+
export function md5(data: string | Uint8Array): string {
17+
const hasher = createHash("md5");
18+
hasher.update(data);
19+
return hasher.toString("base64");
20+
}
1621
export { default as parseXML } from "https://raw.githubusercontent.com/nekobato/deno-xml-parser/0bc4c2bd2f5fad36d274279978ca57eec57c680c/index.ts";
1722
export { decode as decodeXMLEntities } from "https://deno.land/x/[email protected]/lib/xml-entities.js";
1823
export { pooledMap } from "https://deno.land/[email protected]/async/pool.ts";

src/client.ts

+147-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
import { AWSSignerV4 } from "../deps.ts";
2-
import type { CreateBucketOptions } from "./types.ts";
1+
import { AWSSignerV4, md5 } from "../deps.ts";
2+
import type {
3+
CreateBucketOptions,
4+
DeleteBucketPolicyOptions,
5+
GetBucketPolicyOptions,
6+
Policy,
7+
PutBucketPolicyOptions,
8+
Statement,
9+
} from "./types.ts";
310
import { S3Error } from "./error.ts";
411
import { S3Bucket } from "./bucket.ts";
512
import { doRequest, encoder } from "./request.ts";
@@ -88,4 +95,142 @@ export class S3 {
8895
bucket,
8996
});
9097
}
98+
99+
async putBucketPolicy(
100+
bucket: string,
101+
options: PutBucketPolicyOptions,
102+
): Promise<void> {
103+
const headers: Params = {};
104+
const params: Params = {};
105+
const json = JSON.stringify(options.policy);
106+
const body = encoder.encode(json);
107+
108+
headers["Content-MD5"] = md5(json);
109+
110+
if (typeof options.confirmRemoveSelfBucketAccess !== "undefined") {
111+
headers["x-amz-confirm-remove-self-bucket-access"] = options
112+
.confirmRemoveSelfBucketAccess.toString();
113+
}
114+
if (options.expectedBucketOwner) {
115+
headers["x-amz-expected-bucket-owner"] = options.expectedBucketOwner;
116+
}
117+
118+
params["policy"] = "true";
119+
120+
const resp = await doRequest({
121+
host: this.#host,
122+
signer: this.#signer,
123+
path: bucket,
124+
method: "PUT",
125+
headers,
126+
params,
127+
body,
128+
});
129+
130+
if (resp.status !== 204) {
131+
throw new S3Error(
132+
`Failed to create policy for bucket "${bucket}": ${resp.status} ${resp.statusText}`,
133+
await resp.text(),
134+
);
135+
}
136+
137+
// clean up http body
138+
await resp.arrayBuffer();
139+
}
140+
141+
async getBucketPolicy(
142+
bucket: string,
143+
options?: GetBucketPolicyOptions,
144+
): Promise<Policy> {
145+
const headers: Params = {};
146+
const params: Params = {};
147+
148+
if (options?.expectedBucketOwner) {
149+
headers["x-amz-expected-bucket-owner"] = options.expectedBucketOwner;
150+
}
151+
152+
params["policy"] = "true";
153+
154+
const resp = await doRequest({
155+
host: this.#host,
156+
signer: this.#signer,
157+
path: bucket,
158+
method: "GET",
159+
headers,
160+
params,
161+
});
162+
163+
if (resp.status !== 200) {
164+
throw new S3Error(
165+
`Failed to get policy for bucket "${bucket}": ${resp.status} ${resp.statusText}`,
166+
await resp.text(),
167+
);
168+
}
169+
170+
const result = JSON.parse(await resp.text());
171+
172+
return this.#parseGetBucketPolicyResult(result);
173+
}
174+
175+
async deleteBucketPolicy(
176+
bucket: string,
177+
options?: DeleteBucketPolicyOptions,
178+
): Promise<void> {
179+
const headers: Params = {};
180+
const params: Params = {};
181+
182+
if (options?.expectedBucketOwner) {
183+
headers["x-amz-expected-bucket-owner"] = options.expectedBucketOwner;
184+
}
185+
186+
params["policy"] = "true";
187+
188+
const resp = await doRequest({
189+
host: this.#host,
190+
signer: this.#signer,
191+
path: bucket,
192+
method: "DELETE",
193+
headers,
194+
params,
195+
});
196+
197+
if (resp.status !== 204) {
198+
throw new S3Error(
199+
`Failed to delete policy for bucket "${bucket}": ${resp.status} ${resp.statusText}`,
200+
await resp.text(),
201+
);
202+
}
203+
204+
// clean up http body
205+
await resp.arrayBuffer();
206+
}
207+
208+
// deno-lint-ignore no-explicit-any
209+
#parseGetBucketPolicyResult(result: Record<string, any>): Policy {
210+
const policy: Policy = {
211+
statement: Array.isArray(result.Statement)
212+
? result.Statement.map(mapKeys) as Array<Statement>
213+
: mapKeys(result.Statement) as Statement,
214+
};
215+
216+
if (result.ID) {
217+
policy.id = result.ID;
218+
}
219+
220+
if (result.Version) {
221+
policy.version = result.Version;
222+
}
223+
224+
return policy;
225+
226+
// deno-lint-ignore no-explicit-any
227+
function mapKeys(obj: Record<string, unknown>): Record<string, any> {
228+
const mapped: Record<string, unknown> = {};
229+
for (const key in obj) {
230+
const mappedKey = key.slice(0, 1).toLowerCase() + key.slice(1);
231+
mapped[mappedKey] = obj[key];
232+
}
233+
return mapped;
234+
}
235+
}
91236
}

src/client_test.ts

+57
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { S3Error } from "./error.ts";
33
import { S3Bucket } from "./bucket.ts";
44
import { S3 } from "./client.ts";
55
import { encoder } from "./request.ts";
6+
import { Policy } from "./types.ts";
67

78
const s3 = new S3({
89
accessKeyID: Deno.env.get("AWS_ACCESS_KEY_ID")!,
@@ -11,6 +12,25 @@ const s3 = new S3({
1112
endpointURL: Deno.env.get("S3_ENDPOINT_URL"),
1213
});
1314

15+
const policy: Policy = {
16+
version: "2012-10-17",
17+
id: "test",
18+
statement: [
19+
{
20+
effect: "Allow",
21+
principal: {
22+
AWS: ["111122223333", "444455556666"],
23+
},
24+
action: [
25+
"s3:PutObject",
26+
],
27+
resource: [
28+
"arn:aws:s3:::*",
29+
],
30+
},
31+
],
32+
};
33+
1434
Deno.test({
1535
name: "[client] should create a new bucket",
1636
async fn() {
@@ -42,3 +62,40 @@ Deno.test({
4262
);
4363
},
4464
});
65+
66+
Deno.test({
67+
name: "[client] should put a bucket policy",
68+
async fn() {
69+
await s3.putBucketPolicy("test.bucket", { policy });
70+
71+
// teardown
72+
await s3.deleteBucketPolicy("test.bucket");
73+
},
74+
});
75+
76+
Deno.test({
77+
name: "[client] should get a bucket policy",
78+
async fn() {
79+
await s3.putBucketPolicy("test.bucket", { policy });
80+
const resp = await s3.getBucketPolicy("test.bucket");
81+
assertEquals(resp, policy);
82+
83+
// teardown
84+
await s3.deleteBucketPolicy("test.bucket");
85+
},
86+
});
87+
88+
Deno.test({
89+
name: "[client] should delete a bucket policy",
90+
async fn() {
91+
await s3.putBucketPolicy("test.bucket", { policy });
92+
const resp = await s3.getBucketPolicy("test.bucket");
93+
assert(resp);
94+
await s3.deleteBucketPolicy("test.bucket");
95+
await assertThrowsAsync(
96+
() => s3.getBucketPolicy("test.bucket"),
97+
S3Error,
98+
'Failed to get policy for bucket "test.bucket": 404 Not Found',
99+
);
100+
},
101+
});

src/types.ts

+147
Original file line numberDiff line numberDiff line change
@@ -566,3 +566,150 @@ export interface CreateBucketOptions {
566566
/** Allows grantee to write the ACL for the applicable bucket. */
567567
grantWriteAcp?: string;
568568
}
569+
570+
/**
571+
* @see https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html
572+
*/
573+
export interface Statement {
574+
/**
575+
* You can provide an optional identifier, Sid (statement ID) for the policy statement.
576+
*
577+
* @see https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_sid.html
578+
*/
579+
sid?: string;
580+
581+
/**
582+
* The Effect element is required and specifies whether the statement results
583+
* an allow or an explicit deny.
584+
*
585+
* @see https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_effect.html
586+
*/
587+
effect: "Allow" | "Deny";
588+
589+
/**
590+
* The account or user who is allowed access to the actions and resources in
591+
* the statement. In a bucket policy, the principal is the user, account,
592+
* service, or other entity that is the recipient of this permission.
593+
*
594+
* @see https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html
595+
*/
596+
principal?: string | Record<string, string | Array<string>>;
597+
598+
/**
599+
* Use the NotPrincipal element to specify the IAM user, federated user,
600+
* IAM role, AWS account, AWS service, or other principal that is not allowed
601+
* or denied access to a resource.
602+
*
603+
* @see https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_notprincipal.html
604+
*/
605+
notPrincipal?: string | Record<string, string | Array<string>>;
606+
607+
/**
608+
* The Action element describes the specific action or actions that will be
609+
* allowed or denied. Statements must include either an Action or NotAction
610+
* element.
611+
*
612+
* @see https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_action.html
613+
*/
614+
action?: string | Array<string>;
615+
616+
/**
617+
* NotAction is an advanced policy element that explicitly matches everything
618+
* except the specified list of actions. Using NotAction can result in a
619+
* shorter policy by listing only a few actions that should not match, rather
620+
* than including a long list of actions that will match.
621+
*
622+
* @see https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_notaction.html
623+
*/
624+
notAction?: string | Array<string>;
625+
626+
/**
627+
* The Resource element specifies the object or objects that the statement covers.
628+
* Statements must include either a Resource or a NotResource element.
629+
*
630+
* @see https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_resource.html
631+
*/
632+
resource?: string | Array<string>;
633+
634+
/**
635+
* NotResource is an advanced policy element that explicitly matches every
636+
* resource except those specified. Using NotResource can result in a shorter
637+
* policy by listing only a few resources that should not match, rather than
638+
* including a long list of resources that will match.
639+
*
640+
* @see https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_notresource.html
641+
*/
642+
notResource?: string | Array<string>;
643+
644+
/**
645+
* The Condition element (or Condition block) lets you specify conditions for
646+
* when a policy is in effect.
647+
*
648+
* @see https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html
649+
*/
650+
condition?: Record<string, Record<string, string | Array<string>>>;
651+
}
652+
653+
/**
654+
* @see https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html
655+
*/
656+
export interface Policy {
657+
/**
658+
* The Version policy element specifies the language syntax rules that are to
659+
* be used to process a policy.
660+
*
661+
* @see https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_version.html
662+
*/
663+
version?: "2012-10-17" | "2008-10-17";
664+
665+
/**
666+
* The id element specifies an optional identifier for the policy. The ID is
667+
* used differently in different services.
668+
*
669+
* @see https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_id.html
670+
*/
671+
id?: string;
672+
673+
/**
674+
* The policy statement(s).
675+
*
676+
* @see https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_statement.html
677+
*/
678+
statement: Statement | Array<Statement>;
679+
}
680+
681+
export interface PutBucketPolicyOptions {
682+
/**
683+
* Set this parameter to true to confirm that you want to remove your
684+
* permissions to change this bucket policy in the future.
685+
*/
686+
confirmRemoveSelfBucketAccess?: boolean;
687+
688+
/**
689+
* The account ID of the expected bucket owner. If the bucket is owned by a
690+
* different account, the request will fail with an HTTP 403 (Access Denied)
691+
* error.
692+
*/
693+
expectedBucketOwner?: string;
694+
695+
/** The bucket policy. */
696+
policy: Policy;
697+
}
698+
699+
export interface GetBucketPolicyOptions {
700+
/**
701+
* The account ID of the expected bucket owner. If the bucket is owned by a
702+
* different account, the request will fail with an HTTP 403 (Access Denied)
703+
* error.
704+
*/
705+
expectedBucketOwner?: string;
706+
}
707+
708+
export interface DeleteBucketPolicyOptions {
709+
/**
710+
* The account ID of the expected bucket owner. If the bucket is owned by a
711+
* different account, the request will fail with an HTTP 403 (Access Denied)
712+
* error.
713+
*/
714+
expectedBucketOwner?: string;
715+
}

0 commit comments

Comments
 (0)