Add API tokens support #9

Merged
pierre merged 40 commits from api into master 2024-04-23 17:04:45 +00:00
6 changed files with 130 additions and 5 deletions
Showing only changes of commit c7f72882a7 - Show all commits

View File

@ -53,6 +53,7 @@ struct ServerConstraints {
nwfilter_selectors_count: LenConstraints, nwfilter_selectors_count: LenConstraints,
api_token_name_size: LenConstraints, api_token_name_size: LenConstraints,
api_token_description_size: LenConstraints, api_token_description_size: LenConstraints,
api_token_right_path_size: LenConstraints,
} }
pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
@ -110,6 +111,8 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
min: constants::API_TOKEN_DESCRIPTION_MIN_LENGTH, min: constants::API_TOKEN_DESCRIPTION_MIN_LENGTH,
max: constants::API_TOKEN_DESCRIPTION_MAX_LENGTH, max: constants::API_TOKEN_DESCRIPTION_MAX_LENGTH,
}, },
api_token_right_path_size: LenConstraints { min: 0, max: 255 },
}, },
}) })
} }

View File

@ -85,7 +85,7 @@ async fn main() -> std::io::Result<()> {
let mut cors = Cors::default() let mut cors = Cors::default()
.allowed_origin(&AppConfig::get().website_origin) .allowed_origin(&AppConfig::get().website_origin)
.allowed_methods(vec!["GET", "POST", "DELETE", "PUT"]) .allowed_methods(vec!["GET", "POST", "DELETE", "PUT", "PATCH"])
.allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT]) .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])
.allowed_header(header::CONTENT_TYPE) .allowed_header(header::CONTENT_TYPE)
.supports_credentials() .supports_credentials()

View File

@ -29,6 +29,7 @@ export interface ServerConstraints {
nwfilter_selectors_count: LenConstraint; nwfilter_selectors_count: LenConstraint;
api_token_name_size: LenConstraint; api_token_name_size: LenConstraint;
api_token_description_size: LenConstraint; api_token_description_size: LenConstraint;
api_token_right_path_size: LenConstraint;
} }
export interface LenConstraint { export interface LenConstraint {

View File

@ -16,7 +16,7 @@ export interface SelectOption {
export function SelectInput(p: { export function SelectInput(p: {
value?: string; value?: string;
editable: boolean; editable: boolean;
label: string; label?: string;
options: SelectOption[]; options: SelectOption[];
onValueChange: (o?: string) => void; onValueChange: (o?: string) => void;
}): React.ReactElement { }): React.ReactElement {
@ -29,7 +29,7 @@ export function SelectInput(p: {
return ( return (
<FormControl fullWidth variant="standard" style={{ marginBottom: "15px" }}> <FormControl fullWidth variant="standard" style={{ marginBottom: "15px" }}>
<InputLabel>{p.label}</InputLabel> {p.label && <InputLabel>{p.label}</InputLabel>}
<Select <Select
value={p.value ?? ""} value={p.value ?? ""}
label={p.label} label={p.label}

View File

@ -15,6 +15,7 @@ import { EditSection } from "../forms/EditSection";
import { IPInputWithMask } from "../forms/IPInput"; import { IPInputWithMask } from "../forms/IPInput";
import { RadioGroupInput } from "../forms/RadioGroupInput"; import { RadioGroupInput } from "../forms/RadioGroupInput";
import { TextInput } from "../forms/TextInput"; import { TextInput } from "../forms/TextInput";
import { RawRightsEditor } from "./RawRightsEditor";
const SECS_PER_DAY = 3600 * 24; const SECS_PER_DAY = 3600 * 24;
@ -104,14 +105,15 @@ function APITokenDetailsInner(p: DetailsInnerProps): React.ReactElement {
}, },
]} ]}
/> />
{currTab === TokenTab.General && <NetworkDetailsTabGeneral {...p} />} {currTab === TokenTab.General && <APITokenTabGeneral {...p} />}
{/* todo: rights */} {/* todo: rights */}
{currTab === TokenTab.RawRights && <APITokenRawRights {...p} />}
{currTab === TokenTab.Danger && <APITokenTabDanger {...p} />} {currTab === TokenTab.Danger && <APITokenTabDanger {...p} />}
</> </>
); );
} }
function NetworkDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement { function APITokenTabGeneral(p: DetailsInnerProps): React.ReactElement {
const [ipVersion, setIpVersion] = React.useState<4 | 6>( const [ipVersion, setIpVersion] = React.useState<4 | 6>(
(p.token.ip_restriction ?? "").includes(":") ? 6 : 4 (p.token.ip_restriction ?? "").includes(":") ? 6 : 4
); );
@ -196,6 +198,14 @@ function NetworkDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
); );
} }
function APITokenRawRights(p: DetailsInnerProps): React.ReactElement {
return (
<div style={{ padding: "30px" }}>
<RawRightsEditor {...p} editable={p.status !== TokenWidgetStatus.Read} />
</div>
);
}
function APITokenTabDanger(p: DetailsInnerProps): React.ReactElement { function APITokenTabDanger(p: DetailsInnerProps): React.ReactElement {
const confirm = useConfirm(); const confirm = useConfirm();
const snackbar = useSnackbar(); const snackbar = useSnackbar();

View File

@ -0,0 +1,111 @@
import AddIcon from "@mui/icons-material/Add";
import DeleteIcon from "@mui/icons-material/Delete";
import {
IconButton,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Tooltip,
Typography,
} from "@mui/material";
import { ServerApi } from "../../api/ServerApi";
import { APIToken } from "../../api/TokensApi";
import { SelectInput } from "../forms/SelectInput";
import { TextInput } from "../forms/TextInput";
export function RawRightsEditor(p: {
token: APIToken;
editable: boolean;
onChange?: () => void;
}): React.ReactElement {
const addRule = () => {
p.token.rights.push({ path: "/api/", verb: "GET" });
p.onChange?.();
};
const deleteRule = (id: number) => {
p.token.rights.splice(id, 1);
p.onChange?.();
};
return (
<TableContainer component={Paper}>
<div
style={{
padding: "10px 10px 0px 10px",
display: "flex",
justifyContent: "space-between",
}}
>
<Typography variant="h5">Raw rights</Typography>
<div>
{p.editable && (
<Tooltip title="Add a new right rule">
<IconButton onClick={addRule}>
<AddIcon />
</IconButton>
</Tooltip>
)}
</div>
</div>
<Table>
<TableHead>
<TableRow>
<TableCell>Verb</TableCell>
<TableCell>URI</TableCell>
{p.editable && <TableCell>Actions</TableCell>}
</TableRow>
</TableHead>
<TableBody>
{p.token.rights.map((r, num) => (
<TableRow key={num} hover>
<TableCell style={{ width: "100px" }}>
<SelectInput
{...p}
value={r.verb}
onValueChange={(v) => {
r.verb = v as any;
p.onChange?.();
}}
options={[
{ value: "GET" },
{ value: "POST" },
{ value: "PATCH" },
{ value: "PUT" },
{ value: "DELETE" },
]}
/>
</TableCell>
<TableCell>
<TextInput
{...p}
value={r.path}
onValueChange={(v) => {
r.path = v ?? "";
p.onChange?.();
}}
checkValue={(v) => v.startsWith("/api/")}
size={ServerApi.Config.constraints.api_token_right_path_size}
/>
</TableCell>
{p.editable && (
<TableCell style={{ width: "100px" }}>
<IconButton onClick={() => deleteRule(num)}>
<Tooltip title="Remove the rule">
<DeleteIcon />
</Tooltip>
</IconButton>
</TableCell>
)}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
}