diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 14dae335..e8607960 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,6 +7,7 @@ import NavigationBar from "./components/navbar"; import SelectServer from "./components/select-server"; import ClusterList from "./components/cluster-list"; import ClusterManagement from "./components/cluster-management"; +import FederationList from "./components/federation-list"; import AgentList from "./components/agent-list"; import CreateJoinToken from "./components/agent-create-join-token"; import EntryList from "./components/entry-list"; @@ -44,6 +45,7 @@ function App() { {IsManager &&
} + diff --git a/frontend/src/components/apiConfig.ts b/frontend/src/components/apiConfig.ts index 22d64724..7ffcc0f5 100644 --- a/frontend/src/components/apiConfig.ts +++ b/frontend/src/components/apiConfig.ts @@ -9,6 +9,7 @@ const apiEndpoints = { spireAgentsBanApi: `${API_BASE_URL}/spire/agents/ban`, spireJoinTokenApi: `${API_BASE_URL}/spire/agents/jointoken`, spireEntriesApi: `${API_BASE_URL}/spire/entries`, + spireFederationsApi: `${API_BASE_URL}/spire/federations`, tornjakServerInfoApi: `${API_BASE_URL}/tornjak/serverinfo`, tornjakSelectorsApi: `${API_BASE_URL}/tornjak/selectors`, tornjakAgentsApi: `${API_BASE_URL}/tornjak/agents`, diff --git a/frontend/src/components/federation-list.tsx b/frontend/src/components/federation-list.tsx new file mode 100644 index 00000000..37d82348 --- /dev/null +++ b/frontend/src/components/federation-list.tsx @@ -0,0 +1,136 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import Table from "tables/federations-list-table"; +import TornjakApi from './tornjak-api-helpers'; +import { + serverSelectedFunc, + agentsListUpdateFunc, + tornjakServerInfoUpdateFunc, + serverInfoUpdateFunc, + selectorInfoFunc, + tornjakMessageFunc, + workloadSelectorInfoFunc, + agentworkloadSelectorInfoFunc, + clustersListUpdateFunc, + federationsListUpdateFunc +} from 'redux/actions'; +import { RootState } from 'redux/reducers'; +import { FederationsList, ServerInfo, TornjakServerInfo } from './types' + +type FederationsListProp = { + // dispatches a payload for list of federations with their metadata info as an array of FederationsList Type and has a return type of void + federationsListUpdateFunc: (globalFederationsList: FederationsList[]) => void, + // dispatches a payload for the tornjak error messsege and has a return type of void + tornjakMessageFunc: (globalErrorMessage: string) => void, + // dispatches a payload for the server trust domain and nodeAttestorPlugin as a ServerInfoType and has a return type of void + serverInfoUpdateFunc: (globalServerInfo: ServerInfo) => void, + // the selected server for manager mode + globalServerSelected: string, + // error/ success messege returned for a specific function + globalErrorMessage: string, + // tornjak server info of the selected server + globalTornjakServerInfo: TornjakServerInfo, + // list of federations with their metadata info as an array of FederationsList Type + globalFederationsList: FederationsList[], +} + +type FederationsListState = { + message: string // error/ success messege returned for a specific function for this specific component +} + +const Federation = (props: { federation: FederationsList }) => ( + + {props.federation.trust_domain} + {props.federation.bundle_endpoint_url} + {props.federation.BundleEndpointProfile.HttpsSpiffe ? 'https_spiffe' : 'https_web'} +
+
{JSON.stringify(props.federation, null, ' ')}
+
+ +) + +class FederationList extends Component { + TornjakApi: TornjakApi; + constructor(props: FederationsListProp) { + super(props); + this.TornjakApi = new TornjakApi(props); + this.state = { + message: "", + }; + } + + componentDidMount() { + this.TornjakApi.populateLocalFederationsUpdate(this.props.federationsListUpdateFunc, this.props.tornjakMessageFunc); + if (this.props.globalTornjakServerInfo && Object.keys(this.props.globalTornjakServerInfo).length) { + this.TornjakApi.populateServerInfo(this.props.globalTornjakServerInfo, this.props.serverInfoUpdateFunc); + } + } + + componentDidUpdate(prevProps: FederationsListProp) { + if (prevProps.globalTornjakServerInfo !== this.props.globalTornjakServerInfo) { + this.TornjakApi.populateServerInfo(this.props.globalTornjakServerInfo, this.props.serverInfoUpdateFunc); + } + } + + federationList() { + if (typeof this.props.globalFederationsList !== 'undefined') { + return this.props.globalFederationsList.map((currentFederation: FederationsList, index) => { + return ; + }) + } else { + return "" + } + } + + render() { + return ( +
+

Federations List

+ {this.props.globalErrorMessage !== "OK" && +
+
+              {this.props.globalErrorMessage}
+            
+
+ } +

+
+ + + + ) + } +} + +// Note: Needed for UI testing - will be removed after +// FederationsList.propTypes = { +// globalServerSelected: PropTypes.string, +// globalClustersList: PropTypes.array, +// globalTornjakServerInfo: PropTypes.object, +// globalErrorMessage: PropTypes.string, +// serverSelectedFunc: PropTypes.func, +// agentsListUpdateFunc: PropTypes.func, +// tornjakServerInfoUpdateFunc: PropTypes.func, +// serverInfoUpdateFunc: PropTypes.func, +// clusterTypeList: PropTypes.array, +// agentsList: PropTypes.array, +// selectorInfoFunc: PropTypes.func, +// tornjakMessageFunc: PropTypes.func, +// workloadSelectorInfoFunc: PropTypes.func, +// agentworkloadSelectorInfoFunc: PropTypes.func, +// clustersListUpdateFunc: PropTypes.func +// }; + +const mapStateToProps = (state: RootState) => ({ + globalServerSelected: state.servers.globalServerSelected, + globalFederationsList: state.federations.globalFederationsList, + globalTornjakServerInfo: state.servers.globalTornjakServerInfo, + globalErrorMessage: state.tornjak.globalErrorMessage, +}) + +export default connect( + mapStateToProps, + { serverSelectedFunc, agentsListUpdateFunc, tornjakServerInfoUpdateFunc, serverInfoUpdateFunc, selectorInfoFunc, tornjakMessageFunc, workloadSelectorInfoFunc, agentworkloadSelectorInfoFunc, clustersListUpdateFunc, federationsListUpdateFunc } +)(FederationList) + +export { FederationList } diff --git a/frontend/src/components/navbar.tsx b/frontend/src/components/navbar.tsx index d00eb7a6..5b11249a 100644 --- a/frontend/src/components/navbar.tsx +++ b/frontend/src/components/navbar.tsx @@ -145,6 +145,16 @@ class NavigationBar extends Component { } +
+ Federations +
+ Federations List + {/* To be added */} + {/*{(isAdmin || !withAuth) &&*/} + {/* Create Federation*/} + {/*}*/} +
+
diff --git a/frontend/src/components/tornjak-api-helpers.tsx b/frontend/src/components/tornjak-api-helpers.tsx index d58f1016..44edb71f 100644 --- a/frontend/src/components/tornjak-api-helpers.tsx +++ b/frontend/src/components/tornjak-api-helpers.tsx @@ -10,7 +10,8 @@ import { ServerInfo, EntriesList, ClustersList, - DebugServerInfo + DebugServerInfo, + FederationsList } from './types'; import KeycloakService from "auth/KeycloakAuth"; import { showResponseToast } from './error-api'; @@ -228,6 +229,25 @@ class TornjakApi extends Component { }) } + // populateLocalFederationsUpdate - returns the list of federations with their info in Local mode for the server + populateLocalFederationsUpdate = (federationsListUpdateFunc: { + (globalFederationsList: FederationsList[]): void; + }, + tornjakMessageFunc: { (globalErrorMessage: string): void; }) => { + axios.get(GetApiServerUri(apiEndpoints.spireFederationsApi), { crossdomain: true }) + .then(response => { + if (!response.data["federation_relationships"]) { + federationsListUpdateFunc([]); + } else { federationsListUpdateFunc(response.data["federation_relationships"]); } + tornjakMessageFunc(response.statusText); + }) + .catch((error) => { + showResponseToast(error, { caption: "Could not populate local federations." }) + federationsListUpdateFunc([]); + tornjakMessageFunc("Error retrieving: " + error.message); + }) + } + // populateLocalClustersUpdate - returns the list of clusters with their info in Local mode for the server populateLocalClustersUpdate = ( clustersListUpdateFunc: { (globalClustersList: ClustersList[]): void }, diff --git a/frontend/src/components/types.ts b/frontend/src/components/types.ts index 61fdec20..89d257f8 100644 --- a/frontend/src/components/types.ts +++ b/frontend/src/components/types.ts @@ -46,6 +46,38 @@ export interface ClustersList { agentsList: Array; // List of agents associated with the cluster } +// federations +export interface FederationProfileSpiffeResponse { + endpoint_spiffe_id: string; +} + +export interface BundleEndpointProfile { + HttpsSpiffe?: FederationProfileSpiffeResponse + HttpsWeb?: object +} + +export interface x509Authority { + asn1: string +} + +export interface JwtAuthority { + public_key: string + key_id: string +} + +export interface TrustDomainBundle { + trust_domain: string + x509_authorities: Array + jwt_authorities: Array +} + +export interface FederationsList { + trust_domain: string; + bundle_endpoint_url: string; + BundleEndpointProfile: BundleEndpointProfile; + trust_domain_bundle: TrustDomainBundle; +} + // entries export interface EntriesList { // From https://github.com/spiffe/spire-api-sdk/blob/main/proto/spire/api/types/entry.pb.go diff --git a/frontend/src/redux/actions/index.ts b/frontend/src/redux/actions/index.ts index 3a9b2f8a..74bba8d7 100644 --- a/frontend/src/redux/actions/index.ts +++ b/frontend/src/redux/actions/index.ts @@ -44,7 +44,7 @@ import { GLOBAL_SPIRE_HEALTH_CHECK_TIME, SpireHealthCheckTimeAction, GLOBAL_DEBUG_SERVER_INFO, - DebugServerInfoAction + DebugServerInfoAction, GLOBAL_FEDERATIONS_LIST, FederationsListAction } from './types'; import { @@ -58,7 +58,7 @@ import { TornjakServerInfo, WorkloadSelectorInfoLabels, SpireHealthCheckFreq, - DebugServerInfo + DebugServerInfo, FederationsList } from 'components/types'; // Expected input - spire debug server info @@ -319,6 +319,17 @@ export function agentsListUpdateFunc(globalAgentsList: AgentsList[]): ThunkActio } } +// Expected input - List of federations with their info +// federationsListUpdateFunc returns the list of federations with their info +export function federationsListUpdateFunc(globalFederationsList: FederationsList[]): ThunkAction { + return dispatch => { + dispatch({ + type: GLOBAL_FEDERATIONS_LIST, + payload: globalFederationsList + }); + } +} + // Expected input - // [ // "workloadselector1": [ diff --git a/frontend/src/redux/actions/types.ts b/frontend/src/redux/actions/types.ts index d4541e09..7d54c92d 100644 --- a/frontend/src/redux/actions/types.ts +++ b/frontend/src/redux/actions/types.ts @@ -10,7 +10,7 @@ import { TornjakServerInfo, WorkloadSelectorInfoLabels, SpireHealthCheckFreq, - DebugServerInfo + DebugServerInfo, FederationsList } from "components/types"; // auth @@ -50,6 +50,17 @@ export interface AgentWorkloadSelectorInfoAction extends Action { + payload: FederationsList[]; +} + // clusters export const GLOBAL_CLUSTERS_LIST = 'GLOBAL_CLUSTERS_LIST'; export const GLOBAL_CLUSTER_TYPE_INFO = 'GLOBAL_CLUSTER_TYPE_INFO'; diff --git a/frontend/src/redux/reducers/federationsReducer.ts b/frontend/src/redux/reducers/federationsReducer.ts new file mode 100644 index 00000000..8ebabfff --- /dev/null +++ b/frontend/src/redux/reducers/federationsReducer.ts @@ -0,0 +1,21 @@ +import { + FederationsListAction, + FederationsReducerState, + GLOBAL_FEDERATIONS_LIST, +} from '../actions/types'; + +const initialState: FederationsReducerState = { + globalFederationsList: [], +}; + +export default function federationsReducer(state: FederationsReducerState = initialState, action: FederationsListAction) { + switch (action.type) { + case GLOBAL_FEDERATIONS_LIST: + return { + ...state, + globalFederationsList: action.payload + }; + default: + return state; + } +} \ No newline at end of file diff --git a/frontend/src/redux/reducers/index.ts b/frontend/src/redux/reducers/index.ts index ec38764f..ef1c7b85 100644 --- a/frontend/src/redux/reducers/index.ts +++ b/frontend/src/redux/reducers/index.ts @@ -5,10 +5,12 @@ import entriesReducer from './entriesReducer'; import tornjakReducer from './tornjakReducer'; import {combineReducers} from 'redux'; import authReducer from './authReducer'; +import federationsReducer from "./federationsReducer"; const allReducers = combineReducers({ servers : serversReducer, clusters : clustersReducer, + federations: federationsReducer, agents : agentsReducer, entries : entriesReducer, tornjak: tornjakReducer, diff --git a/frontend/src/tables/federations-list-table.tsx b/frontend/src/tables/federations-list-table.tsx new file mode 100644 index 00000000..7ba92f78 --- /dev/null +++ b/frontend/src/tables/federations-list-table.tsx @@ -0,0 +1,125 @@ +import React from "react"; +import { connect } from 'react-redux'; +import { + federationsListUpdateFunc +} from 'redux/actions'; +import Table from './list-table'; +import { FederationsList } from "components/types"; +import { RootState } from "redux/reducers"; +import TornjakApi from 'components/tornjak-api-helpers'; + +// FederationListTable takes in +// listTableData: federation data to be rendered on table +// returns federations data inside a carbon component table with specified functions + +type FederationsListTableProp = { + // dispatches a payload for list of federations with their metadata info as an array of FederationsList Type and has a return type of void + federationsListUpdateFunc: (globalFederationsList: FederationsList[]) => void, + // data provided to the federations table + data: { + key: string, + props: { federation: FederationsList } + }[] | string | JSX.Element[], + id: string, + // list of federations with their metadata info as an array of FederationsList Type + globalFederationsList: FederationsList[], + // the selected server for manager mode + globalServerSelected: string, +} + +type FederationsListTableState = { + listData: { key: string, props: { federation: FederationsList } }[] | FederationsList[] | string | JSX.Element[], + listTableData: { id: string, [x: string]: string; }[] + +} +class FederationsListTable extends React.Component { + TornjakApi: TornjakApi; + constructor(props: FederationsListTableProp) { + super(props); + this.TornjakApi = new TornjakApi(props); + this.state = { + listData: props.data, + listTableData: [], + }; + this.prepareTableData = this.prepareTableData.bind(this); + } + + componentDidMount() { + this.prepareTableData(); + } + componentDidUpdate(prevProps: FederationsListTableProp) { + if (prevProps !== this.props) { + this.setState({ + listData: this.props.globalFederationsList + }) + this.prepareTableData(); + } + } + + prepareTableData() { + const { data } = this.props; + let listData: { props: { federation: FederationsList; }; }[] | ({ key: string; props: { federation: FederationsList; }; } | JSX.Element)[] = []; + if (typeof (data) === "string" || data === undefined) + return + data.forEach(val => listData.push(Object.assign({}, val))); + let listtabledata: { id: string; federationTrustDomain: string; federationBundleUrl: string; federationBundleProfile: string; info: string }[] = []; + for (let i = 0; i < listData.length; i++) { + listtabledata[i] = { id: "", federationTrustDomain: "", federationBundleUrl: "", federationBundleProfile: "", info: "" }; + listtabledata[i]["id"] = (i + 1).toString(); + listtabledata[i]["federationTrustDomain"] = listData[i].props.federation.trust_domain; + listtabledata[i]["federationBundleUrl"] = listData[i].props.federation.bundle_endpoint_url; + listtabledata[i]["federationBundleProfile"] = listData[i].props.federation.BundleEndpointProfile.HttpsSpiffe ? 'https_spiffe' : 'https_web'; + listtabledata[i]["info"] = JSON.stringify(listData[i].props.federation, null, ' '); + } + this.setState({ + listTableData: listtabledata + }) + } + + render() { + const { listTableData } = this.state; + const headerData = [ + { + header: '#No', + key: 'id', + }, + { + header: 'Trust Domain', + key: 'federationTrustDomain', + }, + { + header: 'Bundle Endpoint URL', + key: 'federationBundleUrl', + }, + { + header: 'Bundle Endpoint Profile', + key: 'federationBundleProfile', + }, + { + header: 'Info', + key: 'info', + }, + ]; + return ( +
+
{}} + banEntity={undefined} + downloadEntity={undefined} /> + + ); + } +} + +const mapStateToProps = (state: RootState) => ({ + globalServerSelected: state.servers.globalServerSelected, + globalFederationsList: state.federations.globalFederationsList, +}) + +export default connect( + mapStateToProps, + { federationsListUpdateFunc } +)(FederationsListTable) \ No newline at end of file