Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Federation Experience Frontend - View #525

Merged
merged 13 commits into from
Nov 1, 2024
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -44,6 +45,7 @@ function App() {
{IsManager && <br />}
<Route path="/" exact component={AgentList} />
<Route path="/clusters" exact component={ClusterList} />
<Route path="/federations" exact component={FederationList} />
<Route path="/agents" exact component={AgentList} />
<Route path="/entries" exact component={EntryList} />
<RenderOnAdminRole>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/apiConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
136 changes: 136 additions & 0 deletions frontend/src/components/federation-list.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => (
<tr>
<td>{props.federation.trust_domain}</td>
<td>{props.federation.bundle_endpoint_url}</td>
<td>{props.federation.BundleEndpointProfile.HttpsSpiffe ? 'https_spiffe' : 'https_web'}</td>
<td><div style={{ overflowX: 'auto', width: "400px" }}>
<pre>{JSON.stringify(props.federation, null, ' ')}</pre>
</div></td>
</tr>
)

class FederationList extends Component<FederationsListProp, FederationsListState> {
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 <Federation key={`federation-${index}`} federation={currentFederation} />;
})
} else {
return ""
}
}

render() {
return (
<div>
<h3>Federations List</h3>
{this.props.globalErrorMessage !== "OK" &&
<div className="alert-primary" role="alert">
<pre>
{this.props.globalErrorMessage}
</pre>
</div>
}
<br /><br />
<div className="indvidual-list-table">
<Table data={this.federationList()} id="table-1" />
</div>
</div>
)
}
}

// 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 }
10 changes: 10 additions & 0 deletions frontend/src/components/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,16 @@ class NavigationBar extends Component<NavigationBarProp, NavigationBarState> {
}
</div>
</div>
<div className="dropdown">
<a href="/federations" className="dropbtn">Federations </a>
<div className="dropdown-content">
<a href="/federations" className="nav-link">Federations List</a>
{/* To be added */}
{/*{(isAdmin || !withAuth) &&*/}
{/* <a href="" className="nav-link">Create Federation</a>*/}
{/*}*/}
</div>
</div>
<div className="dropdown">
<a href="/tornjak/serverinfo" className="dropbtn">Tornjak ServerInfo</a>
</div>
Expand Down
22 changes: 21 additions & 1 deletion frontend/src/components/tornjak-api-helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
ServerInfo,
EntriesList,
ClustersList,
DebugServerInfo
DebugServerInfo,
FederationsList
} from './types';
import KeycloakService from "auth/KeycloakAuth";
import { showResponseToast } from './error-api';
Expand Down Expand Up @@ -228,6 +229,25 @@ class TornjakApi extends Component<TornjakApiProp, TornjakApiState> {
})
}

// 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 },
Expand Down
32 changes: 32 additions & 0 deletions frontend/src/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,38 @@ export interface ClustersList {
agentsList: Array<string>; // 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<x509Authority>
jwt_authorities: Array<JwtAuthority>
}

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
Expand Down
15 changes: 13 additions & 2 deletions frontend/src/redux/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import {
GLOBAL_SPIRE_HEALTH_CHECK_TIME,
SpireHealthCheckTimeAction,
GLOBAL_DEBUG_SERVER_INFO,
DebugServerInfoAction
DebugServerInfoAction, GLOBAL_FEDERATIONS_LIST, FederationsListAction
} from './types';

import {
Expand All @@ -58,7 +58,7 @@ import {
TornjakServerInfo,
WorkloadSelectorInfoLabels,
SpireHealthCheckFreq,
DebugServerInfo
DebugServerInfo, FederationsList
} from 'components/types';

// Expected input - spire debug server info
Expand Down Expand Up @@ -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<void, RootState, undefined, FederationsListAction> {
return dispatch => {
dispatch({
type: GLOBAL_FEDERATIONS_LIST,
payload: globalFederationsList
});
}
}

// Expected input -
// [
// "workloadselector1": [
Expand Down
13 changes: 12 additions & 1 deletion frontend/src/redux/actions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
TornjakServerInfo,
WorkloadSelectorInfoLabels,
SpireHealthCheckFreq,
DebugServerInfo
DebugServerInfo, FederationsList
} from "components/types";

// auth
Expand Down Expand Up @@ -50,6 +50,17 @@ export interface AgentWorkloadSelectorInfoAction extends Action<typeof GLOBAL_AG
payload: AgentsWorkLoadAttestorInfo[];
}

// federations
export const GLOBAL_FEDERATIONS_LIST = 'GLOBAL_FEDERATIONS_LIST';

export interface FederationsReducerState {
globalFederationsList: FederationsList[],
}

export interface FederationsListAction extends Action<typeof GLOBAL_FEDERATIONS_LIST> {
payload: FederationsList[];
}

// clusters
export const GLOBAL_CLUSTERS_LIST = 'GLOBAL_CLUSTERS_LIST';
export const GLOBAL_CLUSTER_TYPE_INFO = 'GLOBAL_CLUSTER_TYPE_INFO';
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/redux/reducers/federationsReducer.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 2 additions & 0 deletions frontend/src/redux/reducers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading