From 04e8309e607e750c922d93ee6594ec70ad979f8b Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Mon, 11 Nov 2024 18:51:14 +0100
Subject: [PATCH] Add server config to disable user registered agents (#4206)
---
cmd/server/flags.go | 5 +
cmd/server/setup.go | 3 +
.../30-administration/10-server-config.md | 20 ++
server/api/agent_test.go | 275 ++++++++++++++++++
server/config.go | 3 +
server/queue/mocks/queue.go | 259 +++++++++++++++++
server/queue/queue.go | 2 +
server/router/api.go | 11 +-
server/web/config.go | 14 +-
web/src/compositions/useConfig.ts | 2 +
web/src/views/User.vue | 2 +-
web/src/views/org/OrgSettings.vue | 3 +-
12 files changed, 587 insertions(+), 12 deletions(-)
create mode 100644 server/api/agent_test.go
create mode 100644 server/queue/mocks/queue.go
diff --git a/cmd/server/flags.go b/cmd/server/flags.go
index 3e8bbd8c2..2ad82b9ac 100644
--- a/cmd/server/flags.go
+++ b/cmd/server/flags.go
@@ -213,6 +213,11 @@ var flags = append([]cli.Flag{
Name: "agent-secret",
Usage: "server-agent shared password",
},
+ &cli.BoolFlag{
+ Sources: cli.EnvVars("WOODPECKER_DISABLE_USER_AGENT_REGISTRATION"),
+ Name: "disable-user-agent-registration",
+ Usage: "Disable user registered agents",
+ },
&cli.DurationFlag{
Sources: cli.EnvVars("WOODPECKER_KEEPALIVE_MIN_TIME"),
Name: "keepalive-min-time",
diff --git a/cmd/server/setup.go b/cmd/server/setup.go
index 83a92f01f..608a5ba14 100644
--- a/cmd/server/setup.go
+++ b/cmd/server/setup.go
@@ -167,6 +167,9 @@ func setupEvilGlobals(ctx context.Context, c *cli.Command, s store.Store) (err e
return fmt.Errorf("could not setup log store: %w", err)
}
+ // agents
+ server.Config.Agent.DisableUserRegisteredAgentRegistration = c.Bool("disable-user-agent-registration")
+
// authentication
server.Config.Pipeline.AuthenticatePublicRepos = c.Bool("authenticate-public-repos")
diff --git a/docs/docs/30-administration/10-server-config.md b/docs/docs/30-administration/10-server-config.md
index 17da7abd2..6b9297f6a 100644
--- a/docs/docs/30-administration/10-server-config.md
+++ b/docs/docs/30-administration/10-server-config.md
@@ -54,6 +54,20 @@ Use the `WOODPECKER_REPO_OWNERS` variable to filter which GitHub user's repos sh
WOODPECKER_REPO_OWNERS=my_company,my_company_oss_github_user
```
+## Disallow normal users to create agents
+
+By default, users can create new agents for their repos they have admin access to.
+If an instance admin doesn't want this feature enabled, they can disable the API and hide the Web UI elements.
+
+:::note
+You should set this option if you have, for example,
+global secrets and don't trust your users to create a rogue agent and pipeline for secret extraction.
+:::
+
+```ini
+WOODPECKER_DISABLE_USER_AGENT_REGISTRATION=true
+```
+
## Global registry setting
If you want to make available a specific private registry to all pipelines, use the `WOODPECKER_DOCKER_CONFIG` server configuration.
@@ -422,6 +436,12 @@ A shared secret used by server and agents to authenticate communication. A secre
Read the value for `WOODPECKER_AGENT_SECRET` from the specified filepath
+### `WOODPECKER_DISABLE_USER_AGENT_REGISTRATION`
+
+> Default: false
+
+[Read about "Disallow normal users to create agents"](./10-server-config.md#disallow-normal-users-to-create-agents)
+
### `WOODPECKER_KEEPALIVE_MIN_TIME`
> Default: empty
diff --git a/server/api/agent_test.go b/server/api/agent_test.go
new file mode 100644
index 000000000..0a87d99e0
--- /dev/null
+++ b/server/api/agent_test.go
@@ -0,0 +1,275 @@
+// Copyright 2024 Woodpecker Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package api
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/gin-gonic/gin"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+
+ "go.woodpecker-ci.org/woodpecker/v2/server"
+ "go.woodpecker-ci.org/woodpecker/v2/server/model"
+ "go.woodpecker-ci.org/woodpecker/v2/server/queue"
+ queue_mocks "go.woodpecker-ci.org/woodpecker/v2/server/queue/mocks"
+ mocks_manager "go.woodpecker-ci.org/woodpecker/v2/server/services/mocks"
+ store_mocks "go.woodpecker-ci.org/woodpecker/v2/server/store/mocks"
+ "go.woodpecker-ci.org/woodpecker/v2/server/store/types"
+)
+
+var fakeAgent = &model.Agent{
+ ID: 1,
+ Name: "test-agent",
+ OwnerID: 1,
+ NoSchedule: false,
+}
+
+func TestGetAgents(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+
+ t.Run("should get agents", func(t *testing.T) {
+ agents := []*model.Agent{fakeAgent}
+
+ mockStore := store_mocks.NewStore(t)
+ mockStore.On("AgentList", mock.Anything).Return(agents, nil)
+
+ w := httptest.NewRecorder()
+ c, _ := gin.CreateTestContext(w)
+ c.Set("store", mockStore)
+
+ GetAgents(c)
+ c.Writer.WriteHeaderNow()
+
+ mockStore.AssertCalled(t, "AgentList", mock.Anything)
+ assert.Equal(t, http.StatusOK, w.Code)
+
+ var response []*model.Agent
+ err := json.Unmarshal(w.Body.Bytes(), &response)
+ assert.NoError(t, err)
+ assert.Equal(t, agents, response)
+ })
+}
+
+func TestGetAgent(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+
+ t.Run("should get agent", func(t *testing.T) {
+ mockStore := store_mocks.NewStore(t)
+ mockStore.On("AgentFind", int64(1)).Return(fakeAgent, nil)
+
+ w := httptest.NewRecorder()
+ c, _ := gin.CreateTestContext(w)
+ c.Set("store", mockStore)
+ c.Params = gin.Params{{Key: "agent_id", Value: "1"}}
+
+ GetAgent(c)
+ c.Writer.WriteHeaderNow()
+
+ mockStore.AssertCalled(t, "AgentFind", int64(1))
+ assert.Equal(t, http.StatusOK, w.Code)
+
+ var response model.Agent
+ err := json.Unmarshal(w.Body.Bytes(), &response)
+ assert.NoError(t, err)
+ assert.Equal(t, fakeAgent, &response)
+ })
+
+ t.Run("should return bad request for invalid agent id", func(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := gin.CreateTestContext(w)
+ c.Params = gin.Params{{Key: "agent_id", Value: "invalid"}}
+
+ GetAgent(c)
+ c.Writer.WriteHeaderNow()
+
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+ })
+
+ t.Run("should return not found for non-existent agent", func(t *testing.T) {
+ mockStore := store_mocks.NewStore(t)
+ mockStore.On("AgentFind", int64(2)).Return((*model.Agent)(nil), types.RecordNotExist)
+
+ w := httptest.NewRecorder()
+ c, _ := gin.CreateTestContext(w)
+ c.Set("store", mockStore)
+ c.Params = gin.Params{{Key: "agent_id", Value: "2"}}
+
+ GetAgent(c)
+ c.Writer.WriteHeaderNow()
+
+ assert.Equal(t, http.StatusNotFound, w.Code)
+ })
+}
+
+func TestPatchAgent(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+
+ t.Run("should update agent", func(t *testing.T) {
+ updatedAgent := *fakeAgent
+ updatedAgent.Name = "updated-agent"
+
+ mockStore := store_mocks.NewStore(t)
+ mockStore.On("AgentFind", int64(1)).Return(fakeAgent, nil)
+ mockStore.On("AgentUpdate", mock.AnythingOfType("*model.Agent")).Return(nil)
+
+ mockManager := mocks_manager.NewManager(t)
+ server.Config.Services.Manager = mockManager
+
+ w := httptest.NewRecorder()
+ c, _ := gin.CreateTestContext(w)
+ c.Set("store", mockStore)
+ c.Params = gin.Params{{Key: "agent_id", Value: "1"}}
+ c.Request, _ = http.NewRequest(http.MethodPatch, "/", strings.NewReader(`{"name":"updated-agent"}`))
+ c.Request.Header.Set("Content-Type", "application/json")
+
+ PatchAgent(c)
+ c.Writer.WriteHeaderNow()
+
+ mockStore.AssertCalled(t, "AgentFind", int64(1))
+ mockStore.AssertCalled(t, "AgentUpdate", mock.AnythingOfType("*model.Agent"))
+ assert.Equal(t, http.StatusOK, w.Code)
+
+ var response model.Agent
+ err := json.Unmarshal(w.Body.Bytes(), &response)
+ assert.NoError(t, err)
+ assert.Equal(t, "updated-agent", response.Name)
+ })
+}
+
+func TestPostAgent(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+
+ t.Run("should create agent", func(t *testing.T) {
+ newAgent := &model.Agent{
+ Name: "new-agent",
+ NoSchedule: false,
+ }
+
+ mockStore := store_mocks.NewStore(t)
+ mockStore.On("AgentCreate", mock.AnythingOfType("*model.Agent")).Return(nil)
+
+ w := httptest.NewRecorder()
+ c, _ := gin.CreateTestContext(w)
+ c.Set("store", mockStore)
+ c.Set("user", &model.User{ID: 1})
+ c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"name":"new-agent"}`))
+ c.Request.Header.Set("Content-Type", "application/json")
+
+ PostAgent(c)
+ c.Writer.WriteHeaderNow()
+
+ mockStore.AssertCalled(t, "AgentCreate", mock.AnythingOfType("*model.Agent"))
+ assert.Equal(t, http.StatusOK, w.Code)
+
+ var response model.Agent
+ err := json.Unmarshal(w.Body.Bytes(), &response)
+ assert.NoError(t, err)
+ assert.Equal(t, newAgent.Name, response.Name)
+ assert.NotEmpty(t, response.Token)
+ })
+}
+
+func TestDeleteAgent(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+
+ t.Run("should delete agent", func(t *testing.T) {
+ mockStore := store_mocks.NewStore(t)
+ mockStore.On("AgentFind", int64(1)).Return(fakeAgent, nil)
+ mockStore.On("AgentDelete", mock.AnythingOfType("*model.Agent")).Return(nil)
+
+ mockManager := mocks_manager.NewManager(t)
+ server.Config.Services.Manager = mockManager
+
+ mockQueue := queue_mocks.NewQueue(t)
+ mockQueue.On("Info", mock.Anything).Return(queue.InfoT{})
+ mockQueue.On("KickAgentWorkers", int64(1)).Return()
+ server.Config.Services.Queue = mockQueue
+
+ w := httptest.NewRecorder()
+ c, _ := gin.CreateTestContext(w)
+ c.Set("store", mockStore)
+ c.Params = gin.Params{{Key: "agent_id", Value: "1"}}
+
+ DeleteAgent(c)
+ c.Writer.WriteHeaderNow()
+
+ mockStore.AssertCalled(t, "AgentFind", int64(1))
+ mockStore.AssertCalled(t, "AgentDelete", mock.AnythingOfType("*model.Agent"))
+ mockQueue.AssertCalled(t, "KickAgentWorkers", int64(1))
+ assert.Equal(t, http.StatusNoContent, w.Code)
+ })
+
+ t.Run("should not delete agent with running tasks", func(t *testing.T) {
+ mockStore := store_mocks.NewStore(t)
+ mockStore.On("AgentFind", int64(1)).Return(fakeAgent, nil)
+
+ mockManager := mocks_manager.NewManager(t)
+ server.Config.Services.Manager = mockManager
+
+ mockQueue := queue_mocks.NewQueue(t)
+ mockQueue.On("Info", mock.Anything).Return(queue.InfoT{
+ Running: []*model.Task{{AgentID: 1}},
+ })
+ server.Config.Services.Queue = mockQueue
+
+ w := httptest.NewRecorder()
+ c, _ := gin.CreateTestContext(w)
+ c.Set("store", mockStore)
+ c.Params = gin.Params{{Key: "agent_id", Value: "1"}}
+
+ DeleteAgent(c)
+ c.Writer.WriteHeaderNow()
+
+ mockStore.AssertCalled(t, "AgentFind", int64(1))
+ mockStore.AssertNotCalled(t, "AgentDelete", mock.Anything)
+ assert.Equal(t, http.StatusConflict, w.Code)
+ })
+}
+
+func TestPostOrgAgent(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+
+ t.Run("create org agent should succeed", func(t *testing.T) {
+ mockStore := store_mocks.NewStore(t)
+ mockStore.On("AgentCreate", mock.AnythingOfType("*model.Agent")).Return(nil)
+
+ w := httptest.NewRecorder()
+ c, _ := gin.CreateTestContext(w)
+ c.Set("store", mockStore)
+
+ // Set up a non-admin user
+ c.Set("user", &model.User{
+ ID: 1,
+ Admin: false,
+ })
+
+ c.Params = gin.Params{{Key: "org_id", Value: "1"}}
+ c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"name":"new-agent"}`))
+ c.Request.Header.Set("Content-Type", "application/json")
+
+ PostOrgAgent(c)
+ c.Writer.WriteHeaderNow()
+
+ assert.Equal(t, http.StatusOK, w.Code)
+
+ // Ensure an agent was created
+ mockStore.AssertCalled(t, "AgentCreate", mock.AnythingOfType("*model.Agent"))
+ })
+}
diff --git a/server/config.go b/server/config.go
index fb6c42292..3349e535b 100644
--- a/server/config.go
+++ b/server/config.go
@@ -54,6 +54,9 @@ var Config = struct {
CustomCSSFile string
CustomJsFile string
}
+ Agent struct {
+ DisableUserRegisteredAgentRegistration bool
+ }
WebUI struct {
EnableSwagger bool
SkipVersionCheck bool
diff --git a/server/queue/mocks/queue.go b/server/queue/mocks/queue.go
new file mode 100644
index 000000000..ad8686971
--- /dev/null
+++ b/server/queue/mocks/queue.go
@@ -0,0 +1,259 @@
+// Code generated by mockery. DO NOT EDIT.
+
+//go:build test
+// +build test
+
+package mocks
+
+import (
+ context "context"
+
+ mock "github.com/stretchr/testify/mock"
+ model "go.woodpecker-ci.org/woodpecker/v2/server/model"
+
+ queue "go.woodpecker-ci.org/woodpecker/v2/server/queue"
+)
+
+// Queue is an autogenerated mock type for the Queue type
+type Queue struct {
+ mock.Mock
+}
+
+// Done provides a mock function with given fields: c, id, exitStatus
+func (_m *Queue) Done(c context.Context, id string, exitStatus model.StatusValue) error {
+ ret := _m.Called(c, id, exitStatus)
+
+ if len(ret) == 0 {
+ panic("no return value specified for Done")
+ }
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(context.Context, string, model.StatusValue) error); ok {
+ r0 = rf(c, id, exitStatus)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// Error provides a mock function with given fields: c, id, err
+func (_m *Queue) Error(c context.Context, id string, err error) error {
+ ret := _m.Called(c, id, err)
+
+ if len(ret) == 0 {
+ panic("no return value specified for Error")
+ }
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(context.Context, string, error) error); ok {
+ r0 = rf(c, id, err)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// ErrorAtOnce provides a mock function with given fields: c, ids, err
+func (_m *Queue) ErrorAtOnce(c context.Context, ids []string, err error) error {
+ ret := _m.Called(c, ids, err)
+
+ if len(ret) == 0 {
+ panic("no return value specified for ErrorAtOnce")
+ }
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(context.Context, []string, error) error); ok {
+ r0 = rf(c, ids, err)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// Evict provides a mock function with given fields: c, id
+func (_m *Queue) Evict(c context.Context, id string) error {
+ ret := _m.Called(c, id)
+
+ if len(ret) == 0 {
+ panic("no return value specified for Evict")
+ }
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
+ r0 = rf(c, id)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// EvictAtOnce provides a mock function with given fields: c, ids
+func (_m *Queue) EvictAtOnce(c context.Context, ids []string) error {
+ ret := _m.Called(c, ids)
+
+ if len(ret) == 0 {
+ panic("no return value specified for EvictAtOnce")
+ }
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(context.Context, []string) error); ok {
+ r0 = rf(c, ids)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// Extend provides a mock function with given fields: c, agentID, workflowID
+func (_m *Queue) Extend(c context.Context, agentID int64, workflowID string) error {
+ ret := _m.Called(c, agentID, workflowID)
+
+ if len(ret) == 0 {
+ panic("no return value specified for Extend")
+ }
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(context.Context, int64, string) error); ok {
+ r0 = rf(c, agentID, workflowID)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// Info provides a mock function with given fields: c
+func (_m *Queue) Info(c context.Context) queue.InfoT {
+ ret := _m.Called(c)
+
+ if len(ret) == 0 {
+ panic("no return value specified for Info")
+ }
+
+ var r0 queue.InfoT
+ if rf, ok := ret.Get(0).(func(context.Context) queue.InfoT); ok {
+ r0 = rf(c)
+ } else {
+ r0 = ret.Get(0).(queue.InfoT)
+ }
+
+ return r0
+}
+
+// KickAgentWorkers provides a mock function with given fields: agentID
+func (_m *Queue) KickAgentWorkers(agentID int64) {
+ _m.Called(agentID)
+}
+
+// Pause provides a mock function with given fields:
+func (_m *Queue) Pause() {
+ _m.Called()
+}
+
+// Poll provides a mock function with given fields: c, agentID, f
+func (_m *Queue) Poll(c context.Context, agentID int64, f queue.FilterFn) (*model.Task, error) {
+ ret := _m.Called(c, agentID, f)
+
+ if len(ret) == 0 {
+ panic("no return value specified for Poll")
+ }
+
+ var r0 *model.Task
+ var r1 error
+ if rf, ok := ret.Get(0).(func(context.Context, int64, queue.FilterFn) (*model.Task, error)); ok {
+ return rf(c, agentID, f)
+ }
+ if rf, ok := ret.Get(0).(func(context.Context, int64, queue.FilterFn) *model.Task); ok {
+ r0 = rf(c, agentID, f)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*model.Task)
+ }
+ }
+
+ if rf, ok := ret.Get(1).(func(context.Context, int64, queue.FilterFn) error); ok {
+ r1 = rf(c, agentID, f)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
+// Push provides a mock function with given fields: c, task
+func (_m *Queue) Push(c context.Context, task *model.Task) error {
+ ret := _m.Called(c, task)
+
+ if len(ret) == 0 {
+ panic("no return value specified for Push")
+ }
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(context.Context, *model.Task) error); ok {
+ r0 = rf(c, task)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// PushAtOnce provides a mock function with given fields: c, tasks
+func (_m *Queue) PushAtOnce(c context.Context, tasks []*model.Task) error {
+ ret := _m.Called(c, tasks)
+
+ if len(ret) == 0 {
+ panic("no return value specified for PushAtOnce")
+ }
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(context.Context, []*model.Task) error); ok {
+ r0 = rf(c, tasks)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// Resume provides a mock function with given fields:
+func (_m *Queue) Resume() {
+ _m.Called()
+}
+
+// Wait provides a mock function with given fields: c, id
+func (_m *Queue) Wait(c context.Context, id string) error {
+ ret := _m.Called(c, id)
+
+ if len(ret) == 0 {
+ panic("no return value specified for Wait")
+ }
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
+ r0 = rf(c, id)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// NewQueue creates a new instance of Queue. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewQueue(t interface {
+ mock.TestingT
+ Cleanup(func())
+}) *Queue {
+ mock := &Queue{}
+ mock.Mock.Test(t)
+
+ t.Cleanup(func() { mock.AssertExpectations(t) })
+
+ return mock
+}
diff --git a/server/queue/queue.go b/server/queue/queue.go
index 3301f7e95..cc856b6e1 100644
--- a/server/queue/queue.go
+++ b/server/queue/queue.go
@@ -72,6 +72,8 @@ func (t *InfoT) String() string {
// The int return value represents the matching score (higher is better).
type FilterFn func(*model.Task) (bool, int)
+//go:generate mockery --name Queue --output mocks --case underscore --note "+build test"
+
// Queue defines a task queue for scheduling tasks among
// a pool of workers.
type Queue interface {
diff --git a/server/router/api.go b/server/router/api.go
index 2a514904d..591c9a7d3 100644
--- a/server/router/api.go
+++ b/server/router/api.go
@@ -18,6 +18,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/rs/zerolog"
+ "go.woodpecker-ci.org/woodpecker/v2/server"
"go.woodpecker-ci.org/woodpecker/v2/server/api"
"go.woodpecker-ci.org/woodpecker/v2/server/api/debug"
"go.woodpecker-ci.org/woodpecker/v2/server/router/middleware/session"
@@ -74,10 +75,12 @@ func apiRoutes(e *gin.RouterGroup) {
org.PATCH("/registries/:registry", api.PatchOrgRegistry)
org.DELETE("/registries/:registry", api.DeleteOrgRegistry)
- org.GET("/agents", api.GetOrgAgents)
- org.POST("/agents", api.PostOrgAgent)
- org.PATCH("/agents/:agent_id", api.PatchOrgAgent)
- org.DELETE("/agents/:agent_id", api.DeleteOrgAgent)
+ if !server.Config.Agent.DisableUserRegisteredAgentRegistration {
+ org.GET("/agents", api.GetOrgAgents)
+ org.POST("/agents", api.PostOrgAgent)
+ org.PATCH("/agents/:agent_id", api.PatchOrgAgent)
+ org.DELETE("/agents/:agent_id", api.DeleteOrgAgent)
+ }
}
}
}
diff --git a/server/web/config.go b/server/web/config.go
index 44c6f1fe5..2d204d277 100644
--- a/server/web/config.go
+++ b/server/web/config.go
@@ -40,12 +40,13 @@ func Config(c *gin.Context) {
}
configData := map[string]any{
- "user": user,
- "csrf": csrf,
- "version": version.String(),
- "skip_version_check": server.Config.WebUI.SkipVersionCheck,
- "root_path": server.Config.Server.RootPath,
- "enable_swagger": server.Config.WebUI.EnableSwagger,
+ "user": user,
+ "csrf": csrf,
+ "version": version.String(),
+ "skip_version_check": server.Config.WebUI.SkipVersionCheck,
+ "root_path": server.Config.Server.RootPath,
+ "enable_swagger": server.Config.WebUI.EnableSwagger,
+ "user_registered_agents": !server.Config.Agent.DisableUserRegisteredAgentRegistration,
}
// default func map with json parser.
@@ -79,4 +80,5 @@ window.WOODPECKER_VERSION = "{{ .version }}";
window.WOODPECKER_ROOT_PATH = "{{ .root_path }}";
window.WOODPECKER_ENABLE_SWAGGER = {{ .enable_swagger }};
window.WOODPECKER_SKIP_VERSION_CHECK = {{ .skip_version_check }}
+window.WOODPECKER_USER_REGISTERED_AGENTS = {{ .user_registered_agents }}
`
diff --git a/web/src/compositions/useConfig.ts b/web/src/compositions/useConfig.ts
index 3f12e1f92..f5f4324c0 100644
--- a/web/src/compositions/useConfig.ts
+++ b/web/src/compositions/useConfig.ts
@@ -8,6 +8,7 @@ declare global {
WOODPECKER_CSRF: string | undefined;
WOODPECKER_ROOT_PATH: string | undefined;
WOODPECKER_ENABLE_SWAGGER: boolean | undefined;
+ WOODPECKER_USER_REGISTERED_AGENTS: boolean | undefined;
}
}
@@ -18,4 +19,5 @@ export default () => ({
csrf: window.WOODPECKER_CSRF ?? null,
rootPath: window.WOODPECKER_ROOT_PATH ?? '',
enableSwagger: window.WOODPECKER_ENABLE_SWAGGER === true || false,
+ userRegisteredAgents: window.WOODPECKER_USER_REGISTERED_AGENTS || false,
});
diff --git a/web/src/views/User.vue b/web/src/views/User.vue
index b19639618..9a2f618ae 100644
--- a/web/src/views/User.vue
+++ b/web/src/views/User.vue
@@ -14,7 +14,7 @@
-
+
diff --git a/web/src/views/org/OrgSettings.vue b/web/src/views/org/OrgSettings.vue
index 25d2e92e0..89c8d4fca 100644
--- a/web/src/views/org/OrgSettings.vue
+++ b/web/src/views/org/OrgSettings.vue
@@ -19,7 +19,7 @@
-
+
@@ -35,6 +35,7 @@ import Tab from '~/components/layout/scaffold/Tab.vue';
import OrgAgentsTab from '~/components/org/settings/OrgAgentsTab.vue';
import OrgRegistriesTab from '~/components/org/settings/OrgRegistriesTab.vue';
import OrgSecretsTab from '~/components/org/settings/OrgSecretsTab.vue';
+import useConfig from '~/compositions/useConfig';
import { inject } from '~/compositions/useInjectProvide';
import useNotifications from '~/compositions/useNotifications';
import { useRouteBack } from '~/compositions/useRouteBack';