Merge pull request #360 from Indemnity83/ip-access-control
Client Access Lists
This commit is contained in:
@ -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') {
|
||||
|
@ -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') {
|
||||
|
53
backend/migrations/20200410143839_access_list_client.js
Normal file
53
backend/migrations/20200410143839_access_list_client.js
Normal file
@ -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');
|
||||
});
|
||||
};
|
@ -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;
|
||||
|
58
backend/models/access_list_client.js
Normal file
58
backend/models/access_list_client.js
Normal file
@ -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;
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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" %}
|
||||
|
Reference in New Issue
Block a user