diff --git a/backend/internal/access-list.js b/backend/internal/access-list.js index bac0c1a41..1fdd65b6a 100644 --- a/backend/internal/access-list.js +++ b/backend/internal/access-list.js @@ -42,9 +42,11 @@ const getProxyHostsUsingAccessListInLocations = async (accessListId) => { [`%"access_list_id":${accessListId}%`], ); } - // knex raw() returns [rows, metadata] for MySQL - const rows = Array.isArray(result) && Array.isArray(result[0]) ? result[0] : result; - return rows || []; + // knex raw() returns [rows, metadata] for MySQL, { rows } for Postgres and rows for SQLite + if (!Array.isArray(result)) { + return result?.rows || []; + } + return (Array.isArray(result[0]) ? result[0] : result) || []; }; const internalAccessList = { @@ -233,26 +235,6 @@ const internalAccessList = { .whereIn("id", locationHostIds) .allowGraph(proxyHostModel.defaultAllowGraph) .withGraphFetched("[owner, certificate, access_list.[clients,items]]"); - for (const host of locationHosts) { - // Fetch access lists for locations - if (host.locations?.length) { - for (let i = 0; i < host.locations.length; i++) { - const loc = host.locations[i]; - if (loc.access_list_id && loc.access_list_id > 0) { - const locAccessList = await accessListModel - .query() - .allowGraph("[clients,items]") - .where("is_deleted", 0) - .andWhere("id", loc.access_list_id) - .withGraphFetched("[clients,items]") - .first(); - if (locAccessList) { - host.locations[i].access_list = locAccessList; - } - } - } - } - } await internalNginx.bulkGenerateConfigs("proxy_host", locationHosts); } } @@ -338,50 +320,38 @@ const internalAccessList = { }); // 2. update any proxy hosts that were using it (ignoring permissions) - if (row.proxy_hosts) { + const affectedHostIds = new Set((row.proxy_hosts || []).map((h) => h.id)); + if (affectedHostIds.size) { await proxyHostModel.query().where("access_list_id", "=", row.id).patch({ access_list_id: 0 }); - - // 3. reconfigure those hosts, then reload nginx - // set the access_list_id to zero for these items - row.proxy_hosts.map((_val, idx) => { - row.proxy_hosts[idx].access_list_id = 0; - return true; - }); - - await internalNginx.bulkGenerateConfigs("proxy_host", row.proxy_hosts); } - // Also handle proxy hosts that reference this access list in their locations JSON + // Also clear it from any proxy host locations using it, these will then inherit the host's access list const locationHostRows = await getProxyHostsUsingAccessListInLocations(row.id); - if (locationHostRows?.length) { - const locationHostIds = locationHostRows.map((r) => r.id).filter((id) => { - return !row.proxy_hosts?.find((h) => h.id === id); - }); - if (locationHostIds.length) { - // Clear the access_list_id in locations JSON for these hosts - for (const hostId of locationHostIds) { - const host = await proxyHostModel.query().where("id", hostId).first(); - if (host?.locations) { - const updatedLocations = host.locations.map((loc) => { - if (loc.access_list_id === row.id) { - return { ...loc, access_list_id: 0 }; - } - return loc; - }); - await proxyHostModel.query().where("id", hostId).patch({ locations: updatedLocations }); + for (const { id: hostId } of locationHostRows) { + const host = await proxyHostModel.query().where("id", hostId).first(); + if (host?.locations?.some((loc) => loc.access_list_id === row.id)) { + const updatedLocations = host.locations.map((loc) => { + if (loc.access_list_id === row.id) { + return { ...loc, access_list_id: 0 }; } - } - - // Re-fetch and regenerate configs - const locationHosts = await proxyHostModel.query() - .where("is_deleted", 0) - .whereIn("id", locationHostIds) - .allowGraph(proxyHostModel.defaultExpand) - .withGraphFetched("[owner, certificate, access_list.[clients,items]]"); - await internalNginx.bulkGenerateConfigs("proxy_host", locationHosts); + return loc; + }); + await proxyHostModel.query().where("id", hostId).patch({ locations: updatedLocations }); + affectedHostIds.add(hostId); } } + // 3. reconfigure those hosts from fresh rows, then reload nginx + if (affectedHostIds.size) { + const affectedHosts = await proxyHostModel + .query() + .where("is_deleted", 0) + .whereIn("id", [...affectedHostIds]) + .allowGraph(proxyHostModel.defaultAllowGraph) + .withGraphFetched("[owner, certificate, access_list.[clients,items]]"); + await internalNginx.bulkGenerateConfigs("proxy_host", affectedHosts); + } + await internalNginx.reload(); // delete the htpasswd file diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js index 4b0b2b3c6..a50e0780d 100644 --- a/backend/internal/nginx.js +++ b/backend/internal/nginx.js @@ -6,6 +6,7 @@ import _ from "lodash"; import errs from "../lib/error.js"; import utils from "../lib/utils.js"; import { debug, nginx as logger } from "../logger.js"; +import accessListModel from "../models/access_list.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -168,6 +169,24 @@ const internalNginx = { host.locations[i], ); + // A location with its own access list overrides the host's, + // otherwise it inherits the host's access list + let locationAccessList = null; + if (locationCopy.access_list_id > 0 && locationCopy.access_list_id !== host.access_list?.id) { + locationAccessList = await accessListModel + .query() + .where("is_deleted", 0) + .andWhere("id", locationCopy.access_list_id) + .withGraphFetched("[clients,items]") + .first(); + } + if (locationAccessList) { + locationCopy.access_list = locationAccessList; + } else { + locationCopy.access_list_id = host.access_list_id; + locationCopy.access_list = host.access_list; + } + if (locationCopy.forward_host.indexOf("/") > -1) { const splitted = locationCopy.forward_host.split("/"); @@ -179,7 +198,9 @@ const internalNginx = { } }; - locationRendering().then(() => resolve(renderedLocations)); + locationRendering() + .then(() => resolve(renderedLocations)) + .catch(reject); }); }, @@ -271,6 +292,9 @@ const internalNginx = { debug(logger, `Could not write ${filename}:`, err.message); reject(new errs.ConfigurationError(err.message)); }); + }).catch((err) => { + debug(logger, `Could not render locations for ${filename}:`, err.message); + reject(new errs.ConfigurationError(err.message)); }); }); }, diff --git a/backend/internal/proxy-host.js b/backend/internal/proxy-host.js index 6bd0be1a6..eac0c148a 100644 --- a/backend/internal/proxy-host.js +++ b/backend/internal/proxy-host.js @@ -2,7 +2,6 @@ import _ from "lodash"; import errs from "../lib/error.js"; import { castJsonIfNeed } from "../lib/helpers.js"; import utils from "../lib/utils.js"; -import accessListModel from "../models/access_list.js"; import proxyHostModel from "../models/proxy_host.js"; import internalAuditLog from "./audit-log.js"; import internalCertificate from "./certificate.js"; @@ -13,33 +12,6 @@ const omissions = () => { return ["is_deleted", "owner.is_deleted"]; }; -/** - * Fetches access lists for each location that has its own access_list_id. - * Attaches the expanded access_list object (with clients and items) to each location. - * - * @param {Object} host - * @returns {Promise} - */ -const fetchLocationAccessLists = async (host) => { - if (!host.locations?.length) { - return; - } - for (let i = 0; i < host.locations.length; i++) { - const loc = host.locations[i]; - if (loc.access_list_id && loc.access_list_id > 0) { - const accessList = await accessListModel - .query() - .where("is_deleted", 0) - .andWhere("id", loc.access_list_id) - .withGraphFetched("[clients,items]") - .first(); - if (accessList) { - host.locations[i].access_list = accessList; - } - } - } -}; - const internalProxyHost = { /** * @param {Access} access @@ -112,7 +84,6 @@ const internalProxyHost = { }); }) .then(async (row) => { - await fetchLocationAccessLists(row); // Configure nginx return internalNginx.configure(proxyHostModel, "proxy_host", row).then(() => { return row; @@ -242,7 +213,6 @@ const internalProxyHost = { // No need to add nginx config if host is disabled return row; } - await fetchLocationAccessLists(row); // Configure nginx return internalNginx.configure(proxyHostModel, "proxy_host", row).then((new_meta) => { row.meta = new_meta; @@ -373,8 +343,6 @@ const internalProxyHost = { enabled: 1, }); - await fetchLocationAccessLists(row); - // Configure nginx await internalNginx.configure(proxyHostModel, "proxy_host", row); diff --git a/backend/lib/validator/api.js b/backend/lib/validator/api.js index f4981a80c..0597ef635 100644 --- a/backend/lib/validator/api.js +++ b/backend/lib/validator/api.js @@ -1,3 +1,4 @@ +import net from "node:net"; import Ajv from "ajv/dist/2020.js"; import errs from "../error.js"; @@ -9,6 +10,8 @@ const ajv = new Ajv({ coerceTypes: true, }); +ajv.addFormat("ipv6", { type: "string", validate: (value) => net.isIPv6(value) }); + /** * @param {Object} schema * @param {Object} payload diff --git a/backend/lib/validator/index.js b/backend/lib/validator/index.js index 5d9f8f38a..767353f61 100644 --- a/backend/lib/validator/index.js +++ b/backend/lib/validator/index.js @@ -1,3 +1,4 @@ +import net from "node:net"; import Ajv from "ajv/dist/2020.js"; import _ from "lodash"; import commonDefinitions from "../../schema/common.json" with { type: "json" }; @@ -14,6 +15,8 @@ const ajv = new Ajv({ schemas: [commonDefinitions], }); +ajv.addFormat("ipv6", { type: "string", validate: (value) => net.isIPv6(value) }); + /** * * @param {Object} schema diff --git a/backend/models/access_list.js b/backend/models/access_list.js index 427d447d6..192f045b9 100644 --- a/backend/models/access_list.js +++ b/backend/models/access_list.js @@ -31,6 +31,10 @@ class AccessList extends Model { $parseDatabaseJson(json) { const thisJson = super.$parseDatabaseJson(json); + // Postgres returns COUNT() as a string + if (typeof thisJson.proxy_host_count === "string") { + thisJson.proxy_host_count = Number.parseInt(thisJson.proxy_host_count, 10); + } return convertIntFieldsToBool(thisJson, boolFields); } diff --git a/backend/schema/components/stream-object.json b/backend/schema/components/stream-object.json index 602073cec..3a1cd5210 100644 --- a/backend/schema/components/stream-object.json +++ b/backend/schema/components/stream-object.json @@ -44,7 +44,7 @@ }, { "type": "string", - "format": "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$" + "pattern": "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$" }, { "type": "string", diff --git a/docker/docker-compose.ci.yml b/docker/docker-compose.ci.yml index 1bb3c7450..be6f517d3 100644 --- a/docker/docker-compose.ci.yml +++ b/docker/docker-compose.ci.yml @@ -38,6 +38,16 @@ services: - website2.example.com - website3.example.com + examplesite: + image: "${IMAGE}-examplesite:ci-${BUILD_NUMBER}" + build: + context: ../test/docker + dockerfile: Dockerfile.website + expose: + - "80/tcp" + networks: + - fulltest + stepca: image: nginxproxymanager/testca volumes: diff --git a/frontend/src/components/Form/AccessField.tsx b/frontend/src/components/Form/AccessField.tsx index 1c1004e71..ef955a033 100644 --- a/frontend/src/components/Form/AccessField.tsx +++ b/frontend/src/components/Form/AccessField.tsx @@ -1,4 +1,4 @@ -import { IconLock, IconLockOpen2 } from "@tabler/icons-react"; +import { IconArrowBackUp, IconLock, IconLockOpen2 } from "@tabler/icons-react"; import { Field, useFormikContext } from "formik"; import type { ReactNode } from "react"; import Select, { type ActionMeta, components, type OptionProps } from "react-select"; @@ -32,8 +32,16 @@ interface Props { name?: string; label?: string; onFormChange?: (value: number) => void; + // When set, the 0 option inherits the host's access list instead of being public + inheritHost?: boolean; } -export function AccessField({ name = "accessListId", label = "access-list", id = "accessListId", onFormChange }: Props) { +export function AccessField({ + name = "accessListId", + label = "access-list", + id = "accessListId", + onFormChange, + inheritHost = false, +}: Props) { const { locale } = useLocaleState(); const { isLoading, isError, error, data } = useAccessLists(["owner", "items", "clients"]); const { setFieldValue } = useFormikContext(); @@ -60,13 +68,22 @@ export function AccessField({ name = "accessListId", label = "access-list", id = icon: , })) || []; - // Public option - options?.unshift({ - value: 0, - label: intl.formatMessage({ id: "access-list.public" }), - subLabel: intl.formatMessage({ id: "access-list.public.subtitle" }), - icon: , - }); + // Public or inherit option + options?.unshift( + inheritHost + ? { + value: 0, + label: intl.formatMessage({ id: "access-list.inherit" }), + subLabel: intl.formatMessage({ id: "access-list.inherit.subtitle" }), + icon: , + } + : { + value: 0, + label: intl.formatMessage({ id: "access-list.public" }), + subLabel: intl.formatMessage({ id: "access-list.public.subtitle" }), + icon: , + }, + ); return ( diff --git a/frontend/src/components/Form/LocationsFields.tsx b/frontend/src/components/Form/LocationsFields.tsx index 9c4d8b3a8..a0f64c239 100644 --- a/frontend/src/components/Form/LocationsFields.tsx +++ b/frontend/src/components/Form/LocationsFields.tsx @@ -312,6 +312,7 @@ export function LocationsFields({ initialValues, name = "locations" }: Props) { label="access-list" id={`locations-access-list-${row.id}`} onFormChange={(value) => handleAccessListChange(row.id, value)} + inheritHost /> {advVisible.includes(row.id) && (
diff --git a/frontend/src/locale/src/en.json b/frontend/src/locale/src/en.json index da8765a5e..11d09ea71 100644 --- a/frontend/src/locale/src/en.json +++ b/frontend/src/locale/src/en.json @@ -71,6 +71,12 @@ "access-list.help.rules-order": { "defaultMessage": "Note that the allow and deny directives will be applied in the order they are defined." }, + "access-list.inherit": { + "defaultMessage": "Inherit from Proxy Host" + }, + "access-list.inherit.subtitle": { + "defaultMessage": "Uses the proxy host's access list" + }, "access-list.pass-auth": { "defaultMessage": "Pass Auth to Upstream" }, diff --git a/frontend/src/locale/src/et.json b/frontend/src/locale/src/et.json index 989a153aa..316ede752 100644 --- a/frontend/src/locale/src/et.json +++ b/frontend/src/locale/src/et.json @@ -71,6 +71,12 @@ "access-list.help.rules-order": { "defaultMessage": "Luba ja keela reeglid rakenduvad selles järjekorras, milles need on kirjas." }, + "access-list.inherit": { + "defaultMessage": "Päri puhverserverilt" + }, + "access-list.inherit.subtitle": { + "defaultMessage": "Kasutab puhverserveri juurdepääsuloendit" + }, "access-list.pass-auth": { "defaultMessage": "Edasta autentimine sihtserverile" }, diff --git a/scripts/ci/fulltest-cypress b/scripts/ci/fulltest-cypress index 8fb896967..a43f420eb 100755 --- a/scripts/ci/fulltest-cypress +++ b/scripts/ci/fulltest-cypress @@ -65,7 +65,8 @@ rm -rf "${LOCAL_RESOLVE}" printf "nameserver %s\noptions ndots:0" "${DNSROUTER_IP}" > "${LOCAL_RESOLVE}" # bring up all remaining containers, except cypress! -docker compose up -d --remove-orphans stepca squid +docker compose build examplesite +docker compose up -d --remove-orphans --no-build stepca squid examplesite docker compose pull db-mysql || true # ok to fail docker compose pull db-postgres || true # ok to fail docker compose pull authentik authentik-redis authentik-ldap || true # ok to fail diff --git a/test/cypress/e2e/api/AccessListLocations.cy.js b/test/cypress/e2e/api/AccessListLocations.cy.js new file mode 100644 index 000000000..2ea2742ef --- /dev/null +++ b/test/cypress/e2e/api/AccessListLocations.cy.js @@ -0,0 +1,301 @@ +/// + +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); + }); + }); + +}); diff --git a/test/docker/Dockerfile.website b/test/docker/Dockerfile.website new file mode 100644 index 000000000..311272d8c --- /dev/null +++ b/test/docker/Dockerfile.website @@ -0,0 +1,6 @@ +FROM nginx:stable + +RUN rm -rf /etc/nginx/conf.d +COPY nginx /etc/nginx +COPY www /www +RUN chown -R nginx:nginx /www diff --git a/test/docker/nginx/conf.d/website123.conf b/test/docker/nginx/conf.d/website123.conf new file mode 100644 index 000000000..656f3ade4 --- /dev/null +++ b/test/docker/nginx/conf.d/website123.conf @@ -0,0 +1,29 @@ +server { + listen 80; + server_name website1.example.com website2.example.com website3.example.com; + root /www; + index index.html; + + # redirect requests for .html URLs to their extensionless form + if ($request_uri ~ ^/index\.html(\?.*)?$) { + return 301 /$1; + } + if ($request_uri ~ ^/(.+)\.html(\?.*)?$) { + return 301 /$1$2; + } + + location / { + try_files $uri $uri.html $uri/ =404; + } + + error_page 404 /404.html; + location = /404.html { + internal; + } + + # deny access to .htaccess files, if Apache's document root + # concurs with nginx's one + location ~ /\.ht { + deny all; + } +} diff --git a/test/docker/nginx/nginx.conf b/test/docker/nginx/nginx.conf new file mode 100644 index 000000000..82366ca04 --- /dev/null +++ b/test/docker/nginx/nginx.conf @@ -0,0 +1,30 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log notice; +pid /run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + server_tokens off; + + log_format custom_combined '$remote_addr - $remote_user [$time_local] "$host" "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"'; + + access_log /var/log/nginx/access.log custom_combined; + sendfile on; + tcp_nopush on; + keepalive_timeout 65; + gzip on; + + set_real_ip_from 10.0.0.0/8; + set_real_ip_from 172.16.0.0/12; # Includes Docker subnet + set_real_ip_from 192.168.0.0/16; + + include include/*.conf; + include conf.d/*.conf; + include /sites/*/nginx.conf; +} diff --git a/test/docker/www/404.html b/test/docker/www/404.html new file mode 100644 index 000000000..389d3cafb --- /dev/null +++ b/test/docker/www/404.html @@ -0,0 +1,9 @@ + + + 404 + + + 404 not found + + + diff --git a/test/docker/www/dashboard.html b/test/docker/www/dashboard.html new file mode 100644 index 000000000..d26099e8f --- /dev/null +++ b/test/docker/www/dashboard.html @@ -0,0 +1,9 @@ + + + Dashboard + + + this is the dashboard page + + + diff --git a/test/docker/www/index.html b/test/docker/www/index.html new file mode 100644 index 000000000..3a190926c --- /dev/null +++ b/test/docker/www/index.html @@ -0,0 +1,9 @@ + + + Index + + + this is the index page + + + diff --git a/test/docker/www/profile.html b/test/docker/www/profile.html new file mode 100644 index 000000000..1058ca4a1 --- /dev/null +++ b/test/docker/www/profile.html @@ -0,0 +1,9 @@ + + + Profile + + + this is the profile page + + +