Skip to content

Commit

Permalink
EAM: Add AWS credentials validation (#821)
Browse files Browse the repository at this point in the history
* Add AWS credentials validation

* Add changelog
  • Loading branch information
krzysztofwolski committed Jul 28, 2023
1 parent 54901f8 commit 43d7e47
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/mean-ghosts-beam.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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({
/**
Expand All @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});
Expand Down Expand Up @@ -53,9 +54,14 @@ export const S3ConfigurationForm = (props: Props) => {
placeholder={"eu-west-1"}
/>

<Button type="submit" variant="primary" alignSelf={"end"}>
Save bucket configuration
</Button>
<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>
);
};
Expand All @@ -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(
Expand All @@ -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;
Expand All @@ -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}
/>
);
};
Original file line number Diff line number Diff line change
@@ -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,
})
);
};

0 comments on commit 43d7e47

Please sign in to comment.