Implemented support for entitlements-based access control

If configured, anyone missing the required entitlement will now be denied
access to the application and its api endpoints. If unset, any authenticated
user is accepted.
This commit is contained in:
Erik Thuning 2025-02-27 17:14:43 +01:00
parent a636f4f84a
commit 311ae0276e
5 changed files with 70 additions and 12 deletions

@ -15,6 +15,7 @@ callback_path = '/auth'
public_paths = [login_path, callback_path]
user_cookie = 'username'
token_cookie = 'token'
access_cookie = 'access'
permitted_format = re.compile('^[A-Za-z0-9-]+$')
config = ConfigParser()
@ -23,6 +24,9 @@ config.read('./config.ini')
app = Flask(__name__)
oauth = Oauth(config['oauth'])
app.wg = WireGuard(config['wireguard'])
required_entitlement = config.get('security',
'required_entitlement',
fallback=None)
@app.before_request
@ -35,8 +39,19 @@ def setup() -> None:
if not user_info:
return Response(status=403)
remote_user = user_info['sub']
app.wg.set_user(remote_user)
if required_entitlement is None \
or required_entitlement in user_info['entitlements']:
remote_user = user_info['sub']
app.wg.set_user(remote_user)
return
response = Response(status=403)
response.set_cookie(access_cookie,
'denied',
secure=True,
samesite='Strict')
return response
@app.after_request
def reload(response: Response) -> Response:

@ -24,6 +24,15 @@ client_network = a.network.in.cidr/notation
# Optional.
server_extra_config = path/to/a/conf/fragment
[security]
# The user entitlement (as read from the oauth token) to require
# for users who are to be able to use the service.
# Optional.
required_entitlement = urn:mace:some:entitilement
[oauth]
authorization_url = https://oauth.example/authorize
token_url = https://oauth.example/exchange

@ -98,5 +98,13 @@
</footer>
</form>
</template>
<template id="access-denied">
<div>
<h2>Access denied</h2>
<p>
You do not have permission to access this site.
</p>
</div>
</template>
</body>
</html>

@ -16,6 +16,18 @@
current.parentNode.removeChild(current);
}
function deny_access() {
const topbox = document.querySelector('topbox');
while(topbox.firstChild) {
topbox.removeChild(topbox.lastChild);
}
const placeholder = document.querySelector('placeholder');
placeholder.replaceWith('');
const template = document.querySelector('template#access-denied')
.content.cloneNode(true);
topbox.appendChild(template);
}
function display_create_form() {
const template = document.querySelector('template#create-form')
.content.cloneNode(true);
@ -167,21 +179,33 @@
const request = new Request('/api' + path, data);
const response = await fetch(request);
if(response.status === 403) {
console.log('doing login');
const cookies = getCookies();
const access = cookies['access'];
if(access === 'denied') {
throw new Error('access denied');
}
login();
}
return response.json();
}
async function setupPage(route) {
const configs = await make_api_request('GET', '/configs/');
const cookies = getCookies();
document.querySelector('user#banner-userid')
.textContent = cookies['username'];
document.querySelector('button#create-config')
.addEventListener('click', (event) => {
display_create_form();
});
await display_configs(...configs);
try {
const configs = await make_api_request('GET', '/configs/');
const cookies = getCookies();
document.querySelector('user#banner-userid')
.textContent = cookies['username'];
document.querySelector('button#create-config')
.addEventListener('click', (event) => {
display_create_form();
});
await display_configs(...configs);
} catch(e) {
if(e.message === 'access denied') {
deny_access();
return;
}
throw e;
}
}
})();

@ -65,6 +65,8 @@ topbox > button#create-config {
h2, summary {
font-size: 1.5rem;
font-weight: initial;
margin: initial;
}
configs {