Merge pull request #360 from Indemnity83/ip-access-control

Client Access Lists
This commit is contained in:
jc21 2020-04-15 08:29:40 +10:00 committed by GitHub
commit a9f068daa8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 440 additions and 72 deletions

View File

@ -1,14 +1,15 @@
const _ = require('lodash'); const _ = require('lodash');
const fs = require('fs'); const fs = require('fs');
const batchflow = require('batchflow'); const batchflow = require('batchflow');
const logger = require('../logger').access; const logger = require('../logger').access;
const error = require('../lib/error'); const error = require('../lib/error');
const accessListModel = require('../models/access_list'); const accessListModel = require('../models/access_list');
const accessListAuthModel = require('../models/access_list_auth'); const accessListAuthModel = require('../models/access_list_auth');
const proxyHostModel = require('../models/proxy_host'); const accessListClientModel = require('../models/access_list_client');
const internalAuditLog = require('./audit-log'); const proxyHostModel = require('../models/proxy_host');
const internalNginx = require('./nginx'); const internalAuditLog = require('./audit-log');
const utils = require('../lib/utils'); const internalNginx = require('./nginx');
const utils = require('../lib/utils');
function omissions () { function omissions () {
return ['is_deleted']; return ['is_deleted'];
@ -29,14 +30,16 @@ const internalAccessList = {
.omit(omissions()) .omit(omissions())
.insertAndFetch({ .insertAndFetch({
name: data.name, name: data.name,
satify_any: data.satify_any,
owner_user_id: access.token.getUserId(1) owner_user_id: access.token.getUserId(1)
}); });
}) })
.then((row) => { .then((row) => {
data.id = row.id; data.id = row.id;
// Now add the items
let promises = []; let promises = [];
// Now add the items
data.items.map((item) => { data.items.map((item) => {
promises.push(accessListAuthModel promises.push(accessListAuthModel
.query() .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); return Promise.all(promises);
}) })
.then(() => { .then(() => {
// re-fetch with expansions // re-fetch with expansions
return internalAccessList.get(access, { return internalAccessList.get(access, {
id: data.id, id: data.id,
expand: ['owner', 'items'] expand: ['owner', 'items', 'clients', 'proxy_hosts.access_list.clients']
}, true /* <- skip masking */); }, true /* <- skip masking */);
}) })
.then((row) => { .then((row) => {
@ -64,7 +81,7 @@ const internalAccessList = {
return internalAccessList.build(row) return internalAccessList.build(row)
.then(() => { .then(() => {
if (row.proxy_host_count) { if (row.proxy_host_count) {
return internalNginx.reload(); return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
} }
}) })
.then(() => { .then(() => {
@ -109,7 +126,8 @@ const internalAccessList = {
.query() .query()
.where({id: data.id}) .where({id: data.id})
.patch({ .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(() => { .then(() => {
// Add to audit log // Add to audit log
return internalAuditLog.add(access, { return internalAuditLog.add(access, {
@ -166,14 +216,14 @@ const internalAccessList = {
// re-fetch with expansions // re-fetch with expansions
return internalAccessList.get(access, { return internalAccessList.get(access, {
id: data.id, id: data.id,
expand: ['owner', 'items'] expand: ['owner', 'items', 'clients', 'proxy_hosts.access_list.clients']
}, true /* <- skip masking */); }, true /* <- skip masking */);
}) })
.then((row) => { .then((row) => {
return internalAccessList.build(row) return internalAccessList.build(row)
.then(() => { .then(() => {
if (row.proxy_host_count) { if (row.proxy_host_count) {
return internalNginx.reload(); return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
} }
}) })
.then(() => { .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') .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) .where('access_list.is_deleted', 0)
.andWhere('access_list.id', data.id) .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']) .omit(['access_list.is_deleted'])
.first(); .first();
@ -246,7 +296,7 @@ const internalAccessList = {
delete: (access, data) => { delete: (access, data) => {
return access.can('access_lists:delete', data.id) return access.can('access_lists:delete', data.id)
.then(() => { .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) => { .then((row) => {
if (!row) { if (!row) {
@ -330,7 +380,7 @@ const internalAccessList = {
.where('access_list.is_deleted', 0) .where('access_list.is_deleted', 0)
.groupBy('access_list.id') .groupBy('access_list.id')
.omit(['access_list.is_deleted']) .omit(['access_list.is_deleted'])
.allowEager('[owner,items]') .allowEager('[owner,items,clients]')
.orderBy('access_list.name', 'ASC'); .orderBy('access_list.name', 'ASC');
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {

View File

@ -73,7 +73,7 @@ const internalProxyHost = {
// re-fetch with cert // re-fetch with cert
return internalProxyHost.get(access, { return internalProxyHost.get(access, {
id: row.id, id: row.id,
expand: ['certificate', 'owner', 'access_list'] expand: ['certificate', 'owner', 'access_list.clients']
}); });
}) })
.then((row) => { .then((row) => {
@ -186,7 +186,7 @@ const internalProxyHost = {
.then(() => { .then(() => {
return internalProxyHost.get(access, { return internalProxyHost.get(access, {
id: data.id, id: data.id,
expand: ['owner', 'certificate', 'access_list'] expand: ['owner', 'certificate', 'access_list.clients']
}) })
.then((row) => { .then((row) => {
// Configure nginx // Configure nginx
@ -219,7 +219,7 @@ const internalProxyHost = {
.query() .query()
.where('is_deleted', 0) .where('is_deleted', 0)
.andWhere('id', data.id) .andWhere('id', data.id)
.allowEager('[owner,access_list,certificate]') .allowEager('[owner,access_list,access_list.clients,certificate]')
.first(); .first();
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {

View 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');
});
};

View File

@ -1,10 +1,11 @@
// Objection Docs: // Objection Docs:
// http://vincit.github.io/objection.js/ // http://vincit.github.io/objection.js/
const db = require('../db'); const db = require('../db');
const Model = require('objection').Model; const Model = require('objection').Model;
const User = require('./user'); const User = require('./user');
const AccessListAuth = require('./access_list_auth'); const AccessListAuth = require('./access_list_auth');
const AccessListClient = require('./access_list_client');
Model.knex(db); Model.knex(db);
@ -62,6 +63,17 @@ class AccessList extends Model {
qb.omit(['id', 'created_on', 'modified_on', 'access_list_id', 'meta']); 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: { proxy_hosts: {
relation: Model.HasManyRelation, relation: Model.HasManyRelation,
modelClass: ProxyHost, modelClass: ProxyHost,
@ -76,6 +88,10 @@ class AccessList extends Model {
} }
}; };
} }
get satisfy() {
return this.satify_any ? 'satisfy any' : 'satisfy all';
}
} }
module.exports = AccessList; module.exports = AccessList;

View 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;

View File

@ -19,6 +19,29 @@
"type": "string", "type": "string",
"description": "Name of the Access List" "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": { "meta": {
"type": "object" "type": "object"
} }
@ -78,9 +101,12 @@
"name": { "name": {
"$ref": "#/definitions/name" "$ref": "#/definitions/name"
}, },
"satify_any": {
"$ref": "#/definitions/satify_any"
},
"items": { "items": {
"type": "array", "type": "array",
"minItems": 1, "minItems": 0,
"items": { "items": {
"type": "object", "type": "object",
"additionalProperties": false, "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": { "meta": {
"$ref": "#/definitions/meta" "$ref": "#/definitions/meta"
} }
@ -124,9 +166,12 @@
"name": { "name": {
"$ref": "#/definitions/name" "$ref": "#/definitions/name"
}, },
"satify_any": {
"$ref": "#/definitions/satify_any"
},
"items": { "items": {
"type": "array", "type": "array",
"minItems": 1, "minItems": 0,
"items": { "items": {
"type": "object", "type": "object",
"additionalProperties": false, "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"
}
}
}
} }
} }
}, },

View File

@ -21,11 +21,21 @@ server {
{% if use_default_location %} {% if use_default_location %}
location / { location / {
{%- if access_list_id > 0 -%}
# Access List {% if access_list_id > 0 %}
# Authorization
auth_basic "Authorization required"; auth_basic "Authorization required";
auth_basic_user_file /data/access/{{ access_list_id }}; 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 "_forced_ssl.conf" %}
{% include "_hsts.conf" %} {% include "_hsts.conf" %}

View File

@ -3,28 +3,74 @@
<h5 class="modal-title"><%- i18n('access-lists', 'form-title', {id: id}) %></h5> <h5 class="modal-title"><%- i18n('access-lists', 'form-title', {id: id}) %></h5>
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button> <button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body has-tabs">
<form> <form>
<div class="row"> <ul class="nav nav-tabs" role="tablist">
<div class="col-sm-12 col-md-12"> <li role="presentation" class="nav-item"><a href="#details" aria-controls="tab1" role="tab" data-toggle="tab" class="nav-link active show" aria-selected="true"><i class="fe fe-zap"></i> <%- i18n('access-lists', 'details') %></a></li>
<div class="form-group"> <li role="presentation" class="nav-item"><a href="#auth" aria-controls="tab4" role="tab" data-toggle="tab" class="nav-link" aria-selected="false"><i class="fe fe-users"></i> <%- i18n('access-lists', 'authorization') %></a></li>
<label class="form-label"><%- i18n('str', 'name') %> <span class="form-required">*</span></label> <li role="presentation" class="nav-item"><a href="#access" aria-controls="tab2" role="tab" data-toggle="tab" class="nav-link" aria-selected="false"><i class="fe fe-radio"></i> <%- i18n('access-lists', 'access') %></a></li>
<input type="text" name="name" class="form-control" value="<%- name %>" required> </ul>
</div>
</div>
<div class="col-sm-6 col-md-6">
<div class="form-group">
<label class="form-label"><%- i18n('str', 'username') %></label>
</div>
</div>
<div class="col-sm-6 col-md-6">
<div class="form-group">
<label class="form-label"><%- i18n('str', 'password') %></label>
</div>
</div>
</div>
<div class="items"><!-- items --></div> <div class="tab-content">
<!-- Details -->
<div role="tabpanel" class="tab-pane active show" id="details">
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="form-group">
<label class="form-label"><%- i18n('str', 'name') %> <span class="form-required">*</span></label>
<input type="text" name="name" class="form-control" value="<%- name %>" required>
</div>
</div>
<div class="col-sm-6 col-md-6">
<div class="form-group">
<label class="custom-switch">
<input type="checkbox" class="custom-switch-input" name="satify_any" value="1"<%- typeof satify_any !== 'undefined' && satify_any ? ' checked' : '' %>>
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description"><%- i18n('access-lists', 'satisfy-any') %></span>
</label>
</div>
</div>
</div>
</div>
<!-- Authorization -->
<div class="tab-pane" id="auth">
<div class="row">
<div class="col-sm-6 col-md-6">
<div class="form-group">
<label class="form-label"><%- i18n('str', 'username') %></label>
</div>
</div>
<div class="col-sm-6 col-md-6">
<div class="form-group">
<label class="form-label"><%- i18n('str', 'password') %></label>
</div>
</div>
</div>
<div class="items"><!-- items --></div>
</div>
<!-- Access -->
<div class="tab-pane" id="access">
<div class="clients"><!-- clients --></div>
<div class="row">
<div class="col-sm-3 col-md-3">
<div class="form-group">
<input type="text" class="form-control disabled" value="deny" disabled>
</div>
</div>
<div class="col-sm-9 col-md-9">
<div class="form-group">
<input type="text" class="form-control disabled" value="all" disabled>
</div>
</div>
</div>
<div class="text-muted">Note that the <code>allow</code> and <code>deny</code> directives will be applied in the order they are defined.</div>
</div>
</div>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">

View File

@ -3,6 +3,7 @@ const App = require('../../main');
const AccessListModel = require('../../../models/access-list'); const AccessListModel = require('../../../models/access-list');
const template = require('./form.ejs'); const template = require('./form.ejs');
const ItemView = require('./form/item'); const ItemView = require('./form/item');
const ClientView = require('./form/client');
require('jquery-serializejson'); require('jquery-serializejson');
@ -10,20 +11,26 @@ const ItemsView = Mn.CollectionView.extend({
childView: ItemView childView: ItemView
}); });
const ClientsView = Mn.CollectionView.extend({
childView: ClientView
});
module.exports = Mn.View.extend({ module.exports = Mn.View.extend({
template: template, template: template,
className: 'modal-dialog', className: 'modal-dialog',
ui: { ui: {
items_region: '.items', items_region: '.items',
form: 'form', clients_region: '.clients',
buttons: '.modal-footer button', form: 'form',
cancel: 'button.cancel', buttons: '.modal-footer button',
save: 'button.save' cancel: 'button.cancel',
save: 'button.save'
}, },
regions: { regions: {
items_region: '@ui.items_region' items_region: '@ui.items_region',
clients_region: '@ui.clients_region'
}, },
events: { events: {
@ -35,9 +42,10 @@ module.exports = Mn.View.extend({
return; return;
} }
let view = this; let view = this;
let form_data = this.ui.form.serializeJSON(); let form_data = this.ui.form.serializeJSON();
let items_data = []; let items_data = [];
let clients_data = [];
form_data.username.map(function (val, idx) { form_data.username.map(function (val, idx) {
if (val.trim().length) { if (val.trim().length) {
@ -48,16 +56,29 @@ module.exports = Mn.View.extend({
} }
}); });
if (!items_data.length) { form_data.address.map(function (val, idx) {
alert('You must specify at least 1 Username and Password combination'); if (val.trim().length) {
clients_data.push({
address: val.trim(),
directive: form_data.directive[idx]
})
}
});
if (!items_data.length && !clients_data.length) {
alert('You must specify at least 1 Authorization or Access rule');
return; return;
} }
let data = { let data = {
name: form_data.name, name: form_data.name,
items: items_data satify_any: !!form_data.satify_any,
items: items_data,
clients: clients_data
}; };
console.log(data);
let method = App.Api.Nginx.AccessLists.create; let method = App.Api.Nginx.AccessLists.create;
let is_new = true; let is_new = true;
@ -88,6 +109,7 @@ module.exports = Mn.View.extend({
onRender: function () { onRender: function () {
let items = this.model.get('items'); let items = this.model.get('items');
let clients = this.model.get('clients');
// Add empty items to the end of the list. This is cheating but hey I don't have the time to do it right // Add empty items to the end of the list. This is cheating but hey I don't have the time to do it right
let items_to_add = 5 - items.length; let items_to_add = 5 - items.length;
@ -97,9 +119,20 @@ module.exports = Mn.View.extend({
} }
} }
let clients_to_add = 4 - clients.length;
if (clients_to_add) {
for (let i = 0; i < clients_to_add; i++) {
clients.push({});
}
}
this.showChildView('items_region', new ItemsView({ this.showChildView('items_region', new ItemsView({
collection: new Backbone.Collection(items) collection: new Backbone.Collection(items)
})); }));
this.showChildView('clients_region', new ClientsView({
collection: new Backbone.Collection(clients)
}));
}, },
initialize: function (options) { initialize: function (options) {

View File

@ -0,0 +1,13 @@
<div class="col-sm-3 col-md-3">
<div class="form-group">
<select name="directive[]" class="form-control custom-select" placeholder="http">
<option value="allow" <%- typeof directive == 'undefined' || directive === 'allow' ? 'selected' : '' %>>allow</option>
<option value="deny" <%- typeof directive !== 'undefined' && directive === 'deny' ? 'selected' : '' %>>deny</option>
</select>
</div>
</div>
<div class="col-sm-9 col-md-9">
<div class="form-group">
<input type="text" name="address[]" class="form-control" value="<%- typeof address !== 'undefined' ? address : '' %>" value="">
</div>
</div>

View File

@ -0,0 +1,7 @@
const Mn = require('backbone.marionette');
const template = require('./client.ejs');
module.exports = Mn.View.extend({
template: template,
className: 'row'
});

View File

@ -14,6 +14,16 @@
<td> <td>
<%- i18n('access-lists', 'item-count', {count: items.length || 0}) %> <%- i18n('access-lists', 'item-count', {count: items.length || 0}) %>
</td> </td>
<td>
<%- i18n('access-lists', 'client-count', {count: clients.length || 0}) %>
</td>
<td>
<% if (satify_any) { %>
<%- i18n('str', 'any') %>
<%} else { %>
<%- i18n('str', 'all') %>
<% } %>
</td>
<td> <td>
<%- i18n('access-lists', 'proxy-host-count', {count: proxy_host_count}) %> <%- i18n('access-lists', 'proxy-host-count', {count: proxy_host_count}) %>
</td> </td>

View File

@ -1,7 +1,9 @@
<thead> <thead>
<th width="30">&nbsp;</th> <th width="30">&nbsp;</th>
<th><%- i18n('str', 'name') %></th> <th><%- i18n('str', 'name') %></th>
<th><%- i18n('users', 'title') %></th> <th><%- i18n('access-lists', 'authorization') %></th>
<th><%- i18n('access-lists', 'access') %></th>
<th><%- i18n('access-lists', 'satisfy') %></th>
<th><%- i18n('proxy-hosts', 'title') %></th> <th><%- i18n('proxy-hosts', 'title') %></th>
<% if (canManage) { %> <% if (canManage) { %>
<th>&nbsp;</th> <th>&nbsp;</th>

View File

@ -40,7 +40,7 @@ module.exports = Mn.View.extend({
onRender: function () { onRender: function () {
let view = this; let view = this;
App.Api.Nginx.AccessLists.getAll(['owner', 'items']) App.Api.Nginx.AccessLists.getAll(['owner', 'items', 'clients'])
.then(response => { .then(response => {
if (!view.isDestroyed()) { if (!view.isDestroyed()) {
if (response && response.length) { if (response && response.length) {

View File

@ -3,7 +3,7 @@
<div class="title"> <div class="title">
<i class="fe fe-lock text-teal"></i> <%- name %> <i class="fe fe-lock text-teal"></i> <%- name %>
</div> </div>
<span class="description"><%- i18n('access-lists', 'item-count', {count: items.length || 0}) %> &ndash; Created: <%- formatDbDate(created_on, 'Do MMMM YYYY, h:mm a') %></span> <span class="description"><%- i18n('access-lists', 'item-count', {count: items.length || 0}) %>, <%- i18n('access-lists', 'client-count', {count: clients.length || 0}) %> &ndash; Created: <%- formatDbDate(created_on, 'Do MMMM YYYY, h:mm a') %></span>
<% } else { %> <% } else { %>
<div class="title"> <div class="title">
<i class="fe fe-unlock text-yellow"></i> <%- i18n('access-lists', 'public') %> <i class="fe fe-unlock text-yellow"></i> <%- i18n('access-lists', 'public') %>

View File

@ -222,7 +222,7 @@ module.exports = Mn.View.extend({
} }
}, },
load: function (query, callback) { load: function (query, callback) {
App.Api.Nginx.AccessLists.getAll(['items']) App.Api.Nginx.AccessLists.getAll(['items', 'clients'])
.then(rows => { .then(rows => {
callback(rows); callback(rows);
}) })

View File

@ -33,7 +33,9 @@
"unknown": "Unknown", "unknown": "Unknown",
"expires": "Expires", "expires": "Expires",
"value": "Value", "value": "Value",
"please-wait": "Please wait..." "please-wait": "Please wait...",
"all": "All",
"any": "Any"
}, },
"login": { "login": {
"title": "Login to your account" "title": "Login to your account"
@ -184,10 +186,16 @@
"public": "Publicly Accessible", "public": "Publicly Accessible",
"public-sub": "No Access Restrictions", "public-sub": "No Access Restrictions",
"help-title": "What is an Access List?", "help-title": "What is an Access List?",
"help-content": "Access Lists provide authentication for the Proxy Hosts via Basic HTTP Authentication.\nYou can configure multiple usernames and passwords for a single Access List and then apply that to a Proxy Host.\nThis is most useful for forwarded web services that do not have authentication mechanisms built in.", "help-content": "Access Lists provide a blacklist or whitelist of specific client IP addresses along with authentication for the Proxy Hosts via Basic HTTP Authentication.\nYou can configure multiple client rules, usernames and passwords for a single Access List and then apply that to a Proxy Host.\nThis is most useful for forwarded web services that do not have authentication mechanisms built in or that you want to protect from access by unknown clients.",
"item-count": "{count} {count, select, 1{User} other{Users}}", "item-count": "{count} {count, select, 1{User} other{Users}}",
"client-count": "{count} {count, select, 1{Rule} other{Rules}}",
"proxy-host-count": "{count} {count, select, 1{Proxy Host} other{Proxy Hosts}}", "proxy-host-count": "{count} {count, select, 1{Proxy Host} other{Proxy Hosts}}",
"delete-has-hosts": "This Access List is associated with {count} Proxy Hosts. They will become publicly available upon deletion." "delete-has-hosts": "This Access List is associated with {count} Proxy Hosts. They will become publicly available upon deletion.",
"details": "Details",
"authorization": "Authorization",
"access": "Access",
"satisfy": "Satisfy",
"satisfy-any": "Satify Any"
}, },
"users": { "users": {
"title": "Users", "title": "Users",

View File

@ -10,6 +10,7 @@ const model = Backbone.Model.extend({
modified_on: null, modified_on: null,
name: '', name: '',
items: [], items: [],
clients: [],
// The following are expansions: // The following are expansions:
owner: null owner: null
}; };