Host modal: Authentication tab, wildcard-child default, allow-list autocomplete
- Default the "Parent Wildcard" challenge type when a wildcard parent exists. - Split Authentication (basic auth + SSO) into its own tab; Access keeps IP allow/deny. - Add GET /api/host/auth-suggestions (authenticated host editors, not just admins) and datalist-backed "type to search + Add" pickers for the SSO allowed-users/groups lists. Verified in a browser (tab present, datalist populated, picker appends deduped). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+29
-1
@@ -1,7 +1,10 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const router = require('express').Router();
|
const router = require('express').Router();
|
||||||
const {Host, Domain} = require('../models').models;
|
const conf = require('@simpleworkjs/conf');
|
||||||
|
const {Host, Domain, User} = require('../models').models;
|
||||||
|
const {LocalGroup} = require('../models/local_group');
|
||||||
|
const {Permission} = require('../models/permission');
|
||||||
const authz = require('../middleware/authz');
|
const authz = require('../middleware/authz');
|
||||||
const {normalizeHostFeatures} = require('../utils/host_features');
|
const {normalizeHostFeatures} = require('../utils/host_features');
|
||||||
const {collectHostFieldErrors} = require('../utils/hostname_validate');
|
const {collectHostFieldErrors} = require('../utils/hostname_validate');
|
||||||
@@ -25,6 +28,31 @@ function hashHostSecrets(body){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Autocomplete source for the per-host auth allow-lists (SSO users/groups).
|
||||||
|
// Available to any authenticated host editor (not just global admins). Groups
|
||||||
|
// are derived from local groups, existing permission group-subjects, and the
|
||||||
|
// conf.auth admin/role-map groups.
|
||||||
|
router.get('/auth-suggestions', async function(req, res, next){
|
||||||
|
try{
|
||||||
|
let users = [];
|
||||||
|
try{ users = (await User.list()) || []; }catch(error){ /* none */ }
|
||||||
|
|
||||||
|
let groups = new Set();
|
||||||
|
try{ for(let g of await LocalGroup.list()) groups.add(g); }catch(error){ /* none */ }
|
||||||
|
try{
|
||||||
|
for(let p of await Permission.listDetail()){
|
||||||
|
if(p.subjectType === 'group' && p.subject) groups.add(p.subject);
|
||||||
|
}
|
||||||
|
}catch(error){ /* none */ }
|
||||||
|
for(let g of (conf.auth && conf.auth.adminGroups) || []) groups.add(g);
|
||||||
|
for(let g of Object.keys((conf.auth && conf.auth.groupRoleMap) || {})) groups.add(g);
|
||||||
|
|
||||||
|
return res.json({users, groups: [...groups].sort()});
|
||||||
|
}catch(error){
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/', async function(req, res, next){
|
router.get('/', async function(req, res, next){
|
||||||
try{
|
try{
|
||||||
let results = await Model[req.query.detail ? "listDetail" : "list"]();
|
let results = await Model[req.query.detail ? "listDetail" : "list"]();
|
||||||
|
|||||||
+56
-10
@@ -98,6 +98,29 @@
|
|||||||
bootstrap.Tab.getOrCreateInstance(document.getElementById(id)).show();
|
bootstrap.Tab.getOrCreateInstance(document.getElementById(id)).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Append a picked/typed value to one of the SSO allow-list textareas (deduped).
|
||||||
|
function allowListAdd(input, name){
|
||||||
|
let val = (input.value || '').trim();
|
||||||
|
if(!val) return;
|
||||||
|
let $ta = $('#hostForm textarea[name="' + name + '"]');
|
||||||
|
let lines = ($ta.val() || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
||||||
|
if(lines.indexOf(val) === -1) lines.push(val);
|
||||||
|
$ta.val(lines.join('\n'));
|
||||||
|
input.value = '';
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill the user/group datalists that back the allow-list autocomplete.
|
||||||
|
function hostLoadAuthSuggestions(){
|
||||||
|
app.api.get('host/auth-suggestions', function(error, data){
|
||||||
|
if(error || !data) return;
|
||||||
|
let $u = $('#hostSsoUsers').empty();
|
||||||
|
for(let u of (data.users || [])) $u.append($('<option>').val(u));
|
||||||
|
let $g = $('#hostSsoGroups').empty();
|
||||||
|
for(let g of (data.groups || [])) $g.append($('<option>').val(g));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Return the form to a clean "add" state.
|
// Return the form to a clean "add" state.
|
||||||
function hostFormReset(){
|
function hostFormReset(){
|
||||||
let form = document.getElementById('hostForm');
|
let form = document.getElementById('hostForm');
|
||||||
@@ -215,6 +238,7 @@
|
|||||||
$(document).ready(function(){
|
$(document).ready(function(){
|
||||||
// Populate the host UI table
|
// Populate the host UI table
|
||||||
hostPopulate();
|
hostPopulate();
|
||||||
|
hostLoadAuthSuggestions();
|
||||||
|
|
||||||
// Determine what Let's Encrypt challenge type the given host name can use.
|
// Determine what Let's Encrypt challenge type the given host name can use.
|
||||||
let $hostField = $('#hostForm [name=host]');
|
let $hostField = $('#hostForm [name=host]');
|
||||||
@@ -233,11 +257,13 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a wildcard cert is available for the given host.
|
// Check if a wildcard cert is available for the given host. When it is,
|
||||||
|
// make "Parent Wildcard" the default choice (it reuses an existing cert).
|
||||||
let wildcardParent = await hostMatchWildcard(host);
|
let wildcardParent = await hostMatchWildcard(host);
|
||||||
if(wildcardParent){
|
if(wildcardParent){
|
||||||
$('#challengeType-child-container').removeClass('challengeType-container');
|
$('#challengeType-child-container').removeClass('challengeType-container');
|
||||||
$('#challengeType-child-relatedHost').text(wildcardParent.host);
|
$('#challengeType-child-relatedHost').text(wildcardParent.host);
|
||||||
|
$('#challengeType-wildcardChild').prop('checked', true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,6 +453,7 @@
|
|||||||
<li class="nav-item"><button class="nav-link" id="hostTab-traffic-btn" data-bs-toggle="tab" data-bs-target="#hostTab-traffic" type="button" role="tab">Traffic</button></li>
|
<li class="nav-item"><button class="nav-link" id="hostTab-traffic-btn" data-bs-toggle="tab" data-bs-target="#hostTab-traffic" type="button" role="tab">Traffic</button></li>
|
||||||
<li class="nav-item"><button class="nav-link" id="hostTab-headers-btn" data-bs-toggle="tab" data-bs-target="#hostTab-headers" type="button" role="tab">Headers</button></li>
|
<li class="nav-item"><button class="nav-link" id="hostTab-headers-btn" data-bs-toggle="tab" data-bs-target="#hostTab-headers" type="button" role="tab">Headers</button></li>
|
||||||
<li class="nav-item"><button class="nav-link" id="hostTab-access-btn" data-bs-toggle="tab" data-bs-target="#hostTab-access" type="button" role="tab">Access</button></li>
|
<li class="nav-item"><button class="nav-link" id="hostTab-access-btn" data-bs-toggle="tab" data-bs-target="#hostTab-access" type="button" role="tab">Access</button></li>
|
||||||
|
<li class="nav-item"><button class="nav-link" id="hostTab-auth-btn" data-bs-toggle="tab" data-bs-target="#hostTab-auth" type="button" role="tab">Authentication</button></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<form class="addHost" id="hostForm" method="POST" action="host" onsubmit="formAJAX(this)" evalAJAX="hostModalClose()">
|
<form class="addHost" id="hostForm" method="POST" action="host" onsubmit="formAJAX(this)" evalAJAX="hostModalClose()">
|
||||||
@@ -605,15 +632,18 @@
|
|||||||
<textarea name="ip_deny" class="form-control" rows="2" placeholder="one per line; these are blocked"></textarea>
|
<textarea name="ip_deny" class="form-control" rows="2" placeholder="one per line; these are blocked"></textarea>
|
||||||
<small class="field-help text-muted d-block">These sources are always blocked (deny wins over allow).</small>
|
<small class="field-help text-muted d-block">These sources are always blocked (deny wins over allow).</small>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr>
|
<!-- Authentication -->
|
||||||
<h6 class="text-muted">
|
<div class="tab-pane fade" id="hostTab-auth" role="tabpanel">
|
||||||
Authentication
|
<p class="field-help text-muted">
|
||||||
<small class="fw-normal">— basic auth and SSO are OR'd; either one grants access.</small>
|
Basic auth and SSO are OR'd — if either is enabled, a request
|
||||||
</h6>
|
is allowed when it passes <b>either</b> one. Leave both off for a
|
||||||
|
public host.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h6 class="text-muted">Basic authentication</h6>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Basic authentication</label>
|
|
||||||
<div class="radio"><label>
|
<div class="radio"><label>
|
||||||
<input type="radio" name="basicauth_enabled" id="basicauth_enabled-false" value="false" checked>
|
<input type="radio" name="basicauth_enabled" id="basicauth_enabled-false" value="false" checked>
|
||||||
Off
|
Off
|
||||||
@@ -638,9 +668,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
<h6 class="text-muted">Single sign-on (SSO)</h6>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Single sign-on (SSO)</label>
|
|
||||||
<div class="radio"><label>
|
<div class="radio"><label>
|
||||||
<input type="radio" name="sso_enabled" id="sso_enabled-false" value="false" checked>
|
<input type="radio" name="sso_enabled" id="sso_enabled-false" value="false" checked>
|
||||||
Off
|
Off
|
||||||
@@ -649,19 +678,36 @@
|
|||||||
<input type="radio" name="sso_enabled" id="sso_enabled-true" value="true">
|
<input type="radio" name="sso_enabled" id="sso_enabled-true" value="true">
|
||||||
Require login via the configured OIDC provider
|
Require login via the configured OIDC provider
|
||||||
</label></div>
|
</label></div>
|
||||||
<small class="field-help text-muted d-block">Gates the site behind the same identity provider the admin app uses.</small>
|
<small class="field-help text-muted d-block">Gates the site behind the same identity provider the admin app uses. Empty allow-lists below mean any authenticated user is allowed.</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sso_allow_users" class="form-label">Allowed users</label>
|
<label for="sso_allow_users" class="form-label">Allowed users</label>
|
||||||
|
<div class="input-group mb-1">
|
||||||
|
<input type="text" class="form-control" list="hostSsoUsers" placeholder="type to search users…"
|
||||||
|
onkeydown="if(event.key==='Enter'){event.preventDefault();allowListAdd(this,'sso_allow_users');}">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" onclick="allowListAdd(this.previousElementSibling,'sso_allow_users')">
|
||||||
|
<i class="fa-solid fa-plus"></i> Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<textarea name="sso_allow_users" class="form-control" rows="2" placeholder="one email/username per line; blank = any authenticated user"></textarea>
|
<textarea name="sso_allow_users" class="form-control" rows="2" placeholder="one email/username per line; blank = any authenticated user"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sso_allow_groups" class="form-label">Allowed groups</label>
|
<label for="sso_allow_groups" class="form-label">Allowed groups</label>
|
||||||
|
<div class="input-group mb-1">
|
||||||
|
<input type="text" class="form-control" list="hostSsoGroups" placeholder="type to search groups…"
|
||||||
|
onkeydown="if(event.key==='Enter'){event.preventDefault();allowListAdd(this,'sso_allow_groups');}">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" onclick="allowListAdd(this.previousElementSibling,'sso_allow_groups')">
|
||||||
|
<i class="fa-solid fa-plus"></i> Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<textarea name="sso_allow_groups" class="form-control" rows="2" placeholder="one group per line; blank = any authenticated user"></textarea>
|
<textarea name="sso_allow_groups" class="form-control" rows="2" placeholder="one group per line; blank = any authenticated user"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<datalist id="hostSsoUsers"></datalist>
|
||||||
|
<datalist id="hostSsoGroups"></datalist>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user