|
|
|
@@ -0,0 +1,301 @@
|
|
|
|
|
/// <reference types="cypress" />
|
|
|
|
|
|
|
|
|
|
describe('Per-path Access Lists', () => {
|
|
|
|
|
const domain = 'website3.example.com';
|
|
|
|
|
|
|
|
|
|
const alpha = {
|
|
|
|
|
name: 'Path Alpha',
|
|
|
|
|
username: 'alpha-user',
|
|
|
|
|
password: 'alpha-pass',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const beta = {
|
|
|
|
|
name: 'Path Beta',
|
|
|
|
|
username: 'beta-user',
|
|
|
|
|
password: 'beta-pass',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let token;
|
|
|
|
|
let alphaListId;
|
|
|
|
|
let betaListId;
|
|
|
|
|
let hostId;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Requests a path on the proxy host directly (bypassing squid)
|
|
|
|
|
* and yields the HTTP status code and response body
|
|
|
|
|
*
|
|
|
|
|
* @param {string} path
|
|
|
|
|
* @param {object} [creds]
|
|
|
|
|
* @param {string} creds.username
|
|
|
|
|
* @param {string} creds.password
|
|
|
|
|
*/
|
|
|
|
|
const request = (path, creds) => {
|
|
|
|
|
const auth = creds ? `-u '${creds.username}:${creds.password}'` : '';
|
|
|
|
|
return cy.exec(`curl --noproxy '*' -s -w '\n%{http_code}' ${auth} http://${domain}${path}`)
|
|
|
|
|
.then((result) => {
|
|
|
|
|
expect(result.exitCode).to.eq(0);
|
|
|
|
|
const lines = result.stdout.trim().split('\n');
|
|
|
|
|
const status = lines.pop();
|
|
|
|
|
return { status: status, body: lines.join('\n') };
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// nginx reloads are signalled and return immediately, so the old
|
|
|
|
|
// config can still be served for a moment after an API change.
|
|
|
|
|
// Retry until the expected response is seen.
|
|
|
|
|
const waitForResponse = (path, creds, check, description) => {
|
|
|
|
|
// Copy now, the callback runs later and creds may have been changed by then
|
|
|
|
|
const credsCopy = creds ? { ...creds } : null;
|
|
|
|
|
let last = null;
|
|
|
|
|
cy.waitUntil(() => request(path, credsCopy).then((res) => {
|
|
|
|
|
last = res;
|
|
|
|
|
return check(res);
|
|
|
|
|
}), {
|
|
|
|
|
timeout: 15000,
|
|
|
|
|
interval: 500,
|
|
|
|
|
errorMsg: () => `${path} did not return ${description}, last status: ${last?.status}`,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const expectStatus = (path, creds, status) => {
|
|
|
|
|
waitForResponse(path, creds, (res) => res.status === status, status);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Also checks the body so we know the page came from examplesite
|
|
|
|
|
// and not the NPM default site
|
|
|
|
|
const expectPage = (path, creds, text) => {
|
|
|
|
|
waitForResponse(path, creds, (res) => res.status === '200' && res.body.includes(text), `200 with "${text}"`);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const createAccessList = (list) => {
|
|
|
|
|
return cy.task('backendApiPost', {
|
|
|
|
|
token: token,
|
|
|
|
|
path: '/api/nginx/access-lists',
|
|
|
|
|
data: {
|
|
|
|
|
name: list.name,
|
|
|
|
|
satisfy_any: false,
|
|
|
|
|
pass_auth: false,
|
|
|
|
|
items: [
|
|
|
|
|
{
|
|
|
|
|
username: list.username,
|
|
|
|
|
password: list.password,
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
clients: [],
|
|
|
|
|
}
|
|
|
|
|
}).then((data) => {
|
|
|
|
|
cy.validateSwaggerSchema('post', 201, '/nginx/access-lists', data);
|
|
|
|
|
expect(data).to.have.property('id');
|
|
|
|
|
expect(data.id).to.be.greaterThan(0);
|
|
|
|
|
return cy.wrap(data.id);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const location = (path, accessListId) => {
|
|
|
|
|
return {
|
|
|
|
|
path: path,
|
|
|
|
|
forward_scheme: 'http',
|
|
|
|
|
forward_host: 'examplesite',
|
|
|
|
|
forward_port: 80,
|
|
|
|
|
access_list_id: accessListId,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
before(() => {
|
|
|
|
|
cy.resetUsers();
|
|
|
|
|
cy.getToken().then((tok) => {
|
|
|
|
|
token = tok;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('Should be able to create multiple access lists with credentials', () => {
|
|
|
|
|
createAccessList(alpha).then((id) => {
|
|
|
|
|
alphaListId = id;
|
|
|
|
|
});
|
|
|
|
|
createAccessList(beta).then((id) => {
|
|
|
|
|
betaListId = id;
|
|
|
|
|
expect(betaListId).to.not.equal(alphaListId);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('Should be able to create a proxy host with a different access list per location', () => {
|
|
|
|
|
cy.task('backendApiPost', {
|
|
|
|
|
token: token,
|
|
|
|
|
path: '/api/nginx/proxy-hosts',
|
|
|
|
|
data: {
|
|
|
|
|
domain_names: [domain],
|
|
|
|
|
forward_scheme: 'http',
|
|
|
|
|
forward_host: 'examplesite',
|
|
|
|
|
forward_port: 80,
|
|
|
|
|
access_list_id: 0,
|
|
|
|
|
certificate_id: 0,
|
|
|
|
|
meta: {
|
|
|
|
|
dns_challenge: false
|
|
|
|
|
},
|
|
|
|
|
advanced_config: '',
|
|
|
|
|
locations: [
|
|
|
|
|
location('/dashboard', alphaListId),
|
|
|
|
|
location('/profile', betaListId),
|
|
|
|
|
],
|
|
|
|
|
block_exploits: false,
|
|
|
|
|
caching_enabled: false,
|
|
|
|
|
allow_websocket_upgrade: false,
|
|
|
|
|
http2_support: false,
|
|
|
|
|
hsts_enabled: false,
|
|
|
|
|
hsts_subdomains: false,
|
|
|
|
|
ssl_forced: false
|
|
|
|
|
}
|
|
|
|
|
}).then((data) => {
|
|
|
|
|
cy.validateSwaggerSchema('post', 201, '/nginx/proxy-hosts', data);
|
|
|
|
|
expect(data).to.have.property('id');
|
|
|
|
|
expect(data.id).to.be.greaterThan(0);
|
|
|
|
|
hostId = data.id;
|
|
|
|
|
expect(data).to.have.property('enabled', true);
|
|
|
|
|
expect(data).to.have.property('access_list_id', 0);
|
|
|
|
|
expect(data.locations).to.have.length(2);
|
|
|
|
|
expect(data.locations[0]).to.have.property('access_list_id', alphaListId);
|
|
|
|
|
expect(data.locations[1]).to.have.property('access_list_id', betaListId);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('Should persist the location access lists on the proxy host', () => {
|
|
|
|
|
cy.task('backendApiGet', {
|
|
|
|
|
token: token,
|
|
|
|
|
path: `/api/nginx/proxy-hosts/${hostId}`,
|
|
|
|
|
}).then((data) => {
|
|
|
|
|
cy.validateSwaggerSchema('get', 200, '/nginx/proxy-hosts/{hostID}', data);
|
|
|
|
|
expect(data.locations[0]).to.have.property('path', '/dashboard');
|
|
|
|
|
expect(data.locations[0]).to.have.property('access_list_id', alphaListId);
|
|
|
|
|
expect(data.locations[1]).to.have.property('path', '/profile');
|
|
|
|
|
expect(data.locations[1]).to.have.property('access_list_id', betaListId);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('Should not require auth for the host root', () => {
|
|
|
|
|
expectPage('/', null, 'this is the index page');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('Should only accept the alpha credentials on /dashboard', () => {
|
|
|
|
|
expectStatus('/dashboard', null, '401');
|
|
|
|
|
expectStatus('/dashboard', beta, '401');
|
|
|
|
|
expectPage('/dashboard', alpha, 'this is the dashboard page');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('Should only accept the beta credentials on /profile', () => {
|
|
|
|
|
expectStatus('/profile', null, '401');
|
|
|
|
|
expectStatus('/profile', alpha, '401');
|
|
|
|
|
expectPage('/profile', beta, 'this is the profile page');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('Should apply access list credential changes to locations using it', () => {
|
|
|
|
|
const newPassword = 'alpha-pass-changed';
|
|
|
|
|
|
|
|
|
|
cy.task('backendApiPut', {
|
|
|
|
|
token: token,
|
|
|
|
|
path: `/api/nginx/access-lists/${alphaListId}`,
|
|
|
|
|
data: {
|
|
|
|
|
name: alpha.name,
|
|
|
|
|
satisfy_any: false,
|
|
|
|
|
pass_auth: false,
|
|
|
|
|
items: [
|
|
|
|
|
{
|
|
|
|
|
username: alpha.username,
|
|
|
|
|
password: newPassword,
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
clients: [],
|
|
|
|
|
}
|
|
|
|
|
}).then((data) => {
|
|
|
|
|
cy.validateSwaggerSchema('put', 200, '/nginx/access-lists/{listID}', data);
|
|
|
|
|
expect(data).to.have.property('id', alphaListId);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expectStatus('/dashboard', alpha, '401');
|
|
|
|
|
expectPage('/dashboard', { username: alpha.username, password: newPassword }, 'this is the dashboard page');
|
|
|
|
|
alpha.password = newPassword;
|
|
|
|
|
|
|
|
|
|
// Other locations are unaffected
|
|
|
|
|
expectStatus('/profile', null, '401');
|
|
|
|
|
expectPage('/profile', beta, 'this is the profile page');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('Should inherit the host access list on locations without their own', () => {
|
|
|
|
|
// Host uses beta, /dashboard overrides with alpha, /missing has none and should inherit beta
|
|
|
|
|
cy.task('backendApiPut', {
|
|
|
|
|
token: token,
|
|
|
|
|
path: `/api/nginx/proxy-hosts/${hostId}`,
|
|
|
|
|
data: {
|
|
|
|
|
access_list_id: betaListId,
|
|
|
|
|
locations: [
|
|
|
|
|
location('/dashboard', alphaListId),
|
|
|
|
|
location('/profile', betaListId),
|
|
|
|
|
location('/missing', 0),
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
}).then((data) => {
|
|
|
|
|
// No swagger validation here: with a host access list set, the expanded
|
|
|
|
|
// access_list in the response has no proxy_host_count, which the schema requires
|
|
|
|
|
expect(data).to.have.property('access_list_id', betaListId);
|
|
|
|
|
expect(data.locations[2]).to.have.property('access_list_id', 0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expectStatus('/', null, '401');
|
|
|
|
|
expectStatus('/', alpha, '401');
|
|
|
|
|
expectPage('/', beta, 'this is the index page');
|
|
|
|
|
|
|
|
|
|
// examplesite returns 404 for this path, once past the access list
|
|
|
|
|
expectStatus('/missing', null, '401');
|
|
|
|
|
expectStatus('/missing', alpha, '401');
|
|
|
|
|
expectStatus('/missing', beta, '404');
|
|
|
|
|
|
|
|
|
|
expectStatus('/dashboard', beta, '401');
|
|
|
|
|
expectPage('/dashboard', alpha, 'this is the dashboard page');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('Should remove a deleted access list from the host and locations using it', () => {
|
|
|
|
|
cy.task('backendApiDelete', {
|
|
|
|
|
token: token,
|
|
|
|
|
path: `/api/nginx/access-lists/${betaListId}`,
|
|
|
|
|
}).then((data) => {
|
|
|
|
|
cy.validateSwaggerSchema('delete', 200, '/nginx/access-lists/{listID}', data);
|
|
|
|
|
expect(data).to.be.equal(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
cy.task('backendApiGet', {
|
|
|
|
|
token: token,
|
|
|
|
|
path: `/api/nginx/proxy-hosts/${hostId}`,
|
|
|
|
|
}).then((data) => {
|
|
|
|
|
expect(data).to.have.property('access_list_id', 0);
|
|
|
|
|
expect(data.locations[0]).to.have.property('access_list_id', alphaListId);
|
|
|
|
|
expect(data.locations[1]).to.have.property('access_list_id', 0);
|
|
|
|
|
expect(data.locations[2]).to.have.property('access_list_id', 0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expectPage('/', null, 'this is the index page');
|
|
|
|
|
expectPage('/profile', null, 'this is the profile page');
|
|
|
|
|
expectStatus('/missing', null, '404');
|
|
|
|
|
|
|
|
|
|
// Remaining location access list must still be enforced
|
|
|
|
|
expectStatus('/dashboard', null, '401');
|
|
|
|
|
expectPage('/dashboard', alpha, 'this is the dashboard page');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('Should be able to delete the proxy host and remaining access list', () => {
|
|
|
|
|
cy.task('backendApiDelete', {
|
|
|
|
|
token: token,
|
|
|
|
|
path: `/api/nginx/proxy-hosts/${hostId}`,
|
|
|
|
|
}).then((data) => {
|
|
|
|
|
cy.validateSwaggerSchema('delete', 200, '/nginx/proxy-hosts/{hostID}', data);
|
|
|
|
|
expect(data).to.be.equal(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
cy.task('backendApiDelete', {
|
|
|
|
|
token: token,
|
|
|
|
|
path: `/api/nginx/access-lists/${alphaListId}`,
|
|
|
|
|
}).then((data) => {
|
|
|
|
|
cy.validateSwaggerSchema('delete', 200, '/nginx/access-lists/{listID}', data);
|
|
|
|
|
expect(data).to.be.equal(true);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
});
|