Skip to content
This repository was archived by the owner on Mar 24, 2026. It is now read-only.
61 changes: 53 additions & 8 deletions engine/apps/api/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from common.utils import getattrd

if typing.TYPE_CHECKING:
from apps.user_management.models import User
from apps.user_management.models import Organization, User

RBAC_PERMISSIONS_ATTR = "rbac_permissions"
RBAC_OBJECT_PERMISSIONS_ATTR = "rbac_object_permissions"
Expand Down Expand Up @@ -50,6 +50,12 @@ class GrafanaAPIPermission(typing.TypedDict):
action: str


class GrafanaAPIPermissions:
@classmethod
def construct_permissions(cls, actions: typing.List[str]) -> typing.List[GrafanaAPIPermission]:
return [GrafanaAPIPermission(action=action) for action in actions]


class Resources(enum.Enum):
ALERT_GROUPS = "alert-groups"
INTEGRATIONS = "integrations"
Expand Down Expand Up @@ -103,6 +109,9 @@ def __init__(
self.value = f"{prefix}.{resource.value}:{action.value}"
self.fallback_role = fallback_role

def user_has_permission(self, user: "User") -> bool:
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main changes

return user_is_authorized(user, [self])


LegacyAccessControlCompatiblePermissions = typing.List[LegacyAccessControlCompatiblePermission]
RBACPermissionsAttribute = typing.Dict[str, LegacyAccessControlCompatiblePermissions]
Expand All @@ -126,6 +135,36 @@ def get_most_authorized_role(permissions: LegacyAccessControlCompatiblePermissio
return min({p.fallback_role for p in permissions}, key=lambda r: r.value)


def convert_oncall_permission_to_irm(permission: LegacyAccessControlCompatiblePermission) -> str:
return permission.value.replace(PluginID.ONCALL, PluginID.IRM)


def get_required_permission_values(
organization: "Organization", required_permissions: LegacyAccessControlCompatiblePermissions
) -> typing.List[str]:
"""
This function returns a list of required permission values, taking into account whether or not the organization
is using the IRM plugin.

If the IRM plugin is being used, we substitue `grafana-oncall-app` with `grafana-irm-app`
as the RBAC permission prefix.
"""
permission_values = []

for permission in required_permissions:
permission_value = permission.value
if permission_value.startswith(PluginID.ONCALL) and organization.is_grafana_irm_enabled:
permission_values.append(convert_oncall_permission_to_irm(permission))
else:
permission_values.append(permission_value)

return permission_values


def user_has_minimum_required_basic_role(user: "User", required_basic_role: LegacyAccessControlRole) -> bool:
return user.role <= required_basic_role.value


def user_is_authorized(user: "User", required_permissions: LegacyAccessControlCompatiblePermissions) -> bool:
"""
This function checks whether `user` has all necessary permissions specified in `required_permissions`.
Expand All @@ -134,11 +173,12 @@ def user_is_authorized(user: "User", required_permissions: LegacyAccessControlCo
`user` - The user to check permissions for
`required_permissions` - A list of permissions that a user must have to be considered authorized
"""
if user.organization.is_rbac_permissions_enabled:
organization = user.organization
if organization.is_rbac_permissions_enabled:
user_permissions = [u["action"] for u in user.permissions]
required_permission_values = [p.value for p in required_permissions]
required_permission_values = get_required_permission_values(organization, required_permissions)
return all(permission in user_permissions for permission in required_permission_values)
return user.role <= get_most_authorized_role(required_permissions).value
return user_has_minimum_required_basic_role(user, get_most_authorized_role(required_permissions))


class RBACPermission(permissions.BasePermission):
Expand Down Expand Up @@ -310,18 +350,23 @@ def has_object_permission(self, request: AuthenticatedRequest, view: ViewSetOrAP


ALL_PERMISSION_NAMES = [perm for perm in dir(RBACPermission.Permissions) if not perm.startswith("_")]
ALL_PERMISSION_CLASSES = [
ALL_PERMISSION_CLASSES: LegacyAccessControlCompatiblePermissions = [
getattr(RBACPermission.Permissions, permission_name) for permission_name in ALL_PERMISSION_NAMES
]
ALL_PERMISSION_CHOICES = [
ALL_PERMISSION_CHOICES: typing.List[typing.Tuple[str, str]] = [
(permission_class.value, permission_name)
for permission_class, permission_name in zip(ALL_PERMISSION_CLASSES, ALL_PERMISSION_NAMES)
]


def get_permission_from_permission_string(perm: str) -> typing.Optional[LegacyAccessControlCompatiblePermission]:
def get_permission_from_permission_string(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(see here for more context on how this is used; tldr; query filtering for the user's endpoint)

organization: "Organization", perm: str
) -> typing.Optional[LegacyAccessControlCompatiblePermission]:
for permission_class in ALL_PERMISSION_CLASSES:
if permission_class.value == perm:
permission_class_value = permission_class.value
irm_permission_value = convert_oncall_permission_to_irm(permission_class)

if permission_class_value == perm or organization.is_grafana_irm_enabled and irm_permission_value == perm:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Precedence works ok here, but maybe add parenthesis grouping the and conditions to make it easier to read?

Copy link
Copy Markdown
Contributor Author

@joeyorlando joeyorlando Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decided to get rid of this method in favour of ALL_PERMISSION_NAME_TO_CLASS_MAP (dict lookup should be more performant than the for loop)

return permission_class
return None

Expand Down
Loading