Skip to content

Commit 9c02ec2

Browse files
committed
Templates STS Roles (Ceph Story) #1048
1 parent 256cad8 commit 9c02ec2

11 files changed

Lines changed: 568 additions & 185 deletions

File tree

web/src/core/adapters/onyxiaApi/ApiTypes.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,19 @@ export type ApiTypes = {
9090
sts?: {
9191
URL?: string;
9292
durationSeconds?: number;
93-
role:
94-
| {
95-
roleARN: string;
96-
roleSessionName: string;
97-
}
98-
| undefined;
93+
role?: ArrayOrNot<
94+
{
95+
roleARN: string;
96+
roleSessionName: string;
97+
} & (
98+
| { claimName?: undefined }
99+
| {
100+
claimName: string;
101+
includedClaimPattern?: string;
102+
excludedClaimPattern?: string;
103+
}
104+
)
105+
>;
99106
oidcConfiguration?: Partial<ApiTypes.OidcConfiguration>;
100107
};
101108

@@ -118,6 +125,7 @@ export type ApiTypes = {
118125
title: LocalizedString;
119126
description?: LocalizedString;
120127
tags?: LocalizedString[];
128+
forStsRoleSessionName?: string | string[];
121129
} & (
122130
| { claimName?: undefined }
123131
| {

web/src/core/adapters/onyxiaApi/onyxiaApi.ts

Lines changed: 206 additions & 66 deletions
Large diffs are not rendered by default.

web/src/core/ports/OnyxiaApi/DeploymentRegion.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -180,23 +180,35 @@ export namespace DeploymentRegion {
180180
sts: {
181181
url: string | undefined;
182182
durationSeconds: number | undefined;
183-
role:
184-
| {
185-
roleARN: string;
186-
roleSessionName: string;
187-
}
188-
| undefined;
183+
roles: S3Profile.StsRole[];
189184
oidcParams: OidcParams_Partial;
190185
};
191186
bookmarks: S3Profile.Bookmark[];
192187
};
193188

194189
export namespace S3Profile {
190+
export type StsRole = {
191+
roleARN: string;
192+
roleSessionName: string;
193+
} & (
194+
| {
195+
claimName: undefined;
196+
includedClaimPattern?: never;
197+
excludedClaimPattern?: never;
198+
}
199+
| {
200+
claimName: string;
201+
includedClaimPattern: string | undefined;
202+
excludedClaimPattern: string | undefined;
203+
}
204+
);
205+
195206
export type Bookmark = {
196207
s3UriPrefix: string;
197208
title: LocalizedString;
198209
description: LocalizedString | undefined;
199210
tags: LocalizedString[];
211+
forStsRoleSessionNames: string[];
200212
} & (
201213
| {
202214
claimName: undefined;

web/src/core/usecases/_s3Next/s3ExplorerRootUiController/thunks.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,8 @@ export const thunks = {
3131
s3Profiles.find(s3Profile => s3Profile.origin === "defined in region") ??
3232
s3Profiles[0];
3333

34-
if (s3Profile === undefined) {
35-
return {
36-
routeParams_toSet: {
37-
profile: undefined,
38-
path: ""
39-
}
40-
};
41-
}
42-
4334
const routeParams_toSet: RouteParams = {
44-
profile: s3Profile.id,
35+
profile: s3Profile === undefined ? undefined : s3Profile.id,
4536
path: ""
4637
};
4738

web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/resolveTemplatedBookmark.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type ResolvedTemplateBookmark = {
1010
description: LocalizedString | undefined;
1111
tags: LocalizedString[];
1212
s3UriPrefixObj: S3UriPrefixObj;
13+
forStsRoleSessionNames: string[];
1314
};
1415

1516
export async function resolveTemplatedBookmark(params: {
@@ -27,7 +28,8 @@ export async function resolveTemplatedBookmark(params: {
2728
}),
2829
title: bookmark_region.title,
2930
description: bookmark_region.description,
30-
tags: bookmark_region.tags
31+
tags: bookmark_region.tags,
32+
forStsRoleSessionNames: bookmark_region.forStsRoleSessionNames
3133
})
3234
];
3335
}
@@ -127,7 +129,10 @@ export async function resolveTemplatedBookmark(params: {
127129
bookmark_region.description === undefined
128130
? undefined
129131
: substituteLocalizedString(bookmark_region.description),
130-
tags: bookmark_region.tags.map(tag => substituteLocalizedString(tag))
132+
tags: bookmark_region.tags.map(tag => substituteLocalizedString(tag)),
133+
forStsRoleSessionNames: bookmark_region.forStsRoleSessionNames.map(
134+
stsRoleSessionName => substituteTemplateString(stsRoleSessionName)
135+
)
131136
});
132137
})
133138
.filter(x => x !== undefined);
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import type { DeploymentRegion } from "core/ports/OnyxiaApi";
2+
import { id } from "tsafe/id";
3+
import { z } from "zod";
4+
import { getValueAtPath } from "core/tools/Stringifyable";
5+
6+
export type ResolvedTemplateStsRole = {
7+
roleARN: string;
8+
roleSessionName: string;
9+
};
10+
11+
export async function resolveTemplatedStsRole(params: {
12+
stsRole_region: DeploymentRegion.S3Next.S3Profile.StsRole;
13+
getDecodedIdToken: () => Promise<Record<string, unknown>>;
14+
}): Promise<ResolvedTemplateStsRole[]> {
15+
const { stsRole_region, getDecodedIdToken } = params;
16+
17+
if (stsRole_region.claimName === undefined) {
18+
return [
19+
id<ResolvedTemplateStsRole>({
20+
roleARN: stsRole_region.roleARN,
21+
roleSessionName: stsRole_region.roleSessionName
22+
})
23+
];
24+
}
25+
26+
const { claimName, excludedClaimPattern, includedClaimPattern } = stsRole_region;
27+
28+
const decodedIdToken = await getDecodedIdToken();
29+
30+
const claimValue_arr: string[] = (() => {
31+
let claimValue_untrusted: unknown = (() => {
32+
const candidate = decodedIdToken[claimName];
33+
34+
if (candidate !== undefined) {
35+
return candidate;
36+
}
37+
38+
const claimPath = claimName.split(".");
39+
40+
if (claimPath.length === 1) {
41+
return undefined;
42+
}
43+
44+
return getValueAtPath({
45+
// @ts-expect-error: We know decodedIdToken is Stringifyable
46+
stringifyableObjectOrArray: decodedIdToken,
47+
doDeleteFromSource: false,
48+
doFailOnUnresolved: false,
49+
path: claimPath
50+
});
51+
})();
52+
53+
if (!claimValue_untrusted) {
54+
return [];
55+
}
56+
57+
let claimValue: string | string[];
58+
59+
try {
60+
claimValue = z
61+
.union([z.string(), z.array(z.string())])
62+
.parse(claimValue_untrusted);
63+
} catch (error) {
64+
throw new Error(
65+
[
66+
`decodedIdToken -> ${claimName} is supposed to be`,
67+
`string or array of string`,
68+
`The decoded id token is:`,
69+
JSON.stringify(decodedIdToken, null, 2)
70+
].join(" "),
71+
{ cause: error }
72+
);
73+
}
74+
75+
return claimValue instanceof Array ? claimValue : [claimValue];
76+
})();
77+
78+
const includedRegex =
79+
includedClaimPattern !== undefined ? new RegExp(includedClaimPattern) : /^(.+)$/;
80+
const excludedRegex =
81+
excludedClaimPattern !== undefined ? new RegExp(excludedClaimPattern) : undefined;
82+
83+
return claimValue_arr
84+
.map(value => {
85+
if (excludedRegex !== undefined && excludedRegex.test(value)) {
86+
return undefined;
87+
}
88+
89+
const match = includedRegex.exec(value);
90+
91+
if (match === null) {
92+
return undefined;
93+
}
94+
95+
const substituteTemplateString = (str: string) =>
96+
str.replace(/\$(\d+)/g, (_, i) => match[parseInt(i)] ?? "");
97+
98+
return id<ResolvedTemplateStsRole>({
99+
roleARN: substituteTemplateString(stsRole_region.roleARN),
100+
roleSessionName: substituteTemplateString(stsRole_region.roleSessionName)
101+
});
102+
})
103+
.filter(x => x !== undefined);
104+
}

0 commit comments

Comments
 (0)