summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlejandro Sirgo Rica <asirgo@soleta.eu>2024-07-18 15:34:09 +0200
committerAlejandro Sirgo Rica <asirgo@soleta.eu>2024-07-25 09:48:12 +0200
commitefd0b8acb3f4f098697c8c30bd196dacda39b704 (patch)
tree95959f7e437a531548de90b94ae00181175a04fc
parent594d655d6b13edfd1885d555ecda1a6f912db501 (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.py17
-rw-r--r--ogcp/templates/actions/image_config.html24
-rw-r--r--ogcp/templates/actions/image_details.html22
-rw-r--r--ogcp/templates/auth/user_form.html13
-rw-r--r--ogcp/templates/images.html4
-rw-r--r--ogcp/templates/scopes_checkbox_group.html16
-rw-r--r--ogcp/views.py130
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():