Skip to content

Commit

Permalink
Plugins access control (#3486)
Browse files Browse the repository at this point in the history
* Add Organization.PluginsAccess

* Rename PluginsAccess to PluginsAccessLevel

* Use Organization.plugins_access_level in can_…_plugins_via_api

* Add migration for Organization.plugins_access_level

* Remove unused PLUGINS_CLOUD_WHITELISTED_ORG_IDS

* Update access.py

* Add OrganizationPluginsAccessLevel TS enum

* Fix merge

* Disable LocalPlugin UI on Cloud

* Move away from PluginAccess interface

* Extend PluginsAccessLevel range

* Refactor PluginsAccessLevel for brevity

* Remove PluginAccess interface completely

* Add plugins managed globally

* Update migration

* Show managing org name in "Managed" plugin tag

* Smoothen some rough edges

* Smoothen more edges

* Restore correct MULTI_TENANCY default

* All the edges

* Fix most existing tests

* Remove PLUGINS_*_VIA_API env var support

* Update pluginsNeedingUpdates

* Remove can_*_plugins_via_api from instance status page

* Add tests and polish permissioning

* Update migration

* Fix typing

* Make plugin drawer UI less intrusive

* Update migration

* Fix Uninstall button condition

* Use unified _preflight status endpoint instead of the custom plugins one

* Fix plugin update label condition

* Fix "Check for updates" button condition

* Explain PluginsAccessLevel choices with comments

* Hide global plugin installation option on self-hosted

* Don't actions.loadRepository() as install org

* Improve permissioning with tests

* Satisfy mypy

* Add plugins access level to admin and fix org admin

* Check plugins access level more

* Rename endWithPeriod

* Refactor FE access control checks to accessControl.ts

* Deduplicate permissioning

* Add exception message

* Align backend and frontend plugins access level helpers

* Add plugins access level helper tests

* Fix ChartFilter
  • Loading branch information
Twixes authored Mar 17, 2021
1 parent d3a999a commit 3c0737f
Show file tree
Hide file tree
Showing 37 changed files with 1,168 additions and 711 deletions.
7 changes: 2 additions & 5 deletions cypress/fixtures/api/user.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"name": "Foo",
"billing_plan": null,
"available_features": [],
"plugins_access_level": 9,
"created_at": "2021-02-05T13:55:31.362Z",
"updated_at": "2021-02-05T13:55:44.244Z",
"teams": [
Expand Down Expand Up @@ -452,9 +453,5 @@
"email_service_available": false,
"is_debug": true,
"is_staff": false,
"is_impersonated": false,
"plugin_access": {
"install": true,
"configure": true
}
"is_impersonated": false
}
3 changes: 0 additions & 3 deletions ee/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@
"SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS"
].split(",")

# TODO: eliminate this setting for full rollout of Plugins on EE/Cloud
PLUGINS_CLOUD_WHITELISTED_ORG_IDS: List[str] = os.getenv("PLUGINS_CLOUD_WHITELISTED_ORG_IDS", "").split(",")

# ClickHouse and Kafka
CLICKHOUSE_DENORMALIZED_PROPERTIES = os.getenv("CLICKHOUSE_DENORMALIZED_PROPERTIES", "").split(",")
KAFKA_ENABLED = PRIMARY_DB == RDBMS.CLICKHOUSE and not TEST
5 changes: 3 additions & 2 deletions frontend/src/layout/navigation/MainNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { dashboardsModel } from '~/models'
import { DashboardType } from '~/types'
import { userLogic } from 'scenes/userLogic'
import { organizationLogic } from 'scenes/organizationLogic'
import { canViewPlugins } from '../../scenes/plugins/access'

// to show the right page in the sidebar
const sceneOverride: Record<string, string> = {
Expand Down Expand Up @@ -230,9 +231,9 @@ export function MainNavigation(): JSX.Element {
to="/feature_flags"
/>
<div className="divider" />
{user?.plugin_access.configure ? (
{canViewPlugins(user?.organization) && (
<MenuItem title="Plugins" icon={<ApiFilled />} identifier="plugins" to="/project/plugins" />
) : null}
)}
<MenuItem
title="Annotations"
icon={<MessageOutlined />}
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/lib/components/ChartFilter/ChartFilter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { Select } from 'antd'
import {
ACTIONS_LINE_GRAPH_LINEAR,
ACTIONS_LINE_GRAPH_CUMULATIVE,
STICKINESS,
ACTIONS_PIE_CHART,
ACTIONS_BAR_CHART,
ACTIONS_TABLE,
FUNNEL_VIZ,
ACTIONS_BAR_CHART_VALUE,
ShownAsValue,
} from '~/lib/constants'
import { chartFilterLogic } from './chartFilterLogic'
import { ViewType } from 'scenes/insights/insightLogic'
Expand All @@ -22,11 +22,12 @@ export function ChartFilter(props) {

const linearDisabled = filters.session && filters.session === 'dist'
const cumulativeDisabled =
filters.session || filters.shown_as === STICKINESS || filters.insight === ViewType.RETENTION
filters.session || filters.shown_as === ShownAsValue.STICKINESS || filters.insight === ViewType.RETENTION
const tableDisabled = false
const pieDisabled = filters.session || filters.insight === ViewType.RETENTION
const barDisabled = filters.session || filters.insight === ViewType.RETENTION
const barValueDisabled = barDisabled || filters.shown_as === STICKINESS || filters.insight === ViewType.RETENTION
const barValueDisabled =
barDisabled || filters.shown_as === ShownAsValue.STICKINESS || filters.insight === ViewType.RETENTION
const defaultDisplay =
filters.insight === ViewType.RETENTION
? ACTIONS_TABLE
Expand Down
12 changes: 8 additions & 4 deletions frontend/src/lib/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@ export const ACTIONS_BAR_CHART_VALUE = 'ActionsBarValue'
export const PATHS_VIZ = 'PathsViz'
export const FUNNEL_VIZ = 'FunnelViz'

export const VOLUME = 'Volume'
export const STICKINESS = 'Stickiness'
export const LIFECYCLE = 'Lifecycle'

export enum OrganizationMembershipLevel {
Member = 1,
Admin = 8,
Owner = 15,
}

/** See posthog/api/organization.py for details. */
export enum PluginsAccessLevel {
None = 0,
Config = 3,
Install = 6,
Root = 9,
}

export const organizationMembershipLevelToName = new Map<number, string>([
[OrganizationMembershipLevel.Member, 'member'],
[OrganizationMembershipLevel.Admin, 'administrator'],
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/lib/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
capitalizeFirstLetter,
compactNumber,
pluralize,
endWithPunctation,
} from './utils'

describe('capitalizeFirstLetter()', () => {
Expand Down Expand Up @@ -121,3 +122,14 @@ describe('pluralize()', () => {
expect(pluralize(3, 'word', null, false)).toEqual('words')
})
})

describe('endWithPunctation()', () => {
it('adds period at the end when needed', () => {
expect(endWithPunctation('Hello')).toEqual('Hello.')
expect(endWithPunctation('Learn more! ')).toEqual('Learn more!')
expect(endWithPunctation('Stop.')).toEqual('Stop.')
expect(endWithPunctation(null)).toEqual('')
expect(endWithPunctation(' ')).toEqual('')
expect(endWithPunctation(' Why? ')).toEqual('Why?')
})
})
11 changes: 11 additions & 0 deletions frontend/src/lib/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -747,3 +747,14 @@ export function sortedKeys(object: Record<string, any>): Record<string, any> {
}
return newObject
}

export function endWithPunctation(text?: string | null): string {
let trimmedText = text?.trim()
if (!trimmedText) {
return ''
}
if (!/[.!?]$/.test(trimmedText)) {
trimmedText += '.'
}
return trimmedText
}
11 changes: 7 additions & 4 deletions frontend/src/scenes/insights/BreakdownFilter.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { propertyFilterLogic } from 'lib/components/PropertyFilters/propertyFilt
import { cohortsModel } from '../../models/cohortsModel'
import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo'
import { SelectGradientOverflow } from 'lib/components/SelectGradientOverflow'
import { LIFECYCLE, STICKINESS } from 'lib/constants'
import { ShownAsValue } from 'lib/constants'

const { TabPane } = Tabs

Expand Down Expand Up @@ -144,16 +144,19 @@ export function BreakdownFilter({ filters, onChange }) {
}}
/>
}
trigger={shown_as === STICKINESS || shown_as === LIFECYCLE ? 'none' : 'click'}
trigger={shown_as === ShownAsValue.STICKINESS || shown_as === ShownAsValue.LIFECYCLE ? 'none' : 'click'}
placement="bottomLeft"
>
<Tooltip
title={shown_as === STICKINESS && 'Break down by is not yet available in combination with Stickiness'}
title={
shown_as === ShownAsValue.STICKINESS &&
'Break down by is not yet available in combination with Stickiness'
}
>
<Button
shape="round"
type={breakdown ? 'primary' : 'default'}
disabled={shown_as === STICKINESS || shown_as === LIFECYCLE}
disabled={shown_as === ShownAsValue.STICKINESS || shown_as === ShownAsValue.LIFECYCLE}
data-attr="add-breakdown-button"
>
{label || 'Add breakdown'}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/scenes/insights/InsightTabs/TrendTab/Formula.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'
import { Input } from 'antd'
import { FilterType } from '~/types'
import { LIFECYCLE, STICKINESS } from 'lib/constants'
import { ShownAsValue } from 'lib/constants'

export function Formula({
filters,
Expand All @@ -21,7 +21,7 @@ export function Formula({
allowClear
value={value}
onChange={(e) => setValue(e.target.value.toLocaleUpperCase())}
disabled={filters.shown_as === STICKINESS || filters.shown_as === LIFECYCLE}
disabled={filters.shown_as === ShownAsValue.STICKINESS || filters.shown_as === ShownAsValue.LIFECYCLE}
enterButton="Apply"
onSearch={onChange}
/>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/scenes/insights/Insights.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import {
ACTIONS_TABLE,
ACTIONS_PIE_CHART,
ACTIONS_BAR_CHART_VALUE,
LIFECYCLE,
FUNNEL_VIZ,
ShownAsValue,
} from 'lib/constants'
import { annotationsLogic } from '~/lib/components/Annotations'
import { router } from 'kea-router'
Expand Down Expand Up @@ -221,7 +221,7 @@ export function Insights() {
}
}}
filters={allFilters}
disabled={allFilters.shown_as === LIFECYCLE}
disabled={allFilters.shown_as === ShownAsValue.LIFECYCLE}
/>
)}

Expand Down
20 changes: 11 additions & 9 deletions frontend/src/scenes/insights/ShownAsFilter.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import { Select, Row, Tooltip } from 'antd'
import { ACTIONS_BAR_CHART, LIFECYCLE, STICKINESS, VOLUME } from 'lib/constants'
import { ACTIONS_BAR_CHART, ShownAsValue } from 'lib/constants'

export function ShownAsFilter({ filters, onChange }) {
return (
Expand All @@ -9,25 +9,27 @@ export function ShownAsFilter({ filters, onChange }) {
<Tooltip title={filters.breakdown && 'Shown as is not yet available in combination with breakdown'}>
<Select
defaultValue={filters.shown_as}
value={filters.shown_as || VOLUME}
value={filters.shown_as || ShownAsValue.VOLUME}
onChange={(value) =>
onChange({
shown_as: value,
...(value === LIFECYCLE ? { display: ACTIONS_BAR_CHART, formula: '' } : {}),
...(value === ShownAsValue.LIFECYCLE
? { display: ACTIONS_BAR_CHART, formula: '' }
: {}),
})
}
style={{ width: 200 }}
disabled={filters.breakdown}
data-attr="shownas-filter"
>
<Select.Option data-attr="shownas-volume-option" value={VOLUME}>
{VOLUME}
<Select.Option data-attr="shownas-volume-option" value={ShownAsValue.VOLUME}>
{ShownAsValue.VOLUME}
</Select.Option>
<Select.Option data-attr="shownas-stickiness-option" value={STICKINESS}>
{STICKINESS}
<Select.Option data-attr="shownas-stickiness-option" value={ShownAsValue.STICKINESS}>
{ShownAsValue.STICKINESS}
</Select.Option>
<Select.Option data-attr="shownas-lifecycle-option" value={LIFECYCLE}>
{LIFECYCLE}
<Select.Option data-attr="shownas-lifecycle-option" value={ShownAsValue.LIFECYCLE}>
{ShownAsValue.LIFECYCLE}
</Select.Option>
</Select>
</Tooltip>
Expand Down
21 changes: 12 additions & 9 deletions frontend/src/scenes/plugins/Plugins.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,28 @@ import { InstalledTab } from 'scenes/plugins/tabs/installed/InstalledTab'
import { useActions, useValues } from 'kea'
import { userLogic } from 'scenes/userLogic'
import { pluginsLogic } from './pluginsLogic'
import { Tabs, Tag } from 'antd'
import { Spin, Tabs, Tag } from 'antd'
import { OptInPlugins } from 'scenes/plugins/optin/OptInPlugins'
import { PageHeader } from 'lib/components/PageHeader'
import { PluginTab } from 'scenes/plugins/types'
import { AdvancedTab } from 'scenes/plugins/tabs/advanced/AdvancedTab'
import { canGloballyManagePlugins, canInstallPlugins, canViewPlugins } from './access'

export function Plugins(): JSX.Element {
export function Plugins(): JSX.Element | null {
const { user } = useValues(userLogic)
const { pluginTab } = useValues(pluginsLogic)
const { setPluginTab } = useActions(pluginsLogic)
const { TabPane } = Tabs

if (!user) {
return <div />
return <Spin />
}

if (!user.plugin_access.configure) {
if (!canViewPlugins(user.organization)) {
useEffect(() => {
window.location.href = '/'
}, [])
return <div />
return null
}

return (
Expand All @@ -47,14 +48,16 @@ export function Plugins(): JSX.Element {

{user.team?.plugins_opt_in ? (
<>
{user.plugin_access.install ? (
{canInstallPlugins(user.organization) ? (
<Tabs activeKey={pluginTab} onChange={(activeKey) => setPluginTab(activeKey as PluginTab)}>
<TabPane tab="Installed" key={PluginTab.Installed}>
<InstalledTab />
</TabPane>
<TabPane tab="Repository" key={PluginTab.Repository}>
<RepositoryTab />
</TabPane>
{canGloballyManagePlugins(user.organization) && (
<TabPane tab="Repository" key={PluginTab.Repository}>
<RepositoryTab />
</TabPane>
)}
<TabPane tab="Advanced" key={PluginTab.Advanced}>
<AdvancedTab />
</TabPane>
Expand Down
36 changes: 36 additions & 0 deletions frontend/src/scenes/plugins/access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { PluginsAccessLevel } from '../../lib/constants'
import { OrganizationType } from '../../types'

export function canGloballyManagePlugins(organization: OrganizationType | null | undefined): boolean {
if (!organization) {
return false
}
return organization.plugins_access_level >= PluginsAccessLevel.Root
}

export function canInstallPlugins(
organization: OrganizationType | null | undefined,
specificOrganizationId?: string
): boolean {
if (!organization) {
return false
}
if (specificOrganizationId && organization.id !== specificOrganizationId) {
return false
}
return organization.plugins_access_level >= PluginsAccessLevel.Install
}

export function canConfigurePlugins(organization: OrganizationType | null | undefined): boolean {
if (!organization) {
return false
}
return organization.plugins_access_level >= PluginsAccessLevel.Config
}

export function canViewPlugins(organization: OrganizationType | null | undefined): boolean {
if (!organization) {
return false
}
return organization.plugins_access_level > PluginsAccessLevel.None
}
Loading

0 comments on commit 3c0737f

Please sign in to comment.