1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-10 08:15:25 +02:00

refactor(settings): allow all to use settings api [EE-6923]

fix [EE-6923]
This commit is contained in:
Chaim Lev-Ari 2024-06-09 14:34:22 +03:00 committed by Chaim Lev-Ari
parent b0e3afa0b6
commit 3386b1a18a
55 changed files with 1018 additions and 479 deletions

View file

@ -11,12 +11,6 @@ import (
"github.com/gorilla/mux"
)
func hideFields(settings *portainer.Settings) {
settings.LDAPSettings.Password = ""
settings.OAuthSettings.ClientSecret = ""
settings.OAuthSettings.KubeSecretKey = nil
}
// Handler is the HTTP handler used to handle settings operations.
type Handler struct {
*mux.Router
@ -33,12 +27,17 @@ func NewHandler(bouncer security.BouncerService) *Handler {
Router: mux.NewRouter(),
}
h.Handle("/settings",
bouncer.AdminAccess(httperror.LoggerHandler(h.settingsInspect))).Methods(http.MethodGet)
h.Handle("/settings",
bouncer.AdminAccess(httperror.LoggerHandler(h.settingsUpdate))).Methods(http.MethodPut)
h.Handle("/settings/public",
bouncer.PublicAccess(httperror.LoggerHandler(h.settingsPublic))).Methods(http.MethodGet)
adminRouter := h.NewRoute().Subrouter()
adminRouter.Use(bouncer.AdminAccess)
adminRouter.Handle("/settings", httperror.LoggerHandler(h.settingsUpdate)).Methods(http.MethodPut)
authenticatedRouter := h.NewRoute().Subrouter()
authenticatedRouter.Use(bouncer.AuthenticatedAccess)
authenticatedRouter.Handle("/settings", httperror.LoggerHandler(h.settingsInspect)).Methods(http.MethodGet)
publicRouter := h.NewRoute().Subrouter()
publicRouter.Use(bouncer.PublicAccess)
publicRouter.Handle("/settings/public", httperror.LoggerHandler(h.settingsPublic)).Methods(http.MethodGet)
return h
}

View file

@ -3,27 +3,43 @@ package settings
import (
"net/http"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/rbacutils"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/http/utils"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/response"
)
// @id SettingsInspect
// @id settingsInspect
// @summary Retrieve Portainer settings
// @description Retrieve Portainer settings.
// @description **Access policy**: administrator
// @description Retrieve settings. Will returns settings based on the user role.
// @description **Access policy**: public
// @tags settings
// @security ApiKeyAuth
// @security jwt
// @produce json
// @success 200 {object} portainer.Settings "Success"
// @success 200 {object} settingsInspectResponse "Success"
// @failure 500 "Server error"
// @router /settings [get]
func (handler *Handler) settingsInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return httperror.InternalServerError("Unable to retrieve the settings from the database", err)
}
var roleBasedResponse interface{}
err := handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
settings, err := tx.Settings().Settings()
if err != nil {
return httperror.InternalServerError("Unable to retrieve the settings from the database", err)
}
hideFields(settings)
return response.JSON(w, settings)
user, err := security.RetrieveUserFromRequest(r, tx)
if err != nil {
return httperror.InternalServerError("Unable to retrieve user details from request", err)
}
response := buildResponse(settings)
role := rbacutils.RoleFromUser(user)
roleBasedResponse = response.ForRole(role)
return nil
})
return utils.TxResponse(w, roleBasedResponse, err)
}

View file

@ -0,0 +1,294 @@
package settings
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/pkg/featureflags"
"golang.org/x/oauth2"
)
type settingsInspectResponse struct {
adminResponse
}
type authenticatedResponse struct {
publicSettingsResponse
// Deployment options for encouraging git ops workflows
GlobalDeploymentOptions portainer.GlobalDeploymentOptions `json:"GlobalDeploymentOptions"`
// Whether edge compute features are enabled
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
// The expiry of a Kubeconfig
KubeconfigExpiry string `json:"KubeconfigExpiry" example:"24h"`
// Helm repository URL, defaults to "https://charts.bitnami.com/bitnami"
HelmRepositoryURL string `json:"HelmRepositoryURL" example:"https://charts.bitnami.com/bitnami"`
IsAMTEnabled bool `json:"isAMTEnabled"`
IsFDOEnabled bool `json:"isFDOEnabled"`
}
type edgeSettings struct {
// The command list interval for edge agent - used in edge async mode (in seconds)
CommandInterval int `json:"CommandInterval" example:"5"`
// The ping interval for edge agent - used in edge async mode (in seconds)
PingInterval int `json:"PingInterval" example:"5"`
// The snapshot interval for edge agent - used in edge async mode (in seconds)
SnapshotInterval int `json:"SnapshotInterval" example:"5"`
}
type edgeAdminResponse struct {
authenticatedResponse
Edge edgeSettings
// TrustOnFirstConnect makes Portainer accepting edge agent connection by default
TrustOnFirstConnect bool `json:"TrustOnFirstConnect" example:"false"`
// EnforceEdgeID makes Portainer store the Edge ID instead of accepting anyone
EnforceEdgeID bool `json:"EnforceEdgeID" example:"false"`
// EdgePortainerURL is the URL that is exposed to edge agents
EdgePortainerURL string `json:"EdgePortainerUrl"`
// The default check in interval for edge agent (in seconds)
EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval" example:"5"`
}
type oauthSettings struct {
ClientID string `json:"ClientID"`
AccessTokenURI string `json:"AccessTokenURI"`
AuthorizationURI string `json:"AuthorizationURI"`
ResourceURI string `json:"ResourceURI"`
RedirectURI string `json:"RedirectURI"`
UserIdentifier string `json:"UserIdentifier"`
Scopes string `json:"Scopes"`
OAuthAutoCreateUsers bool `json:"OAuthAutoCreateUsers"`
DefaultTeamID portainer.TeamID `json:"DefaultTeamID"`
SSO bool `json:"SSO"`
LogoutURI string `json:"LogoutURI"`
AuthStyle oauth2.AuthStyle `json:"AuthStyle"`
}
type ldapSettings struct {
// Enable this option if the server is configured for Anonymous access. When enabled, ReaderDN and Password will not be used
AnonymousMode bool `json:"AnonymousMode" example:"true" validate:"validate_bool"`
// Account that will be used to search for users
ReaderDN string `json:"ReaderDN" example:"cn=readonly-account,dc=ldap,dc=domain,dc=tld" validate:"required_if=AnonymousMode false"`
// URL or IP address of the LDAP server
URL string `json:"URL" example:"myldap.domain.tld:389" validate:"hostname_port"`
TLSConfig portainer.TLSConfiguration `json:"TLSConfig"`
// Whether LDAP connection should use StartTLS
StartTLS bool `json:"StartTLS" example:"true"`
SearchSettings []portainer.LDAPSearchSettings `json:"SearchSettings"`
GroupSearchSettings []portainer.LDAPGroupSearchSettings `json:"GroupSearchSettings"`
// Automatically provision users and assign them to matching LDAP group names
AutoCreateUsers bool `json:"AutoCreateUsers" example:"true"`
}
type adminResponse struct {
edgeAdminResponse
// A list of label name & value that will be used to hide containers when querying containers
BlackListedLabels []portainer.Pair `json:"BlackListedLabels"`
LDAPSettings ldapSettings `json:"LDAPSettings"`
OAuthSettings oauthSettings `json:"OAuthSettings"`
InternalAuthSettings portainer.InternalAuthSettings `json:"InternalAuthSettings"`
OpenAMTConfiguration portainer.OpenAMTConfiguration `json:"openAMTConfiguration"`
FDOConfiguration portainer.FDOConfiguration `json:"fdoConfiguration"`
// The interval in which environment(endpoint) snapshots are created
SnapshotInterval string `json:"SnapshotInterval" example:"5m"`
// URL to the templates that will be displayed in the UI when navigating to App Templates
TemplatesURL string `json:"TemplatesURL" example:"https://raw.githubusercontent.com/portainer/templates/v3/templates.json"`
// The duration of a user session
UserSessionTimeout string `json:"UserSessionTimeout" example:"5m"`
// KubectlImage, defaults to portainer/kubectl-shell
KubectlShellImage string `json:"KubectlShellImage" example:"portainer/kubectl-shell"`
// Container environment parameter AGENT_SECRET
AgentSecret string `json:"AgentSecret"`
}
type publicSettingsResponse struct {
// global settings
// URL to a logo that will be displayed on the login page as well as on top of the sidebar. Will use default Portainer logo when value is empty string
LogoURL string `json:"LogoURL" example:"https://mycompany.mydomain.tld/logo.png"`
// Whether telemetry is enabled
EnableTelemetry bool `json:"EnableTelemetry" example:"true"`
// login settings:
// Active authentication method for the Portainer instance. Valid values are: 1 for internal, 2 for LDAP, or 3 for oauth
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod" example:"1"`
// The URL used for oauth login
OAuthLoginURI string `json:"OAuthLoginURI" example:"https://gitlab.com/oauth"`
// The minimum required length for a password of any user when using internal auth mode
RequiredPasswordLength int `json:"RequiredPasswordLength" example:"1"`
// The URL used for oauth logout
OAuthLogoutURI string `json:"OAuthLogoutURI" example:"https://gitlab.com/oauth/logout"`
// Whether team sync is enabled
TeamSync bool `json:"TeamSync" example:"true"`
// Supported feature flags
Features map[featureflags.Feature]bool `json:"Features"`
// Deprecated
// please use `GET /api/settings`
GlobalDeploymentOptions portainer.GlobalDeploymentOptions `json:"GlobalDeploymentOptions"`
// Deprecated
// please use `GET /api/settings`
ShowKomposeBuildOption bool `json:"ShowKomposeBuildOption" example:"false"`
// Deprecated
// please use `GET /api/settings`
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures" example:"true"`
// Deprecated
// please use `GET /api/settings`
KubeconfigExpiry string `example:"24h" default:"0"`
// Deprecated
// please use `GET /api/settings`
IsFDOEnabled bool
// Deprecated
// please use `GET /api/settings`
IsAMTEnabled bool
// Deprecated
// please use `GET /api/settings`
Edge struct {
// Deprecated
// please use `GET /api/settings`
PingInterval int `json:"PingInterval" example:"60"`
// Deprecated
// please use `GET /api/settings`
SnapshotInterval int `json:"SnapshotInterval" example:"60"`
// Deprecated
// please use `GET /api/settings`
CommandInterval int `json:"CommandInterval" example:"60"`
// Deprecated
// please use `GET /api/settings`
CheckinInterval int `example:"60"`
}
}
func (res *settingsInspectResponse) ForRole(role portainer.UserRole) interface{} {
switch role {
case portainer.AdministratorRole:
return res.adminResponse
case portainer.EdgeAdminRole:
return res.edgeAdminResponse
case portainer.StandardUserRole:
return res.authenticatedResponse
default:
return res.publicSettingsResponse
}
}
func buildResponse(settings *portainer.Settings) settingsInspectResponse {
hideFields(settings)
return settingsInspectResponse{
adminResponse: adminResponse{
edgeAdminResponse: edgeAdminResponse{
authenticatedResponse: authenticatedResponse{
publicSettingsResponse: generatePublicSettings(settings),
GlobalDeploymentOptions: settings.GlobalDeploymentOptions,
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
KubeconfigExpiry: settings.KubeconfigExpiry,
HelmRepositoryURL: settings.HelmRepositoryURL,
IsAMTEnabled: settings.EnableEdgeComputeFeatures && settings.OpenAMTConfiguration.Enabled,
IsFDOEnabled: settings.EnableEdgeComputeFeatures && settings.FDOConfiguration.Enabled,
},
Edge: edgeSettings{
CommandInterval: settings.Edge.CommandInterval,
PingInterval: settings.Edge.PingInterval,
SnapshotInterval: settings.Edge.SnapshotInterval,
},
TrustOnFirstConnect: settings.TrustOnFirstConnect,
EnforceEdgeID: settings.EnforceEdgeID,
EdgePortainerURL: settings.EdgePortainerURL,
EdgeAgentCheckinInterval: settings.EdgeAgentCheckinInterval,
},
BlackListedLabels: settings.BlackListedLabels,
LDAPSettings: ldapSettings{
AnonymousMode: settings.LDAPSettings.AnonymousMode,
ReaderDN: settings.LDAPSettings.ReaderDN,
TLSConfig: settings.LDAPSettings.TLSConfig,
StartTLS: settings.LDAPSettings.StartTLS,
SearchSettings: settings.LDAPSettings.SearchSettings,
GroupSearchSettings: settings.LDAPSettings.GroupSearchSettings,
AutoCreateUsers: settings.LDAPSettings.AutoCreateUsers,
URL: settings.LDAPSettings.URL,
},
OAuthSettings: oauthSettings{
ClientID: settings.OAuthSettings.ClientID,
AccessTokenURI: settings.OAuthSettings.AccessTokenURI,
AuthorizationURI: settings.OAuthSettings.AuthorizationURI,
ResourceURI: settings.OAuthSettings.ResourceURI,
RedirectURI: settings.OAuthSettings.RedirectURI,
UserIdentifier: settings.OAuthSettings.UserIdentifier,
Scopes: settings.OAuthSettings.Scopes,
OAuthAutoCreateUsers: settings.OAuthSettings.OAuthAutoCreateUsers,
DefaultTeamID: settings.OAuthSettings.DefaultTeamID,
SSO: settings.OAuthSettings.SSO,
LogoutURI: settings.OAuthSettings.LogoutURI,
AuthStyle: settings.OAuthSettings.AuthStyle,
},
InternalAuthSettings: settings.InternalAuthSettings,
OpenAMTConfiguration: settings.OpenAMTConfiguration,
FDOConfiguration: settings.FDOConfiguration,
SnapshotInterval: settings.SnapshotInterval,
TemplatesURL: settings.TemplatesURL,
UserSessionTimeout: settings.UserSessionTimeout,
KubectlShellImage: settings.KubectlShellImage,
AgentSecret: settings.AgentSecret,
},
}
}
func getTeamSync(settings *portainer.Settings) bool {
if settings.AuthenticationMethod == portainer.AuthenticationLDAP {
return settings.LDAPSettings.GroupSearchSettings != nil && len(settings.LDAPSettings.GroupSearchSettings) > 0 && len(settings.LDAPSettings.GroupSearchSettings[0].GroupBaseDN) > 0
}
return false
}
func generatePublicSettings(appSettings *portainer.Settings) publicSettingsResponse {
publicSettings := publicSettingsResponse{
LogoURL: appSettings.LogoURL,
AuthenticationMethod: appSettings.AuthenticationMethod,
RequiredPasswordLength: appSettings.InternalAuthSettings.RequiredPasswordLength,
EnableEdgeComputeFeatures: appSettings.EnableEdgeComputeFeatures,
GlobalDeploymentOptions: appSettings.GlobalDeploymentOptions,
ShowKomposeBuildOption: appSettings.ShowKomposeBuildOption,
EnableTelemetry: appSettings.EnableTelemetry,
KubeconfigExpiry: appSettings.KubeconfigExpiry,
Features: featureflags.FeatureFlags(),
IsFDOEnabled: appSettings.EnableEdgeComputeFeatures && appSettings.FDOConfiguration.Enabled,
IsAMTEnabled: appSettings.EnableEdgeComputeFeatures && appSettings.OpenAMTConfiguration.Enabled,
TeamSync: getTeamSync(appSettings),
}
publicSettings.Edge.PingInterval = appSettings.Edge.PingInterval
publicSettings.Edge.SnapshotInterval = appSettings.Edge.SnapshotInterval
publicSettings.Edge.CommandInterval = appSettings.Edge.CommandInterval
publicSettings.Edge.CheckinInterval = appSettings.EdgeAgentCheckinInterval
//if OAuth authentication is on, compose the related fields from application settings
if publicSettings.AuthenticationMethod == portainer.AuthenticationOAuth {
publicSettings.OAuthLogoutURI = appSettings.OAuthSettings.LogoutURI
publicSettings.OAuthLoginURI = fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s",
appSettings.OAuthSettings.AuthorizationURI,
appSettings.OAuthSettings.ClientID,
appSettings.OAuthSettings.RedirectURI,
appSettings.OAuthSettings.Scopes)
//control prompt=login param according to the SSO setting
if !appSettings.OAuthSettings.SSO {
publicSettings.OAuthLoginURI += "&prompt=login"
}
}
return publicSettings
}

View file

@ -0,0 +1,170 @@
package settings
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
)
func TestHandler_settingsInspect(t *testing.T) {
t.Run("check that /api/settings returns the right value for admin", func(t *testing.T) {
user := portainer.User{
ID: 1,
Username: "admin",
Role: portainer.AdministratorRole,
}
settings := &portainer.Settings{
LogoURL: "https://nondefault.com/logo.png",
BlackListedLabels: []portainer.Pair{{Name: "customlabel1", Value: "customvalue1"}},
AuthenticationMethod: 2,
InternalAuthSettings: portainer.InternalAuthSettings{
RequiredPasswordLength: 10,
},
LDAPSettings: portainer.LDAPSettings{
AnonymousMode: true,
ReaderDN: "readerDN",
Password: "password",
TLSConfig: portainer.TLSConfiguration{
TLS: true,
TLSSkipVerify: true,
TLSCACertPath: "/path/to/ca-cert",
TLSCertPath: "/path/to/cert",
TLSKeyPath: "/path/to/key",
},
StartTLS: true,
SearchSettings: []portainer.LDAPSearchSettings{{
BaseDN: "baseDN",
Filter: "filter",
UserNameAttribute: "username",
}},
GroupSearchSettings: []portainer.LDAPGroupSearchSettings{{
GroupBaseDN: "groupBaseDN",
GroupFilter: "groupFilter",
GroupAttribute: "groupAttribute",
}},
AutoCreateUsers: true,
URL: "ldap://admin.example.com",
},
OAuthSettings: portainer.OAuthSettings{
ClientID: "clientID",
ClientSecret: "clientSecret",
AccessTokenURI: "https://access-token-uri",
AuthorizationURI: "https://authorization-uri",
ResourceURI: "https://resource-uri",
RedirectURI: "https://redirect-uri",
UserIdentifier: "userIdentifier",
Scopes: "scope1 scope2",
OAuthAutoCreateUsers: true,
DefaultTeamID: 1,
SSO: true,
LogoutURI: "https://logout-uri",
KubeSecretKey: []byte("secretKey"),
AuthStyle: 1,
},
OpenAMTConfiguration: portainer.OpenAMTConfiguration{
Enabled: true,
MPSServer: "mps-server",
MPSUser: "mps-user",
MPSPassword: "mps-password",
MPSToken: "mps-token",
CertFileName: "cert-filename",
CertFileContent: "cert-file-content",
CertFilePassword: "cert-file-password",
DomainName: "domain-name",
},
FDOConfiguration: portainer.FDOConfiguration{
Enabled: true,
OwnerURL: "https://owner-url",
OwnerUsername: "owner-username",
OwnerPassword: "owner-password",
},
SnapshotInterval: "30m",
TemplatesURL: "https://nondefault.com/templates",
GlobalDeploymentOptions: portainer.GlobalDeploymentOptions{
HideStacksFunctionality: true,
},
EnableEdgeComputeFeatures: true,
UserSessionTimeout: "1h",
KubeconfigExpiry: "48h",
EnableTelemetry: true,
HelmRepositoryURL: "https://nondefault.com/helm",
KubectlShellImage: "portainer/kubectl-shell:v2.0.0",
TrustOnFirstConnect: true,
EnforceEdgeID: true,
AgentSecret: "nondefaultsecret",
EdgePortainerURL: "https://edge.nondefault.com",
EdgeAgentCheckinInterval: 20,
Edge: portainer.EdgeSettings{
CommandInterval: 10,
PingInterval: 10,
SnapshotInterval: 10,
AsyncMode: true,
},
}
// copy settings so we can compare later (since we will change the settings struct in the handler)
dbSettings, err := cloneMyStruct(settings)
assert.NoError(t, err)
dataStore := testhelpers.NewDatastore(
testhelpers.WithSettingsService(dbSettings),
testhelpers.WithUsers([]portainer.User{user}),
)
handler := &Handler{
DataStore: dataStore,
}
// Create a mock request
req, err := http.NewRequest("GET", "/settings", nil)
assert.NoError(t, err)
ctx := security.StoreTokenData(req, &portainer.TokenData{ID: user.ID, Username: user.Username, Role: user.Role})
req = req.WithContext(ctx)
restrictedCtx := security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{UserID: user.ID, IsAdmin: user.Role == portainer.AdministratorRole})
req = req.WithContext(restrictedCtx)
// Create a mock response recorder
rr := httptest.NewRecorder()
// Call the handler function
err = handler.settingsInspect(rr, req)
// Check for any handler errors
assert.Nil(t, err)
// Check the response status code
assert.Equal(t, http.StatusOK, rr.Code)
hideFields(settings)
actualSettings := &portainer.Settings{}
err = json.Unmarshal(rr.Body.Bytes(), actualSettings)
assert.EqualExportedValues(t, settings, actualSettings)
})
}
func cloneMyStruct[T any](orig *T) (*T, error) {
origJSON, err := json.Marshal(orig)
if err != nil {
return nil, err
}
clone := new(T)
if err = json.Unmarshal(origJSON, clone); err != nil {
return nil, err
}
return clone, nil
}

View file

@ -1,60 +1,12 @@
package settings
import (
"fmt"
"net/http"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/pkg/featureflags"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/response"
)
type publicSettingsResponse struct {
// URL to a logo that will be displayed on the login page as well as on top of the sidebar. Will use default Portainer logo when value is empty string
LogoURL string `json:"LogoURL" example:"https://mycompany.mydomain.tld/logo.png"`
// Active authentication method for the Portainer instance. Valid values are: 1 for internal, 2 for LDAP, or 3 for oauth
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod" example:"1"`
// The minimum required length for a password of any user when using internal auth mode
RequiredPasswordLength int `json:"RequiredPasswordLength" example:"1"`
// Deployment options for encouraging deployment as code
GlobalDeploymentOptions portainer.GlobalDeploymentOptions `json:"GlobalDeploymentOptions"`
// Show the Kompose build option (discontinued in 2.18)
ShowKomposeBuildOption bool `json:"ShowKomposeBuildOption" example:"false"`
// Whether edge compute features are enabled
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures" example:"true"`
// Supported feature flags
Features map[featureflags.Feature]bool `json:"Features"`
// The URL used for oauth login
OAuthLoginURI string `json:"OAuthLoginURI" example:"https://gitlab.com/oauth"`
// The URL used for oauth logout
OAuthLogoutURI string `json:"OAuthLogoutURI" example:"https://gitlab.com/oauth/logout"`
// Whether telemetry is enabled
EnableTelemetry bool `json:"EnableTelemetry" example:"true"`
// The expiry of a Kubeconfig
KubeconfigExpiry string `example:"24h" default:"0"`
// Whether team sync is enabled
TeamSync bool `json:"TeamSync" example:"true"`
// Whether FDO is enabled
IsFDOEnabled bool
// Whether AMT is enabled
IsAMTEnabled bool
Edge struct {
// The ping interval for edge agent - used in edge async mode [seconds]
PingInterval int `json:"PingInterval" example:"60"`
// The snapshot interval for edge agent - used in edge async mode [seconds]
SnapshotInterval int `json:"SnapshotInterval" example:"60"`
// The command list interval for edge agent - used in edge async mode [seconds]
CommandInterval int `json:"CommandInterval" example:"60"`
// The check in interval for edge agent (in seconds) - used in non async mode [seconds]
CheckinInterval int `example:"60"`
}
IsDockerDesktopExtension bool `json:"IsDockerDesktopExtension" example:"false"`
}
// @id SettingsPublic
// @summary Retrieve Portainer public settings
// @description Retrieve public settings. Returns a small set of settings that are not reserved to administrators only.
@ -73,47 +25,3 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
publicSettings := generatePublicSettings(settings)
return response.JSON(w, publicSettings)
}
func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResponse {
publicSettings := &publicSettingsResponse{
LogoURL: appSettings.LogoURL,
AuthenticationMethod: appSettings.AuthenticationMethod,
RequiredPasswordLength: appSettings.InternalAuthSettings.RequiredPasswordLength,
EnableEdgeComputeFeatures: appSettings.EnableEdgeComputeFeatures,
GlobalDeploymentOptions: appSettings.GlobalDeploymentOptions,
ShowKomposeBuildOption: appSettings.ShowKomposeBuildOption,
EnableTelemetry: appSettings.EnableTelemetry,
KubeconfigExpiry: appSettings.KubeconfigExpiry,
Features: featureflags.FeatureFlags(),
IsFDOEnabled: appSettings.EnableEdgeComputeFeatures && appSettings.FDOConfiguration.Enabled,
IsAMTEnabled: appSettings.EnableEdgeComputeFeatures && appSettings.OpenAMTConfiguration.Enabled,
}
publicSettings.Edge.PingInterval = appSettings.Edge.PingInterval
publicSettings.Edge.SnapshotInterval = appSettings.Edge.SnapshotInterval
publicSettings.Edge.CommandInterval = appSettings.Edge.CommandInterval
publicSettings.Edge.CheckinInterval = appSettings.EdgeAgentCheckinInterval
publicSettings.IsDockerDesktopExtension = appSettings.IsDockerDesktopExtension
//if OAuth authentication is on, compose the related fields from application settings
if publicSettings.AuthenticationMethod == portainer.AuthenticationOAuth {
publicSettings.OAuthLogoutURI = appSettings.OAuthSettings.LogoutURI
publicSettings.OAuthLoginURI = fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s",
appSettings.OAuthSettings.AuthorizationURI,
appSettings.OAuthSettings.ClientID,
appSettings.OAuthSettings.RedirectURI,
appSettings.OAuthSettings.Scopes)
//control prompt=login param according to the SSO setting
if !appSettings.OAuthSettings.SSO {
publicSettings.OAuthLoginURI += "&prompt=login"
}
}
//if LDAP authentication is on, compose the related fields from application settings
if publicSettings.AuthenticationMethod == portainer.AuthenticationLDAP && appSettings.LDAPSettings.GroupSearchSettings != nil {
if len(appSettings.LDAPSettings.GroupSearchSettings) > 0 {
publicSettings.TeamSync = len(appSettings.LDAPSettings.GroupSearchSettings[0].GroupBaseDN) > 0
}
}
return publicSettings
}

View file

@ -143,6 +143,12 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
return response.JSON(w, settings)
}
func hideFields(settings *portainer.Settings) {
settings.LDAPSettings.Password = ""
settings.OAuthSettings.ClientSecret = ""
settings.OAuthSettings.KubeSecretKey = nil
}
func (handler *Handler) updateSettings(tx dataservices.DataStoreTx, payload settingsUpdatePayload) (*portainer.Settings, error) {
settings, err := tx.Settings().Settings()
if err != nil {

View file

@ -0,0 +1,53 @@
package rbacutils
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/endpointutils"
)
// IsAdmin checks if user is a Portainer Admin
func IsAdmin(role portainer.UserRole) bool {
return role == portainer.AdministratorRole
}
// IsAdminOrEdgeAdmin checks if current user is a Portainer Admin, or edge admin of environment
func IsAdminOrEdgeAdmin(role portainer.UserRole, endpoint *portainer.Endpoint) bool {
return IsAdmin(role) || IsEdgeAdmin(role, endpoint)
}
// IsEdgeAdmin checks if current user is edge admin of environment.
// It doesn't check for portainer admin.
func IsEdgeAdmin(role portainer.UserRole, endpoint *portainer.Endpoint) bool {
return role == portainer.EdgeAdminRole && (endpoint == nil || endpointutils.IsEdgeEndpoint(endpoint))
}
// IsAdminOrEndpointAdmin checks if current request is for an admin, edge admin, or an environment(endpoint) admin
//
// EE-6176 TODO later: move this check to RBAC layer performed before in-handler execution (see usage references of this func)
//
// TODO EE-6627: check usage of function
func IsAdminOrEndpointAdmin(user *portainer.User, endpoint *portainer.Endpoint) bool {
if user == nil {
return false
}
return IsAdminOrEdgeAdmin(user.Role, endpoint) || (endpoint != nil && IsEndpointAdmin(user, endpoint.ID))
}
// check if user is endpoint admin of endpoint
func IsEndpointAdmin(user *portainer.User, endpointId portainer.EndpointID) bool {
if user == nil {
return false
}
hasResourceAccess, ok := user.EndpointAuthorizations[endpointId][portainer.EndpointResourcesAccess]
return ok && hasResourceAccess
}
// RoleFromUser returns the role of the user
func RoleFromUser(user *portainer.User) portainer.UserRole {
if user == nil {
return portainer.UserRole(0)
}
return user.Role
}

View file

@ -0,0 +1,35 @@
package utils
import (
"errors"
"net/http"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/response"
)
func TxResponse[T any](w http.ResponseWriter, r T, err error) *httperror.HandlerError {
if err != nil {
var handlerError *httperror.HandlerError
if errors.As(err, &handlerError) {
return handlerError
}
return httperror.InternalServerError("Unexpected error", err)
}
return response.JSON(w, r)
}
func TxEmptyResponse(w http.ResponseWriter, err error) *httperror.HandlerError {
if err != nil {
var handlerError *httperror.HandlerError
if errors.As(err, &handlerError) {
return handlerError
}
return httperror.InternalServerError("Unexpected error", err)
}
return response.Empty(w)
}

View file

@ -146,8 +146,16 @@ type stubUserService struct {
users []portainer.User
}
func (s *stubUserService) BucketName() string { return "users" }
func (s *stubUserService) Read(ID portainer.UserID) (*portainer.User, error) { return nil, nil }
func (s *stubUserService) BucketName() string { return "users" }
func (s *stubUserService) Read(ID portainer.UserID) (*portainer.User, error) {
for _, user := range s.users {
if user.ID == ID {
return &user, nil
}
}
return nil, errors.ErrObjectNotFound
}
func (s *stubUserService) UserByUsername(username string) (*portainer.User, error) { return nil, nil }
func (s *stubUserService) ReadAll() ([]portainer.User, error) { return s.users, nil }
func (s *stubUserService) UsersByRole(role portainer.UserRole) ([]portainer.User, error) {

View file

@ -939,6 +939,18 @@ type (
HideStacksFunctionality bool `json:"hideStacksFunctionality" example:"false"`
}
EdgeSettings struct {
// The command list interval for edge agent - used in edge async mode (in seconds)
CommandInterval int `json:"CommandInterval" example:"5"`
// The ping interval for edge agent - used in edge async mode (in seconds)
PingInterval int `json:"PingInterval" example:"5"`
// The snapshot interval for edge agent - used in edge async mode (in seconds)
SnapshotInterval int `json:"SnapshotInterval" example:"5"`
// Deprecated 2.18
AsyncMode bool
}
// Settings represents the application settings
Settings struct {
// URL to a logo that will be displayed on the login page as well as on top of the sidebar. Will use default Portainer logo when value is empty string
@ -984,33 +996,28 @@ type (
// EdgePortainerURL is the URL that is exposed to edge agents
EdgePortainerURL string `json:"EdgePortainerUrl"`
Edge struct {
// The command list interval for edge agent - used in edge async mode (in seconds)
CommandInterval int `json:"CommandInterval" example:"5"`
// The ping interval for edge agent - used in edge async mode (in seconds)
PingInterval int `json:"PingInterval" example:"5"`
// The snapshot interval for edge agent - used in edge async mode (in seconds)
SnapshotInterval int `json:"SnapshotInterval" example:"5"`
// Deprecated 2.18
AsyncMode bool
}
Edge EdgeSettings
IsDockerDesktopExtension bool `json:"IsDockerDesktopExtension,omitempty"`
// Deprecated fields
DisplayDonationHeader bool `json:"DisplayDonationHeader,omitempty"`
DisplayExternalContributors bool `json:"DisplayExternalContributors,omitempty"`
// Deprecated fields v26
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures,omitempty"`
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers,omitempty"`
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers,omitempty"`
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers,omitempty"`
AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers,omitempty"`
AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers,omitempty"`
AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers,omitempty"`
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures,omitempty"`
// Deprecated fields v26
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers,omitempty"`
// Deprecated fields v26
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers,omitempty"`
// Deprecated fields v26
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers,omitempty"`
// Deprecated fields v26
AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers,omitempty"`
// Deprecated fields v26
AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers,omitempty"`
// Deprecated fields v26
AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers,omitempty"`
// Deprecated fields v26
AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers,omitempty"`
IsDockerDesktopExtension bool `json:"IsDockerDesktopExtension,omitempty"`
}
// SnapshotJob represents a scheduled job that can create environment(endpoint) snapshots
@ -1864,6 +1871,10 @@ const (
AdministratorRole
// StandardUserRole represents a regular user role
StandardUserRole
// EdgeAdminRole represent a user that has access to resources of all environments
// like AdministratorRole but doesn't have access to Portainer settings
// not used in CE, but added to make code sync easier
EdgeAdminRole
)
const (

View file

@ -1,7 +1,8 @@
import angular from 'angular';
import _ from 'lodash-es';
import { DockerHubViewModel } from 'Portainer/models/dockerhub';
import { RegistryTypes } from '@/portainer/models/registryTypes';
import { DockerHubViewModel } from 'Portainer/models/dockerhub';
class porImageRegistryController {
/* @ngInject */

View file

@ -127,8 +127,8 @@ export default class DockerFeaturesConfigurationController {
gpus,
};
const publicSettings = await this.SettingsService.publicSettings();
const analyticsAllowed = publicSettings.EnableTelemetry;
const appSettings = await this.SettingsService.settings();
const analyticsAllowed = appSettings.EnableTelemetry;
if (analyticsAllowed) {
// send analytics if GPU management is changed (with the new state)
if (this.initialEnableGPUManagement !== this.state.enableGPUManagement) {

View file

@ -32,7 +32,7 @@
ng-if="$ctrl.formData.AccessControlEnabled && $ctrl.formData.Ownership === $ctrl.RCO.RESTRICTED && ($ctrl.isAdmin || (!$ctrl.isAdmin && $ctrl.availableTeams.length > 1))"
>
<div class="vertical-center w-full">
<label for="group-access" class="control-label col-sm-3 col-lg-2 !pt-0 text-left">
<label for="teams-selector" class="control-label col-sm-3 col-lg-2 !pt-0 text-left">
Authorized teams
<portainer-tooltip
ng-if="$ctrl.isAdmin && $ctrl.availableTeams.length > 0"
@ -63,7 +63,7 @@
<!-- authorized-users -->
<div class="form-group" ng-if="$ctrl.formData.AccessControlEnabled && $ctrl.formData.Ownership === $ctrl.RCO.RESTRICTED && $ctrl.isAdmin">
<div class="vertical-center w-full">
<label for="group-access" class="control-label col-sm-3 col-lg-2 !pt-0 text-left">
<label for="users-selector" class="control-label col-sm-3 col-lg-2 !pt-0 text-left">
Authorized users
<portainer-tooltip
ng-if="$ctrl.isAdmin && $ctrl.availableUsers.length > 0"

View file

@ -22,24 +22,6 @@ export function SettingsViewModel(data) {
this.EdgePortainerUrl = data.EdgePortainerUrl;
}
export function PublicSettingsViewModel(settings) {
this.AuthenticationMethod = settings.AuthenticationMethod;
this.TeamSync = settings.TeamSync;
this.RequiredPasswordLength = settings.RequiredPasswordLength;
this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
this.EnforceEdgeID = settings.EnforceEdgeID;
this.LogoURL = settings.LogoURL;
this.OAuthLoginURI = settings.OAuthLoginURI;
this.EnableTelemetry = settings.EnableTelemetry;
this.OAuthLogoutURI = settings.OAuthLogoutURI;
this.KubeconfigExpiry = settings.KubeconfigExpiry;
this.Features = settings.Features;
this.Edge = new EdgeSettingsViewModel(settings.Edge);
this.DefaultRegistry = settings.DefaultRegistry;
this.IsAMTEnabled = settings.IsAMTEnabled;
this.IsFDOEnabled = settings.IsFDOEnabled;
}
export function InternalAuthSettingsViewModel(data) {
this.RequiredPasswordLength = data.RequiredPasswordLength;
}

View file

@ -9,7 +9,6 @@ angular.module('portainer.app').factory('Settings', [
{
get: { method: 'GET' },
update: { method: 'PUT', ignoreLoadingBar: true },
publicSettings: { method: 'GET', params: { subResource: 'public' }, ignoreLoadingBar: true },
checkLDAPConnectivity: { method: 'PUT', params: { subResource: 'authentication', action: 'checkLDAP' } },
}
);

View file

@ -1,4 +1,4 @@
import { SettingsViewModel, PublicSettingsViewModel } from '../../models/settings';
import { SettingsViewModel } from '../../models/settings';
angular.module('portainer.app').factory('SettingsService', [
'$q',
@ -26,21 +26,6 @@ angular.module('portainer.app').factory('SettingsService', [
return Settings.update({}, settings).$promise;
};
service.publicSettings = function () {
var deferred = $q.defer();
Settings.publicSettings()
.$promise.then(function success(data) {
var settings = new PublicSettingsViewModel(data);
deferred.resolve(settings);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve application settings', err: err });
});
return deferred.promise;
};
service.checkLDAPConnectivity = function (settings) {
return Settings.checkLDAPConnectivity({}, settings).$promise;
};

View file

@ -1,21 +1,10 @@
import moment from 'moment';
import { getPublicSettings } from '@/react/portainer/settings/queries/usePublicSettings';
angular.module('portainer.app').factory('StateManager', StateManagerFactory);
/* @ngInject */
function StateManagerFactory(
$async,
$q,
SystemService,
InfoHelper,
LocalStorage,
SettingsService,
StatusService,
APPLICATION_CACHE_VALIDITY,
AgentPingService,
$analytics,
EndpointProvider
) {
function StateManagerFactory($async, $q, SystemService, InfoHelper, LocalStorage, StatusService, APPLICATION_CACHE_VALIDITY, AgentPingService, $analytics, EndpointProvider) {
var manager = {};
var state = {
@ -94,11 +83,6 @@ function StateManagerFactory(
LocalStorage.storeApplicationState(state.application);
};
manager.updateEnableEdgeComputeFeatures = function updateEnableEdgeComputeFeatures(enableEdgeComputeFeatures) {
state.application.enableEdgeComputeFeatures = enableEdgeComputeFeatures;
LocalStorage.storeApplicationState(state.application);
};
manager.updateEnableTelemetry = function updateEnableTelemetry(enableTelemetry) {
state.application.enableTelemetry = enableTelemetry;
$analytics.setOptOut(!enableTelemetry);
@ -112,8 +96,6 @@ function StateManagerFactory(
state.application.enableTelemetry = settings.EnableTelemetry;
state.application.logo = settings.LogoURL;
state.application.snapshotInterval = settings.SnapshotInterval;
state.application.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
state.application.validity = moment().unix();
}
@ -121,7 +103,7 @@ function StateManagerFactory(
var deferred = $q.defer();
$q.all({
settings: SettingsService.publicSettings(),
settings: getPublicSettings(),
status: StatusService.status(),
})
.then(function success(data) {

View file

@ -76,11 +76,11 @@ angular.module('portainer.app').controller('AccountController', [
$scope.forceChangePassword = userDetails.forceChangePassword;
$scope.isInitialAdmin = userDetails.ID === 1;
SettingsService.publicSettings()
.then(function success(data) {
$scope.AuthenticationMethod = data.AuthenticationMethod;
SettingsService.settings()
.then(function success(settings) {
$scope.AuthenticationMethod = settings.AuthenticationMethod;
if (state.UI.requiredPasswordLength && state.UI.requiredPasswordLength !== data.RequiredPasswordLength) {
if (state.UI.requiredPasswordLength && state.UI.requiredPasswordLength !== settings.InternalAuthSettings.RequiredPasswordLength) {
StateManager.clearPasswordChangeSkips();
}
@ -89,8 +89,8 @@ angular.module('portainer.app').controller('AccountController', [
? state.UI.timesPasswordChangeSkipped[$scope.userID.toString()]
: 0;
$scope.requiredPasswordLength = data.RequiredPasswordLength;
StateManager.setRequiredPasswordLength(data.RequiredPasswordLength);
$scope.requiredPasswordLength = settings.InternalAuthSettings.RequiredPasswordLength;
StateManager.setRequiredPasswordLength(settings.InternalAuthSettings.RequiredPasswordLength);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve application settings');

View file

@ -2,6 +2,7 @@ import angular from 'angular';
import uuidv4 from 'uuid/v4';
import { getEnvironments } from '@/react/portainer/environments/environment.service';
import { dispatchCacheRefreshEvent } from '@/portainer/services/http-request.helper';
import { getPublicSettings } from '@/react/portainer/settings/queries/usePublicSettings';
class AuthenticationController {
/* @ngInject */
@ -233,7 +234,7 @@ class AuthenticationController {
async onInit() {
try {
const settings = await this.SettingsService.publicSettings();
const settings = await getPublicSettings();
this.state.showOAuthLogin = settings.AuthenticationMethod === 3;
this.state.showStandardLogin = !this.state.showOAuthLogin;
this.state.OAuthLoginURI = settings.OAuthLoginURI;

View file

@ -1,5 +1,6 @@
import { getEnvironments } from '@/react/portainer/environments/environment.service';
import { restoreOptions } from '@/react/portainer/init/InitAdminView/restore-options';
import { getPublicSettings } from '@/react/portainer/settings/queries/usePublicSettings';
angular.module('portainer.app').controller('InitAdminController', [
'$scope',
@ -92,7 +93,7 @@ angular.module('portainer.app').controller('InitAdminController', [
}
function createAdministratorFlow() {
SettingsService.publicSettings()
getPublicSettings()
.then(function success(data) {
$scope.requiredPasswordLength = data.RequiredPasswordLength;
})

View file

@ -1,5 +1,6 @@
import angular from 'angular';
import { dispatchCacheRefreshEvent } from '@/portainer/services/http-request.helper';
import { getPublicSettings } from '@/react/portainer/settings/queries/usePublicSettings';
class LogoutController {
/* @ngInject */
@ -26,7 +27,7 @@ class LogoutController {
*/
async logoutAsync() {
const error = this.$transition$.params().error;
const settings = await this.SettingsService.publicSettings();
const settings = await getPublicSettings();
try {
await this.Authentication.logout();
@ -35,8 +36,9 @@ class LogoutController {
dispatchCacheRefreshEvent();
this.LocalStorage.storeLogoutReason(error);
if (settings.OAuthLogoutURI && this.Authentication.getUserDetails().ID !== 1) {
this.$window.location.href = settings.OAuthLogoutURI;
const logoutUri = settings.OAuthLogoutURI;
if (logoutUri && this.Authentication.getUserDetails().ID !== 1) {
this.$window.location.href = logoutUri;
} else {
this.$state.go('portainer.auth', { reload: true });
}

View file

@ -7,14 +7,13 @@ import { configureAMT } from 'Portainer/hostmanagement/open-amt/open-amt.service
angular.module('portainer.app').controller('SettingsEdgeComputeController', SettingsEdgeComputeController);
/* @ngInject */
export default function SettingsEdgeComputeController($q, $async, $state, Notifications, SettingsService, StateManager) {
export default function SettingsEdgeComputeController($q, $async, $state, Notifications, SettingsService) {
var ctrl = this;
this.onSubmitEdgeCompute = async function (settings) {
try {
await SettingsService.update(settings);
Notifications.success('Success', 'Settings updated');
StateManager.updateEnableEdgeComputeFeatures(settings.EnableEdgeComputeFeatures);
$state.reload();
} catch (err) {
Notifications.error('Failure', err, 'Unable to update settings');

View file

@ -115,7 +115,7 @@ angular.module('portainer.app').controller('UserController', [
$q.all({
user: UserService.user($transition$.params().id),
settings: SettingsService.publicSettings(),
settings: SettingsService.settings(),
})
.then(function success(data) {
var user = data.user;
@ -123,7 +123,7 @@ angular.module('portainer.app').controller('UserController', [
$scope.formValues.Administrator = user.Role === 1;
$scope.formValues.username = user.Username;
$scope.AuthenticationMethod = data.settings.AuthenticationMethod;
$scope.requiredPasswordLength = data.settings.RequiredPasswordLength;
$scope.requiredPasswordLength = data.settings.InternalAuthSettings.RequiredPasswordLength;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve user information');

View file

@ -1,6 +1,7 @@
import _ from 'lodash-es';
import { AuthenticationMethod } from '@/react/portainer/settings/types';
import { processItemsInBatches } from '@/react/common/processItemsInBatches';
import { getSettings } from '@/react/portainer/settings/queries/useSettings';
angular.module('portainer.app').controller('UsersController', [
'$q',
@ -11,8 +12,7 @@ angular.module('portainer.app').controller('UsersController', [
'TeamMembershipService',
'Notifications',
'Authentication',
'SettingsService',
function ($q, $scope, $state, UserService, TeamService, TeamMembershipService, Notifications, Authentication, SettingsService) {
function ($q, $scope, $state, UserService, TeamService, TeamMembershipService, Notifications, Authentication) {
$scope.state = {
userCreationError: '',
validUsername: false,
@ -112,7 +112,7 @@ angular.module('portainer.app').controller('UsersController', [
users: UserService.users(true),
teams: isAdmin ? TeamService.teams() : UserService.userLeadingTeams(userDetails.ID),
memberships: TeamMembershipService.memberships(),
settings: SettingsService.publicSettings(),
settings: getSettings(),
})
.then(function success(data) {
$scope.AuthenticationMethod = data.settings.AuthenticationMethod;
@ -121,7 +121,7 @@ angular.module('portainer.app').controller('UsersController', [
users = assignAuthMethod(users, $scope.AuthenticationMethod);
$scope.users = users;
$scope.teams = _.orderBy(data.teams, 'Name', 'asc');
$scope.requiredPasswordLength = data.settings.RequiredPasswordLength;
$scope.requiredPasswordLength = data.settings.InternalAuthSettings.RequiredPasswordLength;
$scope.teamSync = data.settings.TeamSync;
})
.catch(function error(err) {

View file

@ -13,8 +13,10 @@ export function PasswordCheckHint({
passwordValid,
forceChangePassword,
}: Props) {
const settingsQuery = usePublicSettings();
const minPasswordLength = settingsQuery.data?.RequiredPasswordLength;
const minPassLengthQuery = usePublicSettings({
select: (settings) => settings.RequiredPasswordLength,
});
const minPasswordLength = minPassLengthQuery.data;
return (
<div>

View file

@ -24,10 +24,10 @@ export function useIntervalOptions(
const [{ value: defaultValue }] = initialOptions;
const [options, setOptions] = useState<Option[]>(initialOptions);
const settingsQuery = useSettings(
(settings) => _.get(settings, fieldName, 0) as number,
!isDefaultHidden
);
const settingsQuery = useSettings({
select: (settings) => _.get(settings, fieldName, 0) as number,
enabled: !isDefaultHidden,
});
useEffect(() => {
if (isDefaultHidden) {

View file

@ -5,7 +5,9 @@ import {
useContext,
useMemo,
PropsWithChildren,
useEffect,
} from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { isEdgeAdmin, isPureAdmin } from '@/portainer/users/user.helpers';
import { EnvironmentId } from '@/react/portainer/environments/types';
@ -14,6 +16,7 @@ import { useLoadCurrentUser } from '@/portainer/users/queries/useLoadCurrentUser
import { useEnvironment } from '../portainer/environments/queries';
import { isBE } from '../portainer/feature-flags/feature-flags.service';
import { queryKeys as settingsQueryKeys } from '../portainer/settings/queries';
interface State {
user?: User;
@ -207,6 +210,8 @@ interface UserProviderProps {
export function UserProvider({ children }: UserProviderProps) {
const userQuery = useLoadCurrentUser();
useReloadSettings(userQuery.data?.Role);
const providerState = useMemo(
() => ({ user: userQuery.data }),
[userQuery.data]
@ -222,3 +227,10 @@ export function UserProvider({ children }: UserProviderProps) {
</UserContext.Provider>
);
}
function useReloadSettings(userRole?: User['Role']) {
const queryClient = useQueryClient();
useEffect(() => {
queryClient.invalidateQueries(settingsQueryKeys.base());
}, [queryClient, userRole]);
}

View file

@ -6,8 +6,7 @@ import { useCurrentStateAndParams } from '@uirouter/react';
import { Authorized } from '@/react/hooks/useUser';
import { notifyError, notifySuccess } from '@/portainer/services/notifications';
import { usePublicSettings } from '@/react/portainer/settings/queries';
import { GlobalDeploymentOptions } from '@/react/portainer/settings/types';
import { useSettings } from '@/react/portainer/settings/queries';
import { DetailsTable } from '@@/DetailsTable';
import { Link } from '@@/Link';
@ -74,10 +73,9 @@ export function ApplicationSummaryWidget() {
setApplicationNoteFormValues(applicationNote || '');
}, [applicationNote]);
const globalDeploymentOptionsQuery =
usePublicSettings<GlobalDeploymentOptions>({
select: (settings) => settings.GlobalDeploymentOptions,
});
const globalDeploymentOptionsQuery = useSettings({
select: (settings) => settings.GlobalDeploymentOptions,
});
const failedCreateCondition = application?.status?.conditions?.find(
(condition) => condition.reason === 'FailedCreate'

View file

@ -2,7 +2,7 @@ import _ from 'lodash';
import { useMemo } from 'react';
import { humanize, truncate } from '@/portainer/filters/filters';
import { usePublicSettings } from '@/react/portainer/settings/queries';
import { useSettings } from '@/react/portainer/settings/queries';
import { Link } from '@@/Link';
@ -10,9 +10,9 @@ import { helper } from './columns.helper';
import { name } from './columns.name';
export function useColumns() {
const hideStacksQuery = usePublicSettings<boolean>({
const hideStacksQuery = useSettings({
select: (settings) =>
settings.GlobalDeploymentOptions.hideStacksFunctionality,
!!settings.GlobalDeploymentOptions?.hideStacksFunctionality,
});
return useMemo(

View file

@ -3,7 +3,7 @@ import _ from 'lodash';
import { useMemo } from 'react';
import { humanize, truncate } from '@/portainer/filters/filters';
import { usePublicSettings } from '@/react/portainer/settings/queries';
import { useSettings } from '@/react/portainer/settings/queries';
import { Link } from '@@/Link';
import { ExternalBadge } from '@@/Badge/ExternalBadge';
@ -16,9 +16,9 @@ import { NamespaceApp } from './types';
const columnHelper = createColumnHelper<NamespaceApp>();
export function useColumns() {
const hideStacksQuery = usePublicSettings<boolean>({
const hideStacksQuery = useSettings({
select: (settings) =>
settings.GlobalDeploymentOptions.hideStacksFunctionality,
!!settings.GlobalDeploymentOptions?.hideStacksFunctionality,
});
return useMemo(

View file

@ -2,7 +2,7 @@ import { Link } from 'lucide-react';
import { useState } from 'react';
import { Environment } from '@/react/portainer/environments/types';
import { usePublicSettings } from '@/react/portainer/settings/queries';
import { useSettings } from '@/react/portainer/settings/queries';
import { Query } from '@/react/portainer/environments/queries/useEnvironmentList';
import { isEdgeEnvironment } from '@/react/portainer/environments/utils';
@ -18,9 +18,8 @@ export function AMTButton({
envQueryParams: Query;
}) {
const [isOpenDialog, setOpenDialog] = useState(false);
const isOpenAmtEnabledQuery = usePublicSettings({
select: (settings) =>
settings.EnableEdgeComputeFeatures && settings.IsAMTEnabled,
const isOpenAmtEnabledQuery = useSettings({
select: (settings) => settings.isAMTEnabled,
});
const isOpenAMTEnabled = !!isOpenAmtEnabledQuery.data;

View file

@ -8,7 +8,7 @@ import {
EnvironmentType,
} from '@/react/portainer/environments/types';
import { usePaginationLimitState } from '@/react/hooks/usePaginationLimitState';
import { usePublicSettings } from '@/react/portainer/settings/queries';
import { useSettings } from '@/react/portainer/settings/queries';
import {
Query,
useEnvironmentList,
@ -37,7 +37,7 @@ export function KubeconfigPrompt({
const [page, setPage] = useState(1);
const [pageLimit, setPageLimit] = usePaginationLimitState(storageKey);
const expiryQuery = usePublicSettings({
const expiryQuery = useSettings({
select: (settings) => expiryMessage(settings.KubeconfigExpiry),
});

View file

@ -8,7 +8,7 @@ import { AuthenticationMethod } from '@/react/portainer/settings/types';
import { Widget } from '@@/Widget';
import { PageHeader } from '@@/PageHeader';
import { usePublicSettings } from '../../settings/queries/usePublicSettings';
import { useSettings } from '../../settings/queries';
import { ApiKeyFormValues } from './types';
import { getAPITokenValidationSchema } from './CreateUserAcccessToken.validation';
@ -26,7 +26,7 @@ export function CreateUserAccessToken() {
const { user } = useCurrentUser();
const [newAPIToken, setNewAPIToken] = useState('');
const { trackEvent } = useAnalytics();
const settings = usePublicSettings();
const settings = useSettings();
const requirePassword =
settings.data?.AuthenticationMethod === AuthenticationMethod.Internal ||

View file

@ -9,10 +9,10 @@ import {
export function ImportFdoDeviceButton() {
const flagEnabledQuery = useFeatureFlag(FeatureFlag.FDO);
const isFDOEnabledQuery = useSettings(
(settings) => settings.fdoConfiguration.enabled,
flagEnabledQuery.data
);
const isFDOEnabledQuery = useSettings({
select: (settings) => settings.isFDOEnabled,
enabled: flagEnabledQuery.data,
});
if (!isFDOEnabledQuery.data || !flagEnabledQuery.data) {
return null;

View file

@ -3,12 +3,12 @@ import { type EnvironmentGroupId } from '@/react/portainer/environments/environm
import { type TagId } from '@/portainer/tags/types';
import { UserId } from '@/portainer/users/types';
import { TeamId } from '@/react/portainer/users/teams/types';
import { getSettings } from '@/react/portainer/settings/queries/useSettings';
import {
EdgeStack,
StatusType as EdgeStackStatusType,
} from '@/react/edge/edge-stacks/types';
import { getPublicSettings } from '../../settings/settings.service';
import type {
Environment,
EnvironmentId,
@ -133,17 +133,17 @@ export async function snapshotEndpoints() {
}
export async function getDeploymentOptions(environmentId: EnvironmentId) {
const publicSettings = await getPublicSettings();
const settings = await getSettings();
const endpoint = await getEndpoint(environmentId);
if (
publicSettings.GlobalDeploymentOptions.perEnvOverride &&
settings.GlobalDeploymentOptions?.perEnvOverride &&
endpoint.DeploymentOptions?.overrideGlobalOptions
) {
return endpoint.DeploymentOptions;
}
return publicSettings.GlobalDeploymentOptions;
return settings.GlobalDeploymentOptions;
}
export async function snapshotEndpoint(id: EnvironmentId) {

View file

@ -6,14 +6,10 @@ export enum FeatureFlag {
export function useFeatureFlag(
flag: FeatureFlag,
{
onSuccess,
enabled = true,
}: { onSuccess?: (isEnabled: boolean) => void; enabled?: boolean } = {}
{ enabled = true }: { enabled?: boolean } = {}
) {
return usePublicSettings<boolean>({
select: (settings) => settings.Features[flag],
onSuccess,
enabled,
});
}

View file

@ -1,4 +1,5 @@
import { useRouter } from '@uirouter/react';
import { useEffect } from 'react';
import { FeatureFlag, useFeatureFlag } from './useFeatureFlag';
@ -8,11 +9,11 @@ export function useRedirectFeatureFlag(
) {
const router = useRouter();
useFeatureFlag(flag, {
onSuccess(isEnabled) {
if (!isEnabled) {
router.stateService.go(to);
}
},
});
const query = useFeatureFlag(flag);
useEffect(() => {
if (!query.isLoading && !query.data) {
router.stateService.go(to);
}
}, [query.data, query.isLoading, router.stateService, to]);
}

View file

@ -4,7 +4,7 @@ import { notifySuccess } from '@/portainer/services/notifications';
import { FeatureId } from '@/react/portainer/feature-flags/enums';
import { isLimitedToBE } from '@/react/portainer/feature-flags/feature-flags.service';
import {
usePublicSettings,
useSettings,
useUpdateDefaultRegistrySettingsMutation,
} from '@/react/portainer/settings/queries';
@ -13,7 +13,7 @@ import { Button } from '@@/buttons';
import { BEFeatureIndicator } from '@@/BEFeatureIndicator';
export function DefaultRegistryAction() {
const settingsQuery = usePublicSettings({
const settingsQuery = useSettings({
select: (settings) => settings.DefaultRegistry?.Hide,
});
const defaultRegistryMutation = useUpdateDefaultRegistrySettingsMutation();

View file

@ -1,9 +1,9 @@
import clsx from 'clsx';
import { usePublicSettings } from '@/react/portainer/settings/queries';
import { useSettings } from '@/react/portainer/settings/queries';
export function DefaultRegistryDomain() {
const settingsQuery = usePublicSettings({
const settingsQuery = useSettings({
select: (settings) => settings.DefaultRegistry?.Hide,
});

View file

@ -1,9 +1,9 @@
import clsx from 'clsx';
import { usePublicSettings } from '@/react/portainer/settings/queries';
import { useSettings } from '@/react/portainer/settings/queries';
export function DefaultRegistryName() {
const settingsQuery = usePublicSettings({
const settingsQuery = useSettings({
select: (settings) => settings.DefaultRegistry?.Hide,
});

View file

@ -4,7 +4,7 @@ import { withError } from '@/react-tools/react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { Registry, RegistryTypes } from '../types/registry';
import { usePublicSettings } from '../../settings/queries';
import { useSettings } from '../../settings/queries';
import { queryKeys } from './query-keys';
@ -36,9 +36,8 @@ export function useGenericRegistriesQuery<T = Registry[]>(
hideDefault: hideDefaultOverride,
}: GenericRegistriesQueryOptions<T> = {}
) {
const hideDefaultRegistryQuery = usePublicSettings({
const hideDefaultRegistryQuery = useSettings({
select: (settings) => settings.DefaultRegistry?.Hide,
// We don't need the hideDefaultRegistry info if we're overriding it to true
enabled: enabled && !hideDefaultOverride,
});

View file

@ -12,7 +12,9 @@ import { AddLabelForm } from './AddLabelForm';
import { HiddenContainersTable } from './HiddenContainersTable';
export function HiddenContainersPanel() {
const settingsQuery = useSettings((settings) => settings.BlackListedLabels);
const settingsQuery = useSettings({
select: (settings) => settings.BlackListedLabels,
});
const mutation = useUpdateSettingsMutation();
if (!settingsQuery.data) {

View file

@ -0,0 +1,12 @@
export function buildUrl(subResource?: string, action?: string) {
let url = 'settings';
if (subResource) {
url += `/${subResource}`;
}
if (action) {
url += `/${action}`;
}
return url;
}

View file

@ -1,9 +1,7 @@
export { queryKeys } from './queryKeys';
export {
useSettings,
useUpdateDefaultRegistrySettingsMutation,
useUpdateSettingsMutation,
} from './useSettings';
export { useSettings } from './useSettings';
export { usePublicSettings } from './usePublicSettings';
export { useExperimentalSettings } from './useExperimentalSettings';
export { useUpdateExperimentalSettingsMutation } from './useExperimentalSettingsMutation';
export { useUpdateDefaultRegistrySettingsMutation } from './useUpdateDefaultRegistrySettingsMutation';
export { useUpdateSettingsMutation } from './useUpdateSettingsMutation';

View file

@ -4,7 +4,7 @@ import axios, { parseAxiosError } from '@/portainer/services/axios';
import { withError } from '@/react-tools/react-query';
import { ExperimentalFeatures } from '../types';
import { buildUrl } from '../settings.service';
import { buildUrl } from '../build-url';
import { queryKeys } from './queryKeys';

View file

@ -8,7 +8,7 @@ import {
} from '@/react-tools/react-query';
import { ExperimentalFeatures } from '../types';
import { buildUrl } from '../settings.service';
import { buildUrl } from '../build-url';
import { queryKeys } from './queryKeys';

View file

@ -1,12 +1,74 @@
import { useQuery } from '@tanstack/react-query';
import { withError } from '@/react-tools/react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { getPublicSettings } from '../settings.service';
import { PublicSettingsResponse } from '../types';
import { buildUrl } from '../build-url';
import { AuthenticationMethod } from '../types';
import { queryKeys } from './queryKeys';
export interface PublicSettingsResponse {
/**
* URL to a logo that will be displayed on the login page as well as on top of the sidebar. Will use default Portainer logo when value is empty string
* @example "https://mycompany.mydomain.tld/logo.png"
*/
LogoURL: string;
/**
* Whether telemetry is enabled
* @example true
*/
EnableTelemetry: boolean;
/**
* The content in plaintext used to display in the login page. Will hide when value is empty string
* @example "notice or agreement"
*/
CustomLoginBanner: string;
/**
* Active authentication method for the Portainer instance. Valid values are: 1 for internal, 2 for LDAP, or 3 for oauth
* @example 1
*/
AuthenticationMethod: AuthenticationMethod;
/**
* The URL used for oauth login
* @example "https://gitlab.com/oauth"
*/
OAuthLoginURI: string;
/**
* Whether portainer internal auth view will be hidden
* @example true
*/
OAuthHideInternalAuth: boolean;
/**
* The minimum required length for a password of any user when using internal auth mode
* @example 1
*/
RequiredPasswordLength: number;
/**
* The URL used for oauth logout
* @example "https://gitlab.com/oauth/logout"
*/
OAuthLogoutURI: string;
/**
* Whether team sync is enabled
* @example true
*/
TeamSync: boolean;
/**
* Supported feature flags
*/
Features: Record<string, boolean>;
}
export function usePublicSettings<T = PublicSettingsResponse>({
enabled,
select,
@ -23,3 +85,14 @@ export function usePublicSettings<T = PublicSettingsResponse>({
onSuccess,
});
}
export async function getPublicSettings() {
try {
const { data } = await axios.get<PublicSettingsResponse>(
buildUrl('public')
);
return data;
} catch (e) {
throw parseAxiosError(e, 'Unable to retrieve application settings');
}
}

View file

@ -1,24 +1,85 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import {
mutationOptions,
withError,
withInvalidate,
} from '@/react-tools/react-query';
import { withError } from '@/react-tools/react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { buildUrl } from '../build-url';
import {
updateSettings,
getSettings,
updateDefaultRegistry,
} from '../settings.service';
import { DefaultRegistry, Settings } from '../types';
EdgeSettings,
ExperimentalFeatures,
FDOConfiguration,
GlobalDeploymentOptions,
InternalAuthSettings,
LDAPSettings,
OAuthSettings,
OpenAMTConfiguration,
Pair,
} from '../types';
import { queryKeys } from './queryKeys';
import { PublicSettingsResponse } from './usePublicSettings';
export function useSettings<T = Settings>(
select?: (settings: Settings) => T,
enabled = true
) {
interface AuthenticatedResponse extends PublicSettingsResponse {
/** Deployment options for encouraging git ops workflows */
GlobalDeploymentOptions: GlobalDeploymentOptions;
/** Whether edge compute features are enabled */
EnableEdgeComputeFeatures: boolean;
/** The expiry of a Kubeconfig */
KubeconfigExpiry: string;
DefaultRegistry: {
Hide: boolean;
};
/** Helm repository URL, defaults to "https://charts.bitnami.com/bitnami" */
HelmRepositoryURL: string;
/** Experimental features */
ExperimentalFeatures: ExperimentalFeatures;
isAMTEnabled: boolean;
isFDOEnabled: boolean;
}
interface EdgeAdminResponse extends AuthenticatedResponse {
Edge: EdgeSettings;
/** TrustOnFirstConnect makes Portainer accepting edge agent connection by default */
TrustOnFirstConnect: boolean;
/** EnforceEdgeID makes Portainer store the Edge ID instead of accepting anyone */
EnforceEdgeID: boolean;
/** EdgePortainerUrl is the URL that is exposed to edge agents */
EdgePortainerUrl: string;
/** The default check in interval for edge agent (in seconds) */
EdgeAgentCheckinInterval: number;
}
interface AdminResponse extends EdgeAdminResponse {
/** A list of label name & value that will be used to hide containers when querying containers */
BlackListedLabels: Pair[];
LDAPSettings: LDAPSettings;
OAuthSettings: OAuthSettings;
InternalAuthSettings: InternalAuthSettings;
openAMTConfiguration: OpenAMTConfiguration;
fdoConfiguration: FDOConfiguration;
/** The interval in which environment(endpoint) snapshots are created */
SnapshotInterval: string;
/** URL to the templates that will be displayed in the UI when navigating to App Templates */
TemplatesURL: string;
/** The duration of a user session */
UserSessionTimeout: string;
/** KubectlImage, defaults to portainer/kubectl-shell */
KubectlShellImage: string;
/** Container environment parameter AGENT_SECRET */
AgentSecret: string;
}
interface SettingsResponse extends AdminResponse {}
export function useSettings<T = SettingsResponse>({
enabled,
select,
}: {
select?: (settings: SettingsResponse) => T;
enabled?: boolean;
} = {}) {
return useQuery(queryKeys.base(), getSettings, {
select,
enabled,
@ -27,26 +88,11 @@ export function useSettings<T = Settings>(
});
}
export function useUpdateSettingsMutation() {
const queryClient = useQueryClient();
return useMutation(
updateSettings,
mutationOptions(
withInvalidate(queryClient, [queryKeys.base(), ['cloud']]),
withError('Unable to update settings')
)
);
}
export function useUpdateDefaultRegistrySettingsMutation() {
const queryClient = useQueryClient();
return useMutation(
(payload: Partial<DefaultRegistry>) => updateDefaultRegistry(payload),
mutationOptions(
withInvalidate(queryClient, [queryKeys.base()]),
withError('Unable to update default registry settings')
)
);
export async function getSettings() {
try {
const { data } = await axios.get<SettingsResponse>(buildUrl());
return data;
} catch (e) {
throw parseAxiosError(e, 'Unable to retrieve application settings');
}
}

View file

@ -0,0 +1,35 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import {
mutationOptions,
withInvalidate,
withError,
} from '@/react-tools/react-query';
import { DefaultRegistry } from '../types';
import { buildUrl } from '../build-url';
import { queryKeys } from './queryKeys';
export function useUpdateDefaultRegistrySettingsMutation() {
const queryClient = useQueryClient();
return useMutation(
(payload: Partial<DefaultRegistry>) => updateDefaultRegistry(payload),
mutationOptions(
withInvalidate(queryClient, [queryKeys.base()]),
withError('Unable to update default registry settings')
)
);
}
export async function updateDefaultRegistry(
defaultRegistry: Partial<DefaultRegistry>
) {
try {
await axios.put(buildUrl('default_registry'), defaultRegistry);
} catch (e) {
throw parseAxiosError(e, 'Unable to update default registry settings');
}
}

View file

@ -0,0 +1,38 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import {
mutationOptions,
withError,
withInvalidate,
} from '@/react-tools/react-query';
import { buildUrl } from '../build-url';
import { Settings } from '../types';
import { queryKeys } from './queryKeys';
type OptionalSettings = Omit<Partial<Settings>, 'Edge'> & {
Edge?: Partial<Settings['Edge']>;
};
export async function updateSettings(settings: OptionalSettings) {
try {
const { data } = await axios.put<Settings>(buildUrl(), settings);
return data;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to update application settings');
}
}
export function useUpdateSettingsMutation() {
const queryClient = useQueryClient();
return useMutation(
updateSettings,
mutationOptions(
withInvalidate(queryClient, [queryKeys.base(), ['cloud']]),
withError('Unable to update settings')
)
);
}

View file

@ -1,73 +1,6 @@
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { PublicSettingsResponse, DefaultRegistry, Settings } from './types';
export async function getPublicSettings() {
try {
const { data } = await axios.get<PublicSettingsResponse>(
buildUrl('public')
);
return data;
} catch (e) {
throw parseAxiosError(
e as Error,
'Unable to retrieve application settings'
);
}
}
import { getSettings } from './queries/useSettings';
export async function getGlobalDeploymentOptions() {
const publicSettings = await getPublicSettings();
return publicSettings.GlobalDeploymentOptions;
}
export async function getSettings() {
try {
const { data } = await axios.get<Settings>(buildUrl());
return data;
} catch (e) {
throw parseAxiosError(
e as Error,
'Unable to retrieve application settings'
);
}
}
type OptionalSettings = Omit<Partial<Settings>, 'Edge'> & {
Edge?: Partial<Settings['Edge']>;
};
export async function updateSettings(settings: OptionalSettings) {
try {
const { data } = await axios.put<Settings>(buildUrl(), settings);
return data;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to update application settings');
}
}
export async function updateDefaultRegistry(
defaultRegistry: Partial<DefaultRegistry>
) {
try {
await axios.put(buildUrl('default_registry'), defaultRegistry);
} catch (e) {
throw parseAxiosError(
e as Error,
'Unable to update default registry settings'
);
}
}
export function buildUrl(subResource?: string, action?: string) {
let url = 'settings';
if (subResource) {
url += `/${subResource}`;
}
if (action) {
url += `/${action}`;
}
return url;
const settings = await getSettings();
return settings.GlobalDeploymentOptions;
}

View file

@ -100,7 +100,7 @@ export enum OAuthStyle {
InHeader,
}
type Feature = string;
export type Feature = string;
export interface DefaultRegistry {
Hide: boolean;
@ -110,17 +110,29 @@ export interface ExperimentalFeatures {
OpenAIIntegration: boolean;
}
export interface InternalAuthSettings {
RequiredPasswordLength: number;
}
export interface EdgeSettings {
PingInterval: number;
SnapshotInterval: number;
CommandInterval: number;
AsyncMode: boolean;
TunnelServerAddress: string;
}
export interface Settings {
LogoURL: string;
CustomLoginBanner: string;
BlackListedLabels: Pair[];
AuthenticationMethod: AuthenticationMethod;
InternalAuthSettings: { RequiredPasswordLength: number };
InternalAuthSettings: InternalAuthSettings;
LDAPSettings: LDAPSettings;
OAuthSettings: OAuthSettings;
openAMTConfiguration: OpenAMTConfiguration;
fdoConfiguration: FDOConfiguration;
FeatureFlagSettings: { [key: Feature]: boolean };
Features: { [key: Feature]: boolean };
SnapshotInterval: string;
TemplatesURL: string;
EnableEdgeComputeFeatures: boolean;
@ -134,28 +146,10 @@ export interface Settings {
AgentSecret: string;
EdgePortainerUrl: string;
EdgeAgentCheckinInterval: number;
EdgeCommandInterval: number;
EdgePingInterval: number;
EdgeSnapshotInterval: number;
DisplayDonationHeader: boolean;
DisplayExternalContributors: boolean;
EnableHostManagementFeatures: boolean;
DefaultRegistry: DefaultRegistry;
ExperimentalFeatures?: ExperimentalFeatures;
AllowVolumeBrowserForRegularUsers: boolean;
AllowBindMountsForRegularUsers: boolean;
AllowPrivilegedModeForRegularUsers: boolean;
AllowHostNamespaceForRegularUsers: boolean;
AllowStackManagementForRegularUsers: boolean;
AllowDeviceMappingForRegularUsers: boolean;
AllowContainerCapabilitiesForRegularUsers: boolean;
GlobalDeploymentOptions?: GlobalDeploymentOptions;
Edge: {
PingInterval: number;
SnapshotInterval: number;
CommandInterval: number;
AsyncMode: boolean;
TunnelServerAddress: string;
};
Edge: EdgeSettings;
}
export interface GlobalDeploymentOptions {
@ -173,52 +167,3 @@ export interface GlobalDeploymentOptions {
hideStacksFunctionality: boolean;
}
export interface PublicSettingsResponse {
/** URL to a logo that will be displayed on the login page as well as on top of the sidebar. Will use default Portainer logo when value is empty string */
LogoURL: string;
/** The content in plaintext used to display in the login page. Will hide when value is empty string (only on BE) */
CustomLoginBanner: string;
/** Active authentication method for the Portainer instance. Valid values are: 1 for internal, 2 for LDAP, or 3 for oauth */
AuthenticationMethod: AuthenticationMethod;
/** The minimum required length for a password of any user when using internal auth mode */
RequiredPasswordLength: number;
/** Deployment options for encouraging deployment as code (only on BE) */
GlobalDeploymentOptions: GlobalDeploymentOptions;
/** Whether edge compute features are enabled */
EnableEdgeComputeFeatures: boolean;
/** Supported feature flags */
Features: { [key: Feature]: boolean };
/** The URL used for oauth login */
OAuthLoginURI: string;
/** The URL used for oauth logout */
OAuthLogoutURI: string;
/** Whether portainer internal auth view will be hidden (only on BE) */
OAuthHideInternalAuth: boolean;
/** Whether telemetry is enabled */
EnableTelemetry: boolean;
/** The expiry of a Kubeconfig */
KubeconfigExpiry: string;
/** Whether team sync is enabled */
TeamSync: boolean;
/** Whether FDO is enabled */
IsFDOEnabled: boolean;
/** Whether AMT is enabled */
IsAMTEnabled: boolean;
/** Whether to hide default registry (only on BE) */
DefaultRegistry?: {
Hide: boolean;
};
Edge: {
/** Whether the device has been started in edge async mode */
AsyncMode: boolean;
/** The ping interval for edge agent - used in edge async mode [seconds] */
PingInterval: number;
/** The snapshot interval for edge agent - used in edge async mode [seconds] */
SnapshotInterval: number;
/** The command list interval for edge agent - used in edge async mode [seconds] */
CommandInterval: number;
/** The check in interval for edge agent (in seconds) - used in non async mode [seconds] */
CheckinInterval: number;
};
}

View file

@ -8,7 +8,10 @@ import {
Bell,
} from 'lucide-react';
import { usePublicSettings } from '@/react/portainer/settings/queries';
import {
usePublicSettings,
useSettings,
} from '@/react/portainer/settings/queries';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { SidebarItem } from './SidebarItem';
@ -226,7 +229,7 @@ export function SettingsSidebar({ isPureAdmin, isAdmin, isTeamLeader }: Props) {
}
function EdgeUpdatesSidebarItem() {
const settingsQuery = usePublicSettings();
const settingsQuery = useSettings();
if (!isBE || !settingsQuery.data?.EnableEdgeComputeFeatures) {
return null;

View file

@ -3,7 +3,7 @@ import { Home } from 'lucide-react';
import { useIsEdgeAdmin, useIsPureAdmin } from '@/react/hooks/useUser';
import { useIsCurrentUserTeamLeader } from '@/portainer/users/queries';
import { usePublicSettings } from '@/react/portainer/settings/queries';
import { useSettings } from '@/react/portainer/settings/queries';
import styles from './Sidebar.module.css';
import { EdgeComputeSidebar } from './EdgeComputeSidebar';
@ -30,7 +30,7 @@ function InnerSidebar() {
const isTeamLeader = useIsCurrentUserTeamLeader();
const { isOpen } = useSidebarState();
const settingsQuery = usePublicSettings();
const settingsQuery = useSettings();
if (!settingsQuery.data || isAdminQuery.isLoading) {
return null;

View file

@ -9,9 +9,10 @@ import { EnvironmentGroup } from '@/react/portainer/environments/environment-gro
import { Tag } from '@/portainer/tags/types';
import { StatusResponse } from '@/react/portainer/system/useSystemStatus';
import { createMockTeams } from '@/react-tools/test-mocks';
import { PublicSettingsResponse } from '@/react/portainer/settings/types';
import { PublicSettingsResponse } from '@/react/portainer/settings/queries/usePublicSettings';
import { UserId } from '@/portainer/users/types';
import { VersionResponse } from '@/react/portainer/system/useSystemVersion';
import { Settings } from '@/react/portainer/settings/types';
import { azureHandlers } from './setup-handlers/azure';
import { dockerHandlers } from './setup-handlers/docker';
@ -72,16 +73,10 @@ export const handlers = [
}),
http.get<never, never, Partial<PublicSettingsResponse>>(
'/api/settings/public',
() =>
HttpResponse.json({
Edge: {
AsyncMode: false,
CheckinInterval: 60,
CommandInterval: 60,
PingInterval: 60,
SnapshotInterval: 60,
},
})
() => HttpResponse.json({})
),
http.get<never, never, Partial<Settings>>('/api/settings', () =>
HttpResponse.json({})
),
http.get<never, never, Partial<StatusResponse>>('/api/status', () =>
HttpResponse.json({})