diff --git a/.version b/.version index b1b25a5..5859406 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.2.2 +2.2.3 diff --git a/README.md b/README.md index 0fbecb4..6099b86 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@



- + diff --git a/backend/internal/access-list.js b/backend/internal/access-list.js index bfecf61..6de3310 100644 --- a/backend/internal/access-list.js +++ b/backend/internal/access-list.js @@ -1,14 +1,15 @@ -const _ = require('lodash'); -const fs = require('fs'); -const batchflow = require('batchflow'); -const logger = require('../logger').access; -const error = require('../lib/error'); -const accessListModel = require('../models/access_list'); -const accessListAuthModel = require('../models/access_list_auth'); -const proxyHostModel = require('../models/proxy_host'); -const internalAuditLog = require('./audit-log'); -const internalNginx = require('./nginx'); -const utils = require('../lib/utils'); +const _ = require('lodash'); +const fs = require('fs'); +const batchflow = require('batchflow'); +const logger = require('../logger').access; +const error = require('../lib/error'); +const accessListModel = require('../models/access_list'); +const accessListAuthModel = require('../models/access_list_auth'); +const accessListClientModel = require('../models/access_list_client'); +const proxyHostModel = require('../models/proxy_host'); +const internalAuditLog = require('./audit-log'); +const internalNginx = require('./nginx'); +const utils = require('../lib/utils'); function omissions () { return ['is_deleted']; @@ -29,14 +30,16 @@ const internalAccessList = { .omit(omissions()) .insertAndFetch({ name: data.name, + satify_any: data.satify_any, owner_user_id: access.token.getUserId(1) }); }) .then((row) => { data.id = row.id; - // Now add the items let promises = []; + + // Now add the items data.items.map((item) => { promises.push(accessListAuthModel .query() @@ -48,13 +51,27 @@ const internalAccessList = { ); }); + // Now add the clients + if (typeof data.clients !== 'undefined' && data.clients) { + data.clients.map((client) => { + promises.push(accessListClientModel + .query() + .insert({ + access_list_id: row.id, + address: client.address, + directive: client.directive + }) + ); + }); + } + return Promise.all(promises); }) .then(() => { // re-fetch with expansions return internalAccessList.get(access, { id: data.id, - expand: ['owner', 'items'] + expand: ['owner', 'items', 'clients', 'proxy_hosts.access_list.clients'] }, true /* <- skip masking */); }) .then((row) => { @@ -64,7 +81,7 @@ const internalAccessList = { return internalAccessList.build(row) .then(() => { if (row.proxy_host_count) { - return internalNginx.reload(); + return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts); } }) .then(() => { @@ -109,7 +126,8 @@ const internalAccessList = { .query() .where({id: data.id}) .patch({ - name: data.name + name: data.name, + satify_any: data.satify_any, }); } }) @@ -153,6 +171,38 @@ const internalAccessList = { }); } }) + .then(() => { + // Check for clients and add/update/remove them + if (typeof data.clients !== 'undefined' && data.clients) { + let promises = []; + + data.clients.map(function (client) { + if (client.address) { + promises.push(accessListClientModel + .query() + .insert({ + access_list_id: data.id, + address: client.address, + directive: client.directive + }) + ); + } + }); + + let query = accessListClientModel + .query() + .delete() + .where('access_list_id', data.id); + + return query + .then(() => { + // Add new items + if (promises.length) { + return Promise.all(promises); + } + }); + } + }) .then(() => { // Add to audit log return internalAuditLog.add(access, { @@ -166,14 +216,14 @@ const internalAccessList = { // re-fetch with expansions return internalAccessList.get(access, { id: data.id, - expand: ['owner', 'items'] + expand: ['owner', 'items', 'clients', 'proxy_hosts.access_list.clients'] }, true /* <- skip masking */); }) .then((row) => { return internalAccessList.build(row) .then(() => { if (row.proxy_host_count) { - return internalNginx.reload(); + return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts); } }) .then(() => { @@ -204,7 +254,7 @@ const internalAccessList = { .joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0') .where('access_list.is_deleted', 0) .andWhere('access_list.id', data.id) - .allowEager('[owner,items,proxy_hosts]') + .allowEager('[owner,items,clients,proxy_hosts,proxy_hosts.access_list.clients]') .omit(['access_list.is_deleted']) .first(); @@ -246,7 +296,7 @@ const internalAccessList = { delete: (access, data) => { return access.can('access_lists:delete', data.id) .then(() => { - return internalAccessList.get(access, {id: data.id, expand: ['proxy_hosts', 'items']}); + return internalAccessList.get(access, {id: data.id, expand: ['proxy_hosts', 'items', 'clients']}); }) .then((row) => { if (!row) { @@ -330,7 +380,7 @@ const internalAccessList = { .where('access_list.is_deleted', 0) .groupBy('access_list.id') .omit(['access_list.is_deleted']) - .allowEager('[owner,items]') + .allowEager('[owner,items,clients]') .orderBy('access_list.name', 'ASC'); if (access_data.permission_visibility !== 'all') { diff --git a/backend/internal/proxy-host.js b/backend/internal/proxy-host.js index 0e9ced9..c27d0dd 100644 --- a/backend/internal/proxy-host.js +++ b/backend/internal/proxy-host.js @@ -73,7 +73,7 @@ const internalProxyHost = { // re-fetch with cert return internalProxyHost.get(access, { id: row.id, - expand: ['certificate', 'owner', 'access_list'] + expand: ['certificate', 'owner', 'access_list.clients'] }); }) .then((row) => { @@ -186,7 +186,7 @@ const internalProxyHost = { .then(() => { return internalProxyHost.get(access, { id: data.id, - expand: ['owner', 'certificate', 'access_list'] + expand: ['owner', 'certificate', 'access_list.clients'] }) .then((row) => { // Configure nginx @@ -219,7 +219,7 @@ const internalProxyHost = { .query() .where('is_deleted', 0) .andWhere('id', data.id) - .allowEager('[owner,access_list,certificate]') + .allowEager('[owner,access_list,access_list.clients,certificate]') .first(); if (access_data.permission_visibility !== 'all') { diff --git a/backend/migrations/20200410143839_access_list_client.js b/backend/migrations/20200410143839_access_list_client.js new file mode 100644 index 0000000..3511e35 --- /dev/null +++ b/backend/migrations/20200410143839_access_list_client.js @@ -0,0 +1,53 @@ +const migrate_name = 'access_list_client'; +const logger = require('../logger').migrate; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.up = function (knex/*, Promise*/) { + + logger.info('[' + migrate_name + '] Migrating Up...'); + + return knex.schema.createTable('access_list_client', (table) => { + table.increments().primary(); + table.dateTime('created_on').notNull(); + table.dateTime('modified_on').notNull(); + table.integer('access_list_id').notNull().unsigned(); + table.string('address').notNull(); + table.string('directive').notNull(); + table.json('meta').notNull(); + + }) + .then(function () { + logger.info('[' + migrate_name + '] access_list_client Table created'); + + return knex.schema.table('access_list', function (access_list) { + access_list.integer('satify_any').notNull().defaultTo(0); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] access_list Table altered'); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.down = function (knex/*, Promise*/) { + logger.info('[' + migrate_name + '] Migrating Down...'); + + return knex.schema.dropTable('access_list_client') + .then(() => { + logger.info('[' + migrate_name + '] access_list_client Table dropped'); + }); +}; diff --git a/backend/models/access_list.js b/backend/models/access_list.js index 7704893..482cfc4 100644 --- a/backend/models/access_list.js +++ b/backend/models/access_list.js @@ -1,10 +1,11 @@ // Objection Docs: // http://vincit.github.io/objection.js/ -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); -const AccessListAuth = require('./access_list_auth'); +const db = require('../db'); +const Model = require('objection').Model; +const User = require('./user'); +const AccessListAuth = require('./access_list_auth'); +const AccessListClient = require('./access_list_client'); Model.knex(db); @@ -62,6 +63,17 @@ class AccessList extends Model { qb.omit(['id', 'created_on', 'modified_on', 'access_list_id', 'meta']); } }, + clients: { + relation: Model.HasManyRelation, + modelClass: AccessListClient, + join: { + from: 'access_list.id', + to: 'access_list_client.access_list_id' + }, + modify: function (qb) { + qb.omit(['id', 'created_on', 'modified_on', 'access_list_id', 'meta']); + } + }, proxy_hosts: { relation: Model.HasManyRelation, modelClass: ProxyHost, @@ -76,6 +88,10 @@ class AccessList extends Model { } }; } + + get satisfy() { + return this.satify_any ? 'satisfy any' : 'satisfy all'; + } } module.exports = AccessList; diff --git a/backend/models/access_list_client.js b/backend/models/access_list_client.js new file mode 100644 index 0000000..0386395 --- /dev/null +++ b/backend/models/access_list_client.js @@ -0,0 +1,58 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +const db = require('../db'); +const Model = require('objection').Model; + +Model.knex(db); + +class AccessListClient extends Model { + $beforeInsert () { + this.created_on = Model.raw('NOW()'); + this.modified_on = Model.raw('NOW()'); + + // Default for meta + if (typeof this.meta === 'undefined') { + this.meta = {}; + } + } + + $beforeUpdate () { + this.modified_on = Model.raw('NOW()'); + } + + static get name () { + return 'AccessListClient'; + } + + static get tableName () { + return 'access_list_client'; + } + + static get jsonAttributes () { + return ['meta']; + } + + static get relationMappings () { + return { + access_list: { + relation: Model.HasOneRelation, + modelClass: require('./access_list'), + join: { + from: 'access_list_client.access_list_id', + to: 'access_list.id' + }, + modify: function (qb) { + qb.where('access_list.is_deleted', 0); + qb.omit(['created_on', 'modified_on', 'is_deleted', 'access_list_id']); + } + } + }; + } + + get rule() { + return `${this.directive} ${this.address}`; + } +} + +module.exports = AccessListClient; diff --git a/backend/schema/endpoints/access-lists.json b/backend/schema/endpoints/access-lists.json index da90a05..51a07ba 100644 --- a/backend/schema/endpoints/access-lists.json +++ b/backend/schema/endpoints/access-lists.json @@ -19,6 +19,29 @@ "type": "string", "description": "Name of the Access List" }, + "directive": { + "type": "string", + "enum": ["allow", "deny"] + }, + "address": { + "oneOf": [ + { + "type": "string", + "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$" + }, + { + "type": "string", + "pattern": "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$" + }, + { + "type": "string", + "pattern": "^any$" + } + ] + }, + "satify_any": { + "type": "boolean" + }, "meta": { "type": "object" } @@ -78,9 +101,12 @@ "name": { "$ref": "#/definitions/name" }, + "satify_any": { + "$ref": "#/definitions/satify_any" + }, "items": { "type": "array", - "minItems": 1, + "minItems": 0, "items": { "type": "object", "additionalProperties": false, @@ -96,6 +122,22 @@ } } }, + "clients": { + "type": "array", + "minItems": 0, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "address": { + "$ref": "#/definitions/address" + }, + "directive": { + "$ref": "#/definitions/directive" + } + } + } + }, "meta": { "$ref": "#/definitions/meta" } @@ -124,9 +166,12 @@ "name": { "$ref": "#/definitions/name" }, + "satify_any": { + "$ref": "#/definitions/satify_any" + }, "items": { "type": "array", - "minItems": 1, + "minItems": 0, "items": { "type": "object", "additionalProperties": false, @@ -141,6 +186,22 @@ } } } + }, + "clients": { + "type": "array", + "minItems": 0, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "address": { + "$ref": "#/definitions/address" + }, + "directive": { + "$ref": "#/definitions/directive" + } + } + } } } }, diff --git a/backend/templates/proxy_host.conf b/backend/templates/proxy_host.conf index 6448dca..0da4bed 100644 --- a/backend/templates/proxy_host.conf +++ b/backend/templates/proxy_host.conf @@ -21,11 +21,21 @@ server { {% if use_default_location %} location / { - {%- if access_list_id > 0 -%} - # Access List + + {% if access_list_id > 0 %} + # Authorization auth_basic "Authorization required"; auth_basic_user_file /data/access/{{ access_list_id }}; - {%- endif %} + + # Access Rules + {% for client in access_list.clients %} + {{- client.rule -}}; + {% endfor %}deny all; + + # Access checks must... + {{ access_list.satisfy }}; + + {% endif %} {% include "_forced_ssl.conf" %} {% include "_hsts.conf" %} diff --git a/frontend/js/app/nginx/access/form.ejs b/frontend/js/app/nginx/access/form.ejs index a85e396..3f127cc 100644 --- a/frontend/js/app/nginx/access/form.ejs +++ b/frontend/js/app/nginx/access/form.ejs @@ -3,28 +3,74 @@

-