diff options
author | Alejandro Sirgo Rica <asirgo@soleta.eu> | 2024-07-18 15:34:09 +0200 |
---|---|---|
committer | Alejandro Sirgo Rica <asirgo@soleta.eu> | 2024-07-25 09:48:12 +0200 |
commit | efd0b8acb3f4f098697c8c30bd196dacda39b704 (patch) | |
tree | 95959f7e437a531548de90b94ae00181175a04fc | |
parent | 594d655d6b13edfd1885d555ecda1a6f912db501 (diff) |
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.
-rw-r--r-- | ogcp/forms/action_forms.py | 17 | ||||
-rw-r--r-- | ogcp/templates/actions/image_config.html | 24 | ||||
-rw-r--r-- | ogcp/templates/actions/image_details.html | 22 | ||||
-rw-r--r-- | ogcp/templates/auth/user_form.html | 13 | ||||
-rw-r--r-- | ogcp/templates/images.html | 4 | ||||
-rw-r--r-- | ogcp/templates/scopes_checkbox_group.html | 16 | ||||
-rw-r--r-- | ogcp/views.py | 130 |
7 files changed, 185 insertions, 41 deletions
diff --git a/ogcp/forms/action_forms.py b/ogcp/forms/action_forms.py index e72e37c..82958fb 100644 --- a/ogcp/forms/action_forms.py +++ b/ogcp/forms/action_forms.py @@ -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'), diff --git a/ogcp/templates/actions/image_config.html b/ogcp/templates/actions/image_config.html new file mode 100644 index 0000000..c286683 --- /dev/null +++ b/ogcp/templates/actions/image_config.html @@ -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 %} diff --git a/ogcp/templates/actions/image_details.html b/ogcp/templates/actions/image_details.html index 87105cb..ab66503 100644 --- a/ogcp/templates/actions/image_details.html +++ b/ogcp/templates/actions/image_details.html @@ -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 %} diff --git a/ogcp/templates/auth/user_form.html b/ogcp/templates/auth/user_form.html index bd2f396..ae34490 100644 --- a/ogcp/templates/auth/user_form.html +++ b/ogcp/templates/auth/user_form.html @@ -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"> diff --git a/ogcp/templates/images.html b/ogcp/templates/images.html index 00bb2e4..462e68d 100644 --- a/ogcp/templates/images.html +++ b/ogcp/templates/images.html @@ -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()"> diff --git a/ogcp/templates/scopes_checkbox_group.html b/ogcp/templates/scopes_checkbox_group.html new file mode 100644 index 0000000..0ebf15a --- /dev/null +++ b/ogcp/templates/scopes_checkbox_group.html @@ -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 %}
\ No newline at end of file diff --git a/ogcp/views.py b/ogcp/views.py index b8f43c7..525ed3a 100644 --- a/ogcp/views.py +++ b/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(): |