Skip to content

Commit a132763

Browse files
committed
Add support for adding Client Certificates to access-lists
Client certificate support is added as a new separate type of option for access-lists. This commit is the support code to enable access-lists to contain Client Certificate references.
1 parent d5b3e53 commit a132763

File tree

16 files changed

+369
-36
lines changed

16 files changed

+369
-36
lines changed

backend/internal/access-list.js

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
const _ = require('lodash');
2-
const fs = require('fs');
3-
const batchflow = require('batchflow');
4-
const logger = require('../logger').access;
5-
const error = require('../lib/error');
6-
const utils = require('../lib/utils');
7-
const accessListModel = require('../models/access_list');
8-
const accessListAuthModel = require('../models/access_list_auth');
9-
const accessListClientModel = require('../models/access_list_client');
10-
const proxyHostModel = require('../models/proxy_host');
11-
const internalAuditLog = require('./audit-log');
12-
const internalNginx = require('./nginx');
1+
const _ = require('lodash');
2+
const fs = require('fs');
3+
const batchflow = require('batchflow');
4+
const logger = require('../logger').access;
5+
const error = require('../lib/error');
6+
const utils = require('../lib/utils');
7+
const accessListModel = require('../models/access_list');
8+
const accessListAuthModel = require('../models/access_list_auth');
9+
const accessListClientModel = require('../models/access_list_client');
10+
const accessListClientCAsModel = require('../models/access_list_clientcas');
11+
const proxyHostModel = require('../models/proxy_host');
12+
const internalAuditLog = require('./audit-log');
13+
const internalNginx = require('./nginx');
1314

1415
function omissions () {
1516
return ['is_deleted'];
@@ -66,13 +67,26 @@ const internalAccessList = {
6667
});
6768
}
6869

70+
// Now add the client certificate references
71+
if (typeof data.clientcas !== 'undefined' && data.clientcas) {
72+
data.clientcas.map((certificate_id) => {
73+
promises.push(accessListClientCAsModel
74+
.query()
75+
.insert({
76+
access_list_id: row.id,
77+
certificate_id: certificate_id
78+
})
79+
);
80+
});
81+
}
82+
6983
return Promise.all(promises);
7084
})
7185
.then(() => {
7286
// re-fetch with expansions
7387
return internalAccessList.get(access, {
7488
id: data.id,
75-
expand: ['owner', 'items', 'clients', 'proxy_hosts.access_list.[clients,items]']
89+
expand: ['owner', 'items', 'clients', 'clientcas', 'proxy_hosts.access_list.[clientcas.certificate,clients,items]']
7690
}, true /* <- skip masking */);
7791
})
7892
.then((row) => {
@@ -204,6 +218,35 @@ const internalAccessList = {
204218
});
205219
}
206220
})
221+
.then(() => {
222+
// Check for client certificates and add/update/remove them
223+
if (typeof data.clientcas !== 'undefined' && data.clientcas) {
224+
let promises = [];
225+
226+
data.clientcas.map(function (certificate_id) {
227+
promises.push(accessListClientCAsModel
228+
.query()
229+
.insert({
230+
access_list_id: data.id,
231+
certificate_id: certificate_id
232+
})
233+
);
234+
});
235+
236+
let query = accessListClientCAsModel
237+
.query()
238+
.delete()
239+
.where('access_list_id', data.id);
240+
241+
return query
242+
.then(() => {
243+
// Add new items
244+
if (promises.length) {
245+
return Promise.all(promises);
246+
}
247+
});
248+
}
249+
})
207250
.then(internalNginx.reload)
208251
.then(() => {
209252
// Add to audit log
@@ -218,7 +261,7 @@ const internalAccessList = {
218261
// re-fetch with expansions
219262
return internalAccessList.get(access, {
220263
id: data.id,
221-
expand: ['owner', 'items', 'clients', 'proxy_hosts.[certificate,access_list.[clients,items]]']
264+
expand: ['owner', 'items', 'clients', 'clientcas', 'proxy_hosts.[certificate,access_list.[clientcas.certificate,clients,items]]']
222265
}, true /* <- skip masking */);
223266
})
224267
.then((row) => {
@@ -256,7 +299,7 @@ const internalAccessList = {
256299
.joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0')
257300
.where('access_list.is_deleted', 0)
258301
.andWhere('access_list.id', data.id)
259-
.allowGraph('[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]')
302+
.withGraphFetched('[owner,items,clients,clientcas,proxy_hosts.[certificate,access_list.[clientcas.certificate,clients,items]]]')
260303
.first();
261304

262305
if (access_data.permission_visibility !== 'all') {
@@ -294,7 +337,7 @@ const internalAccessList = {
294337
delete: (access, data) => {
295338
return access.can('access_lists:delete', data.id)
296339
.then(() => {
297-
return internalAccessList.get(access, {id: data.id, expand: ['proxy_hosts', 'items', 'clients']});
340+
return internalAccessList.get(access, {id: data.id, expand: ['proxy_hosts', 'items', 'clients', 'clientcas']});
298341
})
299342
.then((row) => {
300343
if (!row) {
@@ -377,7 +420,7 @@ const internalAccessList = {
377420
.joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0')
378421
.where('access_list.is_deleted', 0)
379422
.groupBy('access_list.id')
380-
.allowGraph('[owner,items,clients]')
423+
.withGraphFetched('[owner,items,clients,clientcas.certificate]')
381424
.orderBy('access_list.name', 'ASC');
382425

383426
if (access_data.permission_visibility !== 'all') {
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
const migrate_name = 'identifier_for_migrate';
2+
const logger = require('../logger').migrate;
3+
4+
/**
5+
* Migrate
6+
*
7+
* @see http://knexjs.org/#Schema
8+
*
9+
* @param {Object} knex
10+
* @param {Promise} Promise
11+
* @returns {Promise}
12+
*/
13+
exports.up = function (knex/*, Promise*/) {
14+
15+
logger.info('[' + migrate_name + '] Migrating Up...');
16+
17+
return knex.schema.createTable('access_list_clientcas', (table) => {
18+
table.increments().primary();
19+
table.dateTime('created_on').notNull();
20+
table.dateTime('modified_on').notNull();
21+
table.integer('access_list_id').notNull().unsigned();
22+
table.integer('certificate_id').notNull().unsigned();
23+
table.json('meta').notNull();
24+
})
25+
.then(function () {
26+
logger.info('[' + migrate_name + '] access_list_clientcas Table created');
27+
})
28+
.then(() => {
29+
logger.info('[' + migrate_name + '] Migrating Up Complete');
30+
});
31+
};
32+
33+
/**
34+
* Undo Migrate
35+
*
36+
* @param {Object} knex
37+
* @param {Promise} Promise
38+
* @returns {Promise}
39+
*/
40+
exports.down = function (knex/*, Promise*/) {
41+
logger.info('[' + migrate_name + '] Migrating Down...');
42+
43+
return knex.schema.dropTable('access_list_clientcas')
44+
.then(() => {
45+
logger.info('[' + migrate_name + '] access_list_clientcas Table dropped');
46+
})
47+
.then(() => {
48+
logger.info('[' + migrate_name + '] Migrating Down Complete');
49+
});
50+
};

backend/models/access_list.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const Model = require('objection').Model;
66
const User = require('./user');
77
const AccessListAuth = require('./access_list_auth');
88
const AccessListClient = require('./access_list_client');
9+
const AccessListClientCAs = require('./access_list_clientcas');
910
const now = require('./now_helper');
1011

1112
Model.knex(db);
@@ -68,6 +69,14 @@ class AccessList extends Model {
6869
to: 'access_list_client.access_list_id'
6970
}
7071
},
72+
clientcas: {
73+
relation: Model.HasManyRelation,
74+
modelClass: AccessListClientCAs,
75+
join: {
76+
from: 'access_list.id',
77+
to: 'access_list_clientcas.access_list_id'
78+
}
79+
},
7180
proxy_hosts: {
7281
relation: Model.HasManyRelation,
7382
modelClass: ProxyHost,
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Objection Docs:
2+
// http://vincit.github.io/objection.js/
3+
4+
const db = require('../db');
5+
const Model = require('objection').Model;
6+
const now = require('./now_helper');
7+
8+
Model.knex(db);
9+
10+
class AccessListClientCAs extends Model {
11+
$beforeInsert () {
12+
this.created_on = now();
13+
this.modified_on = now();
14+
15+
// Default for meta
16+
if (typeof this.meta === 'undefined') {
17+
this.meta = {};
18+
}
19+
}
20+
21+
$beforeUpdate () {
22+
this.modified_on = now();
23+
}
24+
25+
static get name () {
26+
return 'AccessListClientCAs';
27+
}
28+
29+
static get tableName () {
30+
return 'access_list_clientcas';
31+
}
32+
33+
static get jsonAttributes () {
34+
return ['meta'];
35+
}
36+
37+
static get relationMappings () {
38+
return {
39+
access_list: {
40+
relation: Model.HasOneRelation,
41+
modelClass: require('./access_list'),
42+
join: {
43+
from: 'access_list_clientcas.access_list_id',
44+
to: 'access_list.id'
45+
},
46+
modify: function (qb) {
47+
qb.where('access_list.is_deleted', 0);
48+
}
49+
},
50+
certificate: {
51+
relation: Model.HasOneRelation,
52+
modelClass: require('./certificate'),
53+
join: {
54+
from: 'access_list_clientcas.certificate_id',
55+
to: 'certificate.id'
56+
},
57+
modify: function (qb) {
58+
qb.where('certificate.is_deleted', 0);
59+
}
60+
}
61+
};
62+
}
63+
}
64+
65+
module.exports = AccessListClientCAs;

backend/schema/endpoints/access-lists.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,13 @@
142142
}
143143
}
144144
},
145+
"clientcas": {
146+
"type": "array",
147+
"minItems": 0,
148+
"items": {
149+
"type": "integer"
150+
}
151+
},
145152
"meta": {
146153
"$ref": "#/definitions/meta"
147154
}
@@ -209,6 +216,13 @@
209216
}
210217
}
211218
}
219+
},
220+
"clientcas": {
221+
"type": "array",
222+
"minItems": 0,
223+
"items": {
224+
"type": "integer"
225+
}
212226
}
213227
}
214228
},

frontend/js/app/nginx/access/form.ejs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<ul class="nav nav-tabs" role="tablist">
99
<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>
1010
<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>
11+
<li role="presentation" class="nav-item"><a href="#clientca" aria-controls="tab4" role="tab" data-toggle="tab" class="nav-link" aria-selected="false"><i class="fe fe-lock"></i> <%- i18n('access-lists', 'client-certificates') %></a></li>
1112
<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>
1213
</ul>
1314

@@ -71,6 +72,34 @@
7172
</div>
7273
</div>
7374

75+
<!-- Client Certificates -->
76+
<div class="tab-pane" id="clientca">
77+
<p>
78+
Client Certificate Authorization via
79+
<a target="_blank" href="http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_client_certificate">
80+
Nginx HTTP SSL
81+
</a>
82+
</p>
83+
84+
<div class="row">
85+
<div class="col-sm-10 col-md-10">
86+
<select id="certificate_search" class="form-control custom-select" placeholder="<%- i18n('ssl', 'clientca') %>">
87+
</select>
88+
89+
</div>
90+
<div class="col-sm-2 col-md-2">
91+
<div class="btn-list justify-content-end">
92+
<button type="button" class="btn btn-teal clientca_add"><%- i18n('access-lists', 'clientca-add') %></button>
93+
</div>
94+
</div>
95+
</div>
96+
97+
<label class="form-label">Authorized Client Certificate Authorities</label>
98+
<div class="clientcas">
99+
<!-- clientcas -->
100+
</div>
101+
</div>
102+
74103
<!-- Access -->
75104
<div class="tab-pane" id="access">
76105
<p>

0 commit comments

Comments
 (0)