Ensure read only privileges in project settings pages

This commit is contained in:
Joshen Lim committed 2023-02-09 16:57:11 +08:00
1 parent c6bcdc69c4
commit bd09b45a73
12 files changed
+283 -72

No files matched your search

@@ -35,7 +35,7 @@ const BackupsList: FC<Props> = ({}) => {
icon={<IconClock size="large" />}
primaryText="Free Plan does not include project backups."
projectRef={projectRef}
secondaryText="Please upgrade to the Pro plan for up to 7 days of scheduled backups."
secondaryText="Upgrade to the Pro plan for up to 7 days of scheduled backups."
/>
)
}
@@ -1,6 +1,6 @@
import { FC, useState, useEffect, Fragment } from 'react'
import { observer } from 'mobx-react-lite'
import { useStore } from 'hooks'
import { checkPermissions, useStore } from 'hooks'
import { pluckObjectFields } from 'lib/helpers'
import Loading from 'components/ui/Loading'
import Panel from 'components/ui/Panel'
@@ -12,6 +12,7 @@ import ToggleField from 'components/to-be-cleaned/forms/ToggleField'
import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel'
import { Input } from 'ui'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { PermissionAction } from '@supabase/shared-types/out/constants'
interface Props {}
@@ -127,6 +128,11 @@ export const PgbouncerConfig: FC<ConfigProps> = observer(
({ projectRef, bouncerInfo, connectionInfo }) => {
const { ui } = useStore()
const canUpdateConnectionPoolingConfiguration = checkPermissions(
PermissionAction.UPDATE,
'projects'
)
const [updates, setUpdates] = useState<any>({
pgbouncer_enabled: bouncerInfo.pgbouncer_enabled,
pool_mode: bouncerInfo.pool_mode || 'transaction',
@@ -198,6 +204,8 @@ export const PgbouncerConfig: FC<ConfigProps> = observer(
onChangeModel={(model: any) => setUpdates(model)}
onSubmit={(model: any) => updateConfig(model)}
onReset={() => setUpdates(bouncerInfo)}
disabled={!canUpdateConnectionPoolingConfiguration}
disabledMessage="You need additional permissions to update connection pooling settings"
>
<div className="space-y-6 py-4">
<ToggleField name="pgbouncer_enabled" />
@@ -1,8 +1,10 @@
import Link from 'next/link'
import { useState } from 'react'
import { FC, useState } from 'react'
import * as Tooltip from '@radix-ui/react-tooltip'
import { Button, IconAlertCircle, IconExternalLink, IconGlobe, IconLock } from 'ui'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'hooks'
import { checkPermissions, useParams } from 'hooks'
import Panel from 'components/ui/Panel'
import { FormPanel, FormHeader } from 'components/ui/Forms'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
@@ -12,6 +14,62 @@ import DisallowAllModal from './DisallowAllModal'
import AllowAllModal from './AllowAllModal'
import { useNetworkRestrictionsQuery } from 'data/network-restrictions/network-restrictions-query'
const AllowAllAccessButton: FC<{ disabled: boolean; onClick: (value: boolean) => void }> = ({
disabled,
onClick,
}) => (
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Button type="default" disabled={disabled} onClick={() => onClick(true)}>
Allow all access
</Button>
</Tooltip.Trigger>
{disabled && (
<Tooltip.Content align="center" side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
'border border-scale-200 w-[250px]',
].join(' ')}
>
<span className="text-xs text-scale-1200">
You need additional permissions to update network restrictions
</span>
</div>
</Tooltip.Content>
)}
</Tooltip.Root>
)
const DisallowAllAccessButton: FC<{ disabled: boolean; onClick: (value: boolean) => void }> = ({
disabled,
onClick,
}) => (
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Button type="default" disabled={disabled} onClick={() => onClick(true)}>
Restrict all access
</Button>
</Tooltip.Trigger>
{disabled && (
<Tooltip.Content align="center" side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
'border border-scale-200 w-[250px]',
].join(' ')}
>
<span className="text-xs text-scale-1200">
You need additional permissions to update network restrictions
</span>
</div>
</Tooltip.Content>
)}
</Tooltip.Root>
)
const NetworkRestrictions = ({}) => {
const { ref } = useParams()
@@ -21,6 +79,8 @@ const NetworkRestrictions = ({}) => {
const [selectedRestrictionToRemove, setSelectedRestrictionToRemove] = useState<string>()
const { data, isLoading } = useNetworkRestrictionsQuery({ projectRef: ref })
const canUpdateNetworkRestrictions = checkPermissions(PermissionAction.UPDATE, 'projects')
const hasAccessToRestrictions = data?.entitlement === 'allowed'
const restrictedIps = data?.config.dbAllowedCidrs ?? []
const restrictionStatus = data?.status ?? ''
@@ -48,7 +108,31 @@ const NetworkRestrictions = ({}) => {
</Button>
</a>
</Link>
<Button onClick={() => setIsAddingAddress(true)}>Add restriction</Button>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Button
disabled={!canUpdateNetworkRestrictions}
onClick={() => setIsAddingAddress(true)}
>
Add restriction
</Button>
</Tooltip.Trigger>
{!canUpdateNetworkRestrictions && (
<Tooltip.Content align="center" side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
'border border-scale-200 w-[250px]',
].join(' ')}
>
<span className="text-xs text-scale-1200">
You need additional permissions to update network restrictions
</span>
</div>
</Tooltip.Content>
)}
</Tooltip.Root>
</div>
</div>
{isLoading ? (
@@ -75,12 +159,14 @@ const NetworkRestrictions = ({}) => {
</p>
</div>
<div className="flex items-center space-x-2">
<Button type="default" onClick={() => setIsAllowingAll(true)}>
Allow all access
</Button>
<Button type="default" onClick={() => setIsDisallowingAll(true)}>
Restrict all access
</Button>
<AllowAllAccessButton
disabled={!canUpdateNetworkRestrictions}
onClick={setIsAllowingAll}
/>
<DisallowAllAccessButton
disabled={!canUpdateNetworkRestrictions}
onClick={setIsDisallowingAll}
/>
</div>
</div>
</Panel.Content>
@@ -101,9 +187,10 @@ const NetworkRestrictions = ({}) => {
</div>
</div>
<div>
<Button type="default" onClick={() => setIsDisallowingAll(true)}>
Restrict all access
</Button>
<DisallowAllAccessButton
disabled={!canUpdateNetworkRestrictions}
onClick={setIsDisallowingAll}
/>
</div>
</div>
) : isDisallowedAll ? (
@@ -125,9 +212,10 @@ const NetworkRestrictions = ({}) => {
</div>
</div>
<div>
<Button type="default" onClick={() => setIsAllowingAll(true)}>
Allow all access
</Button>
<AllowAllAccessButton
disabled={!canUpdateNetworkRestrictions}
onClick={setIsAllowingAll}
/>
</div>
</div>
) : (
@@ -146,12 +234,14 @@ const NetworkRestrictions = ({}) => {
</p>
</div>
<div className="flex items-center space-x-2">
<Button type="default" onClick={() => setIsAllowingAll(true)}>
Allow all access
</Button>
<Button type="default" onClick={() => setIsDisallowingAll(true)}>
Restrict all access
</Button>
<AllowAllAccessButton
disabled={!canUpdateNetworkRestrictions}
onClick={setIsAllowingAll}
/>
<DisallowAllAccessButton
disabled={!canUpdateNetworkRestrictions}
onClick={setIsDisallowingAll}
/>
</div>
</div>
{restrictedIps.map((ip) => {
@@ -1,8 +1,10 @@
import Link from 'next/link'
import { useState, useEffect } from 'react'
import { useParams, useStore } from 'hooks'
import { checkPermissions, useParams, useStore } from 'hooks'
import * as Tooltip from '@radix-ui/react-tooltip'
import { Button, IconDownload, Toggle, IconExternalLink, IconLoader, Alert } from 'ui'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import {
FormHeader,
FormPanel,
@@ -25,6 +27,8 @@ const SSLConfiguration = () => {
})
const { mutateAsync: updateSSLEnforcement } = useSSLEnforcementUpdateMutation()
const canUpdateSSLEnforcement = checkPermissions(PermissionAction.UPDATE, 'projects')
const hasAccessToSSLEnforcement = !sslEnforcementConfiguration?.isNotAllowed
const env = process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod' ? 'prod' : 'staging'
const hasSSLCertificate =
@@ -115,11 +119,30 @@ const SSLConfiguration = () => {
{(isLoading || isSubmitting) && (
<IconLoader className="animate-spin" strokeWidth={1.5} size={16} />
)}
<Toggle
checked={isEnforced}
disabled={isLoading || isSubmitting}
onChange={toggleSSLEnforcement}
/>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Toggle
checked={isEnforced}
disabled={!canUpdateSSLEnforcement || isLoading || isSubmitting}
onChange={toggleSSLEnforcement}
/>
</Tooltip.Trigger>
{!canUpdateSSLEnforcement && (
<Tooltip.Content align="center" side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
'border border-scale-200 w-[250px]',
].join(' ')}
>
<span className="text-xs text-scale-1200">
You need additional permissions to update SSL configurations
</span>
</div>
</Tooltip.Content>
)}
</Tooltip.Root>
</div>
</FormSectionContent>
</FormSection>
@@ -1,7 +1,10 @@
import { FC, useEffect } from 'react'
import { useRouter } from 'next/router'
import { Badge, Button, IconAlertCircle, IconInfo, Loading } from 'ui'
import * as Tooltip from '@radix-ui/react-tooltip'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useStore } from 'hooks'
import { checkPermissions, useStore } from 'hooks'
import { formatBytes } from 'lib/helpers'
import { PRICING_TIER_PRODUCT_IDS, USAGE_APPROACHING_THRESHOLD } from 'lib/constants'
import SparkBar from 'components/ui/SparkBar'
@@ -9,8 +12,6 @@ import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import InformationBox from 'components/ui/InformationBox'
import { USAGE_BASED_PRODUCTS } from 'components/interfaces/Billing/Billing.constants'
import { ProjectUsageResponse, useProjectUsageQuery } from 'data/usage/project-usage-query'
import { useRouter } from 'next/router'
import * as Tooltip from '@radix-ui/react-tooltip'
interface Props {
projectRef?: string
@@ -21,6 +22,11 @@ const ProjectUsage: FC<Props> = ({ projectRef }) => {
const { data: usage, error, isLoading } = useProjectUsageQuery({ projectRef })
const router = useRouter()
const canUpdateSubscription = checkPermissions(
PermissionAction.BILLING_WRITE,
'stripe.subscriptions'
)
const subscriptionTier = ui.selectedProject?.subscription_tier
const projectHasNoLimits =
@@ -128,14 +134,16 @@ const ProjectUsage: FC<Props> = ({ projectRef }) => {
usageElement = (
<div className="flex items-center justify-between">
<span>Not included in {planName} tier</span>
<Button
size="tiny"
onClick={() =>
router.push(`/project/${projectRef}/settings/billing/update`)
}
>
Upgrade to Pro
</Button>
{canUpdateSubscription && (
<Button
size="tiny"
onClick={() =>
router.push(`/project/${projectRef}/settings/billing/update`)
}
>
Upgrade to Pro
</Button>
)}
</div>
)
} else if (showUsageExceedMessage) {
@@ -1,12 +1,14 @@
import { useState } from 'react'
import * as Tooltip from '@radix-ui/react-tooltip'
import { Button, Form, IconClock, Input, Listbox } from 'ui'
import { useStore } from 'hooks'
import { checkPermissions, useStore } from 'hooks'
import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query'
import { useProjectStorageConfigUpdateUpdateMutation } from 'data/config/project-storage-config-update-mutation'
import UpgradeToPro from 'components/ui/UpgradeToPro'
import { convertFromBytes, convertToBytes } from './StorageSettings.utils'
import { StorageSizeUnits, STORAGE_FILE_SIZE_LIMIT_MAX_BYTES } from './StorageSettings.constants'
import { PermissionAction } from '@supabase/shared-types/out/constants'
export type StorageSettingsProps = {
projectRef: string | undefined
@@ -42,6 +44,8 @@ const StorageConfig = ({ config, projectRef }: any) => {
const [selectedUnit, setSelectedUnit] = useState(unit)
let initialValues = { fileSizeLimit: value, unformattedFileSizeLimit: fileSizeLimit }
const canUpdateStorageSettings = checkPermissions(PermissionAction.UPDATE, 'projects')
const formattedMaxSizeBytes = `${new Intl.NumberFormat('en-US').format(
STORAGE_FILE_SIZE_LIMIT_MAX_BYTES
)} bytes`
@@ -184,13 +188,20 @@ const StorageConfig = ({ config, projectRef }: any) => {
icon={<IconClock size="large" />}
primaryText="Free Plan has a fixed upload file size limit of 50 MB."
projectRef={projectRef}
secondaryText="Please upgrade to Pro plan for a configurable upload file size limit of up to 5 GB."
secondaryText="Upgrade to the Pro plan for a configurable upload file size limit of up to 5 GB."
/>
</div>
)}
<div className="border-t border-scale-400" />
<div className="flex justify-between px-8 py-4">
<div className="flex items-center justify-end w-full gap-2">
<div className="flex items-center justify-between w-full gap-2">
{!canUpdateStorageSettings ? (
<p className="text-sm text-scale-1000">
You need additional permissions to update storage settings
</p>
) : (
<div />
)}
<div className="flex gap-2">
<Button
type="default"
@@ -204,7 +215,9 @@ const StorageConfig = ({ config, projectRef }: any) => {
type="primary"
htmlType="submit"
loading={isSubmitting}
disabled={!hasChanges && hasChanges !== undefined}
disabled={
!canUpdateStorageSettings || (!hasChanges && hasChanges !== undefined)
}
>
Save
</Button>
@@ -2,10 +2,19 @@ import { AutoForm } from 'uniforms-bootstrap4'
import Ajv from 'ajv'
import { JSONSchemaBridge } from 'uniforms-bridge-json-schema'
const SchemaForm = ({ schema, model, children, formRef, onSubmit, onChangeModel = () => {} }) => {
const SchemaForm = ({
schema,
model,
disabled = false,
children,
formRef,
onSubmit,
onChangeModel = () => {},
}) => {
const validatedSchema = new JSONSchemaBridge(schema, createValidator(schema))
return (
<AutoForm
disabled={disabled}
onChangeModel={onChangeModel}
ref={formRef}
schema={validatedSchema}
@@ -1,6 +1,7 @@
import SchemaForm from './SchemaForm'
import React, { useState } from 'react'
import { Button } from 'ui'
import * as Tooltip from '@radix-ui/react-tooltip'
import SchemaForm from './SchemaForm'
export default function SchemaFormPanel({
schema,
@@ -13,6 +14,8 @@ export default function SchemaFormPanel({
submitLabel = 'Save',
cancelLabel = 'Cancel',
message = '',
disabled = false,
disabledMessage = '',
}) {
let formRef
const [submitButtonLoading, setSubmitButtonLoading] = useState(false)
@@ -48,29 +51,34 @@ export default function SchemaFormPanel({
<div className="flex-1 text-left">
<h6>{title || ''}</h6>
</div>
<div
className={`flex transition duration-150 ${
hasChanged ? 'opacity-100' : 'cursor-default opacity-0'
}`}
>
<Button onClick={onClickCancel} type="default" disabled={!hasChanged}>
{cancelLabel || 'Cancel'}
</Button>
<Button
onClick={onClickSubmit}
loading={submitButtonLoading}
disabled={!hasChanged}
type="primary"
className="ml-2 hover:border-green-500"
{disabled && disabledMessage ? (
<p className="text-sm text-scale-1000">{disabledMessage}</p>
) : (
<div
className={`flex transition duration-150 ${
hasChanged ? 'opacity-100' : 'cursor-default opacity-0'
}`}
>
{submitLabel || 'Save'}
</Button>
</div>
<Button onClick={onClickCancel} type="default" disabled={!hasChanged}>
{cancelLabel || 'Cancel'}
</Button>
<Button
onClick={onClickSubmit}
loading={submitButtonLoading}
disabled={disabled || !hasChanged}
type="primary"
className="ml-2 hover:border-green-500"
>
{submitLabel || 'Save'}
</Button>
</div>
)}
<span className="text-sm text-scale-900">{message}</span>
</div>
<div className="Form section-block--body px-6 py-3">
<SchemaForm
disabled={disabled}
onChangeModel={(model) => {
setHasChanged(true)
if (onChangeModel) onChangeModel(model)
+57 -7
View File
@@ -2,8 +2,11 @@ import { Button } from 'ui'
import { observer } from 'mobx-react-lite'
import Link from 'next/link'
import { FC, ReactNode } from 'react'
import { useStore } from 'hooks'
import * as Tooltip from '@radix-ui/react-tooltip'
import { checkPermissions, useStore, useFlag, useParams } from 'hooks'
import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
import { PermissionAction } from '@supabase/shared-types/out/constants'
interface Props {
icon?: ReactNode
@@ -14,8 +17,16 @@ interface Props {
const UpgradeToPro: FC<Props> = ({ icon, primaryText, projectRef, secondaryText }) => {
const { ui } = useStore()
const { ref } = useParams()
const tier = ui.selectedProject?.subscription_tier
const canUpdateSubscription = checkPermissions(
PermissionAction.BILLING_WRITE,
'stripe.subscriptions'
)
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
const isEnterprise = tier === PRICING_TIER_PRODUCT_IDS.ENTERPRISE
return (
<div
className={[
@@ -33,13 +44,52 @@ const UpgradeToPro: FC<Props> = ({ icon, primaryText, projectRef, secondaryText
<p className="text-sm text-scale-1100">{secondaryText}</p>
</div>
</div>
<Link href={`/project/${projectRef}/settings/billing/update`}>
<a>
<Button type="primary">
{tier === PRICING_TIER_PRODUCT_IDS.FREE ? 'Upgrade to Pro' : 'Modify subscription'}
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Button type="primary" disabled={!canUpdateSubscription || projectUpdateDisabled}>
<Link
href={
isEnterprise
? `/project/${ref}/settings/billing/update/enterprise`
: `/project/${ref}/settings/billing/update`
}
>
<a>
{tier === PRICING_TIER_PRODUCT_IDS.FREE
? 'Upgrade to Pro'
: 'Modify subscription'}
</a>
</Link>
</Button>
</a>
</Link>
</Tooltip.Trigger>
{!canUpdateSubscription || projectUpdateDisabled ? (
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'border border-scale-200 text-center', //border
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
].join(' ')}
>
<span className="text-xs text-scale-1200">
{projectUpdateDisabled ? (
<>
Subscription changes are currently disabled.
<br />
Our engineers are working on a fix.
</>
) : !canUpdateSubscription ? (
'You need additional permissions to amend subscriptions'
) : (
''
)}
</span>
</div>
</Tooltip.Content>
) : (
<></>
)}
</Tooltip.Root>
</div>
</div>
</div>
@@ -65,7 +65,7 @@ const PITR = () => {
primaryText="Point in time recovery is a Pro plan add-on."
secondaryText={
tier === PRICING_TIER_PRODUCT_IDS.FREE
? 'Please upgrade to the Pro plan with the PITR add-on selected to enable point in time recovery for your project.'
? 'Upgrade to the Pro plan with the PITR add-on selected to enable point in time recovery for your project.'
: 'Please enable the add-on to enable point in time recovery for your project.'
}
/>
@@ -36,7 +36,6 @@ interface SettingsProps {
const Settings: FC<SettingsProps> = ({ project }) => {
const { ui } = useStore()
const projectTier = ui.selectedProject?.subscription_tier
const {
data: subscription,
+3
View File
@@ -39,6 +39,9 @@ Sentry.init({
// Safe to ignore since it's not a user-facing issue + we've not received any user feedback/report about it
// Ref: https://github.com/supabase/supabase/pull/9729
'The provided `href` (/org/[slug]/general) value is missing query values (slug)',
'The provided `href` (/org/[slug]/team) value is missing query values (slug)',
'The provided `href` (/org/[slug]/billing) value is missing query values (slug)',
'The provided `href` (/org/[slug]/invoices) value is missing query values (slug)',
],
beforeSend: (event) => filterConsoleErrors(event),
})