From 99592d6efbbd0685db1d162be085ceeaeafc995f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Cluseau?= Date: Wed, 17 Jun 2026 14:02:25 +0200 Subject: [PATCH] add download sets and regroup access granting parts --- html/ui/Cluster-bd053ffb62d5a804.js | 22 +++ ...21d.js => ClusterAccess-fede0ff535b7cf.js} | 185 ++++++++++-------- html/ui/ClusterCAs-d6eba07c367b6306.js | 16 ++ html/ui/index.html | 4 +- html/ui/style.css | 16 ++ ui/index.html | 2 + ui/js/Cluster.js | 147 +------------- ui/js/ClusterAccess.js | 178 +++++++++++++++++ ui/js/ClusterCAs.js | 16 ++ ui/js/ClusterDownloadSet.js | 43 ++++ ui/style.css | 16 ++ 11 files changed, 415 insertions(+), 230 deletions(-) create mode 100644 html/ui/Cluster-bd053ffb62d5a804.js rename html/ui/{Cluster-ddf1029883a9a21d.js => ClusterAccess-fede0ff535b7cf.js} (50%) create mode 100644 html/ui/ClusterCAs-d6eba07c367b6306.js create mode 100644 ui/js/ClusterAccess.js create mode 100644 ui/js/ClusterCAs.js create mode 100644 ui/js/ClusterDownloadSet.js diff --git a/html/ui/Cluster-bd053ffb62d5a804.js b/html/ui/Cluster-bd053ffb62d5a804.js new file mode 100644 index 0000000..aa3badf --- /dev/null +++ b/html/ui/Cluster-bd053ffb62d5a804.js @@ -0,0 +1,22 @@ +const Cluster = { + components: { ClusterAccess, ClusterCAs, Downloads, GetCopy }, + props: [ 'cluster', 'token', 'state' ], + template: ` + + +

Tokens

+ + +

Passwords

+ + +

Downloads

+ + + +` +} diff --git a/html/ui/Cluster-ddf1029883a9a21d.js b/html/ui/ClusterAccess-fede0ff535b7cf.js similarity index 50% rename from html/ui/Cluster-ddf1029883a9a21d.js rename to html/ui/ClusterAccess-fede0ff535b7cf.js index 827a01f..0544790 100644 --- a/html/ui/Cluster-ddf1029883a9a21d.js +++ b/html/ui/ClusterAccess-fede0ff535b7cf.js @@ -1,8 +1,8 @@ -const Cluster = { - components: { Downloads, GetCopy }, +const ClusterAccess = { props: [ 'cluster', 'token', 'state' ], data() { return { + accessType: "ssh", signReqValidity: "1d", sshSignReq: { PubKey: "", @@ -16,8 +16,35 @@ const Cluster = { }, kubeUserCert: null, downloadSet: null, + selectedAssets: {}, }; }, + computed: { + availableAssets() { + return [ + "kernel", + "initrd", + "uki", + "bootstrap.tar", + "boot.img.gz", + "boot.img", + "boot.qcow2", + "boot.vmdk", + "boot.iso", + "boot.tar", + "bootstrap-config", + "config", + "config.json", + "ipxe", + ] + }, + selectedAssetList() { + return this.availableAssets.filter(a => this.selectedAssets[a]) + }, + hostCount() { + return (this.state.Hosts||[]).filter(h => h.Cluster == this.cluster.Name).length + }, + }, methods: { sshCASign() { event.preventDefault(); @@ -49,6 +76,28 @@ const Cluster = { }) .catch((e) => { alert('failed to sign: '+e); }) }, + generateDownloadSet() { + event.preventDefault() + + const hosts = this.hostCount ? (this.state.Hosts||[]).filter(h => h.Cluster == this.cluster.Name) : [] + const items = hosts.map(h => ({ + Kind: "host", + Name: h.Name, + Assets: this.selectedAssetList, + })) + + fetch('/sign-download-set', { + method: 'POST', + body: JSON.stringify({Expiry: this.signReqValidity, Items: items}), + headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' }, + }).then((resp) => { + if (resp.ok) { + resp.json().then((set) => { this.downloadSet = set }) + } else { + resp.json().then((resp) => alert('failed to generate: '+resp.message)) + } + }).catch((e) => { alert('failed to generate: '+e) }) + }, readFile(e, onload) { const file = e.target.files[0]; if (!file) { return; } @@ -67,97 +116,63 @@ const Cluster = { this.kubeSignReq.CSR = v; }); }, - generateDownloadSet() { - event.preventDefault() - - const hosts = (this.state.Hosts||[]).filter(h => h.Cluster == this.cluster.Name) - const items = hosts.map(h => ({ - Kind: "host", - Name: h.Name, - Assets: ["kernel", "initrd", "uki", "bootstrap.tar", "boot.img.gz", "boot.img", "boot.qcow2", "boot.iso", "boot.tar", "bootstrap-config", "config", "config.json", "ipxe"], - })) - - fetch('/sign-download-set', { - method: 'POST', - body: JSON.stringify({Expiry: this.signReqValidity, Items: items}), - headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' }, - }).then((resp) => { - if (resp.ok) { - resp.json().then((set) => { this.downloadSet = set }) - } else { - resp.json().then((resp) => alert('failed to generate: '+resp.message)) - } - }).catch((e) => { alert('failed to generate: '+e) }) - }, }, template: `

Access

-

Allow cluster access from a public key

- -

Grant SSH access

+

Grant access to: + + + +

Validity: time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.

-

User:

-

Public key (OpenSSH format):
- - -

-

- -

+
+

Grant SSH access

+

User:

+

Public key (OpenSSH format):
+ + +

+

+ +

+
-

Grant Kubernetes API access

- -

Validity: time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.

-

User: (by default, from the CSR)

-

Group:

-

Certificate signing request (PEM format):
- - -

- -

- -

- -

Tokens

- - -

Passwords

- - -

Downloads

- - -

Download set

-

Validity: time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.

-

-

- Open download set page -
- -

- -

CAs

- - - - - -
NameCertificateSigned certificates
{{ ca.Name }}
+
+

Grant Kubernetes API access

+

User: (by default, from the CSR)

+

Group:

+

Certificate signing request (PEM format):
+ + +

+

+ +

+
+
+

Grant download access

+

Generates a signed download set granting access to the selected assets for all {{ hostCount }} hosts in this cluster.

+

Available assets

+

+ +

+

+

+ Open download set page +
+ +

+
` } diff --git a/html/ui/ClusterCAs-d6eba07c367b6306.js b/html/ui/ClusterCAs-d6eba07c367b6306.js new file mode 100644 index 0000000..75e520f --- /dev/null +++ b/html/ui/ClusterCAs-d6eba07c367b6306.js @@ -0,0 +1,16 @@ +const ClusterCAs = { + components: { GetCopy }, + props: [ 'cluster', 'token' ], + template: ` +

CAs

+ + + + + +
NameCertificateSigned certificates
{{ ca.Name }}
+` +} diff --git a/html/ui/index.html b/html/ui/index.html index 2b5b9c7..9fa80f5 100644 --- a/html/ui/index.html +++ b/html/ui/index.html @@ -13,7 +13,9 @@ - + + + diff --git a/html/ui/style.css b/html/ui/style.css index 98af93b..664d9a1 100644 --- a/html/ui/style.css +++ b/html/ui/style.css @@ -195,3 +195,19 @@ header .utils > * { } .copy { font-size: small; } + +label.radio { + display: inline-block; + margin-right: 1ex; + margin-bottom: 1ex; + padding: 0.5ex; + border: 1pt solid; + border-radius: 1ex; + cursor: pointer; +} +label.radio:has(input:checked) { + color: var(--link); +} +label.radio input { + display: none; +} diff --git a/ui/index.html b/ui/index.html index cd93e2e..dad1bf0 100644 --- a/ui/index.html +++ b/ui/index.html @@ -13,6 +13,8 @@ + + diff --git a/ui/js/Cluster.js b/ui/js/Cluster.js index 827a01f..aa3badf 100644 --- a/ui/js/Cluster.js +++ b/ui/js/Cluster.js @@ -1,130 +1,8 @@ const Cluster = { - components: { Downloads, GetCopy }, + components: { ClusterAccess, ClusterCAs, Downloads, GetCopy }, props: [ 'cluster', 'token', 'state' ], - data() { - return { - signReqValidity: "1d", - sshSignReq: { - PubKey: "", - Principal: "root", - }, - sshUserCert: null, - kubeSignReq: { - CSR: "", - User: "", - Group: "system:masters", - }, - kubeUserCert: null, - downloadSet: null, - }; - }, - methods: { - sshCASign() { - event.preventDefault(); - fetch(`/clusters/${this.cluster.Name}/ssh/user-ca/sign`, { - method: 'POST', - body: JSON.stringify({ ...this.sshSignReq, Validity: this.signReqValidity }), - headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' }, - }).then((resp) => { - if (resp.ok) { - resp.blob().then((cert) => { this.sshUserCert = URL.createObjectURL(cert) }) - } else { - resp.json().then((resp) => alert('failed to sign: '+resp.message)) - } - }) - .catch((e) => { alert('failed to sign: '+e); }) - }, - kubeCASign() { - event.preventDefault(); - fetch(`/clusters/${this.cluster.Name}/kube/sign`, { - method: 'POST', - body: JSON.stringify({ ...this.kubeSignReq, Validity: this.signReqValidity }), - headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' }, - }).then((resp) => { - if (resp.ok) { - resp.blob().then((cert) => { this.kubeUserCert = URL.createObjectURL(cert) }) - } else { - resp.json().then((resp) => alert('failed to sign: '+resp.message)) - } - }) - .catch((e) => { alert('failed to sign: '+e); }) - }, - readFile(e, onload) { - const file = e.target.files[0]; - if (!file) { return; } - const reader = new FileReader(); - reader.onload = () => { onload(reader.result) }; - reader.onerror = () => { alert("error reading file"); }; - reader.readAsText(file); - }, - loadPubKey(e) { - this.readFile(e, (v) => { - this.sshSignReq.PubKey = v; - }); - }, - loadCSR(e) { - this.readFile(e, (v) => { - this.kubeSignReq.CSR = v; - }); - }, - generateDownloadSet() { - event.preventDefault() - - const hosts = (this.state.Hosts||[]).filter(h => h.Cluster == this.cluster.Name) - const items = hosts.map(h => ({ - Kind: "host", - Name: h.Name, - Assets: ["kernel", "initrd", "uki", "bootstrap.tar", "boot.img.gz", "boot.img", "boot.qcow2", "boot.iso", "boot.tar", "bootstrap-config", "config", "config.json", "ipxe"], - })) - - fetch('/sign-download-set', { - method: 'POST', - body: JSON.stringify({Expiry: this.signReqValidity, Items: items}), - headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' }, - }).then((resp) => { - if (resp.ok) { - resp.json().then((set) => { this.downloadSet = set }) - } else { - resp.json().then((resp) => alert('failed to generate: '+resp.message)) - } - }).catch((e) => { alert('failed to generate: '+e) }) - }, - }, template: ` -

Access

- -

Allow cluster access from a public key

- -

Grant SSH access

- -

Validity: time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.

-

User:

-

Public key (OpenSSH format):
- - -

- -

- -

- -

Grant Kubernetes API access

- -

Validity: time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.

-

User: (by default, from the CSR)

-

Group:

-

Certificate signing request (PEM format):
- - -

- -

- -

+

Tokens