diff --git a/src/components/IstioWizards/IstioWizardActions.ts b/src/components/IstioWizards/IstioWizardActions.ts index d1d8b06839..c84d676fc2 100644 --- a/src/components/IstioWizards/IstioWizardActions.ts +++ b/src/components/IstioWizards/IstioWizardActions.ts @@ -10,17 +10,22 @@ import { Condition, DestinationRule, DestinationRules, - HTTPRouteDestination, Gateway, HTTPMatchRequest, HTTPRoute, + HTTPRouteDestination, LoadBalancerSettings, Operation, + PeerAuthentication, + PeerAuthenticationMutualTLSMode, + PeerAuthenticationWorkloadSelector, + RequestAuthentication, Sidecar, Source, StringMatch, VirtualService, - VirtualServices + VirtualServices, + WorkloadEntrySelector, } from '../../types/IstioObjects'; import { serverConfig } from '../../config'; import { ThreeScaleServiceRule } from '../../types/ThreeScale'; @@ -29,6 +34,8 @@ import { ConsistentHashType, MUTUAL, TrafficPolicyState } from './TrafficPolicy' import { GatewayState } from '../../pages/IstioConfigNew/GatewayForm'; import { SidecarState } from '../../pages/IstioConfigNew/SidecarForm'; import { AuthorizationPolicyState } from '../../pages/IstioConfigNew/AuthorizationPolicyForm'; +import { PeerAuthenticationState } from '../../pages/IstioConfigNew/PeerAuthenticationForm'; +import { RequestAuthenticationState } from '../../pages/IstioConfigNew/RequestAuthenticationForm'; export const WIZARD_WEIGHTED_ROUTING = 'weighted_routing'; export const WIZARD_MATCHING_ROUTING = 'matching_routing'; @@ -41,14 +48,14 @@ export const WIZARD_TITLES = { [WIZARD_WEIGHTED_ROUTING]: 'Create Weighted Routing', [WIZARD_MATCHING_ROUTING]: 'Create Matching Routing', [WIZARD_SUSPEND_TRAFFIC]: 'Suspend Traffic', - [WIZARD_THREESCALE_INTEGRATION]: 'Add 3scale API Management Rule' + [WIZARD_THREESCALE_INTEGRATION]: 'Add 3scale API Management Rule', }; export const WIZARD_UPDATE_TITLES = { [WIZARD_WEIGHTED_ROUTING]: 'Update Weighted Routing', [WIZARD_MATCHING_ROUTING]: 'Update Matching Routing', [WIZARD_SUSPEND_TRAFFIC]: 'Update Suspended Traffic', - [WIZARD_THREESCALE_INTEGRATION]: 'Update 3scale API Management Rule' + [WIZARD_THREESCALE_INTEGRATION]: 'Update 3scale API Management Rule', }; export type WizardProps = { @@ -101,8 +108,8 @@ const buildHTTPMatchRequest = (matches: string[]): HTTPMatchRequest[] => { const matchHeaders: HTTPMatchRequest = { headers: {} }; // Headers are grouped matches - .filter(match => match.startsWith('headers')) - .forEach(match => { + .filter((match) => match.startsWith('headers')) + .forEach((match) => { // match follows format: headers [] const i0 = match.indexOf('['); const j0 = match.indexOf(']'); @@ -118,8 +125,8 @@ const buildHTTPMatchRequest = (matches: string[]): HTTPMatchRequest[] => { } // Rest of matches matches - .filter(match => !match.startsWith('headers')) - .forEach(match => { + .filter((match) => !match.startsWith('headers')) + .forEach((match) => { // match follows format: const i = match.indexOf(' '); const j = match.indexOf(' ', i + 1); @@ -128,8 +135,8 @@ const buildHTTPMatchRequest = (matches: string[]): HTTPMatchRequest[] => { const value = match.substring(j + 1).trim(); matchRequests.push({ [name]: { - [op]: value - } + [op]: value, + }, }); }); return matchRequests; @@ -152,7 +159,7 @@ const parseHttpMatchRequest = (httpMatchRequest: HTTPMatchRequest): string[] => const matches: string[] = []; // Headers if (httpMatchRequest.headers) { - Object.keys(httpMatchRequest.headers).forEach(headerName => { + Object.keys(httpMatchRequest.headers).forEach((headerName) => { const value = httpMatchRequest.headers![headerName]; matches.push('headers [' + headerName + '] ' + parseStringMatch(value)); }); @@ -207,12 +214,12 @@ export const buildIstioConfig = ( namespace: wProps.namespace, name: wProps.serviceName, labels: { - [KIALI_WIZARD_LABEL]: wProps.type - } + [KIALI_WIZARD_LABEL]: wProps.type, + }, }, spec: { host: fqdnServiceName(wProps.serviceName, wProps.namespace), - subsets: wProps.workloads.map(workload => { + subsets: wProps.workloads.map((workload) => { // Using version const versionLabelName = serverConfig.istioLabels.versionLabelName; const versionValue = workload.labels![versionLabelName]; @@ -222,10 +229,10 @@ export const buildIstioConfig = ( wkdNameVersion[workload.name] = versionValue; return { name: versionValue, - labels: labels + labels: labels, }; - }) - } + }), + }, }; const wizardVS: VirtualService = { @@ -233,10 +240,10 @@ export const buildIstioConfig = ( namespace: wProps.namespace, name: wProps.serviceName, labels: { - [KIALI_WIZARD_LABEL]: wProps.type - } + [KIALI_WIZARD_LABEL]: wProps.type, + }, }, - spec: {} + spec: {}, }; // Wizard is optional, only when user has explicitly selected "Create a Gateway" @@ -248,24 +255,24 @@ export const buildIstioConfig = ( namespace: wProps.namespace, name: fullNewGatewayName.substr(wProps.namespace.length + 1), labels: { - [KIALI_WIZARD_LABEL]: wProps.type - } + [KIALI_WIZARD_LABEL]: wProps.type, + }, }, spec: { selector: { - istio: 'ingressgateway' + istio: 'ingressgateway', }, servers: [ { port: { number: wState.gateway.port, name: 'http', - protocol: 'HTTP' + protocol: 'HTTP', }, - hosts: wState.gateway.gwHosts.split(',') - } - ] - } + hosts: wState.gateway.gwHosts.split(','), + }, + ], + }, } : undefined; @@ -275,32 +282,32 @@ export const buildIstioConfig = ( wizardVS.spec = { http: [ { - route: wState.workloads.map(workload => { + route: wState.workloads.map((workload) => { return { destination: { host: fqdnServiceName(wProps.serviceName, wProps.namespace), - subset: wkdNameVersion[workload.name] + subset: wkdNameVersion[workload.name], }, - weight: workload.weight + weight: workload.weight, }; - }) - } - ] + }), + }, + ], }; break; } case WIZARD_MATCHING_ROUTING: { // VirtualService from the routes wizardVS.spec = { - http: wState.rules.map(rule => { + http: wState.rules.map((rule) => { const httpRoute: HTTPRoute = {}; httpRoute.route = []; for (let iRoute = 0; iRoute < rule.routes.length; iRoute++) { const destW: HTTPRouteDestination = { destination: { host: fqdnServiceName(wProps.serviceName, wProps.namespace), - subset: wkdNameVersion[rule.routes[iRoute]] - } + subset: wkdNameVersion[rule.routes[iRoute]], + }, }; destW.weight = Math.floor(100 / rule.routes.length); if (iRoute === 0) { @@ -313,18 +320,18 @@ export const buildIstioConfig = ( httpRoute.match = buildHTTPMatchRequest(rule.matches); } return httpRoute; - }) + }), }; break; } case WIZARD_SUSPEND_TRAFFIC: { // VirtualService from the suspendedRoutes const httpRoute: HTTPRoute = { - route: [] + route: [], }; // Let's use the # os suspended notes to create weights const totalRoutes = wState.suspendedRoutes.length; - const closeRoutes = wState.suspendedRoutes.filter(s => s.suspended).length; + const closeRoutes = wState.suspendedRoutes.filter((s) => s.suspended).length; const openRoutes = totalRoutes - closeRoutes; let firstValue = true; // If we have some suspended routes, we need to use weights @@ -334,8 +341,8 @@ export const buildIstioConfig = ( const destW: HTTPRouteDestination = { destination: { host: fqdnServiceName(wProps.serviceName, wProps.namespace), - subset: wkdNameVersion[suspendedRoute.workload] - } + subset: wkdNameVersion[suspendedRoute.workload], + }, }; if (suspendedRoute.suspended) { // A suspended route has a 0 weight @@ -355,21 +362,21 @@ export const buildIstioConfig = ( httpRoute.route = [ { destination: { - host: fqdnServiceName(wProps.serviceName, wProps.namespace) - } - } + host: fqdnServiceName(wProps.serviceName, wProps.namespace), + }, + }, ]; httpRoute.fault = { abort: { httpStatus: SERVICE_UNAVAILABLE, percentage: { - value: 100 - } - } + value: 100, + }, + }, }; } wizardVS.spec = { - http: [httpRoute] + http: [httpRoute], }; break; } @@ -385,14 +392,14 @@ export const buildIstioConfig = ( if (wState.trafficPolicy.tlsModified || wState.trafficPolicy.addLoadBalancer) { wizardDR.spec.trafficPolicy = { tls: null, - loadBalancer: null + loadBalancer: null, }; if (wState.trafficPolicy.tlsModified) { wizardDR.spec.trafficPolicy.tls = { mode: wState.trafficPolicy.mtlsMode, clientCertificate: null, privateKey: null, - caCertificates: null + caCertificates: null, }; if (wState.trafficPolicy.mtlsMode === MUTUAL) { wizardDR.spec.trafficPolicy.tls.clientCertificate = wState.trafficPolicy.clientCertificate; @@ -405,17 +412,17 @@ export const buildIstioConfig = ( // Remember to put a null fields that need to be deleted on a JSON merge patch wizardDR.spec.trafficPolicy.loadBalancer = { simple: wState.trafficPolicy.loadBalancer.simple, - consistentHash: null + consistentHash: null, }; } else { wizardDR.spec.trafficPolicy.loadBalancer = { simple: null, - consistentHash: {} + consistentHash: {}, }; wizardDR.spec.trafficPolicy.loadBalancer.consistentHash = { httpHeaderName: null, httpCookie: null, - useSourceIp: null + useSourceIp: null, }; if (wState.trafficPolicy.loadBalancer.consistentHash) { const consistentHash = wState.trafficPolicy.loadBalancer.consistentHash; @@ -453,18 +460,18 @@ export const buildIstioConfig = ( const getWorkloadsByVersion = (workloads: WorkloadOverview[]): { [key: string]: string } => { const versionLabelName = serverConfig.istioLabels.versionLabelName; const wkdVersionName: { [key: string]: string } = {}; - workloads.forEach(workload => (wkdVersionName[workload.labels![versionLabelName]] = workload.name)); + workloads.forEach((workload) => (wkdVersionName[workload.labels![versionLabelName]] = workload.name)); return wkdVersionName; }; export const getDefaultWeights = (workloads: WorkloadOverview[]): WorkloadWeight[] => { const wkTraffic = workloads.length < 100 ? Math.floor(100 / workloads.length) : 0; const remainTraffic = workloads.length < 100 ? 100 % workloads.length : 0; - const wkWeights: WorkloadWeight[] = workloads.map(workload => ({ + const wkWeights: WorkloadWeight[] = workloads.map((workload) => ({ name: workload.name, weight: wkTraffic, locked: false, - maxWeight: 100 + maxWeight: 100, })); if (remainTraffic > 0) { wkWeights[wkWeights.length - 1].weight = wkWeights[wkWeights.length - 1].weight + remainTraffic; @@ -477,7 +484,7 @@ export const getInitWeights = (workloads: WorkloadOverview[], virtualServices: V const wkdWeights: WorkloadWeight[] = []; if (virtualServices.items.length === 1 && virtualServices.items[0].spec.http!.length === 1) { // Populate WorkloadWeights from a VirtualService - virtualServices.items[0].spec.http![0].route!.forEach(route => { + virtualServices.items[0].spec.http![0].route!.forEach((route) => { // A wkdVersionName[route.destination.subset] === undefined may indicate that a VS contains a removed workload // Checking before to add it to the Init Weights if (route.destination.subset && wkdVersionName[route.destination.subset]) { @@ -485,7 +492,7 @@ export const getInitWeights = (workloads: WorkloadOverview[], virtualServices: V name: wkdVersionName[route.destination.subset], weight: route.weight || 0, locked: false, - maxWeight: 100 + maxWeight: 100, }); } }); @@ -507,7 +514,7 @@ export const getInitWeights = (workloads: WorkloadOverview[], virtualServices: V name: wkd.name, weight: 0, locked: false, - maxWeight: 100 + maxWeight: 100, }); } } @@ -519,16 +526,16 @@ export const getInitRules = (workloads: WorkloadOverview[], virtualServices: Vir const wkdVersionName = getWorkloadsByVersion(workloads); const rules: Rule[] = []; if (virtualServices.items.length === 1) { - virtualServices.items[0].spec.http!.forEach(httpRoute => { + virtualServices.items[0].spec.http!.forEach((httpRoute) => { const rule: Rule = { matches: [], - routes: [] + routes: [], }; if (httpRoute.match) { - httpRoute.match.forEach(m => (rule.matches = rule.matches.concat(parseHttpMatchRequest(m)))); + httpRoute.match.forEach((m) => (rule.matches = rule.matches.concat(parseHttpMatchRequest(m)))); } if (httpRoute.route) { - httpRoute.route.forEach(r => { + httpRoute.route.forEach((r) => { const subset = r.destination.subset; const workload = wkdVersionName[subset || '']; // Not adding a route if a workload is not found with a destination subset @@ -552,10 +559,10 @@ export const getInitSuspendedRoutes = ( virtualServices: VirtualServices ): SuspendedRoute[] => { const wkdVersionName = getWorkloadsByVersion(workloads); - const routes: SuspendedRoute[] = workloads.map(wk => ({ + const routes: SuspendedRoute[] = workloads.map((wk) => ({ workload: wk.name, suspended: true, - httpStatus: SERVICE_UNAVAILABLE + httpStatus: SERVICE_UNAVAILABLE, })); if (virtualServices.items.length === 1 && virtualServices.items[0].spec.http!.length === 1) { // All routes are suspended default value is correct @@ -563,10 +570,10 @@ export const getInitSuspendedRoutes = ( return routes; } // Iterate on route weights to identify the suspended routes - virtualServices.items[0].spec.http![0].route!.forEach(route => { + virtualServices.items[0].spec.http![0].route!.forEach((route) => { if (route.weight && route.weight > 0) { const workloadName = wkdVersionName[route.destination.subset || '']; - routes.filter(w => w.workload === workloadName).forEach(w => (w.suspended = false)); + routes.filter((w) => w.workload === workloadName).forEach((w) => (w.suspended = false)); } }); } @@ -583,7 +590,7 @@ export const getInitTlsMode = (destinationRules: DestinationRules): [string, str destinationRules.items[0].spec.trafficPolicy.tls.mode || '', destinationRules.items[0].spec.trafficPolicy.tls.clientCertificate || '', destinationRules.items[0].spec.trafficPolicy.tls.privateKey || '', - destinationRules.items[0].spec.trafficPolicy.tls.caCertificates || '' + destinationRules.items[0].spec.trafficPolicy.tls.caCertificates || '', ]; } return ['', '', '', '']; @@ -654,10 +661,10 @@ export const buildAuthorizationPolicy = ( name: name, namespace: namespace, labels: { - [KIALI_WIZARD_LABEL]: 'AuthorizationPolicy' - } + [KIALI_WIZARD_LABEL]: 'AuthorizationPolicy', + }, }, - spec: {} + spec: {}, }; // DENY_ALL and ALLOW_ALL are two specific cases @@ -673,9 +680,9 @@ export const buildAuthorizationPolicy = ( // RULES use case if (state.workloadSelector.length > 0) { const workloadSelector: AuthorizationPolicyWorkloadSelector = { - matchLabels: {} + matchLabels: {}, }; - state.workloadSelector.split(',').forEach(label => { + state.workloadSelector.split(',').forEach((label) => { label = label.trim(); const labelDetails = label.split('='); if (labelDetails.length === 2) { @@ -687,38 +694,38 @@ export const buildAuthorizationPolicy = ( if (state.rules.length > 0) { ap.spec.rules = []; - state.rules.forEach(rule => { + state.rules.forEach((rule) => { const appRule: AuthorizationPolicyRule = { from: undefined, to: undefined, - when: undefined + when: undefined, }; if (rule.from.length > 0) { - appRule.from = rule.from.map(fromItem => { + appRule.from = rule.from.map((fromItem) => { const source: Source = {}; - Object.keys(fromItem).forEach(key => { + Object.keys(fromItem).forEach((key) => { source[key] = fromItem[key]; }); return { - source: source + source: source, }; }); } if (rule.to.length > 0) { - appRule.to = rule.to.map(toItem => { + appRule.to = rule.to.map((toItem) => { const operation: Operation = {}; - Object.keys(toItem).forEach(key => { + Object.keys(toItem).forEach((key) => { operation[key] = toItem[key]; }); return { - operation: operation + operation: operation, }; }); } if (rule.when.length > 0) { - appRule.when = rule.when.map(condition => { + appRule.when = rule.when.map((condition) => { const cond: Condition = { - key: condition.key + key: condition.key, }; if (condition.values && condition.values.length > 0) { cond.values = condition.values; @@ -744,52 +751,139 @@ export const buildGateway = (name: string, namespace: string, state: GatewayStat name: name, namespace: namespace, labels: { - [KIALI_WIZARD_LABEL]: 'Gateway' - } + [KIALI_WIZARD_LABEL]: 'Gateway', + }, }, spec: { // Default for istio scenarios, user may change it editing YAML selector: { - istio: 'ingressgateway' + istio: 'ingressgateway', }, - servers: state.gatewayServers.map(s => ({ + servers: state.gatewayServers.map((s) => ({ port: { number: +s.portNumber, protocol: s.portProtocol, - name: s.portName + name: s.portName, }, - hosts: s.hosts - })) - } + hosts: s.hosts, + })), + }, }; return gw; }; +export const buildPeerAuthentication = ( + name: string, + namespace: string, + state: PeerAuthenticationState +): PeerAuthentication => { + const pa: PeerAuthentication = { + metadata: { + name: name, + namespace: namespace, + labels: { + [KIALI_WIZARD_LABEL]: 'PeerAuthentication', + }, + }, + spec: {}, + }; + + if (state.workloadSelector.length > 0) { + const workloadSelector: PeerAuthenticationWorkloadSelector = { + matchLabels: {}, + }; + state.workloadSelector.split(',').forEach((label) => { + label = label.trim(); + const labelDetails = label.split('='); + if (labelDetails.length === 2) { + workloadSelector.matchLabels[labelDetails[0]] = labelDetails[1]; + } + }); + pa.spec.selector = workloadSelector; + } + + // Kiali is always adding this field + pa.spec.mtls = { + mode: PeerAuthenticationMutualTLSMode[state.mtls], + }; + + if (state.portLevelMtls.length > 0) { + pa.spec.portLevelMtls = {}; + state.portLevelMtls.forEach((p) => { + if (pa.spec.portLevelMtls) { + pa.spec.portLevelMtls[Number(p.port)] = { + mode: PeerAuthenticationMutualTLSMode[p.mtls], + }; + } + }); + } + + return pa; +}; + +export const buildRequestAuthentication = ( + name: string, + namespace: string, + state: RequestAuthenticationState +): RequestAuthentication => { + const ra: RequestAuthentication = { + metadata: { + name: name, + namespace: namespace, + labels: { + [KIALI_WIZARD_LABEL]: 'RequestAuthentication', + }, + }, + spec: { + jwtRules: [], + }, + }; + + if (state.workloadSelector.length > 0) { + const workloadSelector: WorkloadEntrySelector = { + matchLabels: {}, + }; + state.workloadSelector.split(',').forEach((label) => { + label = label.trim(); + const labelDetails = label.split('='); + if (labelDetails.length === 2) { + workloadSelector.matchLabels[labelDetails[0]] = labelDetails[1]; + } + }); + ra.spec.selector = workloadSelector; + } + + if (state.jwtRules.length > 0) { + ra.spec.jwtRules = state.jwtRules; + } + return ra; +}; + export const buildSidecar = (name: string, namespace: string, state: SidecarState): Sidecar => { const sc: Sidecar = { metadata: { name: name, namespace: namespace, labels: { - [KIALI_WIZARD_LABEL]: 'Sidecar' - } + [KIALI_WIZARD_LABEL]: 'Sidecar', + }, }, spec: { egress: [ { - hosts: state.egressHosts.map(eh => eh.host) - } - ] - } + hosts: state.egressHosts.map((eh) => eh.host), + }, + ], + }, }; if (state.addWorkloadSelector && state.workloadSelectorValid) { sc.spec.workloadSelector = { - labels: {} + labels: {}, }; state.workloadSelectorLabels .trim() .split(',') - .forEach(split => { + .forEach((split) => { const labels = split.trim().split('='); // It should be already validated with workloadSelectorValid, but just to add extra safe check if (sc.spec.workloadSelector && labels.length === 2) { diff --git a/src/components/MessageCenter/AlertDrawerMessage.tsx b/src/components/MessageCenter/AlertDrawerMessage.tsx index 590eb21eb3..af09d93157 100644 --- a/src/components/MessageCenter/AlertDrawerMessage.tsx +++ b/src/components/MessageCenter/AlertDrawerMessage.tsx @@ -36,13 +36,13 @@ type AlertDrawerMessageProps = ReduxProps & { class AlertDrawerMessage extends React.PureComponent { static readonly body = style({ - paddingTop: 0 + paddingTop: 0, }); static readonly left = style({ - float: 'left' + float: 'left', }); static readonly right = style({ - float: 'right' + float: 'right', }); render() { @@ -57,7 +57,7 @@ class AlertDrawerMessage extends React.PureComponent { onToggle={() => this.props.toggleMessageDetail(this.props.message)} isExpanded={this.props.message.showDetail} > -
{this.props.message.detail}
+
{this.props.message.detail}
)} {this.props.message.count > 1 && ( @@ -77,13 +77,10 @@ class AlertDrawerMessage extends React.PureComponent { const mapDispatchToProps = (dispatch: ThunkDispatch) => { return { - markAsRead: message => dispatch(MessageCenterActions.markAsRead(message.id)), - toggleMessageDetail: message => dispatch(MessageCenterActions.toggleMessageDetail(message.id)) + markAsRead: (message) => dispatch(MessageCenterActions.markAsRead(message.id)), + toggleMessageDetail: (message) => dispatch(MessageCenterActions.toggleMessageDetail(message.id)), }; }; -const AlertDrawerMessageContainer = connect( - null, - mapDispatchToProps -)(AlertDrawerMessage); +const AlertDrawerMessageContainer = connect(null, mapDispatchToProps)(AlertDrawerMessage); export default AlertDrawerMessageContainer; diff --git a/src/helpers/ValidationHelpers.ts b/src/helpers/ValidationHelpers.ts new file mode 100644 index 0000000000..941065b463 --- /dev/null +++ b/src/helpers/ValidationHelpers.ts @@ -0,0 +1,10 @@ +// Kubernetes ID validation helper, used to allow mark a warning in the form edition +const k8sRegExpName = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[-a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/; +export const isValidK8SName = (name: string) => { + return name === '' ? false : name.search(k8sRegExpName) === 0; +}; + +const regExpRequestHeaders = /^request\.headers\[.*\]$/; +export const isValidRequestHeaderName = (name: string) => { + return name === '' ? false : name.search(regExpRequestHeaders) === 0; +}; diff --git a/src/pages/IstioConfigNew/AuthorizationPolicyForm.tsx b/src/pages/IstioConfigNew/AuthorizationPolicyForm.tsx index 2e368795b0..c467c5cabb 100644 --- a/src/pages/IstioConfigNew/AuthorizationPolicyForm.tsx +++ b/src/pages/IstioConfigNew/AuthorizationPolicyForm.tsx @@ -13,68 +13,79 @@ export type AuthorizationPolicyState = { workloadSelector: string; action: string; rules: Rule[]; -}; - -type State = { // Used to identify DENY_ALL, ALLOW_ALL or RULES rulesForm: string; addWorkloadSelector: boolean; workloadSelectorValid: boolean; - workloadSelectorLabels: string; - action: string; - rules: Rule[]; }; -const DENY_ALL = 'DENY_ALL'; -const ALLOW_ALL = 'ALLOW_ALL'; -const RULES = 'RULES'; -const ALLOW = 'ALLOW'; -const DENY = 'DENY'; +export const AUTHORIZACION_POLICY = 'AuthorizationPolicy'; +export const AUTHORIZATION_POLICIES = 'authorizationpolicies'; +export const DENY_ALL = 'DENY_ALL'; +export const ALLOW_ALL = 'ALLOW_ALL'; +export const RULES = 'RULES'; +export const ALLOW = 'ALLOW'; +export const DENY = 'DENY'; const HELPER_TEXT = { DENY_ALL: 'Denies all requests to workloads in given namespace(s)', ALLOW_ALL: 'Allows all requests to workloads in given namespace(s)', - RULES: 'Builds an Authorization Policy based on Rules' + RULES: 'Builds an Authorization Policy based on Rules', }; const rulesFormValues = [DENY_ALL, ALLOW_ALL, RULES]; const actions = [ALLOW, DENY]; -export const INIT_AUTHORIZATION_POLICY = (): AuthorizationPolicyState => ({ - policy: DENY_ALL, +export const initAuthorizationPolicy = (): AuthorizationPolicyState => ({ + policy: DENY, workloadSelector: '', action: ALLOW, - rules: [] + rules: [], + rulesForm: DENY_ALL, + addWorkloadSelector: false, + workloadSelectorValid: false, }); -class AuthorizationPolicyForm extends React.Component { +export const isAuthorizationPolicyStateValid = (ap: AuthorizationPolicyState): boolean => { + const workloadSelectorRule = ap.addWorkloadSelector ? ap.workloadSelectorValid : true; + const denyRule = ap.action === DENY ? ap.rules.length > 0 : true; + + return workloadSelectorRule && denyRule; +}; + +class AuthorizationPolicyForm extends React.Component { constructor(props: Props) { super(props); - this.state = { - rulesForm: this.props.authorizationPolicy.policy, - addWorkloadSelector: false, - workloadSelectorValid: false, - workloadSelectorLabels: this.props.authorizationPolicy.workloadSelector, - action: this.props.authorizationPolicy.action, - rules: [] - }; + this.state = initAuthorizationPolicy(); } componentDidMount() { this.setState({ - rulesForm: this.props.authorizationPolicy.policy, - addWorkloadSelector: false, - workloadSelectorValid: false, - workloadSelectorLabels: this.props.authorizationPolicy.workloadSelector, + policy: this.props.authorizationPolicy.policy, + workloadSelector: this.props.authorizationPolicy.workloadSelector, action: this.props.authorizationPolicy.action, - rules: [] + rules: [], + rulesForm: this.props.authorizationPolicy.rulesForm, + addWorkloadSelector: this.props.authorizationPolicy.addWorkloadSelector, + workloadSelectorValid: this.props.authorizationPolicy.workloadSelectorValid, }); } onRulesFormChange = (value, _) => { this.setState( { - rulesForm: value + rulesForm: value, + }, + () => this.onAuthorizationChange() + ); + }; + + onChangeWorkloadSelector = () => { + this.setState( + (prevState) => { + return { + addWorkloadSelector: !prevState.addWorkloadSelector, + }; }, () => this.onAuthorizationChange() ); @@ -82,10 +93,13 @@ class AuthorizationPolicyForm extends React.Component { addWorkloadLabels = (value: string, _) => { if (value.length === 0) { - this.setState({ - workloadSelectorValid: false, - workloadSelectorLabels: '' - }); + this.setState( + { + workloadSelectorValid: false, + workloadSelector: '', + }, + () => this.onAuthorizationChange() + ); return; } value = value.trim(); @@ -111,7 +125,7 @@ class AuthorizationPolicyForm extends React.Component { this.setState( { workloadSelectorValid: isValid, - workloadSelectorLabels: value + workloadSelector: value, }, () => this.onAuthorizationChange() ); @@ -120,7 +134,7 @@ class AuthorizationPolicyForm extends React.Component { onActionChange = (value, _) => { this.setState( { - action: value + action: value, }, () => this.onAuthorizationChange() ); @@ -128,10 +142,10 @@ class AuthorizationPolicyForm extends React.Component { onAddRule = (rule: Rule) => { this.setState( - prevState => { + (prevState) => { prevState.rules.push(rule); return { - rules: prevState.rules + rules: prevState.rules, }; }, () => this.onAuthorizationChange() @@ -140,10 +154,10 @@ class AuthorizationPolicyForm extends React.Component { onRemoveRule = (index: number) => { this.setState( - prevState => { + (prevState) => { prevState.rules.splice(index, 1); return { - rules: prevState.rules + rules: prevState.rules, }; }, () => this.onAuthorizationChange() @@ -151,13 +165,7 @@ class AuthorizationPolicyForm extends React.Component { }; onAuthorizationChange = () => { - const authorizationPolicy: AuthorizationPolicyState = { - policy: this.state.rulesForm, - workloadSelector: this.state.workloadSelectorLabels, - action: this.state.action, - rules: this.state.rules - }; - this.props.onChange(authorizationPolicy); + this.props.onChange(this.state); }; render() { @@ -177,11 +185,7 @@ class AuthorizationPolicyForm extends React.Component { label={' '} labelOff={' '} isChecked={this.state.addWorkloadSelector} - onChange={() => { - this.setState(prevState => ({ - addWorkloadSelector: !prevState.addWorkloadSelector - })); - }} + onChange={this.onChangeWorkloadSelector} /> )} @@ -197,7 +201,7 @@ class AuthorizationPolicyForm extends React.Component { id="gwHosts" name="gwHosts" isDisabled={!this.state.addWorkloadSelector} - value={this.state.workloadSelectorLabels} + value={this.state.workloadSelector} onChange={this.addWorkloadLabels} isValid={this.state.workloadSelectorValid} /> @@ -213,7 +217,9 @@ class AuthorizationPolicyForm extends React.Component { )} {this.state.rulesForm === RULES && } - {this.state.rulesForm === RULES && } + {this.state.rulesForm === RULES && ( + + )} ); } diff --git a/src/pages/IstioConfigNew/AuthorizationPolicyForm/From/SourceBuilder.tsx b/src/pages/IstioConfigNew/AuthorizationPolicyForm/From/SourceBuilder.tsx index 67a0b198ea..79df323830 100644 --- a/src/pages/IstioConfigNew/AuthorizationPolicyForm/From/SourceBuilder.tsx +++ b/src/pages/IstioConfigNew/AuthorizationPolicyForm/From/SourceBuilder.tsx @@ -3,6 +3,9 @@ import { cellWidth, ICell, Table, TableBody, TableHeader } from '@patternfly/rea // Use TextInputBase like workaround while PF4 team work in https://github.com/patternfly/patternfly-react/issues/4072 import { Button, FormSelect, FormSelectOption, TextInputBase as TextInput } from '@patternfly/react-core'; import { PlusCircleIcon } from '@patternfly/react-icons'; +import { isValidIp } from '../../../../utils/IstioConfigUtils'; +import { style } from 'typestyle'; +import { PfColors } from '../../../../components/Pf/PfColors'; type Props = { onAddFrom: (source: { [key: string]: string[] }) => void; @@ -25,24 +28,28 @@ const INIT_SOURCE_FIELDS = [ 'namespaces', 'notNamespaces', 'ipBlocks', - 'notIpBlocks' + 'notIpBlocks', ].sort(); +const noSourceStyle = style({ + color: PfColors.Red100, +}); + const headerCells: ICell[] = [ { title: 'Source Field', transforms: [cellWidth(20) as any], - props: {} + props: {}, }, { title: 'Values', transforms: [cellWidth(80) as any], - props: {} + props: {}, }, { title: '', - props: {} - } + props: {}, + }, ]; class SourceBuilder extends React.Component { @@ -52,24 +59,24 @@ class SourceBuilder extends React.Component { sourceFields: Object.assign([], INIT_SOURCE_FIELDS), source: {}, newSourceField: INIT_SOURCE_FIELDS[0], - newValues: '' + newValues: '', }; } onAddNewSourceField = (value: string, _) => { this.setState({ - newSourceField: value + newSourceField: value, }); }; onAddNewValues = (value: string, _) => { this.setState({ - newValues: value + newValues: value, }); }; onAddSource = () => { - this.setState(prevState => { + this.setState((prevState) => { const i = prevState.sourceFields.indexOf(prevState.newSourceField); if (i > -1) { prevState.sourceFields.splice(i, 1); @@ -79,7 +86,7 @@ class SourceBuilder extends React.Component { sourceFields: prevState.sourceFields, source: prevState.source, newSourceField: prevState.sourceFields[0], - newValues: '' + newValues: '', }; }); }; @@ -91,7 +98,7 @@ class SourceBuilder extends React.Component { sourceFields: Object.assign([], INIT_SOURCE_FIELDS), source: {}, newSourceField: INIT_SOURCE_FIELDS[0], - newValues: '' + newValues: '', }, () => { this.props.onAddFrom(fromItem); @@ -99,6 +106,21 @@ class SourceBuilder extends React.Component { ); }; + // Helper to identify when some values are valid + isValidSource = (): [boolean, string] => { + if (this.state.newSourceField === 'ipBlocks' || this.state.newSourceField === 'notIpBlocks') { + const validIp = this.state.newValues.split(',').every((ip) => isValidIp(ip)); + if (!validIp) { + return [false, 'Not valid IP']; + } + } + const emptyValues = this.state.newValues.split(',').every((v) => v.length === 0); + if (emptyValues) { + return [false, 'Empty value']; + } + return [true, '']; + }; + // @ts-ignore actionResolver = (rowData, { rowIndex }) => { const removeAction = { @@ -107,7 +129,7 @@ class SourceBuilder extends React.Component { onClick: (event, rowIndex, rowData, extraData) => { // Fetch sourceField from rowData, it's a fixed string on children const removeSourceField = rowData.cells[0].props.children.toString(); - this.setState(prevState => { + this.setState((prevState) => { prevState.sourceFields.push(removeSourceField); delete prevState.source[removeSourceField]; const newSourceFields = prevState.sourceFields.sort(); @@ -115,10 +137,10 @@ class SourceBuilder extends React.Component { sourceFields: newSourceFields, source: prevState.source, newSourceField: newSourceFields[0], - newValues: '' + newValues: '', }; }); - } + }, }; if (rowIndex < Object.keys(this.state.source).length) { return [removeAction]; @@ -127,14 +149,16 @@ class SourceBuilder extends React.Component { }; rows = () => { - return Object.keys(this.state.source) - .map((sourceField, i) => { - return { - key: 'sourceKey' + i, - cells: [<>{sourceField}, <>{this.state.source[sourceField].join(',')}, <>] - }; - }) - .concat([ + const [isValidSource, invalidText] = this.isValidSource(); + + const sourceRows = Object.keys(this.state.source).map((sourceField, i) => { + return { + key: 'sourceKey' + i, + cells: [<>{sourceField}, <>{this.state.source[sourceField].join(',')}, <>], + }; + }); + if (this.state.sourceFields.length > 0) { + return sourceRows.concat([ { key: 'sourceKeyNew', cells: [ @@ -159,16 +183,29 @@ class SourceBuilder extends React.Component { aria-describedby="add new source values" name="addNewValues" onChange={this.onAddNewValues} + isValid={isValidSource} /> + {!isValidSource && ( +
+ {invalidText} +
+ )} , <> {this.state.sourceFields.length > 0 && ( - - - ] - } + , + ], + }, ]); } @@ -274,7 +298,7 @@ class GatewayForm extends React.Component { - {this.props.gatewayServers.length === 0 && ( + {this.state.gatewayServers.length === 0 && (
Gateway has no Servers Defined
)} diff --git a/src/pages/IstioConfigNew/IstioConfigNewPage.tsx b/src/pages/IstioConfigNew/IstioConfigNewPage.tsx index acb1e3b649..0279996c05 100644 --- a/src/pages/IstioConfigNew/IstioConfigNewPage.tsx +++ b/src/pages/IstioConfigNew/IstioConfigNewPage.tsx @@ -6,20 +6,44 @@ import Namespace from '../../types/Namespace'; import { ActionGroup, Button, Form, FormGroup, FormSelect, FormSelectOption, TextInput } from '@patternfly/react-core'; import { RenderContent } from '../../components/Nav/Page'; import { style } from 'typestyle'; -import GatewayForm, { GatewayState } from './GatewayForm'; -import SidecarForm, { SidecarState } from './SidecarForm'; +import GatewayForm, { GATEWAY, GATEWAYS, GatewayState, initGateway, isGatewayStateValid } from './GatewayForm'; +import SidecarForm, { initSidecar, isSidecarStateValid, SIDECAR, SIDECARS, SidecarState } from './SidecarForm'; import { Paths, serverConfig } from '../../config'; import { PromisesRegistry } from '../../utils/CancelablePromises'; import * as API from '../../services/Api'; import { IstioPermissions } from '../../types/IstioConfigDetails'; import * as AlertUtils from '../../utils/AlertUtils'; import history from '../../app/History'; -import { buildAuthorizationPolicy, buildGateway, buildSidecar } from '../../components/IstioWizards/IstioWizardActions'; +import { + buildAuthorizationPolicy, + buildGateway, + buildPeerAuthentication, + buildRequestAuthentication, + buildSidecar, +} from '../../components/IstioWizards/IstioWizardActions'; import { MessageType } from '../../types/MessageCenter'; import AuthorizationPolicyForm, { + AUTHORIZACION_POLICY, + AUTHORIZATION_POLICIES, AuthorizationPolicyState, - INIT_AUTHORIZATION_POLICY + initAuthorizationPolicy, + isAuthorizationPolicyStateValid, } from './AuthorizationPolicyForm'; +import PeerAuthenticationForm, { + initPeerAuthentication, + isPeerAuthenticationStateValid, + PEER_AUTHENTICATION, + PEER_AUTHENTICATIONS, + PeerAuthenticationState, +} from './PeerAuthenticationForm'; +import RequestAuthenticationForm, { + initRequestAuthentication, + isRequestAuthenticationStateValid, + REQUEST_AUTHENTICATION, + REQUEST_AUTHENTICATIONS, + RequestAuthenticationState, +} from './RequestAuthenticationForm'; +import { isValidK8SName } from '../../helpers/ValidationHelpers'; type Props = { activeNamespaces: Namespace[]; @@ -31,49 +55,39 @@ type State = { istioPermissions: IstioPermissions; authorizationPolicy: AuthorizationPolicyState; gateway: GatewayState; + peerAuthentication: PeerAuthenticationState; + requestAuthentication: RequestAuthenticationState; sidecar: SidecarState; }; const formPadding = style({ padding: '30px 20px 30px 20px' }); -const AUTHORIZACION_POLICY = 'AuthorizationPolicy'; -const AUTHORIZATION_POLICIES = 'authorizationpolicies'; -const GATEWAY = 'Gateway'; -const GATEWAYS = 'gateways'; -const SIDECAR = 'Sidecar'; -const SIDECARS = 'sidecars'; - const DIC = { AuthorizationPolicy: AUTHORIZATION_POLICIES, Gateway: GATEWAYS, - Sidecar: SIDECARS + PeerAuthentication: PEER_AUTHENTICATIONS, + RequestAuthentication: REQUEST_AUTHENTICATIONS, + Sidecar: SIDECARS, }; const istioResourceOptions = [ { value: AUTHORIZACION_POLICY, label: AUTHORIZACION_POLICY, disabled: false }, { value: GATEWAY, label: GATEWAY, disabled: false }, - { value: SIDECAR, label: SIDECAR, disabled: false } + { value: PEER_AUTHENTICATION, label: PEER_AUTHENTICATION, disabled: false }, + { value: REQUEST_AUTHENTICATION, label: REQUEST_AUTHENTICATION, disabled: false }, + { value: SIDECAR, label: SIDECAR, disabled: false }, ]; -const INIT_STATE = (): State => ({ +const initState = (): State => ({ istioResource: istioResourceOptions[0].value, name: '', istioPermissions: {}, - authorizationPolicy: INIT_AUTHORIZATION_POLICY(), - gateway: { - gatewayServers: [] - }, - sidecar: { - egressHosts: [ - // Init with the istio-system/* for sidecar - { - host: serverConfig.istioNamespace + '/*' - } - ], - addWorkloadSelector: false, - workloadSelectorValid: false, - workloadSelectorLabels: '' - } + authorizationPolicy: initAuthorizationPolicy(), + gateway: initGateway(), + peerAuthentication: initPeerAuthentication(), + requestAuthentication: initRequestAuthentication(), + // Init with the istio-system/* for sidecar + sidecar: initSidecar(serverConfig.istioNamespace + '/*'), }); class IstioConfigNewPage extends React.Component { @@ -81,7 +95,7 @@ class IstioConfigNewPage extends React.Component { constructor(props: Props) { super(props); - this.state = INIT_STATE(); + this.state = initState(); } componentWillUnmount() { @@ -90,7 +104,7 @@ class IstioConfigNewPage extends React.Component { componentDidMount() { // Init component state - this.setState(Object.assign({}, INIT_STATE)); + this.setState(Object.assign({}, initState)); this.fetchPermissions(); } @@ -111,14 +125,14 @@ class IstioConfigNewPage extends React.Component { fetchPermissions = () => { if (this.props.activeNamespaces.length > 0) { this.promises - .register('permissions', API.getIstioPermissions(this.props.activeNamespaces.map(n => n.name))) - .then(permResponse => { + .register('permissions', API.getIstioPermissions(this.props.activeNamespaces.map((n) => n.name))) + .then((permResponse) => { this.setState( { - istioPermissions: permResponse.data + istioPermissions: permResponse.data, }, () => { - this.props.activeNamespaces.forEach(ns => { + this.props.activeNamespaces.forEach((ns) => { if (!this.canCreate(ns.name)) { AlertUtils.addWarning('User has not permissions to create Istio Config on namespace: ' + ns.name); } @@ -126,7 +140,7 @@ class IstioConfigNewPage extends React.Component { } ); }) - .catch(error => { + .catch((error) => { // Canceled errors are expected on this query when page is unmounted if (!error.isCanceled) { AlertUtils.addError('Could not fetch Permissions.', error); @@ -138,132 +152,151 @@ class IstioConfigNewPage extends React.Component { onIstioResourceChange = (value, _) => { this.setState({ istioResource: value, - name: '' + name: '', }); }; onNameChange = (value, _) => { this.setState({ - name: value + name: value, }); }; onIstioResourceCreate = () => { - switch (this.state.istioResource) { - case AUTHORIZACION_POLICY: - this.promises - .registerAll( - 'Create AuthorizationPolicies', - this.props.activeNamespaces.map(ns => - API.createIstioConfigDetail( - ns.name, - 'authorizationpolicies', - JSON.stringify(buildAuthorizationPolicy(this.state.name, ns.name, this.state.authorizationPolicy)) - ) - ) - ) - .then(results => { - if (results.length > 0) { - AlertUtils.add('Istio AuthorizationPolicy created', 'default', MessageType.SUCCESS); - } - this.backToList(); - }) - .catch(error => { - AlertUtils.addError('Could not create Istio AuthorizationPolicy objects.', error); + const jsonIstioObjects: { namespace: string; json: string }[] = []; + this.props.activeNamespaces.forEach((ns) => { + switch (this.state.istioResource) { + case AUTHORIZACION_POLICY: + jsonIstioObjects.push({ + namespace: ns.name, + json: JSON.stringify(buildAuthorizationPolicy(this.state.name, ns.name, this.state.authorizationPolicy)), }); - break; - case GATEWAY: - this.promises - .registerAll( - 'Create Gateways', - this.props.activeNamespaces.map(ns => - API.createIstioConfigDetail( - ns.name, - 'gateways', - JSON.stringify(buildGateway(this.state.name, ns.name, this.state.gateway)) - ) - ) - ) - .then(results => { - if (results.length > 0) { - AlertUtils.add('Istio Gateway created', 'default', MessageType.SUCCESS); - } - this.backToList(); - }) - .catch(error => { - AlertUtils.addError('Could not create Istio Gateway objects.', error); + break; + case GATEWAY: + jsonIstioObjects.push({ + namespace: ns.name, + json: JSON.stringify(buildGateway(this.state.name, ns.name, this.state.gateway)), }); - break; - case SIDECAR: - this.promises - .registerAll( - 'Create Sidecars', - this.props.activeNamespaces.map(ns => - API.createIstioConfigDetail( - ns.name, - 'sidecars', - JSON.stringify(buildSidecar(this.state.name, ns.name, this.state.sidecar)) - ) - ) - ) - .then(results => { - if (results.length > 0) { - AlertUtils.add('Istio Sidecar created', 'default', MessageType.SUCCESS); - } - this.backToList(); - }) - .catch(error => { - AlertUtils.addError('Could not create Istio Sidecar objects.', error); + break; + case PEER_AUTHENTICATION: + jsonIstioObjects.push({ + namespace: ns.name, + json: JSON.stringify(buildPeerAuthentication(this.state.name, ns.name, this.state.peerAuthentication)), }); - break; - } + break; + case REQUEST_AUTHENTICATION: + jsonIstioObjects.push({ + namespace: ns.name, + json: JSON.stringify( + buildRequestAuthentication(this.state.name, ns.name, this.state.requestAuthentication) + ), + }); + break; + case SIDECAR: + jsonIstioObjects.push({ + namespace: ns.name, + json: JSON.stringify(buildSidecar(this.state.name, ns.name, this.state.sidecar)), + }); + break; + } + }); + + this.promises + .registerAll( + 'Create ' + DIC[this.state.istioResource], + jsonIstioObjects.map((o) => API.createIstioConfigDetail(o.namespace, DIC[this.state.istioResource], o.json)) + ) + .then((results) => { + if (results.length > 0) { + AlertUtils.add('Istio ' + this.state.istioResource + ' created', 'default', MessageType.SUCCESS); + } + this.backToList(); + }) + .catch((error) => { + AlertUtils.addError('Could not create Istio ' + this.state.istioResource + ' objects.', error); + }); }; backToList = () => { - this.setState(INIT_STATE(), () => { + this.setState(initState(), () => { // Back to list page history.push(`/${Paths.ISTIO}?namespaces=${this.props.activeNamespaces.join(',')}`); }); }; - isAuthorizationPolicyValid = (): boolean => { - return this.state.istioResource === AUTHORIZACION_POLICY; + isIstioFormValid = (): boolean => { + switch (this.state.istioResource) { + case AUTHORIZACION_POLICY: + return isAuthorizationPolicyStateValid(this.state.authorizationPolicy); + case GATEWAY: + return isGatewayStateValid(this.state.gateway); + case PEER_AUTHENTICATION: + return isPeerAuthenticationStateValid(this.state.peerAuthentication); + case REQUEST_AUTHENTICATION: + return isRequestAuthenticationStateValid(this.state.requestAuthentication); + case SIDECAR: + return isSidecarStateValid(this.state.sidecar); + default: + return false; + } }; - isGatewayValid = (): boolean => { - return this.state.istioResource === GATEWAY && this.state.gateway.gatewayServers.length > 0; + onChangeAuthorizationPolicy = (authorizationPolicy: AuthorizationPolicyState) => { + this.setState((prevState) => { + Object.keys(prevState.authorizationPolicy).forEach( + (key) => (prevState.authorizationPolicy[key] = authorizationPolicy[key]) + ); + return { + authorizationPolicy: prevState.authorizationPolicy, + }; + }); }; - isSidecarValid = (): boolean => { - return ( - this.state.istioResource === SIDECAR && - this.state.sidecar.egressHosts.length > 0 && - (!this.state.sidecar.addWorkloadSelector || - (this.state.sidecar.addWorkloadSelector && this.state.sidecar.workloadSelectorValid)) - ); + onChangeGateway = (gateway: GatewayState) => { + this.setState((prevState) => { + Object.keys(prevState.gateway).forEach((key) => (prevState.gateway[key] = gateway[key])); + return { + gateway: prevState.gateway, + }; + }); }; - onChangeAuthorizationPolicy = (authorizationPolicy: AuthorizationPolicyState) => { - this.setState(prevState => { - prevState.authorizationPolicy.workloadSelector = authorizationPolicy.workloadSelector; - prevState.authorizationPolicy.action = authorizationPolicy.action; - prevState.authorizationPolicy.policy = authorizationPolicy.policy; - prevState.authorizationPolicy.rules = authorizationPolicy.rules; + onChangePeerAuthentication = (peerAuthentication: PeerAuthenticationState) => { + this.setState((prevState) => { + Object.keys(prevState.peerAuthentication).forEach( + (key) => (prevState.peerAuthentication[key] = peerAuthentication[key]) + ); + return { + peerAuthentication: prevState.peerAuthentication, + }; + }); + }; + + onChangeRequestAuthentication = (requestAuthentication: RequestAuthenticationState) => { + this.setState((prevState) => { + Object.keys(prevState.requestAuthentication).forEach( + (key) => (prevState.requestAuthentication[key] = requestAuthentication[key]) + ); return { - authorizationPolicy: prevState.authorizationPolicy + requestAuthentication: prevState.requestAuthentication, + }; + }); + }; + + onChangeSidecar = (sidecar: SidecarState) => { + this.setState((prevState) => { + Object.keys(prevState.sidecar).forEach((key) => (prevState.sidecar[key] = sidecar[key])); + return { + sidecar: prevState.sidecar, }; }); }; render() { - const canCreate = this.props.activeNamespaces.every(ns => this.canCreate(ns.name)); - const isNameValid = this.state.name.length > 0; + const canCreate = this.props.activeNamespaces.every((ns) => this.canCreate(ns.name)); + const isNameValid = isValidK8SName(this.state.name); const isNamespacesValid = this.props.activeNamespaces.length > 0; - const isFormValid = - canCreate && - isNameValid && - isNamespacesValid && - (this.isGatewayValid() || this.isSidecarValid() || this.isAuthorizationPolicyValid()); + const isFormValid = canCreate && isNameValid && isNamespacesValid && this.isIstioFormValid(); return (
@@ -276,7 +309,7 @@ class IstioConfigNewPage extends React.Component { isValid={isNamespacesValid} > n.name).join(',')} + value={this.props.activeNamespaces.map((n) => n.name).join(',')} isRequired={true} type="text" id="namespaces" @@ -303,7 +336,7 @@ class IstioConfigNewPage extends React.Component { isRequired={true} fieldId="name" helperText={this.state.istioResource + ' name'} - helperTextInvalid={this.state.istioResource + ' name is required'} + helperTextInvalid={'A valid ' + this.state.istioResource + ' name is required'} isValid={isNameValid} > { /> )} {this.state.istioResource === GATEWAY && ( - { - this.setState(prevState => { - prevState.gateway.gatewayServers.push(gatewayServer); - return { - gateway: { - gatewayServers: prevState.gateway.gatewayServers - } - }; - }); - }} - onRemove={index => { - this.setState(prevState => { - prevState.gateway.gatewayServers.splice(index, 1); - return { - gateway: { - gatewayServers: prevState.gateway.gatewayServers - } - }; - }); - }} + + )} + {this.state.istioResource === PEER_AUTHENTICATION && ( + )} - {this.state.istioResource === SIDECAR && ( - { - this.setState(prevState => { - prevState.sidecar.egressHosts.push(egressHost); - return { - sidecar: { - egressHosts: prevState.sidecar.egressHosts, - addWorkloadSelector: prevState.sidecar.addWorkloadSelector, - workloadSelectorValid: prevState.sidecar.workloadSelectorValid, - workloadSelectorLabels: prevState.sidecar.workloadSelectorLabels - } - }; - }); - }} - onChangeSelector={(addWorkloadSelector, workloadSelectorValid, workloadSelectorLabels) => { - this.setState(prevState => { - return { - sidecar: { - egressHosts: prevState.sidecar.egressHosts, - addWorkloadSelector: addWorkloadSelector, - workloadSelectorValid: workloadSelectorValid, - workloadSelectorLabels: workloadSelectorLabels - } - }; - }); - }} - onRemoveEgressHost={index => { - this.setState(prevState => { - prevState.sidecar.egressHosts.splice(index, 1); - return { - sidecar: { - egressHosts: prevState.sidecar.egressHosts, - addWorkloadSelector: prevState.sidecar.addWorkloadSelector, - workloadSelectorValid: prevState.sidecar.workloadSelectorValid, - workloadSelectorLabels: prevState.sidecar.workloadSelectorLabels - } - }; - }); - }} + {this.state.istioResource === REQUEST_AUTHENTICATION && ( + )} + {this.state.istioResource === SIDECAR && ( + + )} + , + ], + }, + ]); + } + + render() { + return ( + <> + + + + {this.state.addWorkloadSelector && ( + + + + )} + + + {Object.keys(PeerAuthenticationMutualTLSMode).map((option, index) => ( + + ))} + + + + + + {this.state.addPortMtls && ( + + + + +
+ {this.props.peerAuthentication.portLevelMtls.length === 0 && ( +
PeerAuthentication has no Ports MTLS defined
+ )} + {!this.state.addWorkloadSelector && ( +
Ports MTLS require a Workload Selector
+ )} +
+ )} + + ); + } +} + +export default PeerAuthenticationForm; diff --git a/src/pages/IstioConfigNew/RequestAuthenticationForm.tsx b/src/pages/IstioConfigNew/RequestAuthenticationForm.tsx new file mode 100644 index 0000000000..f4165ba75d --- /dev/null +++ b/src/pages/IstioConfigNew/RequestAuthenticationForm.tsx @@ -0,0 +1,199 @@ +import * as React from 'react'; +import { FormGroup, Switch } from '@patternfly/react-core'; +import { TextInputBase as TextInput } from '@patternfly/react-core/dist/js/components/TextInput/TextInput'; +import { JWTRule } from '../../types/IstioObjects'; +import JwtRuleBuilder from './RequestAuthorizationForm/JwtRuleBuilder'; +import JwtRuleList from './RequestAuthorizationForm/JwtRuleList'; + +type Props = { + requestAuthentication: RequestAuthenticationState; + onChange: (requestAuthentication: RequestAuthenticationState) => void; +}; + +export type RequestAuthenticationState = { + workloadSelector: string; + jwtRules: JWTRule[]; + addWorkloadSelector: boolean; + workloadSelectorValid: boolean; + addJWTRules: boolean; +}; + +export const REQUEST_AUTHENTICATION = 'RequestAuthentication'; +export const REQUEST_AUTHENTICATIONS = 'requestauthentications'; + +export const initRequestAuthentication = (): RequestAuthenticationState => ({ + workloadSelector: '', + jwtRules: [], + addWorkloadSelector: false, + workloadSelectorValid: false, + addJWTRules: false, +}); + +export const isRequestAuthenticationStateValid = (ra: RequestAuthenticationState): boolean => { + const workloadSelectorRule = ra.addWorkloadSelector ? ra.workloadSelectorValid : true; + const jwtRulesRule = ra.addJWTRules ? ra.jwtRules.length > 0 : true; + // Not yet used + return workloadSelectorRule && jwtRulesRule; +}; + +class RequestAuthenticationForm extends React.Component { + constructor(props: Props) { + super(props); + this.state = initRequestAuthentication(); + } + + componentDidMount() { + this.setState({ + workloadSelector: this.props.requestAuthentication.workloadSelector, + jwtRules: this.props.requestAuthentication.jwtRules, + addWorkloadSelector: this.props.requestAuthentication.addWorkloadSelector, + workloadSelectorValid: this.props.requestAuthentication.workloadSelectorValid, + addJWTRules: this.props.requestAuthentication.addJWTRules, + }); + } + + onRequestAuthenticationChange = () => { + this.props.onChange(this.state); + }; + + onChangeWorkloadSelector = () => { + this.setState( + (prevState) => { + return { + addWorkloadSelector: !prevState.addWorkloadSelector, + }; + }, + () => this.onRequestAuthenticationChange() + ); + }; + + onChangeJwtRules = () => { + this.setState( + (prevState) => { + return { + addJWTRules: !prevState.addJWTRules, + }; + }, + () => this.onRequestAuthenticationChange() + ); + }; + + addWorkloadLabels = (value: string, _) => { + if (value.length === 0) { + this.setState( + { + workloadSelectorValid: false, + workloadSelector: '', + }, + () => this.onRequestAuthenticationChange() + ); + return; + } + value = value.trim(); + const labels: string[] = value.split(','); + let isValid = true; + // Some smoke validation rules for the labels + for (let i = 0; i < labels.length; i++) { + const label = labels[i]; + if (label.indexOf('=') < 0) { + isValid = false; + break; + } + const splitLabel: string[] = label.split('='); + if (splitLabel.length !== 2) { + isValid = false; + break; + } + if (splitLabel[0].trim().length === 0 || splitLabel[1].trim().length === 0) { + isValid = false; + break; + } + } + this.setState( + { + workloadSelectorValid: isValid, + workloadSelector: value, + }, + () => this.onRequestAuthenticationChange() + ); + }; + + onAddJwtRule = (jwtRule: JWTRule) => { + this.setState( + (prevState) => { + prevState.jwtRules.push(jwtRule); + return { + jwtRules: prevState.jwtRules, + }; + }, + () => this.onRequestAuthenticationChange() + ); + }; + + onRemoveJwtRule = (index: number) => { + this.setState( + (prevState) => { + prevState.jwtRules.splice(index, 1); + return { + jwtRules: prevState.jwtRules, + }; + }, + () => this.onRequestAuthenticationChange() + ); + }; + + render() { + return ( + <> + + + + {this.state.addWorkloadSelector && ( + + + + )} + + + + {this.state.addJWTRules && ( + <> + + + + + + + + )} + + ); + } +} + +export default RequestAuthenticationForm; diff --git a/src/pages/IstioConfigNew/RequestAuthorizationForm/JwtRuleBuilder.tsx b/src/pages/IstioConfigNew/RequestAuthorizationForm/JwtRuleBuilder.tsx new file mode 100644 index 0000000000..66ea748413 --- /dev/null +++ b/src/pages/IstioConfigNew/RequestAuthorizationForm/JwtRuleBuilder.tsx @@ -0,0 +1,321 @@ +import * as React from 'react'; +import { JWTHeader, JWTRule } from '../../../types/IstioObjects'; +import { cellWidth, ICell, Table, TableBody, TableHeader } from '@patternfly/react-table'; +import { Button, FormSelect, FormSelectOption } from '@patternfly/react-core'; +import { PlusCircleIcon } from '@patternfly/react-icons'; +import { TextInputBase as TextInput } from '@patternfly/react-core/dist/js/components/TextInput/TextInput'; +import { style } from 'typestyle'; +import { PfColors } from '../../../components/Pf/PfColors'; +import { isValidUrl } from '../../../utils/IstioConfigUtils'; + +type Props = { + onAddJwtRule: (rule: JWTRule) => void; +}; + +type State = { + jwtRuleFields: string[]; + jwtRule: JWTRule; + newJwtField: string; + newValues: string; +}; + +const INIT_JWT_RULE_FIELDS = [ + 'issuer', + 'audiences', + 'jwksUri', + 'jwks', + 'fromHeaders', + 'fromParams', + 'outputPayloadToHeader', + 'forwardOriginalToken', +].sort(); + +const headerCells: ICell[] = [ + { + title: 'JWT Rule Field', + transforms: [cellWidth(30) as any], + props: {}, + }, + { + title: 'Values', + transforms: [cellWidth(70) as any], + props: {}, + }, + { + title: '', + props: {}, + }, +]; + +const noValidStyle = style({ + color: PfColors.Red100, +}); + +const warningStyle = style({ + marginLeft: 25, + color: PfColors.Red100, + textAlign: 'center', +}); + +export const formatJwtField = (jwtField: string, jwtRule: JWTRule): string => { + switch (jwtField) { + case 'issuer': + return jwtRule.issuer ? jwtRule.issuer : ''; + case 'audiences': + return jwtRule.audiences ? jwtRule.audiences.join(',') : ''; + case 'jwks': + return jwtRule.jwks ? jwtRule.jwks : ''; + case 'jwksUri': + return jwtRule.jwksUri ? jwtRule.jwksUri : ''; + case 'fromHeaders': + return jwtRule.fromHeaders + ? jwtRule.fromHeaders + .map((header) => { + if (header.prefix) { + return header.name + ': ' + header.prefix; + } else { + return header.name; + } + }) + .join(',') + : ''; + case 'fromParams': + return jwtRule.fromParams ? jwtRule.fromParams.join(',') : ''; + case 'outputPayloadToHeader': + return jwtRule.outputPayloadToHeader ? jwtRule.outputPayloadToHeader : ''; + case 'forwardOriginalToken': + return jwtRule.forwardOriginalToken ? '' + jwtRule.forwardOriginalToken : 'false'; + default: + } + return ''; +}; + +class JwtRuleBuilder extends React.Component { + constructor(props: Props) { + super(props); + this.state = { + jwtRuleFields: Object.assign([], INIT_JWT_RULE_FIELDS), + jwtRule: {}, + newJwtField: 'issuer', + newValues: '', + }; + } + + onAddJwtField = (value: string, _) => { + this.setState({ + newJwtField: value, + }); + }; + + onAddNewValues = (value: string, _) => { + this.setState({ + newValues: value, + }); + }; + + onUpdateJwtRule = () => { + this.setState((prevState) => { + const i = prevState.jwtRuleFields.indexOf(prevState.newJwtField); + if (i > -1) { + prevState.jwtRuleFields.splice(i, 1); + } + switch (prevState.newJwtField) { + case 'issuer': + prevState.jwtRule.issuer = prevState.newValues; + break; + case 'audiences': + prevState.jwtRule.audiences = prevState.newValues.split(','); + break; + case 'jwks': + prevState.jwtRule.jwks = prevState.newValues; + break; + case 'jwksUri': + prevState.jwtRule.jwksUri = prevState.newValues; + break; + case 'fromHeaders': + // Parse a string like: + // "Authorization: Bearer , Authorization: Bearer, Security " + // In [{name: 'Authorization', prefix: 'Bearer '}, {name: 'Authorization', prefix: 'Bearer'}, {name: 'Security}] + prevState.jwtRule.fromHeaders = []; + prevState.newValues.split(',').forEach((value) => { + const values = value.split(':'); + const header: JWTHeader = { + name: values[0], + }; + if (values.length > 1) { + header.prefix = values[1].trimLeft(); + } + if (prevState.jwtRule.fromHeaders) { + prevState.jwtRule.fromHeaders.push(header); + } + }); + break; + case 'fromParams': + prevState.jwtRule.fromParams = prevState.newValues.split(','); + break; + case 'outputPayloadToHeader': + prevState.jwtRule.outputPayloadToHeader = prevState.newValues; + break; + case 'forwardOriginalToken': + // I don't want to put different types for input, perhaps in the future + prevState.jwtRule.forwardOriginalToken = prevState.newValues.toLowerCase() === 'true'; + break; + default: + // No default action. + } + return { + jwtRuleFields: prevState.jwtRuleFields, + jwtRule: prevState.jwtRule, + newJwtField: prevState.jwtRuleFields[0], + newValues: '', + }; + }); + }; + + onAddJwtRuleToList = () => { + const oldJwtRule = this.state.jwtRule; + this.setState( + { + jwtRuleFields: Object.assign([], INIT_JWT_RULE_FIELDS), + jwtRule: {}, + newJwtField: INIT_JWT_RULE_FIELDS[0], + newValues: '', + }, + () => this.props.onAddJwtRule(oldJwtRule) + ); + }; + + // @ts-ignore + actionResolver = (rowData, { rowIndex }) => { + const removeAction = { + title: 'Remove Field', + // @ts-ignore + onClick: (event, rowIndex, rowData, extraData) => { + // Fetch sourceField from rowData, it's a fixed string on children + const removeJwtRuleField = rowData.cells[0].props.children.toString(); + this.setState((prevState) => { + prevState.jwtRuleFields.push(removeJwtRuleField); + delete prevState.jwtRule[removeJwtRuleField]; + const newJwtRuleFields = prevState.jwtRuleFields.sort(); + return { + jwtRuleFields: newJwtRuleFields, + jwtRule: prevState.jwtRule, + newJwtField: newJwtRuleFields[0], + newValues: '', + }; + }); + }, + }; + if (rowIndex < Object.keys(this.state.jwtRule).length) { + return [removeAction]; + } + return []; + }; + + isJwtFieldValid = (): [boolean, string] => { + const isEmptyValue = this.state.newValues.split(',').every((v) => v.length === 0); + if (isEmptyValue) { + return [false, 'Value cannot be empty']; + } + if (this.state.newJwtField === 'jwksUri' && !isValidUrl(this.state.newValues)) { + return [false, 'jwsUri is not a valid Uri']; + } + return [true, '']; + }; + + isJwtRuleValid = (): boolean => { + return this.state.jwtRule.issuer ? this.state.jwtRule.issuer.length > 0 : false; + }; + + rows = () => { + const jwtRuleRows = Object.keys(this.state.jwtRule).map((jwtField, i) => { + return { + key: 'jwtField' + i, + cells: [<>{jwtField}, <>{formatJwtField(jwtField, this.state.jwtRule)}, <>], + }; + }); + if (this.state.jwtRuleFields.length > 0) { + const [isJwtFieldValid, validText] = this.isJwtFieldValid(); + return jwtRuleRows.concat([ + { + key: 'jwtFieldKeyNew', + cells: [ + <> + + {this.state.jwtRuleFields.map((option, index) => ( + + ))} + + , + <> + + {this.state.newJwtField === 'fromHeaders' && ( +
+ List of header locations from which JWT is expected.
+ I.e. "x-jwt-assertion: Bearer ,Authorization: Bearer " +
+ )} + {!isJwtFieldValid && ( +
+ {validText} +
+ )} + , + <> + {this.state.jwtRuleFields.length > 0 && ( + + + ); + } +} + +export default JwtRuleBuilder; diff --git a/src/pages/IstioConfigNew/RequestAuthorizationForm/JwtRuleList.tsx b/src/pages/IstioConfigNew/RequestAuthorizationForm/JwtRuleList.tsx new file mode 100644 index 0000000000..6cb9a33201 --- /dev/null +++ b/src/pages/IstioConfigNew/RequestAuthorizationForm/JwtRuleList.tsx @@ -0,0 +1,117 @@ +import { JWTRule } from '../../../types/IstioObjects'; +import { cellWidth, ICell, Table, TableBody, TableHeader } from '@patternfly/react-table'; +import { style } from 'typestyle'; +import { PfColors } from '../../../components/Pf/PfColors'; +import * as React from 'react'; +import { formatJwtField } from './JwtRuleBuilder'; + +type Props = { + jwtRules: JWTRule[]; + onRemoveJwtRule: (index: number) => void; +}; + +const headerCells: ICell[] = [ + { + title: 'JWT Rules to be validated', + transforms: [cellWidth(100) as any], + props: {}, + }, + { + title: '', + props: {}, + }, +]; + +const noJWTRulesStyle = style({ + marginTop: 10, + color: PfColors.Red100, + textAlign: 'center', + width: '100%', +}); + +class JwtRuleList extends React.Component { + rows = () => { + return this.props.jwtRules.map((jwtRule, i) => { + return { + key: 'jwtRule' + i, + cells: [ + <> + {jwtRule.issuer ? ( +
+ issuer: [{formatJwtField('issuer', jwtRule)}] +
+ ) : undefined} + {jwtRule.audiences ? ( +
+ audiences: [{formatJwtField('audiences', jwtRule)}] +
+ ) : undefined} + {jwtRule.jwks ? ( +
+ jwks: [{formatJwtField('jwks', jwtRule)}] +
+ ) : undefined} + {jwtRule.jwksUri ? ( +
+ jwksUri: [{formatJwtField('jwksUri', jwtRule)}] +
+ ) : undefined} + {jwtRule.fromHeaders ? ( +
+ fromHeaders: [{formatJwtField('fromHeaders', jwtRule)}] +
+ ) : undefined} + {jwtRule.fromParams ? ( +
+ fromParams: [{formatJwtField('fromParams', jwtRule)}] +
+ ) : undefined} + {jwtRule.outputPayloadToHeader ? ( +
+ outputPayloadToHeader: [{formatJwtField('outputPayloadToHeader', jwtRule)}] +
+ ) : undefined} + {jwtRule.forwardOriginalToken !== undefined ? ( +
+ forwardOriginalToken: [{formatJwtField('forwardOriginalToken', jwtRule)}] +
+ ) : undefined} + , + <>, + ], + }; + }); + }; + + // @ts-ignore + actionResolver = (rowData, { rowIndex }) => { + const removeAction = { + title: 'Remove JWT Rule', + // @ts-ignore + onClick: (event, rowIndex, rowData, extraData) => { + this.props.onRemoveJwtRule(rowIndex); + }, + }; + return [removeAction]; + }; + + render() { + return ( + <> + + + +
+ {this.props.jwtRules.length === 0 &&
No JWT Rules Defined
} + + ); + } +} + +export default JwtRuleList; diff --git a/src/pages/IstioConfigNew/SidecarForm.tsx b/src/pages/IstioConfigNew/SidecarForm.tsx index edc36e044f..3fae638d59 100644 --- a/src/pages/IstioConfigNew/SidecarForm.tsx +++ b/src/pages/IstioConfigNew/SidecarForm.tsx @@ -4,72 +4,78 @@ import { style } from 'typestyle'; import { PfColors } from '../../components/Pf/PfColors'; // Use TextInputBase like workaround while PF4 team work in https://github.com/patternfly/patternfly-react/issues/4072 import { Button, FormGroup, Switch, TextInputBase as TextInput } from '@patternfly/react-core'; -import { isServerHostValid } from '../../utils/IstioConfigUtils'; +import { isSidecarHostValid } from '../../utils/IstioConfigUtils'; const headerCells: ICell[] = [ { title: 'Egress Host', transforms: [cellWidth(60) as any], - props: {} + props: {}, }, { title: '', - props: {} - } + props: {}, + }, ]; const noEgressHostsStyle = style({ marginTop: 15, - color: PfColors.Red100 + color: PfColors.Red100, }); -const hostsHelperText = 'Enter a valid FQDN host.'; +const hostsHelperText = 'Enter a valid namespace/FQDN Egress host.'; export type EgressHost = { host: string; }; type Props = { - egressHosts: EgressHost[]; - addWorkloadSelector: boolean; - workloadSelectorLabels: string; - onAddEgressHost: (host: EgressHost) => void; - onChangeSelector: ( - addWorkloadSelector: boolean, - workloadSelectorValid: boolean, - workloadSelectorLabels: string - ) => void; - onRemoveEgressHost: (index: number) => void; + sidecar: SidecarState; + onChange: (sidecar: SidecarState) => void; }; +export const SIDECAR = 'Sidecar'; +export const SIDECARS = 'sidecars'; + // Gateway and Sidecar states are consolidated in the parent page export type SidecarState = { - egressHosts: EgressHost[]; + addEgressHost: EgressHost; addWorkloadSelector: boolean; + egressHosts: EgressHost[]; + validEgressHost: boolean; workloadSelectorValid: boolean; workloadSelectorLabels: string; }; -type State = { - addEgressHost: EgressHost; - addWorkloadSelector: boolean; - workloadSelectorValid: boolean; - workloadSelectorLabels: string; - validEgressHost: boolean; +export const isSidecarStateValid = (s: SidecarState): boolean => { + return s.egressHosts.length > 0 && (!s.addWorkloadSelector || (s.addWorkloadSelector && s.workloadSelectorValid)); +}; + +export const initSidecar = (initHost: string): SidecarState => { + return { + addEgressHost: { + host: '', + }, + addWorkloadSelector: false, + egressHosts: [ + { + host: initHost, + }, + ], + validEgressHost: false, + workloadSelectorValid: false, + workloadSelectorLabels: '', + }; }; -class SidecarForm extends React.Component { +class SidecarForm extends React.Component { constructor(props: Props) { super(props); - this.state = { - addEgressHost: { - host: '' - }, - addWorkloadSelector: false, - workloadSelectorValid: false, - workloadSelectorLabels: '', - validEgressHost: false - }; + this.state = initSidecar(''); + } + + componentDidMount() { + this.setState(this.props.sidecar); } // @ts-ignore @@ -78,10 +84,18 @@ class SidecarForm extends React.Component { title: 'Remove Server', // @ts-ignore onClick: (event, rowIndex, _rowData, _extraData) => { - this.props.onRemoveEgressHost(rowIndex); - } + this.setState( + (prevState) => { + prevState.egressHosts.splice(rowIndex, 1); + return { + egressHosts: prevState.egressHosts, + }; + }, + () => this.props.onChange(this.state) + ); + }, }; - if (rowIndex < this.props.egressHosts.length) { + if (rowIndex < this.state.egressHosts.length) { return [removeAction]; } return []; @@ -91,27 +105,36 @@ class SidecarForm extends React.Component { const host = value.trim(); this.setState({ addEgressHost: { - host: host + host: host, }, - validEgressHost: isServerHostValid(host) + validEgressHost: isSidecarHostValid(host), }); }; onAddEgressHost = () => { - this.props.onAddEgressHost(this.state.addEgressHost); - this.setState({ - addEgressHost: { - host: '' - } - }); + this.setState( + (prevState) => { + prevState.egressHosts.push(this.state.addEgressHost); + return { + egressHosts: prevState.egressHosts, + addEgressHost: { + host: '', + }, + }; + }, + () => this.props.onChange(this.state) + ); }; addWorkloadLabels = (value: string, _) => { if (value.length === 0) { - this.setState({ - workloadSelectorValid: false, - workloadSelectorLabels: '' - }); + this.setState( + { + workloadSelectorValid: false, + workloadSelectorLabels: '', + }, + () => this.props.onChange(this.state) + ); return; } value = value.trim(); @@ -137,23 +160,17 @@ class SidecarForm extends React.Component { this.setState( { workloadSelectorValid: isValid, - workloadSelectorLabels: value + workloadSelectorLabels: value, }, - () => { - this.props.onChangeSelector( - this.state.addWorkloadSelector, - this.state.workloadSelectorValid, - this.state.workloadSelectorLabels - ); - } + () => this.props.onChange(this.state) ); }; rows() { - return this.props.egressHosts + return this.state.egressHosts .map((eHost, i) => ({ key: 'eH' + i, - cells: [<>{eHost.host}, ''] + cells: [<>{eHost.host}, ''], })) .concat([ { @@ -180,9 +197,9 @@ class SidecarForm extends React.Component { - - ] - } + , + ], + }, ]); } @@ -208,16 +225,10 @@ class SidecarForm extends React.Component { isChecked={this.state.addWorkloadSelector} onChange={() => { this.setState( - prevState => ({ - addWorkloadSelector: !prevState.addWorkloadSelector + (prevState) => ({ + addWorkloadSelector: !prevState.addWorkloadSelector, }), - () => { - this.props.onChangeSelector( - this.state.addWorkloadSelector, - this.state.workloadSelectorValid, - this.state.workloadSelectorLabels - ); - } + () => this.props.onChange(this.state) ); }} /> @@ -240,7 +251,7 @@ class SidecarForm extends React.Component { /> )} - {this.props.egressHosts.length === 0 && ( + {this.state.egressHosts.length === 0 && (
Sidecar has no Egress Hosts Defined
)} diff --git a/src/pages/extensions/threescale/ThreeScaleHandlerDetails/ThreeScaleHandlerDetailsPage.tsx b/src/pages/extensions/threescale/ThreeScaleHandlerDetails/ThreeScaleHandlerDetailsPage.tsx index a20473c917..34ce7b41ea 100644 --- a/src/pages/extensions/threescale/ThreeScaleHandlerDetails/ThreeScaleHandlerDetailsPage.tsx +++ b/src/pages/extensions/threescale/ThreeScaleHandlerDetails/ThreeScaleHandlerDetailsPage.tsx @@ -18,7 +18,7 @@ import { TextVariants, Title, Toolbar, - ToolbarSection + ToolbarSection, } from '@patternfly/react-core'; import { style } from 'typestyle'; import { PfColors } from '../../../../components/Pf/PfColors'; @@ -28,6 +28,7 @@ import { ThreeScaleHandler, ThreeScaleInfo } from '../../../../types/ThreeScale' import { RenderContent } from '../../../../components/Nav/Page'; import history from '../../../../app/History'; import RefreshButtonContainer from '../../../../components/Refresh/RefreshButton'; +import { isValidK8SName } from '../../../../helpers/ValidationHelpers'; // Properties handled by the component/page // Note that ThreeScaleHandlerDetailsPage uses a RouteComponentProps used to capture the parameters in the route @@ -53,10 +54,10 @@ interface State { // i.e. no namespaces controllers, then some styles need to be adjusted manually const extensionHeader = style({ padding: '0px 20px 18px 20px', - backgroundColor: PfColors.White + backgroundColor: PfColors.White, }); const breadcrumbPadding = style({ - padding: '22px 0 5px 0' + padding: '22px 0 5px 0', }); const containerPadding = style({ padding: '20px 20px 20px 20px' }); // Toolbar in 3scale details page is added manually. @@ -66,15 +67,9 @@ const rightToolbarStyle = style({ right: '20px', zIndex: 1, marginTop: '-30px', - backgroundColor: PfColors.White + backgroundColor: PfColors.White, }); -// Kubernetes ID validation helper, used to allow mark a warning in the form edition -const k8sRegExpName = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[-a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/; -const isValidK8SName = (name: string) => { - return name === '' ? false : name.search(k8sRegExpName) === 0; -}; - class ThreeScaleHandlerDetailsPage extends React.Component, State> { constructor(props: RouteComponentProps) { super(props); @@ -86,17 +81,17 @@ class ThreeScaleHandlerDetailsPage extends React.Component { API.getThreeScaleInfo() - .then(result => { + .then((result) => { const threeScaleInfo = result.data; if (threeScaleInfo.enabled) { if (handlerName) { API.getThreeScaleHandlers() - .then(results => { + .then((results) => { let handler: ThreeScaleHandler | undefined = undefined; for (let i = 0; results.data.length; i++) { if (results.data[i].name === handlerName) { @@ -121,25 +116,25 @@ class ThreeScaleHandlerDetailsPage extends React.Component { + .catch((error) => { AlertUtils.addError('Could not fetch ThreeScaleHandlers.', error); }); } else { this.setState({ - threeScaleInfo: threeScaleInfo + threeScaleInfo: threeScaleInfo, }); } } else { AlertUtils.addError('Kiali has 3scale extension enabled but 3scale adapter is not detected in the cluster'); } }) - .catch(error => { + .catch((error) => { AlertUtils.addError('Could not fetch ThreeScaleInfo.', error); }); }; @@ -168,7 +163,7 @@ class ThreeScaleHandlerDetailsPage extends React.Component this.setState({ deleteModalOpen: true })}> Delete - + , ]} /> , + , ]} > @@ -245,7 +240,7 @@ class ThreeScaleHandlerDetailsPage extends React.Component { - this.setState(prevState => { + this.setState((prevState) => { const newThreeScaleHandler = prevState.handler; switch (field) { case 'handlerName': @@ -265,7 +260,7 @@ class ThreeScaleHandlerDetailsPage extends React.Component { if (this.state.isNew) { API.createThreeScaleHandler(JSON.stringify(this.state.handler)) - .then(_ => this.goHandlersPage()) - .catch(error => AlertUtils.addError('Could not create ThreeScaleHandlers.', error)); + .then((_) => this.goHandlersPage()) + .catch((error) => AlertUtils.addError('Could not create ThreeScaleHandlers.', error)); } else { API.updateThreeScaleHandler(this.state.handler.name, JSON.stringify(this.state.handler)) - .then(_ => this.goHandlersPage()) - .catch(error => AlertUtils.addError('Could not update ThreeScaleHandlers.', error)); + .then((_) => this.goHandlersPage()) + .catch((error) => AlertUtils.addError('Could not update ThreeScaleHandlers.', error)); } }; // It invokes backend to delete a 3scale handler deleteHandler = () => { API.deleteThreeScaleHandler(this.state.handler.name) - .then(_ => this.goHandlersPage()) - .catch(error => AlertUtils.addError('Could not delete ThreeScaleHandlers.', error)); + .then((_) => this.goHandlersPage()) + .catch((error) => AlertUtils.addError('Could not delete ThreeScaleHandlers.', error)); }; render() { @@ -334,7 +329,7 @@ class ThreeScaleHandlerDetailsPage extends React.Component this.changeHandler('handlerName', value)} + onChange={(value) => this.changeHandler('handlerName', value)} isDisabled={!this.state.isNew} /> @@ -348,7 +343,7 @@ class ThreeScaleHandlerDetailsPage extends React.Component this.changeHandler('serviceId', value)} + onChange={(value) => this.changeHandler('serviceId', value)} /> this.changeHandler('systemUrl', value)} + onChange={(value) => this.changeHandler('systemUrl', value)} /> this.changeHandler('accessToken', value)} + onChange={(value) => this.changeHandler('accessToken', value)} /> diff --git a/src/types/IstioObjects.ts b/src/types/IstioObjects.ts index 8ca990cca4..8f9affad23 100644 --- a/src/types/IstioObjects.ts +++ b/src/types/IstioObjects.ts @@ -50,7 +50,7 @@ export type Validations = { [key1: string]: { [key2: string]: ObjectValidation } export enum ValidationTypes { Error = 'error', Warning = 'warning', - Correct = 'correct' + Correct = 'correct', } export interface ObjectValidation { @@ -495,7 +495,7 @@ export interface Gateway extends IstioObject { export enum CaptureMode { DEFAULT = 'DEFAULT', IPTABLES = 'IPTABLES', - NONE = 'NONE' + NONE = 'NONE', } // 1.6 @@ -668,7 +668,7 @@ export interface TargetSelector { export enum MutualTlsMode { STRICT = 'STRICT', - PERMISSIVE = 'PERMISSIVE' + PERMISSIVE = 'PERMISSIVE', } export interface MutualTls { @@ -694,7 +694,7 @@ export interface OriginAuthenticationMethod { export enum PrincipalBinding { USE_PEER = 'USE_PEER', - USE_ORIGIN = 'USE_ORIGIN' + USE_ORIGIN = 'USE_ORIGIN', } export interface PolicySpec { @@ -856,7 +856,7 @@ export enum PeerAuthenticationMutualTLSMode { UNSET = 'UNSET', DISABLE = 'DISABLE', PERMISSIVE = 'PERMISSIVE', - STRICT = 'STRICT' + STRICT = 'STRICT', } // 1.6 @@ -884,7 +884,7 @@ export interface JWTHeader { } export interface JWTRule { - issuer: string; + issuer?: string; audiences?: string[]; jwksUri?: string; jwks?: string; diff --git a/src/utils/IstioConfigUtils.ts b/src/utils/IstioConfigUtils.ts index c281e666f4..0352de9179 100644 --- a/src/utils/IstioConfigUtils.ts +++ b/src/utils/IstioConfigUtils.ts @@ -73,9 +73,20 @@ export const getIstioObject = (istioObjectDetails?: IstioConfigDetails) => { const nsRegexp = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[-a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/; const hostRegexp = /(?=^.{4,253}$)(^((?!-)(([a-zA-Z0-9-]{0,62}[a-zA-Z0-9])|\*)\.)+[a-zA-Z]{2,63}$)/; +const ipRegexp = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))?$/; + +// Gateway hosts have namespace/dnsName with namespace optional +export const isGatewayHostValid = (gatewayHost: string): boolean => { + return isServerHostValid(gatewayHost, false); +}; + +// Sidecar host have namespace/dnsName with both namespace/dnsName mandatory +export const isSidecarHostValid = (sidecarHost: string): boolean => { + return isServerHostValid(sidecarHost, true); +}; // Used to check if Sidecar and Gateway host expressions are valid -export const isServerHostValid = (serverHost: string): boolean => { +export const isServerHostValid = (serverHost: string, nsMandatory: boolean): boolean => { if (serverHost.length === 0) { return false; } @@ -85,6 +96,11 @@ export const isServerHostValid = (serverHost: string): boolean => { if (parts.length > 2) { return false; } + // Force that namespace/dnsName are present + if (nsMandatory && parts.length < 2) { + return false; + } + // parts[0] is a dns let dnsValid = true; let hostValid = true; @@ -106,3 +122,16 @@ export const isServerHostValid = (serverHost: string): boolean => { } return dnsValid && hostValid; }; + +export const isValidIp = (ip: string): boolean => { + return ipRegexp.test(ip); +}; + +export const isValidUrl = (url: string): boolean => { + try { + new URL(url); + } catch (_) { + return false; + } + return true; +}; diff --git a/src/utils/__tests__/IstioConfigUtils.test.ts b/src/utils/__tests__/IstioConfigUtils.test.ts index 020dbfacd3..17074fea92 100644 --- a/src/utils/__tests__/IstioConfigUtils.test.ts +++ b/src/utils/__tests__/IstioConfigUtils.test.ts @@ -1,26 +1,26 @@ -import { isServerHostValid, mergeJsonPatch } from '../IstioConfigUtils'; +import { isServerHostValid, isValidUrl, mergeJsonPatch } from '../IstioConfigUtils'; describe('Validate JSON Patchs', () => { const gateway: object = { kind: 'Gateway', namespace: { - name: 'bookinfo' + name: 'bookinfo', }, spec: { selector: { - istio: 'ingressgateway' + istio: 'ingressgateway', }, servers: [ { port: { number: 80, name: 'http', - protocol: 'HTTP' + protocol: 'HTTP', }, - hosts: ['*'] - } - ] - } + hosts: ['*'], + }, + ], + }, }; const gatewayModified: object = { @@ -28,19 +28,19 @@ describe('Validate JSON Patchs', () => { kind: 'Gateway', spec: { selector: { - app: 'myapp' + app: 'myapp', }, servers: [ { port: { number: 80, name: 'http', - protocol: 'HTTP' + protocol: 'HTTP', }, - hosts: ['*'] - } - ] - } + hosts: ['*'], + }, + ], + }, }; it('Basic Test', () => { @@ -56,29 +56,42 @@ describe('Validate JSON Patchs', () => { describe('Validate Gateway/Sidecar Server Host ', () => { it('No Namespace prefix', () => { - expect(isServerHostValid('*')).toBeTruthy(); - expect(isServerHostValid('productpage')).toBeFalsy(); - expect(isServerHostValid('productpage.example.com')).toBeTruthy(); - expect(isServerHostValid('*.example.com')).toBeTruthy(); + expect(isServerHostValid('*', false)).toBeTruthy(); + expect(isServerHostValid('*', true)).toBeFalsy(); + expect(isServerHostValid('productpage', false)).toBeFalsy(); + expect(isServerHostValid('productpage.example.com', false)).toBeTruthy(); + expect(isServerHostValid('*.example.com', false)).toBeTruthy(); }); it('Namespace prefix', () => { - expect(isServerHostValid('bookinfo/*')).toBeTruthy(); - expect(isServerHostValid('*/*')).toBeTruthy(); - expect(isServerHostValid('./*')).toBeTruthy(); - expect(isServerHostValid('bookinfo/productpage')).toBeFalsy(); - expect(isServerHostValid('*/productpage')).toBeFalsy(); - expect(isServerHostValid('./productpage')).toBeFalsy(); - expect(isServerHostValid('bookinfo/productpage.example.com')).toBeTruthy(); - expect(isServerHostValid('*/productpage.example.com')).toBeTruthy(); - expect(isServerHostValid('./productpage.example.com')).toBeTruthy(); - expect(isServerHostValid('bookinfo/*.example.com')).toBeTruthy(); - expect(isServerHostValid('*/*.example.com')).toBeTruthy(); - expect(isServerHostValid('./*.example.com')).toBeTruthy(); + expect(isServerHostValid('bookinfo/*', true)).toBeTruthy(); + expect(isServerHostValid('*/*', true)).toBeTruthy(); + expect(isServerHostValid('./*', true)).toBeTruthy(); + expect(isServerHostValid('bookinfo/productpage', true)).toBeFalsy(); + expect(isServerHostValid('*/productpage', true)).toBeFalsy(); + expect(isServerHostValid('./productpage', true)).toBeFalsy(); + expect(isServerHostValid('bookinfo/productpage.example.com', true)).toBeTruthy(); + expect(isServerHostValid('*/productpage.example.com', true)).toBeTruthy(); + expect(isServerHostValid('./productpage.example.com', true)).toBeTruthy(); + expect(isServerHostValid('bookinfo/*.example.com', true)).toBeTruthy(); + expect(isServerHostValid('*/*.example.com', true)).toBeTruthy(); + expect(isServerHostValid('./*.example.com', true)).toBeTruthy(); }); it('Catch bad urls', () => { - expect(isServerHostValid('bookinfo//reviews')).toBeFalsy(); - expect(isServerHostValid('bookinf*/reviews')).toBeFalsy(); + expect(isServerHostValid('bookinfo//reviews', true)).toBeFalsy(); + expect(isServerHostValid('bookinf*/reviews', true)).toBeFalsy(); + }); +}); + +describe('Validate bad urls', () => { + it('Good urls', () => { + expect(isValidUrl('http://www.googleapis.com/oauth2/v1/certs')).toBeTruthy(); + expect(isValidUrl('https://www.googleapis.com/oauth2/v1/certs')).toBeTruthy(); + }); + + it('Bad urls', () => { + expect(isValidUrl('ramdom')).toBeFalsy(); + expect(isValidUrl('123test')).toBeFalsy(); }); });