Skip to content

Commit

Permalink
fix: could have both global and column-specific privileges + is globa…
Browse files Browse the repository at this point in the history
…l per privilege
  • Loading branch information
HTMHell committed Apr 15, 2023
1 parent 1c8a3fb commit 457218a
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 84 deletions.
8 changes: 5 additions & 3 deletions studio/components/interfaces/Auth/Privileges/Privileges.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PostgresSchema, PostgresTable } from '@supabase/postgres-meta'
import { PrivilegesData } from 'data/database/privileges-query'
import React from 'react'
import { PrivilegesDataUI } from './Privileges.types'
import { arePrivilegesEqual } from './Privileges.utils'
import PrivilegesBody from './PrivilegesBody'
import PrivilegesFooter from './PrivilegesFooter'
Expand All @@ -9,6 +10,7 @@ import PrivilegesModal from './PrivilegesModal'

interface Props {
data: PrivilegesData
dataUI: PrivilegesDataUI
tables: string[]
selectedSchema: string
selectedRole: string
Expand All @@ -26,7 +28,7 @@ interface Props {
}

function Privileges(props: Props) {
const [data, setData] = React.useState<PrivilegesData>(props.data)
const [data, setData] = React.useState<PrivilegesDataUI>(props.dataUI)
const [isModalOpen, setModalOpen] = React.useState(false)

const handleChangePrivileges = (table: string, columnName: string, privileges: string[]) => {
Expand All @@ -44,14 +46,14 @@ function Privileges(props: Props) {
}))
}

const handleReset = () => setData({ ...props.data })
const handleReset = () => setData(props.dataUI)

const handleSuccess = () => {
setModalOpen(false)
props.onRefetch()
}

const hasChanges = !arePrivilegesEqual(props.data, data)
const hasChanges = !arePrivilegesEqual(props.dataUI, data)

return (
<>
Expand Down
23 changes: 23 additions & 0 deletions studio/components/interfaces/Auth/Privileges/Privileges.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export type PrivilegesDataUI = Record<string, Record<string, Record<string, PrivilegeColumnUI[]>>>

export type PrivilegeColumnUI = {
name: string
privileges: string[]
}

export type PrivilegeDataCalculation = Record<
string,
Record<
string,
Record<
string,
Record<
string,
{
columnsOn: string[]
columnsOff: string[]
}
>
>
>
>
117 changes: 89 additions & 28 deletions studio/components/interfaces/Auth/Privileges/Privileges.utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,72 @@
import { PrivilegesData } from 'data/database/privileges-query'
import { PRIVILEGE_TYPES } from './Privileges.constants'
import { PrivilegeDataCalculation, PrivilegesDataUI } from './Privileges.types'

export function arePrivilegesEqual(a: PrivilegesData, b: PrivilegesData) {
export function mapFromUIPrivilegesData(data: PrivilegesDataUI): PrivilegesData {
const result: PrivilegesData = {}

for (const schema in data) {
result[schema] = {}

for (const role in data[schema]) {
result[schema][role] = {}

for (const table in data[schema][role]) {
result[schema][role][table] = {}

for (const privilege of PRIVILEGE_TYPES) {
result[schema][role][table][privilege] = []
}

for (const column of data[schema][role][table]) {
for (const privilege of column.privileges) {
result[schema][role][table][privilege].push({
name: column.name,
isGlobal: false,
isColumnSpecific: true,
})
}
}
}
}
}

return result
}

export function mapToUIPrivilegesData(data: PrivilegesData): PrivilegesDataUI {
const result: PrivilegesDataUI = {}

for (const schema in data) {
result[schema] = {}

for (const role in data[schema]) {
result[schema][role] = {}

for (const table in data[schema][role]) {
result[schema][role][table] = []

for (const [privilege, columns] of Object.entries(data[schema][role][table])) {
for (const column of columns) {
const columnUI = result[schema][role][table].find((c) => c.name === column.name)
if (columnUI) {
columnUI.privileges.push(privilege)
} else {
result[schema][role][table].push({
name: column.name,
privileges: [privilege],
})
}
}
}
}
}
}

return result
}

export function arePrivilegesEqual(a: PrivilegesDataUI, b: PrivilegesDataUI) {
return Object.keys(a).every((schema) => {
return Object.keys(a[schema]).every((role) => {
return Object.keys(a[schema][role]).every((table) => {
Expand All @@ -20,41 +85,45 @@ export function arePrivilegesEqual(a: PrivilegesData, b: PrivilegesData) {

export function generatePrivilegesSQLQuery(
originalData: PrivilegesData,
changesData: PrivilegesData
changesData: PrivilegesDataUI
): string {
const queries: string[] = []

const originalMapped = mapPrivilegesData(originalData)
const changesMapped = mapPrivilegesData(changesData)
const changesMapped = mapCalculationPrivilegesData(changesData)

for (const schema in changesMapped) {
for (const role in changesMapped[schema]) {
for (const table in changesMapped[schema][role]) {
for (const privilege in changesMapped[schema][role][table]) {
let changes = changesMapped[schema][role][table][privilege]
let original = originalMapped[schema][role][table][privilege]
let originalColumnsOn = originalData[schema][role][table][privilege] ?? []

if (arraysEqual(changes.columnsOn, original.columnsOn)) continue
if (
arraysEqual(
changes.columnsOn,
originalColumnsOn.map((c) => c.name)
)
)
continue

if (!original.isColumnSpecific && original.columnsOn.length > 0) {
if (originalColumnsOn.some((c) => c.isGlobal)) {
queries.push(`REVOKE ${privilege} ON TABLE ${schema}.${table} FROM ${role};`)
original = {
...original,
columnsOn: [],
columnsOff: original.columnsOn,
}
originalColumnsOn = originalColumnsOn.filter(
(c) => !(c.isGlobal && !c.isColumnSpecific)
)
}

if (changes.columnsOff.length === 0 && changes.columnsOn.length > 0) {
queries.push(`GRANT ${privilege} ON TABLE ${schema}.${table} TO ${role};`)
changes = {
...changes,
columnsOn: [],
columnsOff: original.columnsOn,
columnsOff: originalColumnsOn.map((c) => c.name),
}
}

const columnsOff = changes.columnsOff.filter((c) => original.columnsOn.includes(c))
const columnsOff = changes.columnsOff.filter((c) =>
originalColumnsOn.some((o) => o.name === c)
)

if (columnsOff.length > 0) {
queries.unshift(
Expand All @@ -64,7 +133,9 @@ export function generatePrivilegesSQLQuery(
)
}

const columnsOn = changes.columnsOn.filter((c) => !original.columnsOn.includes(c))
const columnsOn = changes.columnsOn.filter(
(c) => !originalColumnsOn.some((o) => o.name === c)
)

if (columnsOn.length > 0) {
queries.push(
Expand All @@ -79,17 +150,8 @@ export function generatePrivilegesSQLQuery(
return queries.join('\n')
}

function mapPrivilegesData(data: PrivilegesData) {
const mapped: Record<
string,
Record<
string,
Record<
string,
Record<string, { columnsOn: string[]; columnsOff: string[]; isColumnSpecific: boolean }>
>
>
> = {}
function mapCalculationPrivilegesData(data: PrivilegesDataUI): PrivilegeDataCalculation {
const mapped: PrivilegeDataCalculation = {}

Object.keys(data).forEach((schema) => {
Object.keys(data[schema]).forEach((role) => {
Expand All @@ -104,7 +166,6 @@ function mapPrivilegesData(data: PrivilegesData) {
{
columnsOn: [],
columnsOff: [],
isColumnSpecific: data[schema][role][table].some((c) => c.isColumnSpecific),
},
])
)
Expand Down
14 changes: 7 additions & 7 deletions studio/components/interfaces/Auth/Privileges/PrivilegesBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import type { PostgresTable } from '@supabase/postgres-meta'
import { FC } from 'react'

import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
import { ColumnPrivileges } from 'data/database/privileges-query'
import { PrivilegeColumnUI } from './Privileges.types'
import PrivilegesTable from './PrivilegesTable'

interface Props {
privileges: Record<string, ColumnPrivileges[]>
privileges: Record<string, PrivilegeColumnUI[]>
hasChanges: boolean
table?: PostgresTable
onChange: (table: string, columnName: string, privileges: string[]) => void
Expand All @@ -15,13 +15,13 @@ interface Props {
const PrivilegesBody: FC<Props> = (props) => {
const { table } = props

const handleToggle = (tableName: string, column: ColumnPrivileges, action: string) => {
const handleToggle = (tableName: string, column: PrivilegeColumnUI, privileges: string[]) => {
props.onChange(
tableName,
column.name,
column.privileges.includes(action)
? column.privileges.filter((p) => p !== action)
: [...column.privileges, action]
column.privileges.some((p) => privileges.includes(p))
? column.privileges.filter((p) => !privileges.includes(p))
: [...new Set([...column.privileges, ...privileges])]
)
}

Expand All @@ -37,7 +37,7 @@ const PrivilegesBody: FC<Props> = (props) => {
</div>
<PrivilegesTable
columns={props.privileges[table.name]}
onToggle={(column, action) => handleToggle(table.name, column, action)}
onToggle={(column, privileges) => handleToggle(table.name, column, privileges)}
/>
</section>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte
import { PrivilegesData, PrivilegesDataResponse } from 'data/database/privileges-query'
import { useExecuteSqlMutation } from 'data/sql/execute-sql-mutation'
import { sqlKeys } from 'data/sql/keys'
import { generatePrivilegesSQLQuery } from './Privileges.utils'
import { PrivilegesDataUI } from './Privileges.types'
import { generatePrivilegesSQLQuery, mapFromUIPrivilegesData } from './Privileges.utils'
import PrivilegesReview from './PrivilegesReview'

interface Props {
visible: boolean
original: PrivilegesData
changes: PrivilegesData
changes: PrivilegesDataUI
onCancel: () => void
onSuccess: () => void
}
Expand All @@ -33,7 +34,7 @@ const PrivilegesModal: FC<Props> = (props) => {

if (previous) {
queryClient.setQueryData<PrivilegesDataResponse>(queryKey, {
result: [{ result_json: props.changes }],
result: [{ result_json: mapFromUIPrivilegesData(props.changes) }],
})
}

Expand Down

0 comments on commit 457218a

Please sign in to comment.