Skip to content

Commit 7d26f4e

Browse files
committed
feat: menu/form ownership management (FEATURE-REQUESTS C1)
Manage who owns your menus and forms from the builder, via ccfe-build. - ccfe-build gains list-users / list-groups (getent passwd/group, with an /etc fallback), list-objects (names for a picker) and a `chown` command that changes ownership of one of the user's own objects (NAME.menu/.form/.d). The object name is validated (no '/' or leading dot) so it cannot escape the user objdir -- a system-owned object, which restricted mode locks, is never reachable. chown failure (e.g. a non-root user changing to another user) exits non-zero with a clear message, which the output browser surfaces -- the clean failure path the command-not-found / error-modal work already provides. - New builder form builder.d/chown.form (Object + Owner pickers driven by the new list_cmd commands), added to the builder menu as "Change object ownership (chown)". - t/43 parse-checks the form and drives ccfe-build: enumerate, chown to self (always allowed), and the invalid-name / missing-object refusals. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c0e74b1 commit 7d26f4e

4 files changed

Lines changed: 141 additions & 1 deletion

File tree

src/builder.d/chown.form

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
title { Change object ownership }
2+
top {
3+
Change who owns one of your menus or forms. Pick an object, then an owner
4+
(USER or USER:GROUP). Changing to another user usually needs root; if you
5+
lack the privilege the error is shown. Only your own objects are touched --
6+
system objects (locked by restricted mode) are never reachable here.
7+
}
8+
field {
9+
id = OBJECT
10+
len = 24
11+
type = STRING
12+
required = YES
13+
list_cmd = command:single-val:"${CCFE_BIN_DIR:+$CCFE_BIN_DIR/}ccfe-build" list-objects
14+
label = Object (F2 for a list)
15+
}
16+
field {
17+
id = OWNER
18+
len = 24
19+
type = STRING
20+
htab = 1
21+
required = YES
22+
list_cmd = command:single-val:"${CCFE_BIN_DIR:+$CCFE_BIN_DIR/}ccfe-build" list-users
23+
label = New owner -- USER or USER:GROUP (F2 for users)
24+
}
25+
action { run:
26+
"${CCFE_BIN_DIR:+$CCFE_BIN_DIR/}ccfe-build" chown \
27+
"$CCFE_FIELD_OBJECT" "$CCFE_FIELD_OWNER"
28+
}

src/builder.menu

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ item {
4040
descr = Change a setting (ccfe.conf)
4141
action = form:builder.d/settings
4242
}
43+
item {
44+
id = CHOWN
45+
descr = Change object ownership (chown)
46+
action = form:builder.d/chown
47+
}
4348
item {
4449
id = LIST
4550
descr = List my menus and forms

src/t/43-ownership.t

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/usr/bin/perl
2+
#
3+
# Menu/form ownership management (FEATURE-REQUESTS C1).
4+
#
5+
# ccfe-build gains list-objects / list-users / list-groups and a `chown`
6+
# command that changes ownership of one of the user's own objects (only under
7+
# the user objdir -- a system object, locked by restricted mode, is never
8+
# reachable). Driven directly at the ccfe-build layer: enumerate, chown to
9+
# self (always allowed), and the clean-failure paths (invalid name / missing
10+
# object). Also parse-checks the chown builder form.
11+
#
12+
use strict;
13+
use warnings;
14+
use FindBin qw($Bin);
15+
use File::Temp qw(tempdir);
16+
use File::Path qw(make_path);
17+
use Test::More;
18+
19+
my $src = "$Bin/..";
20+
my $build = "$src/tools/ccfe-build";
21+
plan skip_all => 'no ccfe-build' unless -f $build;
22+
plan skip_all => 'no installer' unless -f "$src/install.sh";
23+
24+
# Parse-check the chown builder form (and the rest) via `ccfe -k`.
25+
my $prefix = tempdir( CLEANUP => 1 );
26+
my $log = `cd "$src" && sh install.sh -b -p "$prefix" 2>&1`;
27+
plan skip_all => "install failed: $log" unless $? == 0 && -x "$prefix/bin/ccfe";
28+
29+
plan tests => 6;
30+
31+
system("$prefix/bin/ccfe -k builder.d/chown >/dev/null 2>&1");
32+
is( $? >> 8, 0, 'the chown builder form parses' );
33+
34+
# Isolated user objdir with one object.
35+
my $home = tempdir( CLEANUP => 1 );
36+
local $ENV{XDG_DATA_HOME} = "$home/data";
37+
local $ENV{XDG_CONFIG_HOME} = "$home/config";
38+
my $objdir = "$home/data/ccfe/ccfe";
39+
make_path($objdir);
40+
open( my $o, '>', "$objdir/myobj.menu" ) or die "obj: $!";
41+
print {$o} "title {\n X\n}\n";
42+
close($o);
43+
44+
my $me = getpwuid($<);
45+
46+
like( scalar(`sh "$build" list-objects 2>&1`), qr/^myobj$/m,
47+
'list-objects lists my object' );
48+
like( scalar(`sh "$build" list-users 2>&1`), qr/\Q$me\E/,
49+
'list-users includes me' );
50+
51+
system(qq{sh "$build" chown myobj "$me" >/dev/null 2>&1});
52+
is( $? >> 8, 0, 'chown to myself succeeds' );
53+
54+
system(qq{sh "$build" chown 'bad/name' "$me" >/dev/null 2>&1});
55+
isnt( $? >> 8, 0, 'an invalid object name is refused' );
56+
57+
system(qq{sh "$build" chown nosuchobject "$me" >/dev/null 2>&1});
58+
isnt( $? >> 8, 0, 'a missing object is refused' );

src/tools/ccfe-build

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
# ccfe-build set-action FORM ACTION
1818
# ccfe-build set-config KEY VALUE # global { } setting in the user conf
1919
# ccfe-build set-var NAME VALUE # variables { } entry in the user conf
20-
# ccfe-build show-config # print the user conf
20+
# ccfe-build show-config # global { } print the user conf
2121
# ccfe-build list # list the user's objects
22+
# ccfe-build chown NAME USER[:GROUP] # change ownership of one of my objects
23+
# ccfe-build list-objects # object names (for a list_cmd)
24+
# ccfe-build list-users / list-groups # local users / groups (for a list_cmd)
2225
#
2326
# Copyright (C) 2026 CCFE contributors. GPL-2.0-or-later.
2427

@@ -224,6 +227,52 @@ case "$cmd" in
224227
done
225228
;;
226229

230+
list-objects) # every menu/form name, deduped (for a form's list_cmd)
231+
for f in "$objdir"/*.menu "$objdir"/*.form; do
232+
[ -e "$f" ] || continue
233+
b=${f##*/}; echo "${b%.*}"
234+
done | sort -u
235+
;;
236+
237+
list-users) # local login names, for the ownership picker
238+
if command -v getent >/dev/null 2>&1; then
239+
getent passwd | cut -d: -f1
240+
else
241+
cut -d: -f1 /etc/passwd
242+
fi | sort -u
243+
;;
244+
245+
list-groups) # local group names, for the ownership picker
246+
if command -v getent >/dev/null 2>&1; then
247+
getent group | cut -d: -f1
248+
else
249+
cut -d: -f1 /etc/group
250+
fi | sort -u
251+
;;
252+
253+
chown) # change ownership of one of MY objects (FEATURE-REQUESTS C1)
254+
name=${1:-}; owner=${2:-}
255+
valid_name "$name" || die "invalid object name \"$name\""
256+
# USER, USER:GROUP or :GROUP, restricted characters (no shell metachars,
257+
# no path separators) so the value cannot escape into the chown arguments.
258+
case "$owner" in
259+
'' | *[!A-Za-z0-9_.:-]* ) die "invalid owner \"$owner\" (USER[:GROUP])" ;;
260+
esac
261+
# Only ever touch objects under MY objdir; valid_name already forbids '/'
262+
# and a leading dot, so $name cannot traverse out of it -- a system-owned
263+
# object (which restricted mode locks) is never reachable here.
264+
found=0
265+
for suffix in .menu .form .d; do
266+
t="$objdir/$name$suffix"
267+
[ -e "$t" ] || continue
268+
found=1
269+
chown -R -- "$owner" "$t" \
270+
|| die "could not change owner of $t to $owner (need more privilege?)"
271+
echo "Changed owner of $t to $owner"
272+
done
273+
[ "$found" = 1 ] || die "no object named \"$name\" under $objdir"
274+
;;
275+
227276
*)
228277
die "unknown command: $cmd"
229278
;;

0 commit comments

Comments
 (0)