mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
161 lines
4.9 KiB
TypeScript
161 lines
4.9 KiB
TypeScript
import {Component, Fragment} from 'react';
|
|
|
|
import SelectField from 'sentry/components/forms/fields/selectField';
|
|
import FormContext from 'sentry/components/forms/formContext';
|
|
import {SENTRY_APP_PERMISSIONS} from 'sentry/constants';
|
|
import {t} from 'sentry/locale';
|
|
import type {PermissionResource, Permissions, PermissionValue} from 'sentry/types';
|
|
|
|
/**
|
|
* Custom form element that presents API scopes in a resource-centric way. Meaning
|
|
* a dropdown for each resource, that rolls up into a flat list of scopes.
|
|
*
|
|
*
|
|
* API Scope vs Permission
|
|
*
|
|
* "API Scopes" are the string identifier that gates an endpoint. For example,
|
|
* `project:read` or `org:admin`. They're made up of two parts:
|
|
*
|
|
* <resource>:<access>
|
|
*
|
|
* "Permissions" are a more user-friendly way of conveying this same information.
|
|
* They're roughly the same with one exception:
|
|
*
|
|
* - No Access
|
|
* - Read
|
|
* - Read & Write
|
|
* - Admin
|
|
*
|
|
* "Read & Write" actually maps to the `write` access level since `read` is
|
|
* implied. Similarly, `admin` implies `read` and `write`.
|
|
*
|
|
* This components displays things per Resource. Meaning the User selects
|
|
* "Read", "Read & Write", or "Admin" for Project or Organization or etc.
|
|
*
|
|
* === Scopes to Permissions
|
|
*
|
|
* The first thing this component does on instantiation is take the list of API
|
|
* Scopes passed via `props` and converts them to "Permissions.
|
|
*
|
|
* So a list of scopes like the following:
|
|
*
|
|
* ['project:read', 'project:write', 'org:admin']
|
|
*
|
|
* will become an object that looks like:
|
|
*
|
|
* {
|
|
* 'Project': 'write',
|
|
* 'Organization': 'admin',
|
|
* }
|
|
*
|
|
*
|
|
* State
|
|
*
|
|
* This component stores state like the example object from above. When the
|
|
* User changes the Permission for a particular resource, it updates the
|
|
* `state.permissions` object to reflect the change.
|
|
*
|
|
*
|
|
* Updating the Form Field Value
|
|
*
|
|
* In addition to updating the state, when a value is changed this component
|
|
* recalculates the full list of API Scopes that need to be passed to the API.
|
|
*
|
|
* So if the User has changed Project to "Admin" and Organization to "Read & Write",
|
|
* we end up with a `state.permissions` like:
|
|
*
|
|
* {
|
|
* 'Project': 'admin',
|
|
* 'Organization': 'write',
|
|
* }
|
|
*
|
|
* From there, we calculate the full list of API Scopes. This list includes all
|
|
* implied scopes, meaning the above state would result in:
|
|
*
|
|
* ['project:read', 'project:write', 'project:admin', 'org:read', 'org:write']
|
|
*
|
|
*/
|
|
|
|
type Props = {
|
|
appPublished: boolean;
|
|
onChange: (permissions: Permissions) => void;
|
|
permissions: Permissions;
|
|
};
|
|
|
|
type State = {
|
|
permissions: Permissions;
|
|
};
|
|
|
|
function findResource(r: PermissionResource) {
|
|
return SENTRY_APP_PERMISSIONS.find(permissions => permissions.resource === r);
|
|
}
|
|
|
|
/**
|
|
* Converts the "Permission" values held in `state` to a list of raw
|
|
* API scopes we can send to the server. For example:
|
|
*
|
|
* ['org:read', 'org:write', ...]
|
|
*
|
|
*/
|
|
function permissionStateToList(permissions: Permissions) {
|
|
return Object.entries(permissions).flatMap(
|
|
([r, p]) => findResource(r as PermissionResource)?.choices?.[p]?.scopes
|
|
);
|
|
}
|
|
|
|
export default class PermissionSelection extends Component<Props, State> {
|
|
state: State = {
|
|
permissions: this.props.permissions,
|
|
};
|
|
|
|
static contextType = FormContext;
|
|
|
|
onChange = (resource: PermissionResource, choice: PermissionValue) => {
|
|
const {permissions} = this.state;
|
|
permissions[resource] = choice;
|
|
this.save(permissions);
|
|
};
|
|
|
|
save = (permissions: Permissions) => {
|
|
this.setState({permissions});
|
|
this.props.onChange(permissions);
|
|
this.context.form.setValue('scopes', permissionStateToList(this.state.permissions));
|
|
};
|
|
|
|
render() {
|
|
const {permissions} = this.state;
|
|
|
|
return (
|
|
<Fragment>
|
|
{SENTRY_APP_PERMISSIONS.map(config => {
|
|
const options = Object.entries(config.choices).map(([value, {label}]) => ({
|
|
value,
|
|
label,
|
|
}));
|
|
|
|
const value = permissions[config.resource];
|
|
|
|
return (
|
|
<SelectField
|
|
// These are not real fields we want submitted, so we use
|
|
// `--permission` as a suffix here, then filter these
|
|
// fields out when submitting the form in
|
|
// sentryApplicationDetails.jsx
|
|
name={`${config.resource}--permission`}
|
|
key={config.resource}
|
|
options={options}
|
|
help={config.help}
|
|
label={config.label || config.resource}
|
|
onChange={this.onChange.bind(this, config.resource)}
|
|
value={value}
|
|
defaultValue={value}
|
|
disabled={this.props.appPublished}
|
|
disabledReason={t('Cannot update permissions on a published integration')}
|
|
/>
|
|
);
|
|
})}
|
|
</Fragment>
|
|
);
|
|
}
|
|
}
|