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 @@