This is an automated email from the ASF dual-hosted git repository.
jbonofre pushed a commit to branch karaf-4.4.x
in repository https://gitbox.apache.org/repos/asf/karaf.git
The following commit(s) were added to refs/heads/karaf-4.4.x by this push:
new 73c1df0104 [karaf-4.4.x] Restrict feature:repo-add -i and
feature:repo-remove -u to admin role (#2884)
73c1df0104 is described below
commit 73c1df0104b834319fcd3573f50b768e27c97734
Author: JB Onofré <[email protected]>
AuthorDate: Sat Sep 12 13:07:18 2026 +0200
[karaf-4.4.x] Restrict feature:repo-add -i and feature:repo-remove -u to
admin role (#2884)
* 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.
Backport of #2883 to karaf-4.4.x.
* Fix CI: add open fallback ACL rule for plain feature:repo-add/repo-remove
Same issue as apache/karaf#2883: 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
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.
* Restore viewer access to plain feature:repo-add/repo-remove
Same fix as apache/karaf#2883. The previous "Fix CI" commit here added
the "repo-add = viewer" / "repo-remove = viewer" fallback needed to keep
the plain command visible to non-admin sessions (Karaf's shell guard
decides command visibility before it knows the actual arguments, so a
command with only a regex/exact-arg ACL entry is otherwise treated as
requiring that entry's role for every invocation). That part fixed
FeatureSshCommandSecurityTest, but the pre-existing FeatureTest cases
that called repo-add/repo-remove with no role at all (fine previously,
since the commands had no ACL entry) still failed: once a command has
any ACL entry, invoking it requires holding a role.
Pass an explicit viewer RolePrincipal from those three FeatureTest cases,
and document the viewer/admin split in security.adoc.
---
.../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 | 25 ++++++++++++++++++++++
manual/src/main/asciidoc/user-guide/security.adoc | 5 +++--
6 files changed, 59 insertions(+), 7 deletions(-)
diff --git a/assemblies/features/standard/src/main/feature/feature.xml
b/assemblies/features/standard/src/main/feature/feature.xml
index 0dab1e9c9e..697ba9a055 100644
--- a/assemblies/features/standard/src/main/feature/feature.xml
+++ b/assemblies/features/standard/src/main/feature/feature.xml
@@ -151,6 +151,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 b34dfff46a..03726969db 100644
--- a/itests/test/src/test/filtered-resources/etc/feature.xml
+++ b/itests/test/src/test/filtered-resources/etc/feature.xml
@@ -110,6 +110,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 6d6a6595fd..d6e4755379 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
@@ -77,7 +77,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());
@@ -151,17 +151,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 662b7c14e2..77eb84e77a 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
@@ -55,4 +55,29 @@ public class FeatureSshCommandSecurityTest extends
SshCommandTestBase {
Assert.assertFalse(feature + " feature should have been uninstalled",
r5.contains(feature));
}
+
+ @Test
+ public void testFeatureRepoCommandSecurityViaSsh() throws Exception {
+ 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.
+ // 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.