consolidatedScopes.tsx 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. import groupBy from 'lodash/groupBy';
  2. import invertBy from 'lodash/invertBy';
  3. import pick from 'lodash/pick';
  4. import type {Permissions} from 'sentry/types';
  5. const PERMISSION_LEVELS = {
  6. 'no-access': 0,
  7. read: 1,
  8. write: 2,
  9. admin: 3,
  10. };
  11. const HUMAN_RESOURCE_NAMES = {
  12. project: 'Project',
  13. team: 'Team',
  14. release: 'Release',
  15. event: 'Event',
  16. org: 'Organization',
  17. member: 'Member',
  18. };
  19. const DEFAULT_RESOURCE_PERMISSIONS: Permissions = {
  20. Project: 'no-access',
  21. Team: 'no-access',
  22. Release: 'no-access',
  23. Event: 'no-access',
  24. Organization: 'no-access',
  25. Member: 'no-access',
  26. };
  27. const PROJECT_RELEASES = 'project:releases';
  28. const ORG_INTEGRATIONS = 'org:integrations';
  29. type PermissionLevelResources = {
  30. admin: string[];
  31. read: string[];
  32. write: string[];
  33. };
  34. /**
  35. * Numerical value of the scope where Admin is higher than Write,
  36. * which is higher than Read. Used to sort scopes by access.
  37. */
  38. const permissionLevel = (scope: string): number => {
  39. const permission = scope.split(':')[1];
  40. return PERMISSION_LEVELS[permission];
  41. };
  42. const compareScopes = (a: string, b: string) => permissionLevel(a) - permissionLevel(b);
  43. const comparePermissionLevels = (a: string, b: string) =>
  44. PERMISSION_LEVELS[a] - PERMISSION_LEVELS[b];
  45. /**
  46. * Return the most permissive scope for each resource.
  47. *
  48. * Example:
  49. * Given the full list of scopes:
  50. * ['project:read', 'project:write', 'team:read', 'team:write', 'team:admin']
  51. *
  52. * this would return:
  53. * ['project:write', 'team:admin']
  54. */
  55. function topScopes(scopeList: string[]) {
  56. return Object.values(groupBy(scopeList, scope => scope.split(':')[0]))
  57. .map(scopes => scopes.sort(compareScopes))
  58. .map(scopes => scopes.pop());
  59. }
  60. /**
  61. * Convert into a list of Permissions, grouped by resource.
  62. *
  63. * This is used in the new/edit Sentry App form. That page displays permissions
  64. * in a per-Resource manner, meaning one row for Project, one for Organization, etc.
  65. *
  66. * This exposes scopes in a way that works for that UI.
  67. *
  68. * Example:
  69. * {
  70. * 'Project': 'read',
  71. * 'Organization': 'write',
  72. * 'Team': 'no-access',
  73. * ...
  74. * }
  75. */
  76. function toResourcePermissions(scopes: string[]): Permissions {
  77. const permissions = {...DEFAULT_RESOURCE_PERMISSIONS};
  78. let filteredScopes = [...scopes];
  79. // The scope for releases is `project:releases`, but instead of displaying
  80. // it as a permission of Project, we want to separate it out into its own
  81. // row for Releases.
  82. if (scopes.includes(PROJECT_RELEASES)) {
  83. permissions.Release = 'admin';
  84. filteredScopes = scopes.filter((scope: string) => scope !== PROJECT_RELEASES); // remove project:releases
  85. }
  86. // We have a special case with the org:integrations scope. This scope is
  87. // added when selecting org:admin for hierarchy, but the reverse is not true.
  88. // It doesn't indicate any specific org permission, so we can remove it
  89. // entirely.
  90. filteredScopes = filteredScopes.filter((scope: string) => scope !== ORG_INTEGRATIONS);
  91. topScopes(filteredScopes).forEach((scope: string | undefined) => {
  92. if (scope) {
  93. const [resource, permission] = scope.split(':');
  94. permissions[HUMAN_RESOURCE_NAMES[resource]] = permission;
  95. }
  96. });
  97. return permissions;
  98. }
  99. /**
  100. * Convert into a list of Permissions, grouped by access and including a
  101. * list of resources per access level.
  102. *
  103. * This is used in the Permissions Modal when installing an App. It displays
  104. * scopes in a per-Permission way, meaning one row for Read, one for Write,
  105. * and one for Admin.
  106. *
  107. * This exposes scopes in a way that works for that UI.
  108. *
  109. * Example:
  110. * {
  111. * read: ['Project', 'Organization'],
  112. * write: ['Member'],
  113. * admin: ['Release']
  114. * }
  115. */
  116. function toPermissions(scopes: string[]): PermissionLevelResources {
  117. const defaultPermissions = {read: [], write: [], admin: []};
  118. const resourcePermissions = toResourcePermissions(scopes);
  119. // Filter out the 'no-access' permissions
  120. const permissions = pick(invertBy(resourcePermissions), ['read', 'write', 'admin']);
  121. return {...defaultPermissions, ...permissions};
  122. }
  123. export {comparePermissionLevels, toPermissions, toResourcePermissions};