Commit d28bfc31 authored by ap4y's avatar ap4y

Implement policy rules for policy editor

This commit implements rules logic for network policies. 4 rule types
are suppported: endpoint, entity, cidr and fqdn.
parent e9dc7f29
export const EditorModeRule = 'rule'; export const EditorModeRule = 'rule';
export const EditorModeYAML = 'yaml'; export const EditorModeYAML = 'yaml';
export const RuleTypeNetwork = 'network';
export const RuleDirectionInbound = 'ingress';
export const RuleDirectionOutbound = 'egress';
export const EndpointMatchModeAny = 'any';
export const EndpointMatchModeLabel = 'label';
export const RuleTypeEndpoint = 'NetworkPolicyRuleEndpoint';
export const RuleTypeEntity = 'NetworkPolicyRuleEntity';
export const RuleTypeCIDR = 'NetworkPolicyRuleCIDR';
export const RuleTypeFQDN = 'NetworkPolicyRuleFQDN';
export const EntityTypes = {
ALL: 'all',
HOST: 'host',
REMOTE_NODE: 'remote-node',
CLUSTER: 'cluster',
INIT: 'init',
HEALTH: 'health',
UNMANAGED: 'unmanaged',
WORLD: 'world',
};
export const PortMatchModeAny = 'any';
export const PortMatchModePortProtocol = 'port/protocol';
import {
RuleTypeEndpoint,
RuleTypeEntity,
RuleTypeCIDR,
RuleTypeFQDN,
RuleDirectionInbound,
PortMatchModeAny,
} from '../constants';
/*
Return kubernetes specification object that is shared by all rule types.
*/
function commonSpec({ portMatchMode, ports }) {
if (portMatchMode === PortMatchModeAny) return {};
const portSelectors = ports.split(/\s/).reduce((acc, item) => {
const [port, protocol = 'tcp'] = item.split('/');
const portNumber = parseInt(port, 10);
if (Number.isNaN(portNumber)) return acc;
acc.push({ port, protocol: protocol.trim().toUpperCase() });
return acc;
}, []);
return { toPorts: [{ ports: portSelectors }] };
}
/*
Return kubernetes specification object for an endpoint rule.
*/
function ruleEndpointSpec({ direction, matchLabels }) {
const matchSelector = matchLabels.split(/\s/).reduce((acc, item) => {
const [key, value = ''] = item.split(':');
if (key.length === 0) return acc;
acc[key] = value.trim();
return acc;
}, {});
if (Object.keys(matchSelector).length === 0) return {};
return {
[direction === RuleDirectionInbound ? 'fromEndpoints' : 'toEndpoints']: [
{
matchLabels: matchSelector,
},
],
};
}
/*
Return kubernetes specification object for an entity rule.
*/
function ruleEntitySpec({ direction, entities }) {
if (entities.length === 0) return {};
return {
[direction === RuleDirectionInbound ? 'fromEntities' : 'toEntities']: entities,
};
}
/*
Return kubernetes specification object for a cidr rule.
*/
function ruleCIDRSpec({ direction, cidr }) {
const cidrList = cidr.length === 0 ? [] : cidr.split(/\s/);
if (cidrList.length === 0) return {};
return {
[direction === RuleDirectionInbound ? 'fromCIDR' : 'toCIDR']: cidrList,
};
}
/*
Return kubernetes specification object for a fqdn rule.
*/
function ruleFQDNSpec({ direction, fqdn }) {
if (direction === RuleDirectionInbound) return {};
const fqdnList = fqdn.length === 0 ? [] : fqdn.split(/\s/);
if (fqdnList.length === 0) return {};
return {
toFQDNs: fqdnList.map(item => ({ matchName: item })),
};
}
/*
Construct a new rule object of the given ruleType.
oldRule: initialize common rule fields using existing rule.
*/
export function buildRule(ruleType = RuleTypeEndpoint, oldRule) {
const direction = oldRule?.direction || RuleDirectionInbound;
const portMatchMode = oldRule?.portMatchMode || PortMatchModeAny;
const ports = oldRule?.ports || '';
const commons = { ruleType, direction, portMatchMode, ports };
switch (ruleType) {
case RuleTypeEntity:
return { ...commons, entities: [] };
case RuleTypeCIDR:
return { ...commons, cidr: '' };
case RuleTypeFQDN:
return { ...commons, fqdn: '' };
default:
return { ...commons, matchLabels: '' };
}
}
/*
Return rule's kubernetes specification object
*/
export function ruleSpec(rule) {
const commons = commonSpec(rule);
switch (rule.ruleType) {
case RuleTypeEntity:
return { ...commons, ...ruleEntitySpec(rule) };
case RuleTypeCIDR:
return { ...commons, ...ruleCIDRSpec(rule) };
case RuleTypeFQDN:
return { ...commons, ...ruleFQDNSpec(rule) };
default:
return { ...commons, ...ruleEndpointSpec(rule) };
}
}
import { safeDump } from 'js-yaml';
import { EndpointMatchModeAny } from '../constants';
import { ruleSpec } from './rules';
/*
Convert enpdoint labels provided as a string into a kubernetes selector.
Expected endpointLabels in format "one two:three"
*/
function endpointSelector({ endpointMatchMode, endpointLabels }) {
if (endpointMatchMode === EndpointMatchModeAny) return {};
return endpointLabels.split(/\s/).reduce((acc, item) => {
const [key, value = ''] = item.split(':');
if (key.length === 0) return acc;
acc[key] = value.trim();
return acc;
}, {});
}
/*
Return kubernetes resource specification object for a policy.
*/
function spec(policy) {
const { description, rules, isEnabled } = policy;
const matchLabels = endpointSelector(policy);
const policySpec = {};
if (description?.length > 0) {
policySpec.description = description;
}
policySpec.endpointSelector = Object.keys(matchLabels).length > 0 ? { matchLabels } : {};
rules.forEach(rule => {
const { direction } = rule;
if (!policySpec[direction]) policySpec[direction] = [];
policySpec[direction].push(ruleSpec(rule));
});
if (!isEnabled) {
policySpec.endpointSelector.matchLabels = {
...policySpec.endpointSelector.matchLabels,
'network-policy.gitlab.com/disabled_by': 'gitlab',
};
}
return policySpec;
}
/*
Return yaml representation of a policy.
*/
export default function toYaml(policy) {
const { name } = policy;
const policySpec = {
apiVersion: 'cilium.io/v2',
kind: 'CiliumNetworkPolicy',
metadata: { name },
spec: spec(policy),
};
return safeDump(policySpec, { noArrayIndent: true });
}
import { buildRule, ruleSpec } from 'ee/threat_monitoring/components/policy_editor/lib/rules';
import {
RuleTypeEndpoint,
RuleTypeEntity,
RuleTypeCIDR,
RuleTypeFQDN,
RuleDirectionInbound,
RuleDirectionOutbound,
PortMatchModeAny,
PortMatchModePortProtocol,
EntityTypes,
} from 'ee/threat_monitoring/components/policy_editor/constants';
describe('buildRule', () => {
const oldRule = {
direction: RuleDirectionOutbound,
portMatchMode: PortMatchModePortProtocol,
ports: '80/tcp',
};
describe.each([RuleTypeEndpoint, RuleTypeEntity, RuleTypeCIDR, RuleTypeFQDN])(
'buildRule $ruleType',
ruleType => {
it('builds correct instance', () => {
const rule = buildRule(ruleType);
expect(rule).toMatchObject({
ruleType,
direction: RuleDirectionInbound,
portMatchMode: PortMatchModeAny,
ports: '',
});
});
describe('with oldRule', () => {
it('builds correct instance', () => {
const rule = buildRule(ruleType, oldRule);
expect(rule).toMatchObject({
ruleType,
direction: RuleDirectionOutbound,
portMatchMode: PortMatchModePortProtocol,
ports: '80/tcp',
});
});
});
},
);
});
describe('ruleSpec', () => {
let rule;
function testPortMatchers() {
describe('given rule has port matchers', () => {
beforeEach(() => {
rule.portMatchMode = PortMatchModePortProtocol;
rule.ports = '80 81/tcp 82/udp invalid';
});
it('includes correct toPorts block', () => {
expect(ruleSpec(rule)).toMatchObject({
toPorts: [
{
ports: [
{ port: '80', protocol: 'TCP' },
{ port: '81', protocol: 'TCP' },
{ port: '82', protocol: 'UDP' },
],
},
],
});
});
});
}
describe('RuleTypeEndpoint', () => {
beforeEach(() => {
rule = buildRule(RuleTypeEndpoint);
});
it('returns empty spec', () => {
expect(ruleSpec(rule)).toEqual({});
});
testPortMatchers();
describe('with match labels', () => {
beforeEach(() => {
rule.matchLabels = 'one two:val three: two:overwrite four: five';
});
it('returns correct spec', () => {
expect(ruleSpec(rule)).toEqual({
fromEndpoints: [
{
matchLabels: {
one: '',
two: 'overwrite',
three: '',
five: '',
four: '',
},
},
],
});
});
testPortMatchers();
});
describe('with outbound direction', () => {
beforeEach(() => {
rule.direction = RuleDirectionOutbound;
rule.matchLabels = 'foo:bar';
});
it('returns correct spec', () => {
expect(ruleSpec(rule)).toEqual({
toEndpoints: [{ matchLabels: { foo: 'bar' } }],
});
});
testPortMatchers();
});
});
describe('RuleTypeEntity', () => {
beforeEach(() => {
rule = buildRule(RuleTypeEntity);
});
it('returns empty spec', () => {
expect(ruleSpec(rule)).toEqual({});
});
testPortMatchers();
describe('with entities', () => {
beforeEach(() => {
rule.entities = [EntityTypes.HOST, EntityTypes.WORLD];
});
it('returns correct spec', () => {
expect(ruleSpec(rule)).toEqual({
fromEntities: [EntityTypes.HOST, EntityTypes.WORLD],
});
});
testPortMatchers();
});
describe('with outbound direction', () => {
beforeEach(() => {
rule.direction = RuleDirectionOutbound;
rule.entities = [EntityTypes.HOST];
});
it('returns correct spec', () => {
expect(ruleSpec(rule)).toEqual({
toEntities: [EntityTypes.HOST],
});
});
testPortMatchers();
});
});
describe('RuleTypeCIDR', () => {
beforeEach(() => {
rule = buildRule(RuleTypeCIDR);
});
it('returns empty spec', () => {
expect(ruleSpec(rule)).toEqual({});
});
testPortMatchers();
describe('with cidr masks', () => {
beforeEach(() => {
rule.cidr = '0.0.0.0/24 1.1.1.1/32';
});
it('returns correct spec', () => {
expect(ruleSpec(rule)).toEqual({
fromCIDR: ['0.0.0.0/24', '1.1.1.1/32'],
});
});
testPortMatchers();
});
describe('with outbound direction', () => {
beforeEach(() => {
rule.direction = RuleDirectionOutbound;
rule.cidr = '0.0.0.0/24';
});
it('returns correct spec', () => {
expect(ruleSpec(rule)).toEqual({ toCIDR: ['0.0.0.0/24'] });
});
testPortMatchers();
});
});
describe('RuleTypeFQDN', () => {
beforeEach(() => {
rule = buildRule(RuleTypeFQDN);
});
it('returns empty spec', () => {
expect(ruleSpec(rule)).toEqual({});
});
testPortMatchers();
describe('with fqdn', () => {
beforeEach(() => {
rule.fqdn = 'some-service.com another-service.com';
});
it('returns empty spec', () => {
expect(ruleSpec(rule)).toEqual({});
});
testPortMatchers();
});
describe('with outbound direction', () => {
beforeEach(() => {
rule.direction = RuleDirectionOutbound;
rule.fqdn = 'some-service.com another-service.com';
});
it('returns correct spec', () => {
expect(ruleSpec(rule)).toEqual({
toFQDNs: [{ matchName: 'some-service.com' }, { matchName: 'another-service.com' }],
});
});
testPortMatchers();
});
});
});
import toYaml from 'ee/threat_monitoring/components/policy_editor/lib/to_yaml';
import { buildRule } from 'ee/threat_monitoring/components/policy_editor/lib/rules';
import { EndpointMatchModeLabel } from 'ee/threat_monitoring/components/policy_editor/constants';
describe('toYaml', () => {
let policy;
beforeEach(() => {
policy = { name: 'test-policy', endpointLabels: '', rules: [] };
});
it('returns yaml representation', () => {
expect(toYaml(policy)).toEqual(`apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: test-policy
spec:
endpointSelector:
matchLabels:
network-policy.gitlab.com/disabled_by: gitlab
`);
});
describe('when description is not empty', () => {
beforeEach(() => {
policy.description = 'test description';
});
it('returns yaml representation', () => {
expect(toYaml(policy)).toEqual(`apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: test-policy
spec:
description: test description
endpointSelector:
matchLabels:
network-policy.gitlab.com/disabled_by: gitlab
`);
});
});
describe('when policy is enabled', () => {
beforeEach(() => {
policy.isEnabled = true;
});
it('returns yaml representation', () => {
expect(toYaml(policy)).toEqual(`apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: test-policy
spec:
endpointSelector: {}
`);
});
});
describe('when endpoint labels are not empty', () => {
beforeEach(() => {
policy.endpointMatchMode = EndpointMatchModeLabel;
policy.endpointLabels = 'one two:val three: two:overwrite four: five';
});
it('returns yaml representation', () => {
expect(toYaml(policy)).toEqual(`apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: test-policy
spec:
endpointSelector:
matchLabels:
one: ''
two: overwrite
three: ''
four: ''
five: ''
network-policy.gitlab.com/disabled_by: gitlab
`);
});
});
describe('with a rule', () => {
beforeEach(() => {
const rule = buildRule();
rule.matchLabels = 'foo:bar';
policy.rules = [rule];
});
it('returns yaml representation', () => {
expect(toYaml(policy)).toEqual(`apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: test-policy
spec:
endpointSelector:
matchLabels:
network-policy.gitlab.com/disabled_by: gitlab
ingress:
- fromEndpoints:
- matchLabels:
foo: bar
`);
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment