Skip to content

Commit 3929df8

Browse files
author
FTMahringer
committed
feat(store): add policy controls for v2.6.3-dev
Add store policy enforcement across backend, CLI role checks, and dashboard controls for the v2.6.3-dev cycle.
1 parent 8f0c212 commit 3929df8

22 files changed

Lines changed: 887 additions & 13 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# v2.6.3-dev
2+
3+
Store Policy & Admin Controls for the v2.7.0 plugin ecosystem milestone.
4+
5+
## Added
6+
7+
- Store policy storage and enforcement for plugin IDs, authors, tags, and trust badges.
8+
- Store policy API endpoints under `/api/store/policy`.
9+
- Dashboard settings controls for editing store policy.
10+
- CLI role checks for mutating plugin commands based on the cached login role.
11+
12+
## Validation
13+
14+
```bash
15+
cd packages/core
16+
mvn -q -Dtest=PluginSafetyServiceTest,StorePolicyServiceTest,PluginRegistryServiceTest test
17+
18+
cd ../cli
19+
go test ./cmd -run 'TestRequirePluginOperator|TestSetTokenPersistsRole'
20+
21+
cd ../dashboard/frontend
22+
npm run build
23+
```

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ Thumbs.db
2727
/.installer-tmp
2828
/go-tui-incubator
2929
/packages/cli/cmd/*.go.*
30+
/packages/dashboard/frontend/dist/
31+
/packages/dashboard/frontend/node_modules/

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
---
1111

12+
## [v2.6.3-dev] - 2026-05-18
13+
14+
**Store Policy & Admin Controls**
15+
16+
### Added
17+
- Store policy storage with allow/block lists for plugin IDs, authors, tags, and trust badges.
18+
- Store policy APIs under `/api/store/policy` and a dashboard settings panel for editing them.
19+
- Store policy filtering in store listings and install-time enforcement for community and untrusted entries.
20+
- Role-gated CLI checks for mutating plugin commands, backed by the cached login role.
21+
22+
### Changed
23+
- Core, dashboard, and plugin registry version defaults now advance to `2.6.3-dev`.
24+
- Community store entries are hidden when store policy disables them.
25+
- Plugin install and bundle install flows now respect store policy before proceeding.
26+
27+
### Fixed
28+
- Linux-first backend, CLI, and dashboard validation paths remain green on Ubuntu 24.04 WSL2.
29+
1230
## [v2.6.2-dev] - 2026-05-18
1331

1432
**Custom Registry Sources**

packages/cli/cmd/auth.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ var loginCmd = &cobra.Command{
5353
return fmt.Errorf("login failed: %w", err)
5454
}
5555

56-
if err := config.SetToken(profile, resp.Token); err != nil {
56+
if err := config.SetToken(profile, resp.Token, resp.Role); err != nil {
5757
return fmt.Errorf("failed to save token: %w", err)
5858
}
5959

@@ -85,6 +85,9 @@ var sessionCmd = &cobra.Command{
8585
tuioutput.Header("Session")
8686
tuioutput.KV("Profile", profile)
8787
tuioutput.KV("Host", p.Host)
88+
if p.Role != "" {
89+
tuioutput.KV("Role", p.Role)
90+
}
8891
if p.Token != "" {
8992
tuioutput.KV("Token", p.Token[:min(12, len(p.Token))]+"...")
9093
} else {

packages/cli/cmd/plugins.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"github.com/spf13/cobra"
88
"github.com/synapse-dev/synapse-cli/internal/commandfeatures"
9+
"github.com/synapse-dev/synapse-cli/internal/config"
910
tuioutput "github.com/synapse-dev/synapse-cli/internal/output"
1011
)
1112

@@ -105,6 +106,9 @@ var pluginsLoadCmd = &cobra.Command{
105106
Args: cobra.ExactArgs(1),
106107
RunE: func(cmd *cobra.Command, args []string) error {
107108
client := clientFromCmd(cmd)
109+
if err := requirePluginOperator(cmd, "load plugin"); err != nil {
110+
return err
111+
}
108112
var resp map[string]any
109113
if err := client.Post("/api/plugins/"+args[0]+"/load", nil, &resp); err != nil {
110114
return err
@@ -120,6 +124,9 @@ var pluginsUnloadCmd = &cobra.Command{
120124
Args: cobra.ExactArgs(1),
121125
RunE: func(cmd *cobra.Command, args []string) error {
122126
client := clientFromCmd(cmd)
127+
if err := requirePluginOperator(cmd, "unload plugin"); err != nil {
128+
return err
129+
}
123130
if err := client.Post("/api/plugins/"+args[0]+"/unload", nil, nil); err != nil {
124131
return err
125132
}
@@ -134,6 +141,9 @@ var pluginsReloadCmd = &cobra.Command{
134141
Args: cobra.ExactArgs(1),
135142
RunE: func(cmd *cobra.Command, args []string) error {
136143
client := clientFromCmd(cmd)
144+
if err := requirePluginOperator(cmd, "reload plugin"); err != nil {
145+
return err
146+
}
137147
var resp map[string]any
138148
if err := client.Post("/api/plugins/"+args[0]+"/reload", nil, &resp); err != nil {
139149
return err
@@ -149,6 +159,9 @@ var pluginsEnableCmd = &cobra.Command{
149159
Args: cobra.ExactArgs(1),
150160
RunE: func(cmd *cobra.Command, args []string) error {
151161
client := clientFromCmd(cmd)
162+
if err := requirePluginOperator(cmd, "enable plugin"); err != nil {
163+
return err
164+
}
152165
var resp map[string]any
153166
if err := client.Post("/api/plugins/"+args[0]+"/enable", nil, &resp); err != nil {
154167
return err
@@ -164,6 +177,9 @@ var pluginsDisableCmd = &cobra.Command{
164177
Args: cobra.ExactArgs(1),
165178
RunE: func(cmd *cobra.Command, args []string) error {
166179
client := clientFromCmd(cmd)
180+
if err := requirePluginOperator(cmd, "disable plugin"); err != nil {
181+
return err
182+
}
167183
var resp map[string]any
168184
if err := client.Post("/api/plugins/"+args[0]+"/disable", nil, &resp); err != nil {
169185
return err
@@ -179,6 +195,9 @@ var pluginsUninstallCmd = &cobra.Command{
179195
Args: cobra.ExactArgs(1),
180196
RunE: func(cmd *cobra.Command, args []string) error {
181197
client := clientFromCmd(cmd)
198+
if err := requirePluginOperator(cmd, "uninstall plugin"); err != nil {
199+
return err
200+
}
182201
if err := client.Delete("/api/plugins/" + args[0]); err != nil {
183202
return err
184203
}
@@ -193,6 +212,9 @@ var pluginsInstallCmd = &cobra.Command{
193212
Args: cobra.ExactArgs(1),
194213
RunE: func(cmd *cobra.Command, args []string) error {
195214
client := clientFromCmd(cmd)
215+
if err := requirePluginOperator(cmd, "install plugin"); err != nil {
216+
return err
217+
}
196218

197219
var manifest map[string]any
198220
if err := client.Post("/api/plugins/install", args[0], &manifest); err != nil {
@@ -405,6 +427,9 @@ var pluginsPromoteCmd = &cobra.Command{
405427
Short: "Promote all staging JARs to system/",
406428
RunE: func(cmd *cobra.Command, args []string) error {
407429
client := clientFromCmd(cmd)
430+
if err := requirePluginOperator(cmd, "promote plugin staging JARs"); err != nil {
431+
return err
432+
}
408433
if err := client.Post("/api/plugins/loader/promote", nil, nil); err != nil {
409434
return err
410435
}
@@ -418,6 +443,9 @@ var pluginsPublishCmd = &cobra.Command{
418443
Short: "Print submission guidance for publishing a plugin",
419444
Args: cobra.ExactArgs(1),
420445
RunE: func(cmd *cobra.Command, args []string) error {
446+
if err := requirePluginOperator(cmd, "publish plugin"); err != nil {
447+
return err
448+
}
421449
tuioutput.Header("Plugin Publishing Guide")
422450
tuioutput.Separator()
423451
tuioutput.Row("Official plugins:", "Submit PR to github.com/FTMahringer/synapse-plugins")
@@ -459,3 +487,15 @@ func init() {
459487
commandfeatures.BindSubcommandListing(pluginsCmd)
460488
rootCmd.AddCommand(pluginsCmd)
461489
}
490+
491+
func requirePluginOperator(cmd *cobra.Command, action string) error {
492+
profile, _ := cmd.Flags().GetString("profile")
493+
role := strings.ToUpper(strings.TrimSpace(config.GetProfile(profile).Role))
494+
if role == "ADMIN" || role == "OWNER" {
495+
return nil
496+
}
497+
if role == "" {
498+
role = "unset"
499+
}
500+
return fmt.Errorf("%s requires ADMIN or OWNER role (current role: %s)", action, role)
501+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package cmd
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
9+
"github.com/spf13/cobra"
10+
"github.com/spf13/viper"
11+
"github.com/synapse-dev/synapse-cli/internal/config"
12+
)
13+
14+
func TestRequirePluginOperatorRejectsViewer(t *testing.T) {
15+
viper.Set("profiles.default.host", "http://localhost:8080")
16+
viper.Set("profiles.default.role", "VIEWER")
17+
18+
cmd := &cobra.Command{}
19+
cmd.Flags().StringP("profile", "p", "default", "")
20+
21+
if err := requirePluginOperator(cmd, "install plugin"); err == nil {
22+
t.Fatal("expected role restriction error")
23+
}
24+
}
25+
26+
func TestRequirePluginOperatorAllowsAdmin(t *testing.T) {
27+
viper.Set("profiles.default.host", "http://localhost:8080")
28+
viper.Set("profiles.default.role", "ADMIN")
29+
30+
cmd := &cobra.Command{}
31+
cmd.Flags().StringP("profile", "p", "default", "")
32+
33+
if err := requirePluginOperator(cmd, "install plugin"); err != nil {
34+
t.Fatalf("expected admin access, got %v", err)
35+
}
36+
}
37+
38+
func TestSetTokenPersistsRole(t *testing.T) {
39+
home := t.TempDir()
40+
t.Setenv("HOME", home)
41+
viper.Reset()
42+
43+
if err := config.SetToken("default", "token-value", "OWNER"); err != nil {
44+
t.Fatalf("set token: %v", err)
45+
}
46+
47+
data, err := os.ReadFile(filepath.Join(home, ".synapse", "config.yaml"))
48+
if err != nil {
49+
t.Fatalf("read config: %v", err)
50+
}
51+
if !strings.Contains(string(data), "role: OWNER") {
52+
t.Fatalf("expected role to be persisted, got %s", string(data))
53+
}
54+
}

packages/cli/internal/config/config.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
type Profile struct {
1212
Host string `mapstructure:"host"`
1313
Token string `mapstructure:"token"`
14+
Role string `mapstructure:"role"`
1415
}
1516

1617
// Init loads config from ~/.synapse/config.yaml
@@ -46,7 +47,7 @@ func GetProfile(name string) Profile {
4647
return p
4748
}
4849

49-
func SetToken(profile, token string) error {
50+
func SetToken(profile, token, role string) error {
5051
home, err := os.UserHomeDir()
5152
if err != nil {
5253
return err
@@ -58,11 +59,13 @@ func SetToken(profile, token string) error {
5859
}
5960

6061
viper.Set("profiles."+profile+".token", token)
62+
viper.Set("profiles."+profile+".role", role)
6163
return viper.WriteConfigAs(filepath.Join(dir, "config.yaml"))
6264
}
6365

6466
func ClearToken(profile string) error {
6567
viper.Set("profiles."+profile+".token", "")
68+
viper.Set("profiles."+profile+".role", "")
6669
home, _ := os.UserHomeDir()
6770
return viper.WriteConfigAs(filepath.Join(home, ".synapse", "config.yaml"))
6871
}

packages/core/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
<groupId>dev.synapse</groupId>
1717
<artifactId>synapse-core</artifactId>
18-
<version>2.6.2-dev</version>
18+
<version>2.6.3-dev</version>
1919
<name>synapse-core</name>
2020
<description>SYNAPSE Spring Boot backend</description>
2121

packages/core/src/main/java/dev/synapse/plugins/BundleInstallService.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,20 @@ public class BundleInstallService {
2525
private final StoreEntryRepository storeEntryRepository;
2626
private final PluginLifecycleService lifecycleService;
2727
private final PluginRepository pluginRepository;
28+
private final StorePolicyService storePolicyService;
2829
private final SystemLogService logService;
2930

3031
public BundleInstallService(
3132
StoreEntryRepository storeEntryRepository,
3233
PluginLifecycleService lifecycleService,
3334
PluginRepository pluginRepository,
35+
StorePolicyService storePolicyService,
3436
SystemLogService logService
3537
) {
3638
this.storeEntryRepository = storeEntryRepository;
3739
this.lifecycleService = lifecycleService;
3840
this.pluginRepository = pluginRepository;
41+
this.storePolicyService = storePolicyService;
3942
this.logService = logService;
4043
}
4144

@@ -76,13 +79,15 @@ public BundleInstallResult installBundle(String bundleId) {
7679

7780
StoreEntry bundle = storeEntryRepository.findById(bundleId)
7881
.orElseThrow(() -> new ResourceNotFoundException("Bundle", bundleId));
82+
storePolicyService.assertInstallAllowed(bundle);
7983

8084
List<String> installed = new ArrayList<>();
8185
List<String> skipped = new ArrayList<>();
8286

8387
for (String pluginId : extractPluginIds(bundle)) {
8488
StoreEntry entry = storeEntryRepository.findById(pluginId).orElse(null);
8589
if (entry == null) continue;
90+
storePolicyService.assertInstallAllowed(entry);
8691

8792
if (pluginRepository.existsById(pluginId)) {
8893
skipped.add(pluginId);

packages/core/src/main/java/dev/synapse/plugins/PluginRegistryService.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public PluginRegistryService(
4848
RegistrySourceRepository registrySourceRepository,
4949
RegistryProperties registryProperties,
5050
RestClient restClient,
51-
@Value("${synapse.version:${spring.application.version:v2.6.2-dev}}") String synapseVersion
51+
@Value("${synapse.version:${spring.application.version:v2.6.3-dev}}") String synapseVersion
5252
) {
5353
this.storeRegistryService = storeRegistryService;
5454
this.registrySourceRepository = registrySourceRepository;
@@ -95,20 +95,42 @@ public RegistryMetadata findEntries(
9595
}
9696

9797
public List<StoreEntry> findStoreEntries(String type, int page, int size) {
98-
return findEntries(null, null, type, false)
98+
return findStoreEntries(type, page, size, null);
99+
}
100+
101+
public List<StoreEntry> findStoreEntries(
102+
String type,
103+
int page,
104+
int size,
105+
StorePolicyService policyService
106+
) {
107+
List<StoreEntry> entries = findEntries(null, null, type, false)
99108
.entries()
100109
.stream()
101110
.filter(this::isStoreVisible)
111+
.toList();
112+
if (policyService != null) {
113+
entries = policyService.filterEntries(entries);
114+
}
115+
return entries
116+
.stream()
102117
.skip((long) Math.max(page, 0) * Math.max(size, 1))
103118
.limit(Math.max(size, 1))
104119
.toList();
105120
}
106121

107122
public StoreEntry findStoreEntry(String id) {
123+
return findStoreEntry(id, null);
124+
}
125+
126+
public StoreEntry findStoreEntry(String id, StorePolicyService policyService) {
108127
StoreEntry entry = storeRegistryService.findById(id);
109128
if (entry == null || !isStoreVisible(entry)) {
110129
return null;
111130
}
131+
if (policyService != null) {
132+
return policyService.requireVisible(entry);
133+
}
112134
return entry;
113135
}
114136

0 commit comments

Comments
 (0)