feat: Add access control to activity logs (#40801)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
@@ -79,6 +79,12 @@ class AccessControlSerializer(serializers.ModelSerializer):
|
||||
f"Access level cannot be set below the minimum '{min_level}' for {resource}."
|
||||
)
|
||||
|
||||
max_level = highest_access_level(resource)
|
||||
if levels.index(access_level) > levels.index(max_level):
|
||||
raise serializers.ValidationError(
|
||||
f"Access level cannot be set above the maximum '{max_level}' for {resource}."
|
||||
)
|
||||
|
||||
return access_level
|
||||
|
||||
def validate(self, data):
|
||||
@@ -219,6 +225,7 @@ class AccessControlViewSetMixin(_GenericViewSet):
|
||||
else ordered_access_levels(resource),
|
||||
"default_access_level": "editor" if is_resource_level else default_access_level(resource),
|
||||
"minimum_access_level": minimum_access_level(resource) if not is_resource_level else "none",
|
||||
"maximum_access_level": highest_access_level(resource) if not is_resource_level else "manager",
|
||||
"user_access_level": user_access_level,
|
||||
"user_can_edit_access_levels": user_access_control.check_can_modify_access_levels_for_object(obj),
|
||||
}
|
||||
|
||||
@@ -146,6 +146,42 @@ class TestAccessControlMinimumLevelValidation(BaseAccessControlTest):
|
||||
)
|
||||
assert res.status_code == status.HTTP_200_OK, f"Failed for level {level}: {res.json()}"
|
||||
|
||||
def test_activity_log_access_level_cannot_be_above_viewer(self):
|
||||
"""Test that activity_log access level cannot be set above maximum 'viewer'"""
|
||||
self._org_membership(OrganizationMembership.Level.ADMIN)
|
||||
|
||||
for level in ["editor", "manager"]:
|
||||
res = self.client.put(
|
||||
"/api/projects/@current/resource_access_controls",
|
||||
{"resource": "activity_log", "access_level": level},
|
||||
)
|
||||
assert res.status_code == status.HTTP_400_BAD_REQUEST, f"Failed for level {level}: {res.json()}"
|
||||
assert "cannot be set above the maximum 'viewer'" in res.json()["detail"]
|
||||
|
||||
def test_activity_log_access_restricted_for_users_without_access(self):
|
||||
"""Test that users without access to activity_log cannot access activity log endpoints"""
|
||||
self._org_membership(OrganizationMembership.Level.ADMIN)
|
||||
|
||||
res = self.client.put(
|
||||
"/api/projects/@current/resource_access_controls",
|
||||
{"resource": "activity_log", "access_level": "none"},
|
||||
)
|
||||
assert res.status_code == status.HTTP_200_OK, f"Failed to set access control: {res.json()}"
|
||||
|
||||
from ee.models.rbac.access_control import AccessControl
|
||||
|
||||
ac = AccessControl.objects.filter(team=self.team, resource="activity_log", resource_id=None).first()
|
||||
assert ac is not None, "Access control was not created"
|
||||
assert ac.access_level == "none", f"Access level is {ac.access_level}, expected 'none'"
|
||||
|
||||
self._org_membership(OrganizationMembership.Level.MEMBER)
|
||||
|
||||
res = self.client.get("/api/projects/@current/activity_log/")
|
||||
assert res.status_code == status.HTTP_403_FORBIDDEN, f"Expected 403, got {res.status_code}: {res.json()}"
|
||||
|
||||
res = self.client.get("/api/projects/@current/advanced_activity_logs/")
|
||||
assert res.status_code == status.HTTP_403_FORBIDDEN, f"Expected 403, got {res.status_code}: {res.json()}"
|
||||
|
||||
|
||||
class TestAccessControlResourceLevelAPI(BaseAccessControlTest):
|
||||
def setUp(self):
|
||||
@@ -186,6 +222,7 @@ class TestAccessControlResourceLevelAPI(BaseAccessControlTest):
|
||||
"default_access_level": "editor",
|
||||
"user_can_edit_access_levels": True,
|
||||
"minimum_access_level": "none",
|
||||
"maximum_access_level": "manager",
|
||||
}
|
||||
|
||||
def test_change_rejected_if_not_org_admin(self):
|
||||
|
||||
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 47 KiB |
@@ -18,7 +18,7 @@ import {
|
||||
import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini'
|
||||
import { UserSelectItem } from 'lib/components/UserSelectItem'
|
||||
import { fullName } from 'lib/utils'
|
||||
import { getMinimumAccessLevel, pluralizeResource } from 'lib/utils/accessControlUtils'
|
||||
import { getMaximumAccessLevel, getMinimumAccessLevel, pluralizeResource } from 'lib/utils/accessControlUtils'
|
||||
|
||||
import { APIScopeObject, AccessControlLevel, AvailableFeature } from '~/types'
|
||||
|
||||
@@ -514,10 +514,12 @@ function ResourceAccessControlModal(props: {
|
||||
resource: APIScopeObject
|
||||
): { value: AccessControlLevel | null; label: string; disabledReason?: string }[] => {
|
||||
const minimumLevel = getMinimumAccessLevel(resource)
|
||||
const maxLevel = getMaximumAccessLevel(resource)
|
||||
const options: { value: AccessControlLevel | null; label: string; disabledReason?: string }[] =
|
||||
availableLevels.map((level) => {
|
||||
const isDisabled =
|
||||
minimumLevel && availableLevels.indexOf(level) < availableLevels.indexOf(minimumLevel)
|
||||
(minimumLevel && availableLevels.indexOf(level) < availableLevels.indexOf(minimumLevel)) ||
|
||||
(maxLevel && availableLevels.indexOf(level) > availableLevels.indexOf(maxLevel))
|
||||
return {
|
||||
value: level as AccessControlLevel,
|
||||
label: capitalizeFirstLetter(level ?? ''),
|
||||
@@ -679,10 +681,12 @@ function DefaultResourceAccessControlModal(props: {
|
||||
resource: APIScopeObject
|
||||
): { value: AccessControlLevel | null; label: string; disabledReason?: string }[] => {
|
||||
const minimumLevel = getMinimumAccessLevel(resource)
|
||||
const maxLevel = getMaximumAccessLevel(resource)
|
||||
const options: { value: AccessControlLevel | null; label: string; disabledReason?: string }[] =
|
||||
availableLevels.map((level) => {
|
||||
const isDisabled =
|
||||
minimumLevel && availableLevels.indexOf(level) < availableLevels.indexOf(minimumLevel)
|
||||
(minimumLevel && availableLevels.indexOf(level) < availableLevels.indexOf(minimumLevel)) ||
|
||||
(maxLevel && availableLevels.indexOf(level) > availableLevels.indexOf(maxLevel))
|
||||
return {
|
||||
value: level as AccessControlLevel,
|
||||
label: capitalizeFirstLetter(level ?? ''),
|
||||
|
||||
@@ -7,6 +7,7 @@ import api from 'lib/api'
|
||||
import { OrganizationMembershipLevel } from 'lib/constants'
|
||||
import { membersLogic } from 'scenes/organization/membersLogic'
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
import { userLogic } from 'scenes/userLogic'
|
||||
|
||||
import {
|
||||
APIScopeObject,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
AccessControlType,
|
||||
AccessControlTypeRole,
|
||||
AccessControlUpdateType,
|
||||
AvailableFeature,
|
||||
OrganizationMemberType,
|
||||
RoleType,
|
||||
} from '~/types'
|
||||
@@ -33,10 +35,23 @@ export type RoleResourceAccessControls = DefaultResourceAccessControls & {
|
||||
role?: RoleType
|
||||
}
|
||||
|
||||
const RESOURCE_FEATURE_REQUIREMENTS: Partial<Record<AccessControlResourceType, AvailableFeature>> = {
|
||||
[AccessControlResourceType.ActivityLog]: AvailableFeature.AUDIT_LOGS,
|
||||
}
|
||||
|
||||
export const resourcesAccessControlLogic = kea<resourcesAccessControlLogicType>([
|
||||
path(['scenes', 'accessControl', 'resourcesAccessControlLogic']),
|
||||
connect(() => ({
|
||||
values: [roleAccessControlLogic, ['roles'], teamLogic, ['currentTeam'], membersLogic, ['sortedMembers']],
|
||||
values: [
|
||||
roleAccessControlLogic,
|
||||
['roles'],
|
||||
teamLogic,
|
||||
['currentTeam'],
|
||||
membersLogic,
|
||||
['sortedMembers'],
|
||||
userLogic,
|
||||
['hasAvailableFeature'],
|
||||
],
|
||||
})),
|
||||
actions({
|
||||
updateResourceAccessControls: (
|
||||
@@ -262,10 +277,11 @@ export const resourcesAccessControlLogic = kea<resourcesAccessControlLogicType>(
|
||||
],
|
||||
|
||||
resources: [
|
||||
() => [],
|
||||
(): AccessControlType['resource'][] => {
|
||||
return [
|
||||
(s) => [s.hasAvailableFeature],
|
||||
(hasAvailableFeature): AccessControlType['resource'][] => {
|
||||
const allResources = [
|
||||
AccessControlResourceType.Action,
|
||||
AccessControlResourceType.ActivityLog,
|
||||
AccessControlResourceType.Dashboard,
|
||||
AccessControlResourceType.Experiment,
|
||||
AccessControlResourceType.FeatureFlag,
|
||||
@@ -276,6 +292,14 @@ export const resourcesAccessControlLogic = kea<resourcesAccessControlLogicType>(
|
||||
AccessControlResourceType.Survey,
|
||||
AccessControlResourceType.WebAnalytics,
|
||||
]
|
||||
|
||||
return allResources.filter((resource) => {
|
||||
const requiredFeature = RESOURCE_FEATURE_REQUIREMENTS[resource]
|
||||
if (!requiredFeature) {
|
||||
return true
|
||||
}
|
||||
return hasAvailableFeature(requiredFeature)
|
||||
})
|
||||
},
|
||||
],
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useOnMountEffect } from 'lib/hooks/useOnMountEffect'
|
||||
import { LemonMenuItems } from 'lib/lemon-ui/LemonMenu/LemonMenu'
|
||||
import { IconWithCount } from 'lib/lemon-ui/icons'
|
||||
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
|
||||
import { userHasAccess } from 'lib/utils/accessControlUtils'
|
||||
import { HOG_FUNCTION_SUB_TEMPLATES } from 'scenes/hog-functions/sub-templates/sub-templates'
|
||||
import { urls } from 'scenes/urls'
|
||||
import { userLogic } from 'scenes/userLogic'
|
||||
@@ -25,7 +26,14 @@ import {
|
||||
} from '~/layout/navigation-3000/sidepanel/panels/activity/sidePanelActivityLogic'
|
||||
import { sidePanelNotificationsLogic } from '~/layout/navigation-3000/sidepanel/panels/activity/sidePanelNotificationsLogic'
|
||||
import { sidePanelStateLogic } from '~/layout/navigation-3000/sidepanel/sidePanelStateLogic'
|
||||
import { AvailableFeature, CyclotronJobFilterPropertyFilter, PropertyFilterType, PropertyOperator } from '~/types'
|
||||
import {
|
||||
AccessControlLevel,
|
||||
AccessControlResourceType,
|
||||
AvailableFeature,
|
||||
CyclotronJobFilterPropertyFilter,
|
||||
PropertyFilterType,
|
||||
PropertyOperator,
|
||||
} from '~/types'
|
||||
|
||||
import { SidePanelPaneHeader } from '../../components/SidePanelPaneHeader'
|
||||
import { SidePanelActivityMetalytics } from './SidePanelActivityMetalytics'
|
||||
@@ -56,6 +64,8 @@ export const SidePanelActivity = (): JSX.Element => {
|
||||
const { user } = useValues(userLogic)
|
||||
const { featureFlags } = useValues(featureFlagLogic)
|
||||
|
||||
const hasAccess = userHasAccess(AccessControlResourceType.ActivityLog, AccessControlLevel.Viewer)
|
||||
|
||||
useOnMountEffect(() => {
|
||||
loadImportantChanges(false)
|
||||
|
||||
@@ -83,6 +93,24 @@ export const SidePanelActivity = (): JSX.Element => {
|
||||
const hasListContext = Boolean(contextFromPage?.scope && !contextFromPage?.item_id)
|
||||
const hasAnyContext = hasItemContext || hasListContext
|
||||
|
||||
if (!hasAccess) {
|
||||
return (
|
||||
<>
|
||||
<SidePanelPaneHeader title="Team activity" />
|
||||
<div className="flex flex-col items-center justify-center gap-3 p-6 text-center h-full">
|
||||
<IconNotification className="text-5xl text-muted" />
|
||||
<div>
|
||||
<div className="font-semibold mb-1">Access denied</div>
|
||||
<div className="text-xs text-muted-alt">
|
||||
You don't have sufficient permissions to view activity logs. Please contact your project
|
||||
administrator.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanelPaneHeader title="Team activity" />
|
||||
|
||||
@@ -16,11 +16,13 @@ import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton'
|
||||
import { PaginationControl, usePagination } from 'lib/lemon-ui/PaginationControl'
|
||||
import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture'
|
||||
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
|
||||
import { userHasAccess } from 'lib/utils/accessControlUtils'
|
||||
import { billingLogic } from 'scenes/billing/billingLogic'
|
||||
import { userLogic } from 'scenes/userLogic'
|
||||
|
||||
import { AvailableFeature, ProductKey } from '~/types'
|
||||
import { AccessControlLevel, AccessControlResourceType, AvailableFeature, ProductKey } from '~/types'
|
||||
|
||||
import { AccessDenied } from '../AccessDenied'
|
||||
import MonacoDiffEditor from '../MonacoDiffEditor'
|
||||
import { PayGateMini } from '../PayGateMini/PayGateMini'
|
||||
import { ProductIntroduction } from '../ProductIntroduction/ProductIntroduction'
|
||||
@@ -211,8 +213,14 @@ export const ActivityLog = ({ scope, id, caption, startingPage = 1 }: ActivityLo
|
||||
const { featureFlags } = useValues(featureFlagLogic)
|
||||
const { billingLoading } = useValues(billingLogic)
|
||||
|
||||
const hasAccess = userHasAccess(AccessControlResourceType.ActivityLog, AccessControlLevel.Viewer)
|
||||
|
||||
const paginationState = usePagination(humanizedActivity || [], pagination)
|
||||
|
||||
if (!hasAccess) {
|
||||
return <AccessDenied object="activity logs" />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ActivityLog">
|
||||
{caption && <div className="page-caption">{caption}</div>}
|
||||
|
||||
@@ -16,6 +16,20 @@ export const getMinimumAccessLevel = (resource: APIScopeObject): AccessControlLe
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum allowed access level for a resource.
|
||||
* Matches the backend maximum_access_level function in user_access_control.py
|
||||
*
|
||||
* @param resource - The API scope object to check maximum access for
|
||||
* @returns The maximum access level required, or null if no maximum is set
|
||||
*/
|
||||
export const getMaximumAccessLevel = (resource: APIScopeObject): AccessControlLevel | null => {
|
||||
if (resource === AccessControlResourceType.ActivityLog) {
|
||||
return AccessControlLevel.Viewer
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a resource name to its plural form for display purposes.
|
||||
* Handles special cases for specific resources that have custom plural forms.
|
||||
@@ -28,6 +42,8 @@ export const pluralizeResource = (resource: APIScopeObject): string => {
|
||||
return 'revenue analytics'
|
||||
} else if (resource === AccessControlResourceType.WebAnalytics) {
|
||||
return 'web analytics'
|
||||
} else if (resource === AccessControlResourceType.ActivityLog) {
|
||||
return 'activity logs'
|
||||
}
|
||||
|
||||
return resource.replace(/_/g, ' ') + 's'
|
||||
@@ -44,6 +60,9 @@ export const orderedAccessLevels = (resourceType: AccessControlResourceType): Ac
|
||||
if (resourceType === AccessControlResourceType.Project || resourceType === AccessControlResourceType.Organization) {
|
||||
return [AccessControlLevel.None, AccessControlLevel.Member, AccessControlLevel.Admin]
|
||||
}
|
||||
if (resourceType === AccessControlResourceType.ActivityLog) {
|
||||
return [AccessControlLevel.None, AccessControlLevel.Viewer]
|
||||
}
|
||||
return [AccessControlLevel.None, AccessControlLevel.Viewer, AccessControlLevel.Editor, AccessControlLevel.Manager]
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,14 @@ import { useActions, useValues } from 'kea'
|
||||
import { IconNotification } from '@posthog/icons'
|
||||
import { LemonTabs } from '@posthog/lemon-ui'
|
||||
|
||||
import { AccessDenied } from 'lib/components/AccessDenied'
|
||||
import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini'
|
||||
import { userHasAccess } from 'lib/utils/accessControlUtils'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
import { SceneContent } from '~/layout/scenes/components/SceneContent'
|
||||
import { SceneTitleSection } from '~/layout/scenes/components/SceneTitleSection'
|
||||
import { AvailableFeature } from '~/types'
|
||||
import { AccessControlLevel, AccessControlResourceType, AvailableFeature } from '~/types'
|
||||
|
||||
import { AdvancedActivityLogFiltersPanel } from './AdvancedActivityLogFiltersPanel'
|
||||
import { AdvancedActivityLogsList } from './AdvancedActivityLogsList'
|
||||
@@ -24,6 +26,8 @@ export function AdvancedActivityLogsScene(): JSX.Element | null {
|
||||
const { activeTab } = useValues(advancedActivityLogsLogic)
|
||||
const { setActiveTab } = useActions(advancedActivityLogsLogic)
|
||||
|
||||
const hasAccess = userHasAccess(AccessControlResourceType.ActivityLog, AccessControlLevel.Viewer)
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
key: 'logs',
|
||||
@@ -42,6 +46,14 @@ export function AdvancedActivityLogsScene(): JSX.Element | null {
|
||||
},
|
||||
]
|
||||
|
||||
if (!hasAccess) {
|
||||
return (
|
||||
<SceneContent>
|
||||
<AccessDenied object="activity logs" />
|
||||
</SceneContent>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<SceneContent>
|
||||
<SceneTitleSection
|
||||
|
||||
@@ -314,6 +314,7 @@ export enum AccessControlResourceType {
|
||||
Survey = 'survey',
|
||||
Experiment = 'experiment',
|
||||
WebAnalytics = 'web_analytics',
|
||||
ActivityLog = 'activity_log',
|
||||
}
|
||||
|
||||
interface UserBaseType {
|
||||
|
||||
@@ -181,7 +181,7 @@ class AdvancedActivityLogsViewSet(TeamAndOrgViewSetMixin, viewsets.GenericViewSe
|
||||
pagination_class = ActivityLogPagination
|
||||
logger = logging.getLogger(__name__)
|
||||
filter_rewrite_rules = {"project_id": "team_id"}
|
||||
scope_object = "INTERNAL"
|
||||
scope_object = "activity_log"
|
||||
queryset = ActivityLog.objects.all()
|
||||
|
||||
def _should_skip_parents_filter(self) -> bool:
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
'/home/runner/work/posthog/posthog/ee/clickhouse/views/person.py: Warning [EnterprisePersonViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.person.person.Person" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".',
|
||||
'/home/runner/work/posthog/posthog/posthog/api/action.py: Warning [ActionViewSet > ActionSerializer]: unable to resolve type hint for function "get_creation_context". Consider using a type hint or @extend_schema_field. Defaulting to string.',
|
||||
'/home/runner/work/posthog/posthog/posthog/api/advanced_activity_logs/viewset.py: Warning [ActivityLogViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.activity_logging.activity_log.ActivityLog" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".',
|
||||
'/home/runner/work/posthog/posthog/posthog/api/advanced_activity_logs/viewset.py: Warning [AdvancedActivityLogsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.activity_logging.activity_log.ActivityLog" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".',
|
||||
'/home/runner/work/posthog/posthog/posthog/api/annotation.py: Warning [AnnotationsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.annotation.Annotation" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".',
|
||||
"/home/runner/work/posthog/posthog/posthog/api/app_metrics.py: Error [AppMetricsViewSet]: exception raised while getting serializer. Hint: Is get_serializer_class() returning None or is get_queryset() not working without a request? Ignoring the view for now. (Exception: 'AppMetricsViewSet' should either include a `serializer_class` attribute, or override the `get_serializer_class()` method.)",
|
||||
'/home/runner/work/posthog/posthog/posthog/api/app_metrics.py: Error [HistoricalExportsAppMetricsViewSet]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.',
|
||||
|
||||
@@ -60,6 +60,7 @@ ACCESS_CONTROL_RESOURCES: tuple[APIScopeObject, ...] = (
|
||||
"survey",
|
||||
"experiment",
|
||||
"web_analytics",
|
||||
"activity_log",
|
||||
)
|
||||
|
||||
# Resource inheritance mapping - child resources inherit access from parent resources
|
||||
@@ -111,7 +112,6 @@ def resource_to_display_name(resource: APIScopeObject) -> str:
|
||||
def ordered_access_levels(resource: APIScopeObject) -> list[AccessControlLevel]:
|
||||
if resource in ["project", "organization"]:
|
||||
return list(ACCESS_CONTROL_LEVELS_MEMBER)
|
||||
|
||||
return list(ACCESS_CONTROL_LEVELS_RESOURCE)
|
||||
|
||||
|
||||
@@ -120,6 +120,8 @@ def default_access_level(resource: APIScopeObject) -> AccessControlLevel:
|
||||
return "admin"
|
||||
if resource in ["organization"]:
|
||||
return "member"
|
||||
if resource in ["activity_log"]:
|
||||
return "viewer"
|
||||
return "editor"
|
||||
|
||||
|
||||
@@ -131,6 +133,9 @@ def minimum_access_level(resource: APIScopeObject) -> AccessControlLevel:
|
||||
|
||||
|
||||
def highest_access_level(resource: APIScopeObject) -> AccessControlLevel:
|
||||
"""Returns the highest allowed access level for a resource."""
|
||||
if resource in ["activity_log"]:
|
||||
return "viewer"
|
||||
return ordered_access_levels(resource)[-1]
|
||||
|
||||
|
||||
|
||||