apitoken create updates

This commit is contained in:
its-a-feature
2026-05-05 17:32:06 -05:00
parent c839abcbc1
commit 45e8910faa
3 changed files with 336 additions and 76 deletions
@@ -1,5 +1,5 @@
import React from 'react';
import {Box, Checkbox, Chip, Divider, FormControlLabel, InputAdornment, TextField, Typography} from '@mui/material';
import {Box, Checkbox, Chip, InputAdornment, TextField, Typography} from '@mui/material';
import Button from '@mui/material/Button';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
@@ -247,13 +247,20 @@ export function SettingsAPITokenDialog(props) {
);
});
}, [filter, groupedScopes]);
const visibleScopeCount = React.useMemo(() => {
return visibleResources.reduce((prev, resource) => prev + groupedScopes[resource].length, 0);
}, [groupedScopes, visibleResources]);
const scopeLoadFailed = scopeQueryError !== undefined || scopeData?.apiTokenScopeDefinitions?.status === "error";
const scopesUnavailable = scopeLoading || scopeLoadFailed;
const selectedScopesLabel = selectedScopes.includes("*") ? "Full access selected" : `${selectedScopes.length} selected`;
const fullAccessDisabled = !grantableWildcards.includes("*");
const onUsernameChange = (name, value, error) => {
setName(value);
}
const onAccept = () =>{
props.onAccept(name, selectedScopes);
props.onAccept(name.trim(), selectedScopes);
}
const onFilterChange = (event) => {
setFilter(event.target.value);
@@ -295,12 +302,18 @@ export function SettingsAPITokenDialog(props) {
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
<DialogTitle id="form-dialog-title">
{props.title}
</DialogTitle>
<DialogContent dividers={true} >
<MythicDialogBody>
<MythicDialogSection title="Token Details">
<MythicFormGrid minWidth="18rem">
<MythicFormField label="Token Name" required>
<MythicFormField
label="Token Name"
description="Use a short name that describes where this token will be used."
required
>
<MythicTextField
autoFocus
placeholder={props.name}
@@ -320,108 +333,126 @@ export function SettingsAPITokenDialog(props) {
description="Tokens with no scopes are created with no API access. Write scopes include read access for the same resource."
actions={
<>
<Chip size="small" label={`${selectedScopes.length} selected`} />
<Button size="small" onClick={selectVisibleScopes}>
<Chip className="mythic-api-token-scope-count" size="small" label={`${visibleScopeCount} visible`} />
<Chip className="mythic-api-token-scope-count" size="small" label={selectedScopesLabel} />
<Button disabled={scopesUnavailable || visibleScopeCount === 0 || scopeIsSelected("*")} size="small" onClick={selectVisibleScopes}>
Select Visible
</Button>
<Button size="small" onClick={clearScopes}>
<Button disabled={selectedScopes.length === 0} size="small" onClick={clearScopes}>
Clear
</Button>
</>
}
>
<TextField
className="mythic-api-token-scope-search"
size="small"
fullWidth
value={filter}
onChange={onFilterChange}
placeholder="Search scopes"
disabled={scopeLoading || scopeQueryError !== undefined || scopeData?.apiTokenScopeDefinitions?.status === "error"}
disabled={scopesUnavailable}
InputProps={{startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment>}}
sx={{mb: 1}}
/>
<Box sx={{border: theme => `1px solid ${theme.palette.divider}`, borderRadius: 1, maxHeight: "45vh", overflowY: "auto"}}>
{(scopeLoading || scopeQueryError !== undefined || scopeData?.apiTokenScopeDefinitions?.status === "error") &&
<Box sx={{p: 1.25}}>
<Typography variant="body2" color={scopeLoading ? "text.secondary" : "error"}>
<Box className="mythic-api-token-scope-library">
{scopesUnavailable &&
<Box className={`mythic-api-token-scope-state${scopeLoadFailed ? " mythic-api-token-scope-state-error" : ""}`}>
<Typography variant="body2">
{scopeLoading ? "Loading scopes..." : (scopeQueryError?.message || scopeData?.apiTokenScopeDefinitions?.error || "Failed to load scopes")}
</Typography>
</Box>
}
{!scopeLoading && scopeQueryError === undefined && scopeData?.apiTokenScopeDefinitions?.status !== "error" &&
{!scopesUnavailable &&
<>
<Box sx={{p: 1.25}}>
<FormControlLabel
control={<Checkbox
<Box
className={`mythic-api-token-scope-card mythic-api-token-scope-card-full${scopeIsSelected("*") ? " mythic-api-token-scope-card-selected" : ""}${fullAccessDisabled ? " mythic-api-token-scope-card-disabled" : ""}`}
component="label"
>
<Checkbox
disabled={!grantableWildcards.includes("*")}
checked={scopeIsSelected("*")}
onChange={() => toggleScope("*")}
/>}
label={
<Box>
<Typography variant="body2">Full access (*)</Typography>
<Typography variant="caption" color="text.secondary">
Grants every current and future API scope available to this operator.
</Typography>
</Box>
}
/>
<Box className="mythic-api-token-scope-card-copy">
<Typography className="mythic-api-token-scope-card-title">Full access (*)</Typography>
<Typography className="mythic-api-token-scope-card-description">
Grants every current and future API scope available to this operator.
</Typography>
</Box>
</Box>
<Divider />
{visibleResources.length === 0 &&
<Box className="mythic-api-token-scope-state">
<Typography variant="body2">No scopes match your search.</Typography>
</Box>
}
{visibleResources.map(resource => {
const resourceWildcard = `${resource}.*`;
const canGrantResourceWildcard = grantableWildcards.includes(resourceWildcard);
const resourceScopes = groupedScopes[resource].sort((a, b) => a.access.localeCompare(b.access));
const resourceScopes = [...groupedScopes[resource]].sort((a, b) => (a.access || "").localeCompare(b.access || ""));
const resourceWildcardSelected = scopeIsSelected("*") || scopeIsSelected(resourceWildcard);
return (
<Box key={resource} sx={{p: 1.25, borderBottom: theme => `1px solid ${theme.palette.divider}`}}>
<Box sx={{display: "flex", alignItems: "center", gap: 1, mb: 0.5}}>
<Typography variant="subtitle2" sx={{textTransform: "capitalize", flexGrow: 1}}>
{resource.split("_").join(" ")}
</Typography>
<FormControlLabel
sx={{mr: 0}}
control={<Checkbox
<Box className="mythic-api-token-resource-card" key={resource}>
<Box className="mythic-api-token-resource-header">
<Box sx={{minWidth: 0}}>
<Typography className="mythic-api-token-resource-title">
{resource.split("_").join(" ")}
</Typography>
<Typography className="mythic-api-token-resource-subtitle">
{resourceScopes.length === 1 ? "1 available scope" : `${resourceScopes.length} available scopes`}
</Typography>
</Box>
<Box
className={`mythic-api-token-resource-wildcard${resourceWildcardSelected ? " mythic-api-token-resource-wildcard-selected" : ""}${!canGrantResourceWildcard ? " mythic-api-token-resource-wildcard-disabled" : ""}`}
component="label"
>
<Checkbox
size="small"
disabled={scopeIsSelected("*") || !canGrantResourceWildcard}
checked={scopeIsSelected(resourceWildcard)}
checked={resourceWildcardSelected}
onChange={() => toggleScope(resourceWildcard)}
/>}
label={<Typography variant="body2">{resourceWildcard}</Typography>}
/>
/>
<span>{resourceWildcard}</span>
</Box>
</Box>
<Box sx={{display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(260px, 1fr))", gap: 1}}>
{resourceScopes.map(scope => (
<Box className="mythic-api-token-scope-grid">
{resourceScopes.map(scope => {
const includedByWildcard = scopeIsSelected("*") || scopeIsSelected(resourceWildcard);
const scopeSelected = scopeIsSelected(scope.name) || includedByWildcard;
return (
<Box
className={`mythic-api-token-scope-card${scopeSelected ? " mythic-api-token-scope-card-selected" : ""}${includedByWildcard ? " mythic-api-token-scope-card-inherited" : ""}`}
component="label"
key={scope.name}
sx={{
border: theme => `1px solid ${theme.palette.divider}`,
borderRadius: 1,
px: 1,
py: 0.5,
}}
>
<FormControlLabel
sx={{alignItems: "flex-start", mr: 0}}
control={<Checkbox
<Checkbox
size="small"
disabled={scopeIsSelected("*") || scopeIsSelected(resourceWildcard)}
checked={scopeIsSelected(scope.name) || scopeIsSelected("*") || scopeIsSelected(resourceWildcard)}
disabled={includedByWildcard}
checked={scopeSelected}
onChange={() => toggleScope(scope.name)}
/>}
label={
<Box>
<Typography variant="body2">{scope.display_name || scope.name}</Typography>
<Typography variant="caption" color="text.secondary">
{scope.name}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{display: "block"}}>
{scope.description}
</Typography>
</Box>
}
/>
<Box className="mythic-api-token-scope-card-copy">
<Box className="mythic-api-token-scope-card-title-row">
<Typography className="mythic-api-token-scope-card-title">{scope.display_name || scope.name}</Typography>
<Chip
className={`mythic-api-token-access-chip mythic-api-token-access-chip-${scope.access || "unknown"}`}
label={scope.access || "scope"}
size="small"
/>
</Box>
<Typography className="mythic-api-token-scope-name">
{scope.name}
</Typography>
<Typography className="mythic-api-token-scope-card-description">
{scope.description}
</Typography>
{scope.includes?.length > 0 &&
<Typography className="mythic-api-token-scope-includes">
Includes {scope.includes.join(", ")}
</Typography>
}
</Box>
</Box>
))}
)})}
</Box>
</Box>
);
@@ -436,7 +467,7 @@ export function SettingsAPITokenDialog(props) {
<MythicDialogButton onClick={props.handleClose}>
Cancel
</MythicDialogButton>
<MythicDialogButton intent="primary" onClick={onAccept}>
<MythicDialogButton disabled={name.trim() === ""} intent="primary" onClick={onAccept}>
{"Create New"}
</MythicDialogButton>
</MythicDialogFooter>
@@ -45,6 +45,7 @@ import {
MythicDialogBody,
MythicDialogButton,
MythicDialogFooter,
MythicDialogSection,
MythicFormNote
} from "../../MythicComponents/MythicDialogLayout";
@@ -467,20 +468,26 @@ const APITokenValueDialog = ({tokenValue, onClose}) => {
<DialogTitle id="form-dialog-title">Copy API Token</DialogTitle>
<DialogContent dividers={true}>
<MythicDialogBody>
<MythicFormNote>
<MythicFormNote className="mythic-api-token-copy-warning">
This token value is only shown once. Copy it now before closing this dialog.
</MythicFormNote>
<TextField
fullWidth
multiline
minRows={4}
value={tokenValue}
InputProps={{readOnly: true}}
/>
<MythicDialogSection
title="Token Value"
description="Store this somewhere safe before you close the dialog."
>
<TextField
className="mythic-api-token-value-field"
fullWidth
multiline
minRows={4}
value={tokenValue}
InputProps={{readOnly: true}}
/>
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<MythicDialogFooter>
<MythicDialogButton onClick={onCopyTokenValue} startIcon={<ContentCopyIcon />}>
<MythicDialogButton className="mythic-table-row-action-hover-info" onClick={onCopyTokenValue} startIcon={<ContentCopyIcon />}>
Copy
</MythicDialogButton>
<MythicDialogButton intent="primary" onClick={onClose}>
+222
View File
@@ -1124,6 +1124,228 @@ tspan {
.mythic-form-switch-control {
flex: 0 0 auto;
}
.mythic-api-token-scope-count {
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.06) : alpha(props.theme.palette.common.black, 0.035)} !important;
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
color: ${(props) => props.theme.palette.text.primary} !important;
font-size: 0.72rem;
font-weight: 750;
}
.mythic-api-token-scope-search {
margin-bottom: 0.65rem !important;
}
.mythic-api-token-scope-search .MuiInputBase-root {
background-color: ${(props) => props.theme.surfaces?.raised || props.theme.palette.background.paper};
}
.mythic-api-token-scope-library {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.025)" : "rgba(0,0,0,0.014)"};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: flex;
flex-direction: column;
gap: 0.65rem;
max-height: 48vh;
min-height: 14rem;
overflow: auto;
padding: 0.65rem;
}
.mythic-api-token-scope-state {
align-items: center;
background-color: ${(props) => props.theme.surfaces?.raised || props.theme.palette.background.paper};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.text.secondary};
display: flex;
justify-content: center;
min-height: 7rem;
padding: 1rem;
text-align: center;
}
.mythic-api-token-scope-state-error {
background-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.14 : 0.08)};
border-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.5 : 0.32)};
color: ${(props) => props.theme.palette.error.main};
}
.mythic-api-token-resource-card {
background-color: ${(props) => props.theme.surfaces?.raised || props.theme.palette.background.paper};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: flex;
flex-direction: column;
gap: 0.65rem;
min-width: 0;
padding: 0.75rem;
}
.mythic-api-token-resource-header {
align-items: flex-start;
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
justify-content: space-between;
min-width: 0;
}
.mythic-api-token-resource-title {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.9rem;
font-weight: 800;
line-height: 1.2;
text-transform: capitalize;
}
.mythic-api-token-resource-subtitle {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.74rem;
line-height: 1.35;
margin-top: 0.15rem;
}
.mythic-api-token-resource-wildcard {
align-items: center;
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.045)" : "rgba(0,0,0,0.03)"};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.text.secondary};
cursor: pointer;
display: inline-flex;
flex: 0 0 auto;
font-size: 0.76rem;
font-weight: 750;
gap: 0.25rem;
min-height: 32px;
padding: 0.1rem 0.55rem 0.1rem 0.2rem;
}
.mythic-api-token-resource-wildcard-selected {
background-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.2 : 0.1)};
border-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.58 : 0.38)};
color: ${(props) => props.theme.palette.primary.main};
}
.mythic-api-token-resource-wildcard-disabled {
cursor: default;
opacity: 0.68;
}
.mythic-api-token-resource-wildcard .MuiCheckbox-root {
padding: 0.1rem;
}
.mythic-api-token-scope-grid {
display: grid;
gap: 0.55rem;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
min-width: 0;
}
.mythic-api-token-scope-card {
align-items: flex-start;
background-color: ${(props) => props.theme.palette.background.paper};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.text.primary};
cursor: pointer;
display: flex;
gap: 0.35rem;
min-width: 0;
padding: 0.55rem 0.65rem 0.6rem 0.35rem;
transition: background-color 120ms ease, border-color 120ms ease, box-shadow 120ms ease;
}
.mythic-api-token-scope-card:hover {
background-color: ${(props) => props.theme.surfaces?.hover || props.theme.palette.action.hover};
border-color: ${(props) => props.theme.table?.border || props.theme.borderColor};
}
.mythic-api-token-scope-card-selected {
background-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.16 : 0.08)};
border-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.58 : 0.38)};
}
.mythic-api-token-scope-card-inherited {
cursor: default;
}
.mythic-api-token-scope-card-disabled {
cursor: default;
opacity: 0.68;
}
.mythic-api-token-scope-card .MuiCheckbox-root {
padding: 0.1rem;
}
.mythic-api-token-scope-card-full {
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.12 : 0.07)};
border-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.4 : 0.26)};
}
.mythic-api-token-scope-card-copy {
min-width: 0;
width: 100%;
}
.mythic-api-token-scope-card-title-row {
align-items: flex-start;
display: flex;
gap: 0.45rem;
justify-content: space-between;
min-width: 0;
}
.mythic-api-token-scope-card-title {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.82rem;
font-weight: 800;
line-height: 1.25;
min-width: 0;
overflow-wrap: anywhere;
}
.mythic-api-token-scope-name {
color: ${(props) => props.theme.palette.text.secondary};
font-family: ${(props) => props.theme.typography.fontFamilyMono || "monospace"};
font-size: 0.7rem;
line-height: 1.35;
margin-top: 0.18rem;
overflow-wrap: anywhere;
}
.mythic-api-token-scope-card-description,
.mythic-api-token-scope-includes {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.74rem;
line-height: 1.35;
margin-top: 0.25rem;
overflow-wrap: anywhere;
}
.mythic-api-token-scope-includes {
color: ${(props) => props.theme.palette.info.main};
font-weight: 700;
}
.mythic-api-token-access-chip {
border-radius: ${(props) => props.theme.shape.borderRadius}px !important;
flex: 0 0 auto;
font-size: 0.66rem !important;
font-weight: 800 !important;
height: 20px !important;
text-transform: capitalize;
}
.mythic-api-token-access-chip-read {
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.2 : 0.12)} !important;
border: 1px solid ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.54 : 0.34)} !important;
color: ${(props) => props.theme.palette.info.main} !important;
}
.mythic-api-token-access-chip-write,
.mythic-api-token-access-chip-create,
.mythic-api-token-access-chip-update {
background-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.2 : 0.12)} !important;
border: 1px solid ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.54 : 0.34)} !important;
color: ${(props) => props.theme.palette.success.main} !important;
}
.mythic-api-token-access-chip-delete,
.mythic-api-token-access-chip-admin {
background-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.2 : 0.12)} !important;
border: 1px solid ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.54 : 0.34)} !important;
color: ${(props) => props.theme.palette.error.main} !important;
}
.mythic-api-token-access-chip-unknown {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)"} !important;
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
color: ${(props) => props.theme.palette.text.secondary} !important;
}
.mythic-api-token-copy-warning {
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.16 : 0.08)};
border-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.45 : 0.3)};
color: ${(props) => props.theme.palette.text.primary};
}
.mythic-api-token-value-field .MuiInputBase-root {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(0,0,0,0.26)" : "rgba(15,23,42,0.045)"};
font-family: ${(props) => props.theme.typography.fontFamilyMono || "monospace"};
font-size: 0.78rem;
line-height: 1.45;
}
.mythic-single-task-list {
display: flex;
flex-direction: column;