EAM: Add AWS credentials validation (#821)

* Add AWS credentials validation

* Add changelog
This commit is contained in:
Krzysztof Wolski 2023-07-28 11:34:03 +02:00 committed by GitHub
parent 54901f8d08
commit 43d7e47bd4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 105 additions and 7 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-app-products-feed": patch
---
Added validation for AWS credentials. If provided configuration for S3 Bucket is invalid, it won't be saved.

View file

@ -5,6 +5,9 @@ import { createLogger } from "@saleor/apps-shared";
import { updateCacheForConfigurations } from "../metadata-cache/update-cache-for-configurations"; import { updateCacheForConfigurations } from "../metadata-cache/update-cache-for-configurations";
import { AppConfigSchema } from "./app-config"; import { AppConfigSchema } from "./app-config";
import { z } from "zod"; import { z } from "zod";
import { createS3ClientFromConfiguration } from "../file-storage/s3/create-s3-client-from-configuration";
import { checkBucketAccess } from "../file-storage/s3/check-bucket-access";
import { TRPCError } from "@trpc/server";
export const appConfigurationRouter = router({ export const appConfigurationRouter = router({
/** /**
@ -17,13 +20,54 @@ export const appConfigurationRouter = router({
return c.getRootConfig(); return c.getRootConfig();
}); });
}), }),
testS3BucketConfiguration: protectedClientProcedure
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(AppConfigSchema.s3Bucket)
.mutation(async ({ ctx: { saleorApiUrl, getConfig, appConfigMetadataManager }, input }) => {
const logger = createLogger({ saleorApiUrl: saleorApiUrl });
logger.debug("Validate the credentials");
const s3Client = createS3ClientFromConfiguration(input);
try {
await checkBucketAccess({
bucketName: input.bucketName,
s3Client,
});
} catch {
logger.debug("Validation failed");
throw new TRPCError({
code: "BAD_REQUEST",
message: "Could not access the S3 bucket using the provided credentials",
});
}
}),
setS3BucketConfiguration: protectedClientProcedure setS3BucketConfiguration: protectedClientProcedure
.meta({ requiredClientPermissions: ["MANAGE_APPS"] }) .meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(AppConfigSchema.s3Bucket) .input(AppConfigSchema.s3Bucket)
.mutation(async ({ ctx: { saleorApiUrl, getConfig, appConfigMetadataManager }, input }) => { .mutation(async ({ ctx: { saleorApiUrl, getConfig, appConfigMetadataManager }, input }) => {
const logger = createLogger({ saleorApiUrl: saleorApiUrl }); const logger = createLogger({ saleorApiUrl: saleorApiUrl });
logger.debug(input, "Input"); logger.debug("Validate credentials");
const s3Client = createS3ClientFromConfiguration(input);
try {
await checkBucketAccess({
bucketName: input.bucketName,
s3Client,
});
} catch {
logger.debug("Validation failed");
throw new TRPCError({
code: "BAD_REQUEST",
message: "Could not access the S3 bucket using the provided credentials",
});
}
logger.debug("Credentials validated, saving");
const config = await getConfig(); const config = await getConfig();

View file

@ -14,10 +14,11 @@ type S3BucketConfiguration = Exclude<RootConfig["s3"], null>;
type Props = { type Props = {
initialData: S3BucketConfiguration; initialData: S3BucketConfiguration;
onSubmit(data: S3BucketConfiguration): Promise<void>; onSubmit(data: S3BucketConfiguration): Promise<void>;
onValidate(data: S3BucketConfiguration): Promise<void>;
}; };
export const S3ConfigurationForm = (props: Props) => { export const S3ConfigurationForm = (props: Props) => {
const { handleSubmit, control } = useForm<S3BucketConfiguration>({ const { handleSubmit, control, getValues } = useForm<S3BucketConfiguration>({
defaultValues: props.initialData, defaultValues: props.initialData,
resolver: zodResolver(AppConfigSchema.s3Bucket), resolver: zodResolver(AppConfigSchema.s3Bucket),
}); });
@ -53,9 +54,14 @@ export const S3ConfigurationForm = (props: Props) => {
placeholder={"eu-west-1"} placeholder={"eu-west-1"}
/> />
<Button type="submit" variant="primary" alignSelf={"end"}> <Box display={"flex"} flexDirection={"row"} gap={4} justifyContent={"flex-end"}>
Save bucket configuration <Button variant="secondary" onClick={() => props.onValidate(getValues())}>
</Button> Test credentials
</Button>
<Button type="submit" variant="primary">
Save bucket configuration
</Button>
</Box>
</Box> </Box>
); );
}; };
@ -67,10 +73,25 @@ export const ConnectedS3ConfigurationForm = () => {
onSuccess() { onSuccess() {
notifySuccess("Success", "Updated S3 configuration"); notifySuccess("Success", "Updated S3 configuration");
}, },
onError() { onError({ message }) {
if (message) {
notifyError("Error", message);
return;
}
notifyError("Error", "Failed to update, please refresh and try again"); notifyError("Error", "Failed to update, please refresh and try again");
}, },
}); });
const { mutate: testConfigurationMutate } =
trpcClient.appConfiguration.testS3BucketConfiguration.useMutation({
onSuccess() {
notifySuccess("Configuration is valid");
},
onError({ message }) {
notifyError("Error", message);
},
});
const { data, isLoading } = trpcClient.appConfiguration.fetch.useQuery(); const { data, isLoading } = trpcClient.appConfiguration.fetch.useQuery();
const handleSubmit = useCallback( const handleSubmit = useCallback(
@ -80,6 +101,13 @@ export const ConnectedS3ConfigurationForm = () => {
[mutate] [mutate]
); );
const handleValidate = useCallback(
async (data: S3BucketConfiguration) => {
testConfigurationMutate(data);
},
[testConfigurationMutate]
);
const formData: S3BucketConfiguration = useMemo(() => { const formData: S3BucketConfiguration = useMemo(() => {
if (data?.s3) { if (data?.s3) {
return data.s3; return data.s3;
@ -97,5 +125,11 @@ export const ConnectedS3ConfigurationForm = () => {
return <Text>Loading...</Text>; return <Text>Loading...</Text>;
} }
return <S3ConfigurationForm onSubmit={handleSubmit} initialData={formData} />; return (
<S3ConfigurationForm
onSubmit={handleSubmit}
initialData={formData}
onValidate={handleValidate}
/>
);
}; };

View file

@ -0,0 +1,15 @@
import { HeadBucketCommand, S3Client } from "@aws-sdk/client-s3";
interface checkBucketAccessArgs {
s3Client: S3Client;
bucketName: string;
}
// Check if client can access the bucket. Throws an error otherwise
export const checkBucketAccess = async ({ s3Client, bucketName }: checkBucketAccessArgs) => {
await s3Client.send(
new HeadBucketCommand({
Bucket: bucketName,
})
);
};