Files
portainer/api/http/proxy/factory/docker/services_test.go
T

469 lines
14 KiB
Go

package docker
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/http/security"
"github.com/segmentio/encoding/json"
"github.com/stretchr/testify/require"
)
const serviceCreationAPIVersion = "1.51"
type serviceCreationFixtures struct {
dockerSrv *httptest.Server
ds dataservices.DataStore
stdUser portainer.User
adminUser portainer.User
endpointID portainer.EndpointID
}
func newServiceCreationFixtures(t *testing.T) *serviceCreationFixtures {
t.Helper()
const serviceID = "some-service-id"
dockerSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodHead && r.URL.Path == "/_ping" {
w.Header().Add("Api-Version", serviceCreationAPIVersion)
_, _ = w.Write([]byte{})
return
}
if r.Method == http.MethodPost {
data, err := json.Marshal(map[string]string{"ID": serviceID})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_, _ = w.Write(data)
return
}
http.NotFound(w, r)
}))
t.Cleanup(dockerSrv.Close)
_, store := datastore.MustNewTestStore(t, true, false)
f := &serviceCreationFixtures{
dockerSrv: dockerSrv,
ds: store,
stdUser: portainer.User{ID: 1, Username: "std", Role: portainer.StandardUserRole},
adminUser: portainer.User{ID: 2, Username: "admin", Role: portainer.AdministratorRole},
endpointID: portainer.EndpointID(1),
}
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
err := tx.User().Create(&f.stdUser)
require.NoError(t, err)
err = tx.User().Create(&f.adminUser)
require.NoError(t, err)
err = tx.Endpoint().Create(&portainer.Endpoint{ID: f.endpointID, Name: "test-env"})
require.NoError(t, err)
return nil
})
require.NoError(t, err)
return f
}
func (f *serviceCreationFixtures) setSecuritySettings(t *testing.T, settings portainer.EndpointSecuritySettings) {
t.Helper()
err := f.ds.UpdateTx(func(tx dataservices.DataStoreTx) error {
return tx.Endpoint().UpdateEndpoint(f.endpointID, &portainer.Endpoint{
ID: f.endpointID,
Name: "test-env",
SecuritySettings: settings,
})
})
require.NoError(t, err)
}
func (f *serviceCreationFixtures) newTransport() *Transport {
return &Transport{
endpoint: &portainer.Endpoint{ID: f.endpointID},
dataStore: f.ds,
HTTPTransport: &http.Transport{},
}
}
type serviceBody struct {
TaskTemplate struct {
ContainerSpec struct {
CapabilityAdd []string `json:"CapabilityAdd,omitempty"`
CapabilityDrop []string `json:"CapabilityDrop,omitempty"`
Sysctls map[string]any `json:"Sysctls,omitempty"`
Privileges *struct {
Seccomp *struct{ Mode string } `json:"Seccomp,omitempty"`
AppArmor *struct{ Mode string } `json:"AppArmor,omitempty"`
} `json:"Privileges,omitempty"`
Mounts []struct {
Type string `json:"Type"`
} `json:"Mounts,omitempty"`
} `json:"ContainerSpec"`
} `json:"TaskTemplate"`
}
func (f *serviceCreationFixtures) newRequest(t *testing.T, body serviceBody, user portainer.User) *http.Request {
t.Helper()
data, err := json.Marshal(body)
require.NoError(t, err)
req, err := http.NewRequestWithContext(
t.Context(),
http.MethodPost,
f.dockerSrv.URL+"/v"+serviceCreationAPIVersion+"/services/create",
bytes.NewReader(data),
)
require.NoError(t, err)
return req.WithContext(security.StoreTokenData(req, &portainer.TokenData{
ID: user.ID,
Username: user.Username,
Role: user.Role,
}))
}
var (
restrictiveSettings = portainer.EndpointSecuritySettings{
AllowContainerCapabilitiesForRegularUsers: false,
AllowSysctlSettingForRegularUsers: false,
AllowSecurityOptForRegularUsers: false,
AllowBindMountsForRegularUsers: false,
}
permissiveSettings = portainer.EndpointSecuritySettings{
AllowContainerCapabilitiesForRegularUsers: true,
AllowSysctlSettingForRegularUsers: true,
AllowSecurityOptForRegularUsers: true,
AllowBindMountsForRegularUsers: true,
}
)
func TestDecorateServiceCreationOperation_CapabilityAddForbidden(t *testing.T) {
f := newServiceCreationFixtures(t)
f.setSecuritySettings(t, restrictiveSettings)
var body serviceBody
body.TaskTemplate.ContainerSpec.CapabilityAdd = []string{"NET_ADMIN"}
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, body, f.stdUser))
require.ErrorIs(t, err, ErrContainerCapabilitiesForbidden)
require.NotNil(t, resp)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
}
func TestDecorateServiceCreationOperation_CapabilityDropForbidden(t *testing.T) {
f := newServiceCreationFixtures(t)
f.setSecuritySettings(t, restrictiveSettings)
var body serviceBody
body.TaskTemplate.ContainerSpec.CapabilityDrop = []string{"MKNOD"}
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, body, f.stdUser))
require.ErrorIs(t, err, ErrContainerCapabilitiesForbidden)
require.NotNil(t, resp)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
}
func TestDecorateServiceCreationOperation_CapabilitiesAllowed(t *testing.T) {
f := newServiceCreationFixtures(t)
f.setSecuritySettings(t, permissiveSettings)
var body serviceBody
body.TaskTemplate.ContainerSpec.CapabilityAdd = []string{"NET_ADMIN"}
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, body, f.stdUser))
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEqual(t, http.StatusForbidden, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
}
func TestDecorateServiceCreationOperation_NoCapabilitiesAllowed(t *testing.T) {
f := newServiceCreationFixtures(t)
f.setSecuritySettings(t, restrictiveSettings)
var body serviceBody
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, body, f.stdUser))
require.NotErrorIs(t, err, ErrContainerCapabilitiesForbidden)
require.NotNil(t, resp)
require.NotEqual(t, http.StatusForbidden, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
}
func TestDecorateServiceCreationOperation_SysctlForbidden(t *testing.T) {
f := newServiceCreationFixtures(t)
f.setSecuritySettings(t, restrictiveSettings)
var body serviceBody
body.TaskTemplate.ContainerSpec.Sysctls = map[string]any{"net.ipv4.ip_forward": "1"}
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, body, f.stdUser))
require.ErrorIs(t, err, ErrSysCtlSettingsForbidden)
require.NotNil(t, resp)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
}
func TestDecorateServiceCreationOperation_SysctlAllowed(t *testing.T) {
f := newServiceCreationFixtures(t)
f.setSecuritySettings(t, permissiveSettings)
var body serviceBody
body.TaskTemplate.ContainerSpec.Sysctls = map[string]any{"net.ipv4.ip_forward": "1"}
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, body, f.stdUser))
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEqual(t, http.StatusForbidden, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
}
func TestDecorateServiceCreationOperation_SeccompForbidden(t *testing.T) {
f := newServiceCreationFixtures(t)
f.setSecuritySettings(t, restrictiveSettings)
var body serviceBody
body.TaskTemplate.ContainerSpec.Privileges = &struct {
Seccomp *struct{ Mode string } `json:"Seccomp,omitempty"`
AppArmor *struct{ Mode string } `json:"AppArmor,omitempty"`
}{
Seccomp: &struct{ Mode string }{Mode: "custom"},
}
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, body, f.stdUser))
require.ErrorIs(t, err, ErrSecurityOptSettingsForbidden)
require.NotNil(t, resp)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
}
func TestDecorateServiceCreationOperation_AppArmorForbidden(t *testing.T) {
f := newServiceCreationFixtures(t)
f.setSecuritySettings(t, restrictiveSettings)
var body serviceBody
body.TaskTemplate.ContainerSpec.Privileges = &struct {
Seccomp *struct{ Mode string } `json:"Seccomp,omitempty"`
AppArmor *struct{ Mode string } `json:"AppArmor,omitempty"`
}{
AppArmor: &struct{ Mode string }{Mode: "localhost"},
}
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, body, f.stdUser))
require.ErrorIs(t, err, ErrSecurityOptSettingsForbidden)
require.NotNil(t, resp)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
}
func TestDecorateServiceCreationOperation_NilPrivilegesNotForbidden(t *testing.T) {
f := newServiceCreationFixtures(t)
f.setSecuritySettings(t, restrictiveSettings)
var body serviceBody
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, body, f.stdUser))
require.NotErrorIs(t, err, ErrSecurityOptSettingsForbidden)
require.NotNil(t, resp)
require.NotEqual(t, http.StatusForbidden, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
}
func TestDecorateServiceCreationOperation_PrivilegesWithNilSeccompAndAppArmorNotForbidden(t *testing.T) {
f := newServiceCreationFixtures(t)
f.setSecuritySettings(t, restrictiveSettings)
var body serviceBody
body.TaskTemplate.ContainerSpec.Privileges = &struct {
Seccomp *struct{ Mode string } `json:"Seccomp,omitempty"`
AppArmor *struct{ Mode string } `json:"AppArmor,omitempty"`
}{}
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, body, f.stdUser))
require.NotErrorIs(t, err, ErrSecurityOptSettingsForbidden)
require.NotNil(t, resp)
require.NotEqual(t, http.StatusForbidden, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
}
func TestDecorateServiceCreationOperation_PrivilegesAllowed(t *testing.T) {
f := newServiceCreationFixtures(t)
f.setSecuritySettings(t, permissiveSettings)
var body serviceBody
body.TaskTemplate.ContainerSpec.Privileges = &struct {
Seccomp *struct{ Mode string } `json:"Seccomp,omitempty"`
AppArmor *struct{ Mode string } `json:"AppArmor,omitempty"`
}{
Seccomp: &struct{ Mode string }{Mode: "custom"},
}
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, body, f.stdUser))
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEqual(t, http.StatusForbidden, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
}
func TestDecorateServiceCreationOperation_BindMountForbidden(t *testing.T) {
f := newServiceCreationFixtures(t)
f.setSecuritySettings(t, restrictiveSettings)
var body serviceBody
body.TaskTemplate.ContainerSpec.Mounts = []struct {
Type string `json:"Type"`
}{{Type: "bind"}}
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, body, f.stdUser))
require.ErrorIs(t, err, ErrBindMountsForbidden)
require.NotNil(t, resp)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
}
func TestDecorateServiceCreationOperation_NonBindMountNotForbidden(t *testing.T) {
f := newServiceCreationFixtures(t)
f.setSecuritySettings(t, portainer.EndpointSecuritySettings{
AllowContainerCapabilitiesForRegularUsers: true,
AllowSysctlSettingForRegularUsers: true,
AllowSecurityOptForRegularUsers: true,
AllowBindMountsForRegularUsers: false,
})
var body serviceBody
body.TaskTemplate.ContainerSpec.Mounts = []struct {
Type string `json:"Type"`
}{{Type: "volume"}}
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, body, f.stdUser))
require.NotErrorIs(t, err, ErrBindMountsForbidden)
require.NotNil(t, resp)
require.NotEqual(t, http.StatusForbidden, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
}
func TestDecorateServiceCreationOperation_BindMountAllowed(t *testing.T) {
f := newServiceCreationFixtures(t)
f.setSecuritySettings(t, permissiveSettings)
var body serviceBody
body.TaskTemplate.ContainerSpec.Mounts = []struct {
Type string `json:"Type"`
}{{Type: "bind"}}
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, body, f.stdUser))
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEqual(t, http.StatusForbidden, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
}
func TestDecorateServiceCreationOperation_AdminBypassesAllSecurityChecks(t *testing.T) {
f := newServiceCreationFixtures(t)
f.setSecuritySettings(t, restrictiveSettings)
var body serviceBody
body.TaskTemplate.ContainerSpec.CapabilityAdd = []string{"NET_ADMIN"}
body.TaskTemplate.ContainerSpec.CapabilityDrop = []string{"MKNOD"}
body.TaskTemplate.ContainerSpec.Sysctls = map[string]any{"net.ipv4.ip_forward": "1"}
body.TaskTemplate.ContainerSpec.Privileges = &struct {
Seccomp *struct{ Mode string } `json:"Seccomp,omitempty"`
AppArmor *struct{ Mode string } `json:"AppArmor,omitempty"`
}{
Seccomp: &struct{ Mode string }{Mode: "custom"},
}
body.TaskTemplate.ContainerSpec.Mounts = []struct {
Type string `json:"Type"`
}{{Type: "bind"}}
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, body, f.adminUser))
require.NotErrorIs(t, err, ErrContainerCapabilitiesForbidden)
require.NotErrorIs(t, err, ErrSysCtlSettingsForbidden)
require.NotErrorIs(t, err, ErrSecurityOptSettingsForbidden)
require.NotErrorIs(t, err, ErrBindMountsForbidden)
require.NotNil(t, resp)
require.NotEqual(t, http.StatusForbidden, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
}
func TestDecorateServiceCreationOperation_StandardUserPermissiveSettingsSucceeds(t *testing.T) {
f := newServiceCreationFixtures(t)
f.setSecuritySettings(t, permissiveSettings)
var body serviceBody
body.TaskTemplate.ContainerSpec.CapabilityAdd = []string{"NET_ADMIN"}
body.TaskTemplate.ContainerSpec.Sysctls = map[string]any{"net.core.somaxconn": "128"}
body.TaskTemplate.ContainerSpec.Mounts = []struct {
Type string `json:"Type"`
}{{Type: "bind"}}
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, body, f.stdUser))
require.NoError(t, err)
require.NotNil(t, resp)
require.Equal(t, http.StatusCreated, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
}