Browse Source

Declare the guest console capture private, rather than guarding it

guests.tx wrapped its console captures in $can_edit by hand.  Now that a type
can say which of its fields are for editors only, it says so: preview is
declared private in guests.json, so the url is withheld from a reader who
cannot use it and the template just asks whether it has one.

That is the better shape.  The template no longer encodes who may see what --
it draws an img if it was given a url, and the type decides who gets one.  The
action form keeps its $can_edit guard, correctly: powering a guest off is
something you do, not something you are told, and no amount of withholding data
makes a button that submits to an admin route appropriate for a stranger.

The test now redacts its fixture the way posts() does before rendering, so it
covers the two halves working together rather than the template alone.
Undeclaring the field fails the suite.

Note that the wizard could not have generated this type: _wizard_template
always emits an editor posting to /post/save, and a guest is synthesized from
libvirt on every view and has nothing to save.  A type backed by a datasource
wants no editor at all, which the wizard has no way to say yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U3k2hhSZpcsoe6uBmhF9nB
George Baugh 2 tháng trước cách đây
mục cha
commit
49b75cc124

+ 18 - 1
t/Trog-DataSource-Virt.t

@@ -258,6 +258,7 @@ subtest 'guests show nothing an unprivileged viewer cannot use' => sub {
     );
 
     my %post = (
+        form    => 'guests.tx',
         id      => 'uuid-alpha', title  => 'alpha',  state   => 'running', is_active => 1,
         preview => '/guest/screenshot/hv-1/alpha',   domain  => 'alpha',
         hypervisor => 'hv-1', hypervisor_title => 'spec-hv', vcpus => 2,
@@ -270,10 +271,19 @@ subtest 'guests show nothing an unprivileged viewer cannot use' => sub {
         'untiled, admin'      => { tiled => 0, can_edit => 1 },
     );
 
+    require Trog::Routes::HTML;
+
     foreach my $view ( sort keys %views ) {
-        my $out = $tx->render( 'forms/guests.tx', { post => \%post, style => '', route => '/vm', %{ $views{$view} } } );
         my $admin = $views{$view}{can_edit};
 
+        # Redact the way posts() does before rendering, so this covers the
+        # thing that actually protects the capture -- the template only decides
+        # whether to draw an img for a value it was given.
+        my %shown = %post;
+        Trog::Routes::HTML::_redact_private( \%shown ) unless $admin;
+
+        my $out = $tx->render( 'forms/guests.tx', { post => \%shown, style => '', route => '/vm', %{ $views{$view} } } );
+
         # The guest is always named -- that is the point of the page.
         like( $out, qr/alpha/, "$view: the guest is listed" );
 
@@ -290,6 +300,13 @@ subtest 'guests show nothing an unprivileged viewer cannot use' => sub {
     }
 };
 
+subtest 'the guests type declares its console capture private' => sub {
+    require Trog::DataModule;
+
+    is_deeply( Trog::DataModule::private_fields_for('guests.tx'), ['preview'],
+        'so the url to it is withheld rather than guarded in the template' );
+};
+
 subtest 'private fields are dropped before a non-editor sees them' => sub {
     require Trog::Routes::HTML;
     require Trog::DataModule;

+ 16 - 10
www/templates/html/components/forms/guests.json

@@ -1,15 +1,21 @@
 {
-   "properties" : {},
-   "type" : "object",
-   "x-tcms-datasource" : "Trog::DataSource::Virt",
-   "x-tcms-post-type" : {
-      "description" : "A libvirt guest.  Not written by anyone -- built from the hypervisors this series' relations pull in, and rebuilt on every view.",
-      "generated" : false,
-      "name" : "guests"
+   "properties": {
+      "preview": {
+         "type": "string",
+         "x-tcms-label": "Console capture",
+         "x-tcms-private": true
+      }
+   },
+   "type": "object",
+   "x-tcms-datasource": "Trog::DataSource::Virt",
+   "x-tcms-post-type": {
+      "description": "A libvirt guest.  Not written by anyone -- built from the hypervisors this series' relations pull in, and rebuilt on every view.",
+      "generated": false,
+      "name": "guests"
    },
-   "x-tcms-relations" : {
-      "hypervisors" : {
-         "form" : "hypervisor.tx"
+   "x-tcms-relations": {
+      "hypervisors": {
+         "form": "hypervisor.tx"
       }
    }
 }

+ 5 - 4
www/templates/html/components/forms/guests.tx

@@ -11,9 +11,10 @@
                     <: $post.title :>
                     <span class="guest-state guest-<: $post.is_active ? 'up' : 'down' :>"><: $post.state :></span>
                 </p>
-                : # A console capture is admin-only: the route that serves it
-                : # requires the admin acl, so anyone else gets a broken image.
-                : if ( $can_edit ) {
+                : # The capture is declared private in guests.json, so a reader
+                : # who cannot edit never receives it -- there is nothing here
+                : # to remember to guard.
+                : if ( $post.preview ) {
                     <a href="<: $post.preview :>" class="undecorated" title="<: $post.title :> (<: $post.state :>)">
                         <img src="<: $post.preview :>" class="responsive-img guest-screenshot" alt="console of <: $post.title :>" loading="lazy" />
                     </a>
@@ -24,7 +25,7 @@
                     <span class="guest-state guest-<: $post.is_active ? 'up' : 'down' :>"><: $post.state :></span>
                 </h3>
                 <div class="postData responsive-text" id="postData-<: $post.id :>">
-                    : if ( $can_edit ) {
+                    : if ( $post.preview ) {
                         <a href="<: $post.preview :>" class="undecorated" title="console of <: $post.title :>">
                             <img src="<: $post.preview :>" class="responsive-img guest-screenshot" alt="console of <: $post.title :>" loading="lazy" />
                         </a>