This is an automated email from the ASF dual-hosted git repository.

jbonofre pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/karaf.git


The following commit(s) were added to refs/heads/main by this push:
     new 511df4e0fc Restrict feature:repo-add -i and feature:repo-remove -u to 
admin role (#2883)
511df4e0fc is described below

commit 511df4e0fcdc66139dbcf2b00586801a2fece7ac
Author: JB Onofré <[email protected]>
AuthorDate: Sat Sep 12 13:07:01 2026 +0200

    Restrict feature:repo-add -i and feature:repo-remove -u to admin role 
(#2883)
    
    * Restrict feature:repo-add -i and feature:repo-remove -u to admin role
    
    feature:repo-add -i and feature:repo-remove -u call
    FeaturesServiceImpl.addRepository()/removeRepository() with the
    install/uninstall flag set, which installs or uninstalls every
    feature in the repository. Neither command had an ACL entry, so
    any SSH user with just the viewer role could reach this and have
    Karaf install and start an arbitrary attacker-supplied feature
    repository, bypassing the admin-only ACL on feature:install and
    feature:uninstall.
    
    Add option-specific ACL rules (same pattern already used for
    bundle:refresh -f and friends) so only -i/-u require admin, while
    plain repo-add/repo-remove stay open as before.
    
    * Fix CI: add open fallback ACL rule for plain feature:repo-add/repo-remove
    
    The regex-conditioned ACL rules (repo-add[/.*[-][i].*/], 
repo-remove[/.*[-][u].*/])
    had no fallback entry for the plain command. Karaf's ACL guard treats a 
command
    name that has at least one ACL entry but no rule matching the current 
invocation
    as "no roles allowed" (empty role list), which denies everyone -- including
    admin -- rather than leaving it unrestricted. This broke feature:repo-add/
    feature:repo-remove without -i/-u for all callers, causing FeatureTest and 
the
    new FeatureSshCommandSecurityTest to fail in CI with 
CommandNotFoundException.
    
    Add an explicit "repo-add = viewer" / "repo-remove = viewer" fallback, 
mirroring
    the existing pattern used for bundle:refresh -f, so the plain commands 
remain
    open while -i/-u stay admin-only.
    
    * Revert incorrect open fallback for feature:repo-add/repo-remove
    
    The previous "Fix CI" commit added a bare `repo-add = viewer` /
    `repo-remove = viewer` entry, on the theory that a command with a
    regex-specific ACL rule but no matching entry falls back to "deny
    all, including admin". That's not what happens: ACLConfigurationParser
    returns Specificity.NO_MATCH for a non-matching invocation, and
    checkSecurity() treats NO_MATCH as unrestricted access.
    
    What the bare `= viewer` entry actually did was make the *plain*
    command name itself require the viewer role for visibility
    (SecuredSessionFactoryImpl.isVisible(), which checks the bare command
    name regardless of arguments). Callers with no roles at all -- like
    the pax-exam-based FeatureTest itests, which invoke commands directly
    via executeCommand() without any RolePrincipal -- were no longer able
    to even see feature:repo-add/repo-remove, failing with
    CommandNotFoundException instead of running the command.
    
    Dropping the bare fallback restores the pre-existing behavior: the
    plain commands stay open to everyone (no ACL entry at all for the
    non-`-i`/`-u` case), while the regex-specific rules keep requiring
    admin only when `-i`/`-u` is present.
    
    * Restore viewer access to plain feature:repo-add/repo-remove
    
    The regex-conditioned ACL rules added for -i/-u (repo-add[/.*[-][i].*/],
    repo-remove[/.*[-][u].*/]) had no plain fallback entry. Karaf's shell
    guard decides command *visibility* before it knows the actual arguments
    (SecuredSessionFactoryImpl.isVisible() calls getRolesForInvocation with
    params=null), and a command with only a regex/exact-arg ACL entry is
    treated as requiring that entry's role for every invocation at that
    stage - so feature:repo-add/feature:repo-remove became invisible
    ("Command not found") to any non-admin session, even without -i/-u.
    This broke the PR's own FeatureSshCommandSecurityTest (which expects a
    viewer to run the plain command) and the pre-existing FeatureTest cases.
    
    Add a "= viewer" fallback for both commands, mirroring the existing
    bundle:refresh/-f pattern in org.apache.karaf.command.acl.bundle.cfg,
    so the plain form stays open to viewer/manager/admin while -i/-u keeps
    requiring admin.
    
    The three pre-existing FeatureTest cases that called repo-add/repo-remove
    with no role at all (previously fine since the commands had no ACL entry)
    now pass an explicit viewer RolePrincipal, since introducing any ACL entry
    for a command means invoking it requires holding a role.
---
 .../features/standard/src/main/feature/feature.xml | 11 ++++++++
 .../etc/org.apache.karaf.command.acl.feature.cfg   | 11 ++++++++
 .../src/test/filtered-resources/etc/feature.xml    |  4 +++
 .../java/org/apache/karaf/itests/FeatureTest.java  | 10 ++++----
 .../itests/ssh/FeatureSshCommandSecurityTest.java  | 30 ++++++++++++++++++++++
 manual/src/main/asciidoc/user-guide/security.adoc  |  5 ++--
 6 files changed, 64 insertions(+), 7 deletions(-)

diff --git a/assemblies/features/standard/src/main/feature/feature.xml 
b/assemblies/features/standard/src/main/feature/feature.xml
index 3f7d1c7bff..06bb632a51 100644
--- a/assemblies/features/standard/src/main/feature/feature.xml
+++ b/assemblies/features/standard/src/main/feature/feature.xml
@@ -147,6 +147,17 @@ install = admin
 uninstall = admin
 start = admin
 stop = admin
+#
+# repo-add/repo-remove stay open to the viewer role (the plain form only 
reads/lists
+# a repository), but installing/uninstalling every feature in it via -i/-u 
requires
+# admin. The bare "= viewer" fallback is required: a command with a regex-only 
ACL
+# entry and no fallback is treated as admin-only for *every* invocation, 
because the
+# command guard decides visibility before it knows the actual arguments.
+#
+repo-add[/.*[-][i].*/] = admin
+repo-add = viewer
+repo-remove[/.*[-][u].*/] = admin
+repo-remove = viewer
             </config>
             <bundle 
start-level="30">mvn:org.apache.karaf.features/org.apache.karaf.features.command/${project.version}</bundle>
         </conditional>
diff --git 
a/instance/src/main/resources/org/apache/karaf/instance/resources/etc/org.apache.karaf.command.acl.feature.cfg
 
b/instance/src/main/resources/org/apache/karaf/instance/resources/etc/org.apache.karaf.command.acl.feature.cfg
index 823fe3e2cc..cefaa7a7fc 100644
--- 
a/instance/src/main/resources/org/apache/karaf/instance/resources/etc/org.apache.karaf.command.acl.feature.cfg
+++ 
b/instance/src/main/resources/org/apache/karaf/instance/resources/etc/org.apache.karaf.command.acl.feature.cfg
@@ -24,3 +24,14 @@ install = admin
 uninstall = admin
 start = admin
 stop = admin
+#
+# repo-add/repo-remove stay open to the viewer role (the plain form only 
reads/lists
+# a repository), but installing/uninstalling every feature in it via -i/-u 
requires
+# admin. The bare "= viewer" fallback is required: a command with a regex-only 
ACL
+# entry and no fallback is treated as admin-only for *every* invocation, 
because the
+# command guard decides visibility before it knows the actual arguments.
+#
+repo-add[/.*[-][i].*/] = admin
+repo-add = viewer
+repo-remove[/.*[-][u].*/] = admin
+repo-remove = viewer
diff --git a/itests/test/src/test/filtered-resources/etc/feature.xml 
b/itests/test/src/test/filtered-resources/etc/feature.xml
index ec36a8c8d6..b4c139af3d 100644
--- a/itests/test/src/test/filtered-resources/etc/feature.xml
+++ b/itests/test/src/test/filtered-resources/etc/feature.xml
@@ -107,6 +107,10 @@
                 uninstall = admin
                 start = admin
                 stop = admin
+                repo-add[/.*[-][i].*/] = admin
+                repo-add = viewer
+                repo-remove[/.*[-][u].*/] = admin
+                repo-remove = viewer
             </config>
             <bundle 
start-level="30">mvn:org.apache.karaf.features/org.apache.karaf.features.command/${project.version}</bundle>
         </conditional>
diff --git a/itests/test/src/test/java/org/apache/karaf/itests/FeatureTest.java 
b/itests/test/src/test/java/org/apache/karaf/itests/FeatureTest.java
index 18c3a839ef..cd28549e7f 100644
--- a/itests/test/src/test/java/org/apache/karaf/itests/FeatureTest.java
+++ b/itests/test/src/test/java/org/apache/karaf/itests/FeatureTest.java
@@ -76,7 +76,7 @@ public class FeatureTest extends BaseTest {
 
     @Test
     public void listCommandFromRepository() {
-        executeCommand("feature:repo-add 
mvn:org.apache.karaf.cellar/apache-karaf-cellar/3.0.0/xml/features");
+        executeCommand("feature:repo-add 
mvn:org.apache.karaf.cellar/apache-karaf-cellar/3.0.0/xml/features", new 
RolePrincipal("viewer"));
         String repositoryName = "karaf-cellar-3.0.0";
         String featureListOutput = executeCommand("feature:list --repository " 
+ repositoryName);
         assertFalse(featureListOutput.isEmpty());
@@ -150,17 +150,17 @@ public class FeatureTest extends BaseTest {
 
     @Test
     public void repoAddRemoveCommand() throws Exception {
-        System.out.println(executeCommand("feature:repo-add 
mvn:org.apache.karaf.cellar/apache-karaf-cellar/3.0.0/xml/features"));
+        System.out.println(executeCommand("feature:repo-add 
mvn:org.apache.karaf.cellar/apache-karaf-cellar/3.0.0/xml/features", new 
RolePrincipal("viewer")));
         assertContains("apache-karaf-cellar", 
executeCommand("feature:repo-list"));
-        System.out.println(executeCommand("feature:repo-remove 
mvn:org.apache.karaf.cellar/apache-karaf-cellar/3.0.0/xml/features"));
+        System.out.println(executeCommand("feature:repo-remove 
mvn:org.apache.karaf.cellar/apache-karaf-cellar/3.0.0/xml/features", new 
RolePrincipal("viewer")));
         assertContainsNot("apache-karaf-cellar", 
executeCommand("feature:repo-list"));
     }
 
     @Test
     public void repoAddRemoveCommandWithRegex() throws Exception {
-        System.out.println(executeCommand("feature:repo-add 
mvn:org.apache.karaf.cellar/apache-karaf-cellar/3.0.0/xml/features"));
+        System.out.println(executeCommand("feature:repo-add 
mvn:org.apache.karaf.cellar/apache-karaf-cellar/3.0.0/xml/features", new 
RolePrincipal("viewer")));
         assertContains("apache-karaf-cellar", 
executeCommand("feature:repo-list"));
-        System.out.println(executeCommand("feature:repo-remove 
'.*apache-karaf-cellar.*'"));
+        System.out.println(executeCommand("feature:repo-remove 
'.*apache-karaf-cellar.*'", new RolePrincipal("viewer")));
         assertContainsNot("apache-karaf-cellar", 
executeCommand("feature:repo-list"));
     }
 
diff --git 
a/itests/test/src/test/java/org/apache/karaf/itests/ssh/FeatureSshCommandSecurityTest.java
 
b/itests/test/src/test/java/org/apache/karaf/itests/ssh/FeatureSshCommandSecurityTest.java
index 0ec55c3d72..288a085026 100644
--- 
a/itests/test/src/test/java/org/apache/karaf/itests/ssh/FeatureSshCommandSecurityTest.java
+++ 
b/itests/test/src/test/java/org/apache/karaf/itests/ssh/FeatureSshCommandSecurityTest.java
@@ -64,4 +64,34 @@ public class FeatureSshCommandSecurityTest extends 
SshCommandTestBase {
         Assert.assertFalse(feature + " feature should have been uninstalled",
                 r5.contains(feature));
     }
+
+    @Test
+    public void testFeatureRepoCommandSecurityViaSsh() throws Exception {
+        // Skip on Windows where PTY output can be garbled,
+        // when upgrading to Junit5, this can be replaced with 
@DisabledOnOs(OS.WINDOWS)
+        // TODO: remove this once we have a better solution for PTY output on 
Windows
+        Assume.assumeFalse(System.getProperty("os.name", 
"").toLowerCase().contains("win"));
+
+        String vieweruser = "viewer" + System.nanoTime() + "_repos";
+        // Deliberately non-existent so repo-add/repo-remove never actually 
add, install, remove
+        // or uninstall anything; this test only cares about the ACL decision, 
not the outcome of
+        // the underlying operation (same approach as bundle:refresh -f 999 in 
BundleSshCommandSecurityTest).
+        // Note: the ACL regex matches against the whole argument list, so 
this URL must not
+        // itself contain "-i" or "-u" or it would coincidentally match the 
option-specific rules.
+        String bogusUrl = "file:///nonexistent/karaf-test-repo-features.xml";
+
+        addViewer(vieweruser);
+
+        // repo-add/repo-remove without options are not gated by any role, 
same as before this ACL
+        // was introduced: only the -i/-u options (which install/uninstall 
every feature in the
+        // repository) must require admin.
+        assertCommand(vieweruser, "feature:repo-add " + bogusUrl, Result.OK);
+        assertCommand(vieweruser, "feature:repo-remove " + bogusUrl, 
Result.OK);
+
+        assertCommand(vieweruser, "feature:repo-add -i " + bogusUrl, 
Result.NO_CREDENTIALS);
+        assertCommand(vieweruser, "feature:repo-remove -u " + bogusUrl, 
Result.NO_CREDENTIALS);
+
+        assertCommand("karaf", "feature:repo-add -i " + bogusUrl, Result.OK);
+        assertCommand("karaf", "feature:repo-remove -u " + bogusUrl, 
Result.OK);
+    }
 }
diff --git a/manual/src/main/asciidoc/user-guide/security.adoc 
b/manual/src/main/asciidoc/user-guide/security.adoc
index 7a736b02a1..fdc6817bbe 100644
--- a/manual/src/main/asciidoc/user-guide/security.adoc
+++ b/manual/src/main/asciidoc/user-guide/security.adoc
@@ -492,8 +492,9 @@ By default, Apache Karaf defines the following commands 
ACLs:
  the users with the `manager` role can execute `config:*` commands. As 
`config:install` writes an arbitrary file into
  the `etc` folder, it is restricted to the users with the `admin` role.
 * `etc/org.apache.karaf.command.acl.feature.cfg` configuration file defines 
the ACL for `feature:*` commands.
- Only the users with `admin` role can execute `feature:install`, 
`feature:uninstall`,`feature:start`, `feature:stop` and `feature:update` 
commands. The other `feature:*`
- commands can be executed by any user.
+ Only the users with `admin` role can execute `feature:install`, 
`feature:uninstall`,`feature:start`, `feature:stop` and `feature:update` 
commands. `feature:repo-add`
+ and `feature:repo-remove` require the `admin` role only when used with the 
`-i`/`--install` or `-u`/`--uninstall` option, which installs or uninstalls 
every
+ feature in the repository; otherwise they only require the `viewer` role. The 
other `feature:*` commands can be executed by any user.
 * `etc/org.apache.karaf.command.acl.jaas.cfg` configuration file defines the 
ACL for `jaas:*` commands.
  Only the users with `admin` role can execute `jaas:update` command. The other 
`jaas:*` commands can be executed by any
  user.

Reply via email to