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:
parent
b0e3afa0b6
commit
3386b1a18a
55 changed files with 1018 additions and 479 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
294
api/http/handler/settings/settings_inspect_response_helper.go
Normal file
294
api/http/handler/settings/settings_inspect_response_helper.go
Normal 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
|
||||
}
|
170
api/http/handler/settings/settings_inspect_test.go
Normal file
170
api/http/handler/settings/settings_inspect_test.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
53
api/http/rbacutils/rbac_utils.go
Normal file
53
api/http/rbacutils/rbac_utils.go
Normal 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
|
||||
}
|
35
api/http/utils/response.go
Normal file
35
api/http/utils/response.go
Normal 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)
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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' } },
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
})
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
|
||||
|
|
|
@ -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 ||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
12
app/react/portainer/settings/build-url.ts
Normal file
12
app/react/portainer/settings/build-url.ts
Normal 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;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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')
|
||||
)
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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({})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue