mirror of https://git.48k.eu/ogcp
ogcp: add image restrict functionality
Add center scope restriction using /image/restrict. Add view in Images to update scope permissions. Disable images in Commands for image update and restore if the client belongs to a disabled center. Consolidate template code to render scope selection checkboxes.master
parent
594d655d6b
commit
efd0b8acb3
|
@ -7,7 +7,8 @@
|
|||
|
||||
from wtforms import (
|
||||
Form, SubmitField, HiddenField, SelectField, BooleanField, IntegerField,
|
||||
StringField, RadioField, FormField, FieldList, DecimalField, TextAreaField
|
||||
StringField, RadioField, FormField, FieldList, DecimalField, TextAreaField,
|
||||
SelectMultipleField
|
||||
)
|
||||
from wtforms.validators import InputRequired
|
||||
from flask_wtf import FlaskForm
|
||||
|
@ -236,6 +237,20 @@ class ImageDetailsForm(FlaskForm):
|
|||
permissions = StringField(label=_l('Permissions'))
|
||||
software_id = StringField(label=_l('Software id'))
|
||||
checksum = StringField(label=_l('Checksum'))
|
||||
scopes = SelectMultipleField(
|
||||
label=_l('Allowed scopes'),
|
||||
description=_l('No scope selection gives full access'),
|
||||
)
|
||||
|
||||
class ImageConfigForm(FlaskForm):
|
||||
image_id = HiddenField()
|
||||
server = HiddenField()
|
||||
name = HiddenField()
|
||||
scopes = SelectMultipleField(
|
||||
label=_l('Allowed scopes'),
|
||||
description=_l('No scope selection gives full access'),
|
||||
)
|
||||
submit = SubmitField(label=_l('Submit'))
|
||||
|
||||
class ServerForm(FlaskForm):
|
||||
name = StringField(label=_l('Name'),
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
{% extends 'images.html' %}
|
||||
{% import "bootstrap/wtf.html" as wtf %}
|
||||
|
||||
{% set btn_back = true %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h2 class="mx-5 subhead-heading">{{_('Update image')}}</h2>
|
||||
|
||||
<form class="form mx-5" method="POST" action="{{ url_for('action_image_config') }}">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
{{ form.image_id() }}
|
||||
{{ form.server() }}
|
||||
{{ form.name() }}
|
||||
|
||||
{% include 'scopes_checkbox_group.html' %}
|
||||
|
||||
<div class="form-group">
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
|
@ -7,9 +7,23 @@
|
|||
|
||||
<h2 class="mx-5 subhead-heading">{{_('Image details')}}</h2>
|
||||
|
||||
{{ wtf.quick_form(form,
|
||||
method='post',
|
||||
button_map={'create': 'primary'},
|
||||
extra_classes="mx-5") }}
|
||||
<div class="container mx-5">
|
||||
<form class="form" method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
{% for field in form if field.type != 'CSRFToken' and field.name not in ['scopes'] %}
|
||||
{% if not field.flags.hidden %}
|
||||
<div class="form-group row">
|
||||
<label for="name" class="col-sm-2 col-form-label">{{ field.label.text }}</label>
|
||||
<div class="col-sm-9">
|
||||
{{ field(class="form-control") }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% include 'scopes_checkbox_group.html' %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -104,18 +104,7 @@
|
|||
</table>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
{{ form.scopes.label(class_='form-label') }}
|
||||
<div class="form-text text-muted">{{ form.scopes.description }}</div>
|
||||
<div>
|
||||
{% for value, label, checked in form.scopes.iter_choices() %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="{{ form.scopes.name }}" value="{{ value }}" {% if checked %} checked {% endif %}>
|
||||
<label class="form-check-label">{{ label }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'scopes_checkbox_group.html' %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
|
|
@ -67,6 +67,10 @@
|
|||
<input class="btn btn-light" type="submit" value="{{ _('Delete image') }}"
|
||||
form="imagesForm" formaction="{{ url_for('action_image_delete') }}" formmethod="get">
|
||||
{% endif %}
|
||||
{% if current_user.get_permission('IMAGE', 'UPDATE') %}
|
||||
<input class="btn btn-light" type="submit" value="{{ _('Update image') }}"
|
||||
form="imagesForm" formaction="{{ url_for('action_image_config') }}" formmethod="get">
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if btn_back %}
|
||||
<button class="btn btn-danger ml-3" type="button" id="backButton" onclick="history.back()">
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
{% if form is defined and form.scopes is defined %}
|
||||
|
||||
<div class="form-group">
|
||||
{{ form.scopes.label(class_='form-label') }}
|
||||
<div class="mx-4">
|
||||
<div class="form-text text-muted">{{ form.scopes.description }}</div>
|
||||
{% for value, label, checked in form.scopes.iter_choices() %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="{{ form.scopes.name }}" value="{{ value }}" {% if checked %} checked {% endif %}>
|
||||
<label class="form-check-label">{{ label }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
130
ogcp/views.py
130
ogcp/views.py
|
@ -14,7 +14,7 @@ from ogcp.forms.action_forms import (
|
|||
RoomForm, DeleteRoomForm, CenterForm, DeleteCenterForm, OgliveForm,
|
||||
GenericForm, SelectClientForm, ImageUpdateForm, ImportClientsForm,
|
||||
ServerForm, DeleteRepositoryForm, RepoForm, FolderForm, CacheForm,
|
||||
ClientMoveForm, RunScriptForm
|
||||
ClientMoveForm, RunScriptForm, ImageConfigForm
|
||||
)
|
||||
from flask_login import (
|
||||
current_user, LoginManager,
|
||||
|
@ -779,22 +779,31 @@ def search_image(images_list, image_id):
|
|||
return image
|
||||
return False
|
||||
|
||||
def get_images_grouped_by_repos_from_server(server):
|
||||
def filter_images_allowed_in_center(server, images, center_id):
|
||||
res = []
|
||||
for image in images:
|
||||
r = server.get('/image/restrict', {'image': image['id']})
|
||||
if not r:
|
||||
raise ServerError
|
||||
if r.status_code != requests.codes.ok:
|
||||
raise ServerErrorCode
|
||||
allowed_scopes = r.json().get('scopes')
|
||||
if not allowed_scopes or center_id in allowed_scopes:
|
||||
res.append(image)
|
||||
return res
|
||||
|
||||
def get_images_from_repo(server, repo_id):
|
||||
r = server.get('/images')
|
||||
if not r:
|
||||
raise ServerError
|
||||
if r.status_code != requests.codes.ok:
|
||||
raise ServerErrorCode
|
||||
images = r.json()['images']
|
||||
repos={}
|
||||
|
||||
res=[]
|
||||
for image in images:
|
||||
repo_id=image['repo_id']
|
||||
if repo_id not in repos:
|
||||
repos[repo_id] = [image]
|
||||
else:
|
||||
repos[repo_id].append(image)
|
||||
return repos
|
||||
if image['repo_id'] == repo_id:
|
||||
res.append(image)
|
||||
return res
|
||||
|
||||
def get_clients_repo(server, ips):
|
||||
repo_id=None
|
||||
|
@ -912,7 +921,9 @@ def action_image_restore():
|
|||
flash(_(f'There was a problem sending the image restore command'), category='error')
|
||||
return redirect(url_for('commands'))
|
||||
else:
|
||||
ips = parse_elements(request.args.to_dict())
|
||||
params = request.args.to_dict()
|
||||
center_id = int(params.get('scope-center'))
|
||||
ips = parse_elements(params)
|
||||
if not validate_elements(ips):
|
||||
return redirect(url_for('commands'))
|
||||
form.ips.data = ' '.join(ips)
|
||||
|
@ -930,16 +941,19 @@ def action_image_restore():
|
|||
flash(_(f'Computers have different repos assigned'), category='error')
|
||||
return redirect(url_for('commands'))
|
||||
try:
|
||||
images = get_images_grouped_by_repos_from_server(server)
|
||||
images = get_images_from_repo(server, repo_id)
|
||||
|
||||
if not images:
|
||||
flash(_(f'Computer(s) assigned to a repo with no images'), category='error')
|
||||
return redirect(url_for('commands'))
|
||||
|
||||
images = filter_images_allowed_in_center(server, images, center_id)
|
||||
except ServerError:
|
||||
return ogserver_down('commands')
|
||||
except ServerErrorCode:
|
||||
return ogserver_error('commands')
|
||||
|
||||
if repo_id not in images:
|
||||
flash(_(f'Computer(s) assigned to a repo with no images'), category='error')
|
||||
return redirect(url_for('commands'))
|
||||
for image in images[repo_id]:
|
||||
for image in images:
|
||||
form.image.choices.append((image['id'], image['name']))
|
||||
|
||||
part_choices = []
|
||||
|
@ -2276,6 +2290,8 @@ def action_image_create():
|
|||
if client_repo_id == repo['id']]
|
||||
form.repository.render_kw = {'readonly': True}
|
||||
|
||||
form.scopes.choices = get_available_centers()
|
||||
|
||||
scopes, clients = get_scopes(set(ips))
|
||||
return render_template('actions/image_create.html', form=form,
|
||||
scopes=scopes)
|
||||
|
@ -2326,7 +2342,9 @@ def action_image_update():
|
|||
category='error')
|
||||
return redirect(url_for('commands'))
|
||||
|
||||
ips = parse_elements(request.args.to_dict())
|
||||
params = request.args.to_dict()
|
||||
center_id = int(params.get('scope-center'))
|
||||
ips = parse_elements(params)
|
||||
if not validate_elements(ips, max_len=1):
|
||||
return redirect(url_for('commands'))
|
||||
form.ip.data = ' '.join(ips)
|
||||
|
@ -2340,17 +2358,19 @@ def action_image_update():
|
|||
|
||||
repo_id = r.json()['repo_id']
|
||||
try:
|
||||
images = get_images_grouped_by_repos_from_server(server)
|
||||
images = get_images_from_repo(server, repo_id)
|
||||
|
||||
if not images:
|
||||
flash(_(f'Computer(s) assigned to a repo with no images'), category='error')
|
||||
return redirect(url_for('commands'))
|
||||
|
||||
images = filter_images_allowed_in_center(server, images, center_id)
|
||||
except ServerError:
|
||||
return ogserver_down('commands')
|
||||
except ServerErrorCode:
|
||||
return ogserver_error('commands')
|
||||
|
||||
if repo_id not in images:
|
||||
flash(_('Computer is assigned to a repo with no images'),
|
||||
category='error')
|
||||
return redirect(url_for('commands'))
|
||||
for image in images[repo_id]:
|
||||
for image in images:
|
||||
form.image.choices.append((image['id'], image['name']))
|
||||
|
||||
r = server.get('/client/setup', payload={'client': list(ips)})
|
||||
|
@ -2381,7 +2401,7 @@ def action_image_update():
|
|||
)
|
||||
|
||||
if part['image']:
|
||||
for image in images[repo_id]:
|
||||
for image in images:
|
||||
if image['id'] == part['image']:
|
||||
part_content[partition_value] = part['image']
|
||||
break
|
||||
|
@ -3450,6 +3470,15 @@ def action_image_info():
|
|||
except ServerErrorCode:
|
||||
return ogserver_error('images')
|
||||
|
||||
r = server.get('/image/restrict', {'image': image['id']})
|
||||
if not r:
|
||||
raise ServerError
|
||||
if r.status_code != requests.codes.ok:
|
||||
raise ServerErrorCode
|
||||
|
||||
form.scopes.choices = get_available_centers()
|
||||
form.scopes.data = [str(scope) for scope in r.json().get('scopes')]
|
||||
|
||||
return render_template('actions/image_details.html', form=form,
|
||||
responses=responses)
|
||||
|
||||
|
@ -3492,6 +3521,59 @@ def action_image_delete():
|
|||
image_name=image_name.split('_', 1)[0], image_id=image_id,
|
||||
responses=responses)
|
||||
|
||||
@app.route('/action/image/config', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def action_image_config():
|
||||
form = ImageConfigForm(request.form)
|
||||
if request.method == 'POST':
|
||||
image_id = int(form.image_id.data)
|
||||
server = get_server_from_ip_port(form.server.data)
|
||||
|
||||
scope_list = [int(scope) for scope in form.scopes.data]
|
||||
|
||||
payload = {'image': image_id, 'scopes': scope_list}
|
||||
|
||||
r = server.post('/image/restrict', payload)
|
||||
if not r:
|
||||
return ogserver_down('images')
|
||||
if r.status_code != requests.codes.ok:
|
||||
return ogserver_error('images')
|
||||
|
||||
flash(_('Image updated successfully'), category='info')
|
||||
return redirect(url_for('images'))
|
||||
else:
|
||||
params = request.args.to_dict()
|
||||
images = [(name, imgid) for name, imgid in params.items()
|
||||
if name != 'csrf_token' and name != 'image-server']
|
||||
if not validate_elements(images, max_len=1):
|
||||
return redirect(url_for('images'))
|
||||
|
||||
image_name, image_id = images[0]
|
||||
image_name=image_name.split('_', 1)[0]
|
||||
server = get_server_from_ip_port(params['image-server'])
|
||||
|
||||
form.image_id.data = image_id
|
||||
|
||||
r = server.get('/image/restrict', {'image': int(image_id)})
|
||||
if not r:
|
||||
return ogserver_down('images')
|
||||
if r.status_code != requests.codes.ok:
|
||||
return ogserver_error('images')
|
||||
|
||||
form.server.data = params['image-server']
|
||||
form.scopes.choices = get_available_centers()
|
||||
form.scopes.data = [str(scope) for scope in r.json().get('scopes')]
|
||||
|
||||
try:
|
||||
responses = get_images_grouped_by_repos()
|
||||
except ServerError:
|
||||
return ogserver_down('images')
|
||||
except ServerErrorCode:
|
||||
return ogserver_error('images')
|
||||
|
||||
return render_template('actions/image_config.html', form=form,
|
||||
responses=responses)
|
||||
|
||||
@app.route('/action/log', methods=['GET'])
|
||||
@login_required
|
||||
def action_legacy_log():
|
||||
|
|
Loading…
Reference in New Issue