Skip to content

Commit d83a34d

Browse files
committed
1 parent 256cad8 commit d83a34d

10 files changed

Lines changed: 567 additions & 175 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/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+
}

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

Lines changed: 116 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { assert, type Equals } from "tsafe";
77
import type * as s3CredentialsTest from "core/usecases/_s3Next/s3CredentialsTest";
88
import type { LocalizedString } from "core/ports/OnyxiaApi";
99
import type { ResolvedTemplateBookmark } from "./resolveTemplatedBookmark";
10+
import type { ResolvedTemplateStsRole } from "./resolveTemplatedStsRole";
1011
import type { S3UriPrefixObj } from "core/tools/S3Uri";
1112

1213
export type S3Profile = S3Profile.DefinedInRegion | S3Profile.CreatedByUser;
@@ -45,9 +46,17 @@ export namespace S3Profile {
4546

4647
export function aggregateS3ProfilesFromVaultAndRegionIntoAnUnifiedSet(params: {
4748
fromVault: projectManagement.ProjectConfigs["s3"];
48-
fromRegion: (Omit<DeploymentRegion.S3Next.S3Profile, "bookmarks"> & {
49-
bookmarks: ResolvedTemplateBookmark[];
50-
})[];
49+
fromRegion: {
50+
s3Profiles: DeploymentRegion.S3Next.S3Profile[];
51+
resolvedTemplatedBookmarks: {
52+
correspondingS3ConfigIndexInRegion: number;
53+
bookmarks: ResolvedTemplateBookmark[];
54+
}[];
55+
resolvedTemplatedStsRoles: {
56+
correspondingS3ConfigIndexInRegion: number;
57+
stsRoles: ResolvedTemplateStsRole[];
58+
}[];
59+
};
5160
credentialsTestState: s3CredentialsTest.State;
5261
}): S3Profile[] {
5362
const { fromVault, fromRegion, credentialsTestState } = params;
@@ -114,40 +123,110 @@ export function aggregateS3ProfilesFromVaultAndRegionIntoAnUnifiedSet(params: {
114123
};
115124
})
116125
.sort((a, b) => b.creationTime - a.creationTime),
117-
...fromRegion.map((c): S3Profile.DefinedInRegion => {
118-
const url = c.url;
119-
const pathStyleAccess = c.pathStyleAccess;
120-
const region = c.region;
121-
122-
const paramsOfCreateS3Client: ParamsOfCreateS3Client.Sts = {
123-
url,
124-
pathStyleAccess,
125-
isStsEnabled: true,
126-
stsUrl: c.sts.url,
127-
region,
128-
oidcParams: c.sts.oidcParams,
129-
durationSeconds: c.sts.durationSeconds,
130-
role: c.sts.role,
131-
nameOfBucketToCreateIfNotExist: undefined
132-
};
133-
134-
return {
135-
origin: "defined in region",
136-
id: `region-${fnv1aHashToHex(
137-
JSON.stringify([c.url, c.sts.oidcParams.clientId ?? ""])
138-
)}`,
139-
bookmarks: c.bookmarks.map(({ title, s3UriPrefixObj }) => ({
140-
displayName: title,
141-
s3UriPrefixObj
142-
})),
143-
paramsOfCreateS3Client,
144-
credentialsTestStatus: getCredentialsTestStatus({
145-
paramsOfCreateS3Client
146-
}),
147-
isXOnyxiaDefault: false,
148-
isExplorerConfig: false
149-
};
150-
})
126+
...fromRegion.s3Profiles
127+
.map((c, index): S3Profile.DefinedInRegion[] => {
128+
const resolvedTemplatedBookmarks_forThisProfile = (() => {
129+
const entry = fromRegion.resolvedTemplatedBookmarks.find(
130+
e => e.correspondingS3ConfigIndexInRegion === index
131+
);
132+
133+
assert(entry !== undefined);
134+
135+
return entry.bookmarks;
136+
})();
137+
138+
const buildFromRole = (params: {
139+
resolvedTemplatedStsRole: ResolvedTemplateStsRole | undefined;
140+
}): S3Profile.DefinedInRegion => {
141+
const { resolvedTemplatedStsRole } = params;
142+
143+
const paramsOfCreateS3Client: ParamsOfCreateS3Client.Sts = {
144+
url: c.url,
145+
pathStyleAccess: c.pathStyleAccess,
146+
isStsEnabled: true,
147+
stsUrl: c.sts.url,
148+
region: c.region,
149+
oidcParams: c.sts.oidcParams,
150+
durationSeconds: c.sts.durationSeconds,
151+
role: resolvedTemplatedStsRole,
152+
nameOfBucketToCreateIfNotExist: undefined
153+
};
154+
155+
return {
156+
origin: "defined in region",
157+
id: `region-${fnv1aHashToHex(
158+
JSON.stringify([c.url, c.sts.oidcParams.clientId ?? ""])
159+
)}`,
160+
bookmarks: resolvedTemplatedBookmarks_forThisProfile
161+
.filter(({ forStsRoleSessionNames }) => {
162+
if (forStsRoleSessionNames.length === 0) {
163+
return true;
164+
}
165+
166+
if (resolvedTemplatedStsRole === undefined) {
167+
return false;
168+
}
169+
170+
const getDoMatch = (params: {
171+
stringWithWildcards: string;
172+
candidate: string;
173+
}): boolean => {
174+
const { stringWithWildcards, candidate } = params;
175+
176+
if (!stringWithWildcards.includes("*")) {
177+
return stringWithWildcards === candidate;
178+
}
179+
180+
const escapedRegex = stringWithWildcards
181+
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
182+
.replace(/\\\*/g, ".*");
183+
184+
return new RegExp(`^${escapedRegex}$`).test(
185+
candidate
186+
);
187+
};
188+
189+
return forStsRoleSessionNames.some(stsRoleSessionName =>
190+
getDoMatch({
191+
stringWithWildcards: stsRoleSessionName,
192+
candidate:
193+
resolvedTemplatedStsRole.roleSessionName
194+
})
195+
);
196+
})
197+
.map(({ title, s3UriPrefixObj }) => ({
198+
displayName: title,
199+
s3UriPrefixObj
200+
})),
201+
paramsOfCreateS3Client,
202+
credentialsTestStatus: getCredentialsTestStatus({
203+
paramsOfCreateS3Client
204+
}),
205+
isXOnyxiaDefault: false,
206+
isExplorerConfig: false
207+
};
208+
};
209+
210+
if (fromRegion.resolvedTemplatedStsRoles.length === 0) {
211+
return [buildFromRole({ resolvedTemplatedStsRole: undefined })];
212+
}
213+
214+
const resolvedTemplatedStsRoles_forThisProfile = (() => {
215+
const entry = fromRegion.resolvedTemplatedStsRoles.find(
216+
e => e.correspondingS3ConfigIndexInRegion === index
217+
);
218+
219+
assert(entry !== undefined);
220+
221+
return entry.stsRoles;
222+
})();
223+
224+
return resolvedTemplatedStsRoles_forThisProfile.map(
225+
resolvedTemplatedStsRole =>
226+
buildFromRole({ resolvedTemplatedStsRole })
227+
);
228+
})
229+
.flat()
151230
];
152231

153232
(

0 commit comments

Comments
 (0)