Skip to content

Commit

Permalink
feat: [WD-16972] TLS user management spike. (#1026)
Browse files Browse the repository at this point in the history
## Done
- Created "Create Identity" Button
- Create modal to create new TLS fine grained identities.
- A few new functions and a new LXDTrustToken type.

## User Management Spike
[✔️] A user needs to be able to create a TLS user (Creates a pending
user)
[✔️] A user needs to be able to destroy a TLS user
([WD-16894](#1008))

Fixes:
- Inability to create TLS fine grained tokens through the UI.

## QA

1. Run the LXD-UI:
- On the demo server via the link posted by @webteam-app below. This is
only available for PRs created by collaborators of the repo. Ask
@mas-who or @edlerd for access.
- With a local copy of this branch, [build and run as described in the
docs](../CONTRIBUTING.md#setting-up-for-development).
2. Perform the following QA steps:
- [List the steps to QA the new feature(s) or prove that a bug has been
resolved]

## Screenshots

![image](https://github.com/user-attachments/assets/9549e7f1-c217-4d45-8afa-9ef2c1845f43)
![Screenshot from 2024-12-10
22-47-10](https://github.com/user-attachments/assets/71f60924-98ac-47a1-a0c4-7498792610a4)


[WD-16894]:
https://warthogs.atlassian.net/browse/WD-16894?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
  • Loading branch information
Kxiru authored Dec 16, 2024
2 parents 9665053 + 613cf93 commit 6116d36
Show file tree
Hide file tree
Showing 9 changed files with 246 additions and 12 deletions.
21 changes: 20 additions & 1 deletion src/api/auth-identities.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { handleResponse, handleSettledResult } from "util/helpers";
import { LxdApiResponse } from "types/apiResponse";
import { LxdIdentity } from "types/permissions";
import { LxdIdentity, TlsIdentityTokenDetail } from "types/permissions";

export const fetchIdentities = (): Promise<LxdIdentity[]> => {
return new Promise((resolve, reject) => {
Expand Down Expand Up @@ -75,3 +75,22 @@ export const deleteIdentities = (identities: LxdIdentity[]): Promise<void> => {
.catch(reject);
});
};

export const createFineGrainedTlsIdentity = (
clientName: string,
): Promise<TlsIdentityTokenDetail> => {
return new Promise((resolve, reject) => {
fetch(`/1.0/auth/identities/tls`, {
method: "POST",
body: JSON.stringify({
name: clientName,
token: true,
}),
})
.then(handleResponse)
.then((data: LxdApiResponse<TlsIdentityTokenDetail>) =>
resolve(data.metadata),
)
.catch(reject);
});
};
145 changes: 145 additions & 0 deletions src/pages/permissions/CreateIdentityModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { FC, useState } from "react";
import {
ActionButton,
Button,
Icon,
Input,
Modal,
Notification,
} from "@canonical/react-components";
import { useFormik } from "formik";
import { createFineGrainedTlsIdentity } from "api/auth-identities";
import { base64EncodeObject } from "util/helpers";
import { useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";

interface Props {
onClose: () => void;
}

const CreateIdentityModal: FC<Props> = ({ onClose }) => {
const [token, setToken] = useState<string | null>(null);
const [error, setError] = useState(false);
const [copied, setCopied] = useState(false);
const queryClient = useQueryClient();

const formik = useFormik({
initialValues: {
identityName: "",
},
onSubmit: (values) => {
setError(false);
createFineGrainedTlsIdentity(values.identityName)
.then((response) => {
const encodedToken = base64EncodeObject(response);
setToken(encodedToken);

void queryClient.invalidateQueries({
queryKey: [queryKeys.identities],
});
})
.catch(() => {
setError(true);
});
},
});

const handleCopy = async () => {
if (token) {
try {
await navigator.clipboard.writeText(token);
setCopied(true);

setTimeout(() => {
setCopied(false);
}, 5000);
} catch (error) {
console.error(error);
}
}
};

return (
<Modal
close={onClose}
className="create-tls-identity"
title={token ? "Identity has been created" : "Generate trust token"}
buttonRow={
<>
{token && (
<>
<Button
aria-label={
copied ? "Copied to clipboard" : "Copy to clipboard"
}
title="Copy token"
className="u-no-margin--bottom"
onClick={() => handleCopy()}
type="button"
hasIcon
>
<Icon name={copied ? "task-outstanding" : "copy"} />
</Button>

<Button
aria-label="Close"
className="u-no-margin--bottom"
onClick={onClose}
type="button"
>
Close
</Button>
</>
)}
{!token && (
<ActionButton
aria-label="Generate token"
appearance="positive"
className="u-no-margin--bottom"
onClick={() => void formik.submitForm()}
disabled={
formik.values.identityName.length === 0 || formik.isSubmitting
}
loading={formik.isSubmitting}
>
Generate token
</ActionButton>
)}
</>
}
>
{error && (
<Notification
severity="negative"
title="Token creation failed"
></Notification>
)}
{!token && (
<Input
id="identityName"
type="text"
label="Identity Name"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
value={formik.values.identityName}
/>
)}
{token && (
<>
<p>The token below can be used to log in as the new identity.</p>

<Notification
severity="caution"
title="Make sure to copy the token now as it will not be shown again."
></Notification>

<div className="token-code-block">
<code>{token}</code>
</div>
</>
)}
</Modal>
);
};

export default CreateIdentityModal;
31 changes: 31 additions & 0 deletions src/pages/permissions/CreateTlsIdentityBtn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Button, Icon } from "@canonical/react-components";
import { useSmallScreen } from "context/useSmallScreen";
import { FC } from "react";
import usePortal from "react-useportal";
import CreateIdentityModal from "./CreateIdentityModal";

const CreateTlsIdentityBtn: FC = () => {
const isSmallScreen = useSmallScreen();
const { openPortal, closePortal, isOpen, Portal } = usePortal();

return (
<>
{isOpen && (
<Portal>
<CreateIdentityModal onClose={closePortal} />
</Portal>
)}
<Button
appearance="positive"
className="u-float-right u-no-margin--bottom"
onClick={openPortal}
hasIcon={!isSmallScreen}
>
{!isSmallScreen && <Icon name="plus" light />}
<span>Create TLS Identity</span>
</Button>
</>
);
};

export default CreateTlsIdentityBtn;
4 changes: 4 additions & 0 deletions src/pages/permissions/PermissionIdentities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import DeleteIdentityBtn from "./actions/DeleteIdentityBtn";
import { useSupportedFeatures } from "context/useSupportedFeatures";
import { isUnrestricted } from "util/helpers";
import IdentityResource from "components/IdentityResource";
import CreateTlsIdentityBtn from "./CreateTlsIdentityBtn";

const PermissionIdentities: FC = () => {
const notify = useNotify();
Expand Down Expand Up @@ -269,6 +270,9 @@ const PermissionIdentities: FC = () => {
/>
)}
</PageHeader.Left>
<PageHeader.BaseActions>
<CreateTlsIdentityBtn />
</PageHeader.BaseActions>
</PageHeader>
}
>
Expand Down
10 changes: 0 additions & 10 deletions src/sass/_instance_detail_page.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,3 @@
margin: 0 $sph--large;
}
}

.create-instance-from-snapshot-modal,
.duplicate-instances-modal,
.create-image-from-instance-modal {
.p-modal__dialog {
@include large {
width: 35rem;
}
}
}
9 changes: 9 additions & 0 deletions src/sass/_permission_confirm_modal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,12 @@
margin-top: $spv--large;
}
}

.token-code-block {
background-color: rgb(0 0 0 / 5%);
margin-bottom: 1.5rem;

code {
background-color: transparent;
}
}
11 changes: 11 additions & 0 deletions src/sass/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,17 @@ body {
cursor: pointer;
}

.create-instance-from-snapshot-modal,
.duplicate-instances-modal,
.create-image-from-instance-modal,
.create-tls-identity {
.p-modal__dialog {
@include large {
width: 35rem;
}
}
}

.host-path-device-modal {
.p-modal__dialog {
@include large {
Expand Down
16 changes: 15 additions & 1 deletion src/types/permissions.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
export interface LxdIdentity {
id: string; // fingerprint for tls and email for oidc
type: string;
type:
| "Client certificate"
| "Client certificate (pending)"
| "Client certificate (unrestricted)"
| "OIDC client";
name: string;
authentication_method: "tls" | "oidc";
tls_certificate: string;
groups?: string[] | null;
effective_groups?: string[];
effective_permissions?: LxdPermission[];
Expand Down Expand Up @@ -30,3 +35,12 @@ export interface IdpGroup {
name: string;
groups: string[]; // these should be names of lxd groups
}

export interface TlsIdentityTokenDetail {
client_name: string;
addresses: string[];
expires_at: string;
fingerprint: string;
type: "Client certificate (pending)" | "Client certificate";
secret: string;
}
11 changes: 11 additions & 0 deletions src/util/helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -340,3 +340,14 @@ export const getDefaultStoragePool = (profile: LxdProfile) => {
export const isUnrestricted = (identity: LxdIdentity) => {
return identity.type === "Client certificate (unrestricted)";
};

export const isFineGrainedTls = (identity: LxdIdentity) => {
return ["Client certificate (pending)", "Client certificate"].includes(
identity.type,
);
};

export const base64EncodeObject = (data: object) => {
const jsonString = JSON.stringify(data);
return btoa(jsonString);
};

0 comments on commit 6116d36

Please sign in to comment.