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 { AppConfigSchema } from "./app-config";
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({
/**
@ -17,13 +20,54 @@ export const appConfigurationRouter = router({
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
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(AppConfigSchema.s3Bucket)
.mutation(async ({ ctx: { saleorApiUrl, getConfig, appConfigMetadataManager }, input }) => {
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();

View file

@ -14,10 +14,11 @@ type S3BucketConfiguration = Exclude<RootConfig["s3"], null>;
type Props = {
initialData: S3BucketConfiguration;
onSubmit(data: S3BucketConfiguration): Promise<void>;
onValidate(data: S3BucketConfiguration): Promise<void>;
};
export const S3ConfigurationForm = (props: Props) => {
const { handleSubmit, control } = useForm<S3BucketConfiguration>({
const { handleSubmit, control, getValues } = useForm<S3BucketConfiguration>({
defaultValues: props.initialData,
resolver: zodResolver(AppConfigSchema.s3Bucket),
});
@ -53,10 +54,15 @@ export const S3ConfigurationForm = (props: Props) => {
placeholder={"eu-west-1"}
/>
<Button type="submit" variant="primary" alignSelf={"end"}>
<Box display={"flex"} flexDirection={"row"} gap={4} justifyContent={"flex-end"}>
<Button variant="secondary" onClick={() => props.onValidate(getValues())}>
Test credentials
</Button>
<Button type="submit" variant="primary">
Save bucket configuration
</Button>
</Box>
</Box>
);
};
@ -67,10 +73,25 @@ export const ConnectedS3ConfigurationForm = () => {
onSuccess() {
notifySuccess("Success", "Updated S3 configuration");
},
onError() {
onError({ message }) {
if (message) {
notifyError("Error", message);
return;
}
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 handleSubmit = useCallback(
@ -80,6 +101,13 @@ export const ConnectedS3ConfigurationForm = () => {
[mutate]
);
const handleValidate = useCallback(
async (data: S3BucketConfiguration) => {
testConfigurationMutate(data);
},
[testConfigurationMutate]
);
const formData: S3BucketConfiguration = useMemo(() => {
if (data?.s3) {
return data.s3;
@ -97,5 +125,11 @@ export const ConnectedS3ConfigurationForm = () => {
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,
})
);
};