-
Step 2. EDIT ENTRY
-
- {!this.state.entrySelected &&
-
-
-
- }
- {this.state.entrySelected &&
-
-
-
e.g. select if no Parent ID provided
-
- }
-
-
-
{this.onChangeParentIdInput(e)}}
- />
+
+
+
+
+ {JSON.stringify(this.state.uploadedEntries, null, ' ')}
+
+
+
+
+
+
+
+
+
-
-
{
- const input = e.target.value
- e.target.value = this.state.spiffeIdPrefix + input.substr(this.state.spiffeIdPrefix.length);
- this.onChangeSpiffeId(e);
- }}
+
+
Step 2. EDIT ENTRY
+
+ {!this.state.entrySelected &&
+
+
+
+ }
+ {this.state.entrySelected &&
+
+
+
e.g. select if no Parent ID provided
+
+ }
+
+
+ {this.onChangeParentIdInput(e)}}
+ />
+
+
+ {
+ const input = e.target.value
+ e.target.value = this.state.spiffeIdPrefix + input.substr(this.state.spiffeIdPrefix.length);
+ this.onChangeSpiffeId(e);
+ }}
+ />
+
+
-
-
-
-
-
+
+
+
+
-
-
+
+
}
diff --git a/frontend/src/components/federation-create.tsx b/frontend/src/components/federation-create.tsx
new file mode 100644
index 00000000..86419360
--- /dev/null
+++ b/frontend/src/components/federation-create.tsx
@@ -0,0 +1,494 @@
+import React, { Component, ChangeEvent } from 'react';
+import axios from 'axios';
+import {
+ FileUploader,
+ Button,
+ Accordion,
+ AccordionItem,
+ ToastNotification,
+ ModalWrapper,
+ TextInput,
+ Link
+} from 'carbon-components-react';
+import { ToastContainer } from 'react-toastify';
+import GetApiServerUri from './helpers';
+import './style.css';
+import {
+ tornjakMessageFunc,
+ federationsListUpdateFunc,
+} from 'redux/actions';
+import { RootState } from 'redux/reducers';
+import { connect } from 'react-redux';
+import { link } from './types';
+import { Launch, NextOutline } from '@carbon/icons-react';
+
+type FederationCreateProps = {
+ tornjakMessageFunc: (globalErrorMessage: string) => void,
+ federationsListUpdateFunc: Function,
+ globalServerSelected: string,
+ globalErrorMessage: string,
+};
+
+type Federation = {
+ trust_domain?: string,
+ federation_relationships?: {
+ trust_domain?: string,
+ bundle_endpoint_url?: string,
+ [key: string]: any
+ }[],
+ [key: string]: any
+};
+
+type FederationCreateState = {
+ federationJson: string,
+ uploadedFederation: Federation | Federation[],
+ federationLoaded: boolean,
+ loading: boolean,
+ statusOK: string,
+ successJsonMessage: string,
+ message: string,
+ exposedBundleEndpoint: string,
+ newFederationsIds: { trustDomain: string }[],
+ selectedFederationId: number,
+ federationSelected: boolean,
+};
+
+const NewFederationJsonFormatLink = (props: { link: link }) => (
+
+);
+
+class FederationCreate extends Component
{
+ constructor(props: FederationCreateProps) {
+ super(props);
+ this.state = {
+ federationJson: "",
+ uploadedFederation: [],
+ federationLoaded: false,
+ loading: false,
+ statusOK: "",
+ successJsonMessage: "",
+ message: "",
+ exposedBundleEndpoint: "",
+ newFederationsIds: [],
+ selectedFederationId: -1,
+ federationSelected: false,
+ };
+ this.handleFileUpload = this.handleFileUpload.bind(this);
+ this.onSubmit = this.onSubmit.bind(this);
+ this.applyEditToFederation = this.applyEditToFederation.bind(this);
+ this.setSelectedFederation = this.setSelectedFederation.bind(this);
+ }
+
+ handleFileUpload(e: ChangeEvent) {
+ const files = e.target.files;
+ if (!files || files.length === 0) {
+ return;
+ }
+
+ const file = files[0];
+ if (!file) return;
+
+ const reader = new FileReader();
+ reader.onload = (evt) => {
+ const result = evt.target?.result;
+ if (typeof result === 'string') {
+ try {
+ const parsed = JSON.parse(result);
+ const uploadedFederation = parsed;
+
+ let newFederationsIds: { trustDomain: string }[];
+ if (Array.isArray(uploadedFederation)) {
+ newFederationsIds = uploadedFederation.map((fed: Federation) => {
+ const trustDomain = (fed.federation_relationships && fed.federation_relationships.length > 0)
+ ? fed.federation_relationships[0].trust_domain || "No Trust Domain"
+ : "No Trust Domain";
+ return { trustDomain: trustDomain };
+ });
+ } else {
+ const singleFed = uploadedFederation as Federation;
+ const trustDomain = (singleFed.federation_relationships && singleFed.federation_relationships.length > 0)
+ ? singleFed.federation_relationships[0].trust_domain || "No Trust Domain"
+ : "No Trust Domain";
+ newFederationsIds = [{ trustDomain: trustDomain }];
+ }
+
+ this.setState({
+ federationJson: result,
+ uploadedFederation: uploadedFederation,
+ federationLoaded: true,
+ message: "",
+ statusOK: "",
+ newFederationsIds: newFederationsIds,
+ });
+ } catch (err) {
+ this.setState({ message: "Invalid JSON format.", statusOK: "ERROR" });
+ }
+ }
+ };
+ reader.onerror = () => {
+ this.setState({ message: "Error reading the selected file.", statusOK: "ERROR" });
+ };
+ reader.readAsText(file);
+ }
+
+ onSubmit() {
+ const { uploadedFederation } = this.state;
+
+ if (!uploadedFederation || (Array.isArray(uploadedFederation) && uploadedFederation.length === 0)) {
+ this.setState({ message: "No federation JSON loaded.", statusOK: "ERROR" });
+ return;
+ }
+
+ let jsonData: Federation | Federation[] = uploadedFederation;
+
+ if (Array.isArray(jsonData) && jsonData.length === 1) {
+ jsonData = jsonData[0];
+ }
+
+ console.log("Final JSON to be submitted:", jsonData);
+
+ const endpoint = GetApiServerUri('/api/v1/spire/federations');
+ this.setState({ loading: true, statusOK: "" });
+
+ axios
+ .post(endpoint, jsonData)
+ .then((res) => {
+ const responseMessage = res.data?.results?.[0]?.status?.message || "OK";
+ this.setState({
+ loading: false,
+ statusOK: responseMessage === "OK" ? "OK" : "ERROR",
+ successJsonMessage: responseMessage,
+ message: JSON.stringify(res.data, null, ' '),
+ });
+ })
+ .catch((err) => {
+ const errorMessage = err.response?.data?.results?.[0]?.status?.message || "Failed to create federation.";
+ this.setState({
+ loading: false,
+ statusOK: "ERROR",
+ successJsonMessage: errorMessage,
+ message: JSON.stringify(err.response?.data || err.message, null, ' '),
+ });
+ });
+ }
+
+ setSelectedFederation(index: number) {
+ if (this.state.federationSelected) {
+ if (!window.confirm("All changes will be lost! Press 'Apply' to save or 'Cancel' to continue without saving.")) {
+ return;
+ }
+ }
+
+ let fed: Federation | undefined;
+ if (Array.isArray(this.state.uploadedFederation)) {
+ fed = this.state.uploadedFederation[index];
+ } else {
+ if (index !== 0) {
+ console.warn("Index out of range for single federation object.");
+ return;
+ }
+ fed = this.state.uploadedFederation;
+ }
+
+ const fedRel = fed?.federation_relationships && fed.federation_relationships.length > 0
+ ? fed.federation_relationships[0]
+ : null;
+
+ const exposedBundleEndpoint = fedRel?.bundle_endpoint_url || "";
+
+ this.setState({
+ selectedFederationId: index,
+ federationSelected: true,
+ exposedBundleEndpoint: exposedBundleEndpoint,
+ });
+ }
+
+ applyEditToFederation() {
+ const { selectedFederationId, uploadedFederation, exposedBundleEndpoint } = this.state;
+
+ if (selectedFederationId === -1) {
+ alert("Please select a Federation from the list, and make necessary changes to apply edit!");
+ return false;
+ }
+
+ let updatedFederations: Federation[];
+ if (Array.isArray(uploadedFederation)) {
+ updatedFederations = [...uploadedFederation];
+ } else {
+ updatedFederations = [uploadedFederation];
+ }
+
+ if (
+ selectedFederationId >= 0 &&
+ selectedFederationId < updatedFederations.length &&
+ updatedFederations[selectedFederationId].federation_relationships &&
+ updatedFederations[selectedFederationId].federation_relationships!.length > 0
+ ) {
+ const cleanedUrl = exposedBundleEndpoint.trim();
+ updatedFederations[selectedFederationId].federation_relationships![0].bundle_endpoint_url = cleanedUrl;
+ } else {
+ console.warn("No federation_relationships to update, or index out of range.");
+ }
+
+ let finalUploadedFederation: Federation | Federation[] = updatedFederations;
+ if (updatedFederations.length === 1 && !Array.isArray(this.state.uploadedFederation)) {
+ finalUploadedFederation = updatedFederations[0];
+ }
+
+ this.setState({
+ uploadedFederation: finalUploadedFederation,
+ selectedFederationId: -1,
+ federationSelected: false,
+ exposedBundleEndpoint: "",
+ });
+
+ console.log("Updated Federation JSON:", finalUploadedFederation);
+ alert(`Federation ${selectedFederationId + 1} Updated!`);
+ return true;
+ }
+
+ render() {
+ const { loading, statusOK, message, successJsonMessage, federationLoaded, uploadedFederation, newFederationsIds, selectedFederationId, federationSelected, exposedBundleEndpoint } = this.state;
+ const newFederationFormatLink = "https://github.com/spiffe/tornjak/blob/main/docs/newFederation-json-format.md";
+
+ return (
+
+
Create Federation
+
+ {statusOK !== "" && (
+
+
+
+
+ {statusOK === "OK" && successJsonMessage === "OK" && (
+
--FEDERATION SUCCESSFULLY CREATED--
+ )}
+ {statusOK === "ERROR" && (
+
--FEDERATION CREATION FAILED--
+ )}
+
+
+
+
+ }
+ timeout={0}
+ title="Federation Creation Notification"
+ />
+ {window.scrollTo({ top: 0, behavior: 'smooth' })}
+
+ )}
+
+
+ Upload Federation Trust Bundle} open>
+
+
Choose your local file:
+
only .json files
+
+
{
+ this.setState({
+ federationJson: "",
+ uploadedFederation: [],
+ federationLoaded: false,
+ message: "",
+ statusOK: "",
+ newFederationsIds: []
+ });
+ }}
+ />
+
+ {federationLoaded && (
+
+
+
+
+ {Array.isArray(uploadedFederation) && uploadedFederation.length === 1 && (
+ 1 Federation Loaded from File
+ )}
+ {Array.isArray(uploadedFederation) && uploadedFederation.length > 1 && (
+ {uploadedFederation.length} Federations Loaded from File
+ )}
+ {!Array.isArray(uploadedFederation) && (
+ 1 Federation Loaded from File
+ )}
+
+ }
+ timeout={60000}
+ title="Federations Upload Notification"
+ />
+
+
+
+
true}
+ size='lg'
+ triggerButtonKind="ghost"
+ buttonTriggerText="View Uploaded Bundle/s"
+ modalHeading="Federation JSON"
+ modalLabel="View Uploaded Federation(s)"
+ >
+ {JSON.stringify(uploadedFederation, null, 2)}
+
+
+
+
+
+
+
+
+
+
+
+
+
Step 2. Edit Bundle
+
this.setState({ exposedBundleEndpoint: e.target.value })}
+ />
+
+
+
+
+
+
+ )}
+
+
+
+ {((!Array.isArray(uploadedFederation) && !uploadedFederation) || (Array.isArray(uploadedFederation) && uploadedFederation.length === 0)) && (
+ (Upload JSON File to Enable)
+ )}
+
+ {loading && (
+
+ )}
+
+
+
+
+ Custom Federation Form
+ (click to expand)
+ >
+ }
+ >
+
+
+
+
+
+
+ );
+ }
+}
+
+const mapStateToProps = (state: RootState) => ({
+ globalServerSelected: state.servers.globalServerSelected,
+ globalErrorMessage: state.tornjak.globalErrorMessage,
+});
+
+export default connect(mapStateToProps, {
+ tornjakMessageFunc,
+ federationsListUpdateFunc,
+})(FederationCreate);
+
+export { FederationCreate };
diff --git a/frontend/src/components/navbar.tsx b/frontend/src/components/navbar.tsx
index 3e294a68..1cf9ec60 100644
--- a/frontend/src/components/navbar.tsx
+++ b/frontend/src/components/navbar.tsx
@@ -43,7 +43,9 @@ import {
IbmCloudKubernetesService,
Add,
ZosSysplex,
- IbmCloudBareMetalServer
+ IbmCloudBareMetalServer,
+ Partnership,
+ IbmCloudAppId
} from "@carbon/icons-react";
import SpireHealthCheck from 'components/spire-health-check';
@@ -239,10 +241,12 @@ class NavigationBar extends Component {
withAuth={Boolean(withAuth)}
subLinks={[
{ label: 'Federations List', to: '/federations' },
+ { label: 'Obtain Trust Bundle', to: '/trustbundle', adminOnly: true},
+ { label: 'Create Federation', to: '/federation/create', adminOnly: true},
]}
/>