mirror of
https://github.com/khairul169/garage-webui.git
synced 2025-11-30 04:51:04 +07:00
feat: must have a 'owner' access key before configuring CORS
This commit is contained in:
parent
9344247d19
commit
48211dd50e
@ -33,7 +33,7 @@ func (b *Browse) GetObjects(w http.ResponseWriter, r *http.Request) {
|
|||||||
limit = 100
|
limit = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := getS3Client(bucket)
|
client, err := getS3Client(bucket, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ResponseError(w, err)
|
utils.ResponseError(w, err)
|
||||||
return
|
return
|
||||||
@ -88,7 +88,7 @@ func (b *Browse) GetOneObject(w http.ResponseWriter, r *http.Request) {
|
|||||||
thumbnail := queryParams.Get("thumb") == "1"
|
thumbnail := queryParams.Get("thumb") == "1"
|
||||||
download := queryParams.Get("dl") == "1"
|
download := queryParams.Get("dl") == "1"
|
||||||
|
|
||||||
client, err := getS3Client(bucket)
|
client, err := getS3Client(bucket, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ResponseError(w, err)
|
utils.ResponseError(w, err)
|
||||||
return
|
return
|
||||||
@ -184,7 +184,7 @@ func (b *Browse) PutObject(w http.ResponseWriter, r *http.Request) {
|
|||||||
defer file.Close()
|
defer file.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := getS3Client(bucket)
|
client, err := getS3Client(bucket, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ResponseError(w, err)
|
utils.ResponseError(w, err)
|
||||||
return
|
return
|
||||||
@ -220,7 +220,7 @@ func (b *Browse) DeleteObject(w http.ResponseWriter, r *http.Request) {
|
|||||||
recursive := r.URL.Query().Get("recursive") == "true"
|
recursive := r.URL.Query().Get("recursive") == "true"
|
||||||
isDirectory := strings.HasSuffix(key, "/")
|
isDirectory := strings.HasSuffix(key, "/")
|
||||||
|
|
||||||
client, err := getS3Client(bucket)
|
client, err := getS3Client(bucket, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ResponseError(w, err)
|
utils.ResponseError(w, err)
|
||||||
return
|
return
|
||||||
@ -284,10 +284,15 @@ func (b *Browse) DeleteObject(w http.ResponseWriter, r *http.Request) {
|
|||||||
utils.ResponseSuccess(w, res)
|
utils.ResponseSuccess(w, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBucketCredentials(bucket string) (aws.CredentialsProvider, error) {
|
func getBucketCredentials(bucket string, permission *string) (aws.CredentialsProvider, error) {
|
||||||
cacheKey := fmt.Sprintf("key:%s", bucket)
|
cacheKey := fmt.Sprintf("key:%s", bucket)
|
||||||
cacheData := utils.Cache.Get(cacheKey)
|
|
||||||
|
|
||||||
|
// Some CORS operations with the SDK are only achievable using a "owner" access key
|
||||||
|
if permission != nil {
|
||||||
|
cacheKey = fmt.Sprintf("key:%s:%s", *permission, bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheData := utils.Cache.Get(cacheKey)
|
||||||
if cacheData != nil {
|
if cacheData != nil {
|
||||||
return cacheData.(aws.CredentialsProvider), nil
|
return cacheData.(aws.CredentialsProvider), nil
|
||||||
}
|
}
|
||||||
@ -305,7 +310,10 @@ func getBucketCredentials(bucket string) (aws.CredentialsProvider, error) {
|
|||||||
var key schema.KeyElement
|
var key schema.KeyElement
|
||||||
|
|
||||||
for _, k := range bucketData.Keys {
|
for _, k := range bucketData.Keys {
|
||||||
if !k.Permissions.Read || !k.Permissions.Write {
|
if permission == nil && (!k.Permissions.Read || !k.Permissions.Write){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if permission != nil && !k.Permissions.HasPermission(*permission) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,14 +327,18 @@ func getBucketCredentials(bucket string) (aws.CredentialsProvider, error) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if key.AccessKeyID == "" {
|
||||||
|
return nil, fmt.Errorf("a valid access key was not found for this bucket")
|
||||||
|
}
|
||||||
|
|
||||||
credential := credentials.NewStaticCredentialsProvider(key.AccessKeyID, key.SecretAccessKey, "")
|
credential := credentials.NewStaticCredentialsProvider(key.AccessKeyID, key.SecretAccessKey, "")
|
||||||
utils.Cache.Set(cacheKey, credential, time.Hour)
|
utils.Cache.Set(cacheKey, credential, time.Hour)
|
||||||
|
|
||||||
return credential, nil
|
return credential, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getS3Client(bucket string) (*s3.Client, error) {
|
func getS3Client(bucket string, permission *string) (*s3.Client, error) {
|
||||||
creds, err := getBucketCredentials(bucket)
|
creds, err := getBucketCredentials(bucket, permission)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot get credentials for bucket %s: %w", bucket, err)
|
return nil, fmt.Errorf("cannot get credentials for bucket %s: %w", bucket, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,20 +61,33 @@ func (b *Buckets) GetAll(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (b*Buckets) GetCors(w http.ResponseWriter, r *http.Request){
|
func (b*Buckets) GetCors(w http.ResponseWriter, r *http.Request){
|
||||||
bucket := r.PathValue("bucket")
|
bucket := r.PathValue("bucket")
|
||||||
|
|
||||||
client, err := getS3Client(bucket)
|
perm := "owner"
|
||||||
|
client, err := getS3Client(bucket, &perm)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ResponseError(w, err)
|
utils.ResponseError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := client.GetBucketCors(context.Background(), &s3.GetBucketCorsInput{
|
cors, err := client.GetBucketCors(context.Background(), &s3.GetBucketCorsInput{
|
||||||
Bucket: aws.String(bucket),
|
Bucket: aws.String(bucket),
|
||||||
})
|
})
|
||||||
|
|
||||||
res := []schema.BucketCors{}
|
if err != nil {
|
||||||
|
utils.ResponseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for _, rule := range out.CORSRules {
|
var defaultCors = []schema.BucketCors{{
|
||||||
|
AllowedOrigins: []string{},
|
||||||
|
AllowedMethods: []string{},
|
||||||
|
AllowedHeaders: []string{},
|
||||||
|
ExposeHeaders: []string{},
|
||||||
|
MaxAgeSeconds: nil,
|
||||||
|
}}
|
||||||
|
|
||||||
|
res := make([]schema.BucketCors, 0, len(cors.CORSRules))
|
||||||
|
for _, rule := range cors.CORSRules {
|
||||||
res = append(res, schema.BucketCors{
|
res = append(res, schema.BucketCors{
|
||||||
AllowedOrigins: rule.AllowedOrigins,
|
AllowedOrigins: rule.AllowedOrigins,
|
||||||
AllowedMethods: rule.AllowedMethods,
|
AllowedMethods: rule.AllowedMethods,
|
||||||
@ -84,9 +97,8 @@ func (b*Buckets) GetCors(w http.ResponseWriter, r *http.Request){
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if len(res) == 0 {
|
||||||
utils.ResponseError(w, err)
|
res = defaultCors
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.ResponseSuccess(w, res)
|
utils.ResponseSuccess(w, res)
|
||||||
@ -95,7 +107,6 @@ func (b*Buckets) GetCors(w http.ResponseWriter, r *http.Request){
|
|||||||
func (b*Buckets) PutCors(w http.ResponseWriter, r *http.Request){
|
func (b*Buckets) PutCors(w http.ResponseWriter, r *http.Request){
|
||||||
bucket := r.PathValue("bucket")
|
bucket := r.PathValue("bucket")
|
||||||
|
|
||||||
|
|
||||||
var body struct {
|
var body struct {
|
||||||
Rules []schema.BucketCors `json:"rules"`
|
Rules []schema.BucketCors `json:"rules"`
|
||||||
}
|
}
|
||||||
@ -105,7 +116,8 @@ func (b*Buckets) PutCors(w http.ResponseWriter, r *http.Request){
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := getS3Client(bucket)
|
perm := "owner"
|
||||||
|
client, err := getS3Client(bucket, &perm)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ResponseError(w, err)
|
utils.ResponseError(w, err)
|
||||||
@ -127,7 +139,7 @@ func (b*Buckets) PutCors(w http.ResponseWriter, r *http.Request){
|
|||||||
ExposeHeaders: utils.NilIfEmpty(rule.ExposeHeaders),
|
ExposeHeaders: utils.NilIfEmpty(rule.ExposeHeaders),
|
||||||
MaxAgeSeconds: rule.MaxAgeSeconds,
|
MaxAgeSeconds: rule.MaxAgeSeconds,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := client.PutBucketCors(context.Background(), &s3.PutBucketCorsInput{
|
res, err := client.PutBucketCors(context.Background(), &s3.PutBucketCorsInput{
|
||||||
Bucket: aws.String(bucket),
|
Bucket: aws.String(bucket),
|
||||||
|
|||||||
@ -60,3 +60,16 @@ type WebsiteConfig struct {
|
|||||||
IndexDocument string `json:"indexDocument"`
|
IndexDocument string `json:"indexDocument"`
|
||||||
ErrorDocument string `json:"errorDocument"`
|
ErrorDocument string `json:"errorDocument"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Permissions) HasPermission(perm string) bool {
|
||||||
|
switch perm {
|
||||||
|
case "read":
|
||||||
|
return p.Read
|
||||||
|
case "write":
|
||||||
|
return p.Write
|
||||||
|
case "owner":
|
||||||
|
return p.Owner
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -117,6 +117,7 @@ export const useBucketCors = (bucketName?: string) => {
|
|||||||
queryKey: ["bucket_cors", bucketName],
|
queryKey: ["bucket_cors", bucketName],
|
||||||
queryFn: () => api.get<BucketCors[]>(`/buckets/${bucketName}/cors`),
|
queryFn: () => api.get<BucketCors[]>(`/buckets/${bucketName}/cors`),
|
||||||
enabled: !!bucketName,
|
enabled: !!bucketName,
|
||||||
|
retry: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -39,7 +39,9 @@ const tabs: Tab[] = [
|
|||||||
const ManageBucketPage = () => {
|
const ManageBucketPage = () => {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { data, error, isLoading, refetch } = useBucket(id);
|
const { data, error, isLoading, refetch } = useBucket(id);
|
||||||
const { data: cors } = useBucketCors(data?.globalAliases[0]);
|
const { data: cors, isLoading: corsLoading } = useBucketCors(
|
||||||
|
data?.globalAliases[0]
|
||||||
|
);
|
||||||
|
|
||||||
const name = data?.globalAliases[0];
|
const name = data?.globalAliases[0];
|
||||||
|
|
||||||
@ -63,7 +65,7 @@ const ManageBucketPage = () => {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{data && cors && (
|
{data && !corsLoading && (
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<BucketContext.Provider
|
<BucketContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
|||||||
@ -19,7 +19,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AllowKeyDialog = ({ currentKeys }: Props) => {
|
const AllowKeyDialog = ({ currentKeys }: Props) => {
|
||||||
const { bucket } = useBucketContext();
|
const { bucket, bucketName } = useBucketContext();
|
||||||
const { dialogRef, isOpen, onOpen, onClose } = useDisclosure();
|
const { dialogRef, isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const { data: keys } = useKeys();
|
const { data: keys } = useKeys();
|
||||||
const form = useForm<AllowKeysSchema>({
|
const form = useForm<AllowKeysSchema>({
|
||||||
@ -37,6 +37,7 @@ const AllowKeyDialog = ({ currentKeys }: Props) => {
|
|||||||
onClose();
|
onClose();
|
||||||
toast.success("Key allowed!");
|
toast.success("Key allowed!");
|
||||||
queryClient.invalidateQueries({ queryKey: ["bucket", bucket.id] });
|
queryClient.invalidateQueries({ queryKey: ["bucket", bucket.id] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["bucket_cors", bucketName] });
|
||||||
},
|
},
|
||||||
onError: handleError,
|
onError: handleError,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { Card, Modal } from "react-daisyui";
|
import { Alert, Card, Modal } from "react-daisyui";
|
||||||
import Button from "@/components/ui/button";
|
import Button from "@/components/ui/button";
|
||||||
import { CheckCircle, Plus } from "lucide-react";
|
import { CheckCircle, CircleXIcon, Plus } from "lucide-react";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useBucketContext } from "../context";
|
import { useBucketContext } from "../context";
|
||||||
import { bucketCorsSchema, BucketCorsSchema } from "../schema";
|
import { bucketCorsSchema, BucketCorsSchema } from "../schema";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Path, useFieldArray, useForm, UseFormReturn } from "react-hook-form";
|
import { Path, useForm, UseFormReturn } from "react-hook-form";
|
||||||
import Input, { InputField } from "@/components/ui/input";
|
import Input, { InputField } from "@/components/ui/input";
|
||||||
import FormControl from "@/components/ui/form-control";
|
import FormControl from "@/components/ui/form-control";
|
||||||
import { useDisclosure } from "@/hooks/useDisclosure";
|
import { useDisclosure } from "@/hooks/useDisclosure";
|
||||||
@ -17,7 +17,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
|||||||
|
|
||||||
const CorsConfiguration = () => {
|
const CorsConfiguration = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { bucketName, cors } = useBucketContext();
|
const { bucketName, cors, bucket } = useBucketContext();
|
||||||
|
|
||||||
const { mutate, isPending } = useBucketCorsMutation({
|
const { mutate, isPending } = useBucketCorsMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@ -31,30 +31,18 @@ const CorsConfiguration = () => {
|
|||||||
resolver: zodResolver(bucketCorsSchema),
|
resolver: zodResolver(bucketCorsSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
bucketName: bucketName,
|
bucketName: bucketName,
|
||||||
rules:
|
rules: cors,
|
||||||
cors.length > 0
|
|
||||||
? cors
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
allowedHeaders: [],
|
|
||||||
allowedMethods: [],
|
|
||||||
allowedOrigins: [],
|
|
||||||
exposeHeaders: [],
|
|
||||||
maxAgeSeconds: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { fields } = useFieldArray({
|
const fields = form.watch("rules");
|
||||||
control: form.control,
|
|
||||||
name: "rules",
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleSubmit(data: BucketCorsSchema) {
|
function handleSubmit(data: BucketCorsSchema) {
|
||||||
mutate(data);
|
mutate(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allowConfiguration = bucket.keys.some((key) => key.permissions.owner);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="card-body">
|
<Card className="card-body">
|
||||||
<form onSubmit={form.handleSubmit(handleSubmit)}>
|
<form onSubmit={form.handleSubmit(handleSubmit)}>
|
||||||
@ -66,15 +54,24 @@ const CorsConfiguration = () => {
|
|||||||
icon={CheckCircle}
|
icon={CheckCircle}
|
||||||
color="primary"
|
color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isPending}
|
disabled={isPending || !allowConfiguration}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{fields.map((rule, idx) => (
|
{!allowConfiguration && (
|
||||||
|
<Alert status="warning" icon={<CircleXIcon />} className="mt-5">
|
||||||
|
<span>
|
||||||
|
You must configure an access key with owner permissions for this
|
||||||
|
bucket before setting up CORS.
|
||||||
|
</span>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{allowConfiguration &&
|
||||||
|
fields.map((_, idx) => (
|
||||||
<div
|
<div
|
||||||
className="grid md:grid-cols-2 xl:grid-cols-5 gap-5 mt-5"
|
className="grid md:grid-cols-2 xl:grid-cols-5 gap-5 mt-5"
|
||||||
key={rule.id}
|
key={idx}
|
||||||
>
|
>
|
||||||
<FormControl
|
<FormControl
|
||||||
form={form}
|
form={form}
|
||||||
@ -113,7 +110,6 @@ const CorsConfiguration = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
key={rule.id}
|
|
||||||
form={form}
|
form={form}
|
||||||
name={`rules.${idx}.exposeHeaders`}
|
name={`rules.${idx}.exposeHeaders`}
|
||||||
title={"Expose Headers"}
|
title={"Expose Headers"}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user