1
0
Fork 0

Add KV secret version viewing again

This commit is contained in:
ChaotiCryptidz 2022-01-11 14:41:33 +00:00
parent bfaa43ce3a
commit a8add31ff1
6 changed files with 243 additions and 41 deletions

View file

@ -18,7 +18,7 @@ import { SecretMetadataType } from "./types/secret";
import { Settings } from "../settings/Settings";
import { TokenInfo } from "./types/token";
import { UserType, UserTypeAPIResp } from "./types/user";
import { removeDoubleSlash } from "../utils";
import { getObjectKeys, removeDoubleSlash } from "../utils";
async function checkResponse(resp: Response): Promise<void> {
if (resp.ok) return;
@ -352,20 +352,68 @@ export class API {
await checkResponse(resp);
}
async deleteSecret(baseMount: string, secretPath: string[], name: string): Promise<void> {
async deleteSecret(
baseMount: string,
secretPath: string[],
name: string,
version: string = "null",
): Promise<void> {
let secretURL = "";
let request;
const mountInfo = await this.getMount(baseMount);
if (mountInfo.options.version == "2") {
const mountVersion = mountInfo.options.version;
if (mountVersion == "2" && version != "null") {
secretURL = `/v1/${baseMount}/delete/${secretPath.join("/")}/${name}`;
secretURL = removeDoubleSlash(secretURL).replace(/\/$/, "");
request = new Request(this.appendAPIURL(secretURL), {
method: "POST",
headers: {
...this.getHeaders(),
"Content-Type": "application/json",
},
body: JSON.stringify({ versions: [version] }),
});
} else {
if (mountVersion == "2") {
secretURL = `/v1/${baseMount}/metadata/${secretPath.join("/")}/${name}`;
} else {
secretURL = `/v1/${baseMount}/${secretPath.join("/")}/${name}`;
}
secretURL = removeDoubleSlash(secretURL).replace(/\/$/, "");
const request = new Request(this.appendAPIURL(secretURL), {
request = new Request(this.appendAPIURL(secretURL), {
method: "DELETE",
headers: this.getHeaders(),
});
}
const resp = await fetch(request);
await checkResponse(resp);
}
async undeleteSecret(
baseMount: string,
secretPath: string[],
name: string,
version: string = "null",
): Promise<void> {
let secretURL = `/v1/${baseMount}/undelete/${secretPath.join("/")}/${name}`;
secretURL = removeDoubleSlash(secretURL).replace(/\/$/, "");
if (version == "null") {
const meta = await this.getSecretMetadata(baseMount, secretPath, name);
const versions = getObjectKeys(meta.versions);
version = String(versions[versions.length - 1]);
}
const request = new Request(this.appendAPIURL(secretURL), {
method: "POST",
headers: {
...this.getHeaders(),
"Content-Type": "application/json",
},
body: JSON.stringify({ versions: [version] }),
});
const resp = await fetch(request);
await checkResponse(resp);
}
@ -376,7 +424,7 @@ export class API {
name: string,
): Promise<Record<string, unknown>> {
const request = new Request(
this.appendAPIURL(`/v1/${baseMount}/${secretPath.join("")}/${name}`),
this.appendAPIURL(`/v1/${baseMount}/${secretPath.join("/")}/${name}`),
{
headers: this.getHeaders(),
},
@ -394,10 +442,12 @@ export class API {
baseMount: string,
secretPath: string[],
name: string,
version: string = "null",
): Promise<Record<string, unknown>> {
let secretURL = "";
secretURL = `/v1/${baseMount}/data/${secretPath.join("")}/${name}`;
secretURL = `/v1/${baseMount}/data/${secretPath.join("/")}/${name}`;
if (version != "null") secretURL += `?version=${version}`;
const request = new Request(this.appendAPIURL(secretURL), {
headers: this.getHeaders(),
@ -414,10 +464,11 @@ export class API {
baseMount: string,
secretPath: string[],
name: string,
version: string = "null",
): Promise<Record<string, unknown>> {
const mountInfo = await this.getMount(baseMount);
if (mountInfo.options.version == "2") {
return await this.getSecretKV2(baseMount, secretPath, name);
return await this.getSecretKV2(baseMount, secretPath, name, version);
} else {
return await this.getSecretKV1(baseMount, secretPath, name);
}

View file

@ -43,6 +43,7 @@ import { UserPassUserEdit } from "./ui/pages/Access/Auth/userpass/UserPassUserEd
import { UserPassUserNew } from "./ui/pages/Access/Auth/userpass/UserPassUserNew";
import { UserPassUserView } from "./ui/pages/Access/Auth/userpass/UserPassUserView";
import { UserPassUsersList } from "./ui/pages/Access/Auth/userpass/UserPassUsersList";
import { KeyValueVersions } from "./ui/pages/Secrets/KeyValue/KeyValueVersions";
export const Main = () => (
<Router>
@ -65,7 +66,12 @@ export const Main = () => (
<KeyValueNew path="/secrets/kv/new/:baseMount/:secretPath*?" settings={settings} api={api} />
<KeyValueList path="/secrets/kv/list/:baseMount/:secretPath*?" settings={settings} api={api} />
<KeyValueView
path="/secrets/kv/view/:item/:baseMount/:secretPath*?"
path="/secrets/kv/view/:item/:version/:baseMount/:secretPath*?"
settings={settings}
api={api}
/>
<KeyValueVersions
path="/secrets/kv/versions/:item/:baseMount/:secretPath*?"
settings={settings}
api={api}
/>
@ -75,7 +81,7 @@ export const Main = () => (
api={api}
/>
<KeyValueDelete
path="/secrets/kv/delete/:item/:baseMount/:secretPath*?"
path="/secrets/kv/delete/:item/:version/:baseMount/:secretPath*?"
settings={settings}
api={api}
/>
@ -83,7 +89,7 @@ export const Main = () => (
<TOTPList path="/secrets/totp/list/:baseMount" settings={settings} api={api} />
<TOTPNew path="/secrets/totp/new/:baseMount" settings={settings} api={api} />
<TOTPNewGenerated path="/secrets/totp/new_generated/:baseMount" settings={settings} api={api} />
<TOTPDelete path="/secrets/totp/delete/:baseMount/:item" settings={settings} api={api} />
<TOTPDelete path="/secrets/totp/delete/:version/:baseMount/:item" settings={settings} api={api} />
<TransitNew path="/secrets/transit/new/:baseMount" settings={settings} api={api} />
<TransitList path="/secrets/transit/list/:baseMount" settings={settings} api={api} />

View file

@ -2,12 +2,15 @@ import { Component } from "preact";
import { DefaultPageProps } from "../../../../types/DefaultPageProps";
import { SecretTitleElement } from "../SecretTitleElement";
import i18next from "i18next";
import { route } from "preact-router";
import { kvListURL, kvViewURL } from "../../pageLinks";
export class KeyValueDelete extends Component<DefaultPageProps> {
render() {
const baseMount = this.props.matches["baseMount"];
const secretPath = this.props.matches["secretPath"].split("/");
const item = this.props.matches["item"];
const version = this.props.matches["version"];
return (
<>
@ -23,8 +26,13 @@ export class KeyValueDelete extends Component<DefaultPageProps> {
<button
class="uk-button uk-button-danger"
onClick={async () => {
await this.props.api.deleteSecret(baseMount, secretPath, item);
window.history.back();
await this.props.api.deleteSecret(baseMount, secretPath, item, version);
if (version == "null") {
route(kvListURL(baseMount, secretPath))
} else {
route(kvViewURL(baseMount, secretPath, item, "null"))
}
}}
>
{i18next.t("common_delete")}

View file

@ -0,0 +1,55 @@
import { Component } from "preact";
import { DefaultPageProps } from "../../../../types/DefaultPageProps";
import { SecretTitleElement } from "../SecretTitleElement";
import { kvViewURL } from "../../pageLinks";
import { route } from "preact-router";
import { objectToMap } from "../../../../utils";
export class KeyValueVersions extends Component<DefaultPageProps, { versions: string[] }> {
async componentDidMount() {
const baseMount = this.props.matches["baseMount"];
const secretPath = this.props.matches["secretPath"].split("/");
const secretItem = this.props.matches["item"];
const metadata = await this.props.api.getSecretMetadata(
baseMount,
secretPath,
secretItem,
);
const versions = Array.from(objectToMap(metadata.versions).keys());
this.setState({ versions });
}
render() {
if (!this.state.versions) return;
const baseMount = this.props.matches["baseMount"];
const secretPath = this.props.matches["secretPath"].split("/");
const secretItem = this.props.matches["item"];
return (
<>
<SecretTitleElement
type="kv"
baseMount={baseMount}
secretPath={secretPath}
item={this.props.matches["item"]}
/>
<ul class="uk-nav uk-nav-default">
{this.state.versions.map((ver) => (
<li>
<a
onClick={async () => {
route(kvViewURL(baseMount, secretPath, secretItem, ver));
}}
>
{`v${ver}`}
</a>
</li>
))}
</ul>
</>
);
}
}

View file

@ -4,10 +4,11 @@ import { CopyableInputBox } from "../../../elements/CopyableInputBox";
import { DefaultPageProps } from "../../../../types/DefaultPageProps";
import { Grid, GridSizes } from "../../../elements/Grid";
import { SecretTitleElement } from "../SecretTitleElement";
import { kvDeleteURL, kvEditURL } from "../../pageLinks";
import { kvDeleteURL, kvEditURL, kvVersionsURL } from "../../pageLinks";
import { route } from "preact-router";
import { sortedObjectMap } from "../../../../utils";
import i18next from "i18next";
import { DoesNotExistError } from "../../../../types/internalErrors";
export type KVSecretViewProps = {
kvData: Record<string, unknown>;
@ -46,6 +47,9 @@ type KeyValueViewState = {
secretItem: string;
caps: string[];
secretInfo: Record<string, unknown>;
kvVersion: string;
isDeleted: boolean;
secretVersion: string;
};
export class KeyValueView extends Component<DefaultPageProps, KeyValueViewState> {
@ -53,27 +57,43 @@ export class KeyValueView extends Component<DefaultPageProps, KeyValueViewState>
const baseMount = this.props.matches["baseMount"];
const secretPath = this.props.matches["secretPath"].split("/");
const secretItem = this.props.matches["item"];
const secretVersion = this.props.matches["version"];
const caps = (await this.props.api.getCapabilities(baseMount, secretPath, secretItem))
.capabilities;
const secretPathAPI = secretPath.map((e) => e + "/");
// TODO: this is a big hacky, fix when redo how api arguments work
secretPathAPI[secretPathAPI.length - 1] = String(secretPathAPI[secretPathAPI.length - 1])
.replace("/", "")
.toString();
const mountInfo = await this.props.api.getMount(baseMount);
let kvVersion = mountInfo.options.version;
let secretInfo: Record<string, unknown>;
if (kvVersion == "2") {
try {
secretInfo = await this.props.api.getSecretKV2(baseMount, secretPath, secretItem, secretVersion);
} catch (e) {
if (e == DoesNotExistError) {
secretInfo = null;
}
}
} else {
secretInfo = await this.props.api.getSecretKV1(baseMount, secretPath, secretItem);
}
let isDeleted = (secretInfo == null) && (kvVersion == "2");
const secretInfo = await this.props.api.getSecret(baseMount, secretPathAPI, secretItem);
this.setState({
baseMount,
secretPath,
secretItem,
caps,
secretInfo,
kvVersion,
isDeleted,
secretVersion,
});
}
render() {
if (!this.state.baseMount) return;
if (!this.state.secretInfo) return;
return (
<>
@ -85,19 +105,43 @@ export class KeyValueView extends Component<DefaultPageProps, KeyValueViewState>
/>
<div>
<p id="buttonsBlock">
{this.state.caps.includes("delete") && (
{
// Delete Button
!this.state.isDeleted && this.state.caps.includes("delete") && (
<button
class="uk-button uk-button-danger"
onClick={async () => {
onClick={() => {
route(
kvDeleteURL(this.state.baseMount, this.state.secretPath, this.state.secretItem),
kvDeleteURL(
this.state.baseMount,
this.state.secretPath,
this.state.secretItem,
this.state.secretVersion,
),
);
}}
>
{i18next.t("kv_secret_delete_btn")}
{((): string => {
// Delete Secret on kv-v1
let deleteButtonText = i18next.t("kv_secret_delete_btn");
if (this.state.kvVersion == "2" && this.state.secretVersion == "null") {
// Delete All
deleteButtonText = i18next.t("kv_secret_delete_all_btn");
} else if (
this.state.kvVersion == "2" &&
this.state.secretVersion != "null"
) {
// Delete Version X
deleteButtonText = i18next.t("kv_secret_delete_version_btn", {
version: this.state.secretVersion,
});
}
return deleteButtonText;
})()}
</button>
)}
{this.state.caps.includes("update") && (
)
}
{this.state.secretVersion == "null" && this.state.caps.includes("update") && (
<button
class="uk-button uk-button-primary"
onClick={async () => {
@ -109,9 +153,43 @@ export class KeyValueView extends Component<DefaultPageProps, KeyValueViewState>
{i18next.t("common_edit")}
</button>
)}
{!this.state.isDeleted && this.state.kvVersion == "2" && (
<button
class="uk-button uk-button-secondary"
onClick={async () => {
route(
kvVersionsURL(this.state.baseMount, this.state.secretPath, this.state.secretItem),
);
}}
>
{i18next.t("kv_secret_versions_btn")}
</button>
)}
</p>
{<KVSecretVew kvData={this.state.secretInfo} />}
{!this.state.isDeleted && <KVSecretVew kvData={this.state.secretInfo} />}
{this.state.isDeleted && (
<>
<p>{i18next.t("kv_secret_deleted_text")}</p>
<button
class="uk-button uk-button-primary"
onClick={async () => {
await this.props.api.undeleteSecret(
this.state.baseMount,
this.state.secretPath,
this.state.secretItem,
this.state.secretVersion,
);
this.setState({});
await this.componentDidMount();
}}
>
{i18next.t("kv_secret_restore_btn")}
</button>
</>
)}
</div>
</>
);

View file

@ -10,16 +10,20 @@ export function kvNewURL(baseMount: string, secretPath?: string[]): string {
return `/secrets/kv/new/${baseMount}` + (secretPath ? `/${secretPath.join("/")}` : "");
}
export function kvDeleteURL(baseMount: string, secretPath: string[], secret: string): string {
return `/secrets/kv/delete/${secret}/${baseMount}/${secretPath.join("/")}`;
export function kvDeleteURL(baseMount: string, secretPath: string[], secret: string, version: string = "null"): string {
return `/secrets/kv/delete/${secret}/${version}/${baseMount}/${secretPath.join("/")}`;
}
export function kvEditURL(baseMount: string, secretPath: string[], secret: string): string {
return `/secrets/kv/edit/${secret}/${baseMount}/${secretPath.join("/")}`;
}
export function kvViewURL(baseMount: string, secretPath: string[], secret: string): string {
return `/secrets/kv/view/${secret}/${baseMount}/${secretPath.join("/")}`;
export function kvVersionsURL(baseMount: string, secretPath: string[], secret: string): string {
return `/secrets/kv/versions/${secret}/${baseMount}/${secretPath.join("/")}`;
}
export function kvViewURL(baseMount: string, secretPath: string[], secret: string, version: string = "null"): string {
return `/secrets/kv/view/${secret}/${version}/${baseMount}/${secretPath.join("/")}`;
}
export function kvListURL(baseMount: string, secretPath: string[]): string {