Compare commits
9 Commits
feat/EE-16
...
feat/EE-60
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f3ac41904 | ||
|
|
c324bfadaf | ||
|
|
50ed81daaa | ||
|
|
c75f3ff0aa | ||
|
|
130f53cbe0 | ||
|
|
6bf3eb4afd | ||
|
|
b99cf4b3c8 | ||
|
|
0a8d722aed | ||
|
|
16507ded13 |
11
api/bolt/migrator/migrate_dbversion30.go
Normal file
11
api/bolt/migrator/migrate_dbversion30.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package migrator
|
||||
|
||||
func (m *Migrator) updateSettingsToDB31() error {
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
legacySettings.OAuthSettings.SSO = false
|
||||
legacySettings.OAuthSettings.LogoutURI = ""
|
||||
return m.settingsService.UpdateSettings(legacySettings)
|
||||
}
|
||||
64
api/bolt/migrator/migrate_dbversion30_test.go
Normal file
64
api/bolt/migrator/migrate_dbversion30_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer/api/bolt/settings"
|
||||
)
|
||||
|
||||
var (
|
||||
testingDBStorePath string
|
||||
testingDBFileName string
|
||||
dummyLogoURL string
|
||||
dbConn *bolt.DB
|
||||
settingsService *settings.Service
|
||||
)
|
||||
|
||||
func setup() error {
|
||||
testingDBStorePath, _ = os.Getwd()
|
||||
testingDBFileName = "portainer-ee-mig-30.db"
|
||||
dummyLogoURL = "example.com"
|
||||
var err error
|
||||
dbConn, err = initTestingDBConn(testingDBStorePath, testingDBFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dummySettingsObj := map[string]interface{}{
|
||||
"LogoURL": dummyLogoURL,
|
||||
}
|
||||
settingsService, err = initTestingSettingsService(dbConn, dummySettingsObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestUpdateSettingsToDB31(t *testing.T) {
|
||||
if err := setup(); err != nil {
|
||||
t.Errorf("failed to complete testing setups, err: %v", err)
|
||||
}
|
||||
defer dbConn.Close()
|
||||
defer os.Remove(testingDBFileName)
|
||||
m := &Migrator{
|
||||
db: dbConn,
|
||||
settingsService: settingsService,
|
||||
}
|
||||
if err := m.updateSettingsToDB31(); err != nil {
|
||||
t.Errorf("failed to update settings: %v", err)
|
||||
}
|
||||
updatedSettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
t.Errorf("failed to retrieve the updated settings: %v", err)
|
||||
}
|
||||
if updatedSettings.LogoURL != dummyLogoURL {
|
||||
t.Errorf("unexpected value changes in the updated settings, want LogoURL value: %s, got LogoURL value: %s", dummyLogoURL, updatedSettings.LogoURL)
|
||||
}
|
||||
if updatedSettings.OAuthSettings.SSO != false {
|
||||
t.Errorf("unexpected default OAuth SSO setting, want: false, got: %t", updatedSettings.OAuthSettings.SSO)
|
||||
}
|
||||
if updatedSettings.OAuthSettings.LogoutURI != "" {
|
||||
t.Errorf("unexpected default OAuth HideInternalAuth setting, want:, got: %s", updatedSettings.OAuthSettings.LogoutURI)
|
||||
}
|
||||
}
|
||||
38
api/bolt/migrator/migrate_test_helper.go
Normal file
38
api/bolt/migrator/migrate_test_helper.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
"github.com/portainer/portainer/api/bolt/settings"
|
||||
)
|
||||
|
||||
// initTestingDBConn creates a raw bolt DB connection
|
||||
// for unit testing usage only since using NewStore will cause cycle import inside migrator pkg
|
||||
func initTestingDBConn(storePath, fileName string) (*bolt.DB, error) {
|
||||
databasePath := path.Join(storePath, fileName)
|
||||
dbConn, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dbConn, nil
|
||||
}
|
||||
|
||||
// initTestingDBConn creates a settings service with raw bolt DB connection
|
||||
// for unit testing usage only since using NewStore will cause cycle import inside migrator pkg
|
||||
func initTestingSettingsService(dbConn *bolt.DB, preSetObj map[string]interface{}) (*settings.Service, error) {
|
||||
internalDBConn := &internal.DbConnection{
|
||||
DB: dbConn,
|
||||
}
|
||||
settingsService, err := settings.NewService(internalDBConn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//insert a obj
|
||||
if err := internal.UpdateObject(internalDBConn, "settings", []byte("SETTINGS"), preSetObj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return settingsService, nil
|
||||
}
|
||||
@@ -358,5 +358,13 @@ func (m *Migrator) Migrate() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.5.0
|
||||
if m.currentDBVersion < 31 {
|
||||
err := m.updateSettingsToDB31()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ type publicSettingsResponse struct {
|
||||
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures" example:"true"`
|
||||
// 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"`
|
||||
}
|
||||
@@ -34,20 +36,32 @@ type publicSettingsResponse struct {
|
||||
func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve the settings from the database", err}
|
||||
}
|
||||
|
||||
publicSettings := &publicSettingsResponse{
|
||||
LogoURL: settings.LogoURL,
|
||||
AuthenticationMethod: settings.AuthenticationMethod,
|
||||
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
|
||||
EnableTelemetry: settings.EnableTelemetry,
|
||||
OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login",
|
||||
settings.OAuthSettings.AuthorizationURI,
|
||||
settings.OAuthSettings.ClientID,
|
||||
settings.OAuthSettings.RedirectURI,
|
||||
settings.OAuthSettings.Scopes),
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve the settings from the database", Err: err}
|
||||
}
|
||||
|
||||
publicSettings := generatePublicSettings(settings)
|
||||
return response.JSON(w, publicSettings)
|
||||
}
|
||||
|
||||
func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResponse {
|
||||
publicSettings := &publicSettingsResponse{
|
||||
LogoURL: appSettings.LogoURL,
|
||||
AuthenticationMethod: appSettings.AuthenticationMethod,
|
||||
EnableEdgeComputeFeatures: appSettings.EnableEdgeComputeFeatures,
|
||||
EnableTelemetry: appSettings.EnableTelemetry,
|
||||
}
|
||||
//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
|
||||
}
|
||||
|
||||
70
api/http/handler/settings/settings_public_test.go
Normal file
70
api/http/handler/settings/settings_public_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
const (
|
||||
dummyOAuthClientID = "1a2b3c4d"
|
||||
dummyOAuthScopes = "scopes"
|
||||
dummyOAuthAuthenticationURI = "example.com/auth"
|
||||
dummyOAuthRedirectURI = "example.com/redirect"
|
||||
dummyOAuthLogoutURI = "example.com/logout"
|
||||
)
|
||||
|
||||
var (
|
||||
dummyOAuthLoginURI string
|
||||
mockAppSettings *portainer.Settings
|
||||
)
|
||||
|
||||
func setup() {
|
||||
dummyOAuthLoginURI = fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s",
|
||||
dummyOAuthAuthenticationURI,
|
||||
dummyOAuthClientID,
|
||||
dummyOAuthRedirectURI,
|
||||
dummyOAuthScopes)
|
||||
mockAppSettings = &portainer.Settings{
|
||||
AuthenticationMethod: portainer.AuthenticationOAuth,
|
||||
OAuthSettings: portainer.OAuthSettings{
|
||||
AuthorizationURI: dummyOAuthAuthenticationURI,
|
||||
ClientID: dummyOAuthClientID,
|
||||
Scopes: dummyOAuthScopes,
|
||||
RedirectURI: dummyOAuthRedirectURI,
|
||||
LogoutURI: dummyOAuthLogoutURI,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePublicSettingsWithSSO(t *testing.T) {
|
||||
setup()
|
||||
mockAppSettings.OAuthSettings.SSO = true
|
||||
publicSettings := generatePublicSettings(mockAppSettings)
|
||||
if publicSettings.AuthenticationMethod != portainer.AuthenticationOAuth {
|
||||
t.Errorf("wrong AuthenticationMethod, want: %d, got: %d", portainer.AuthenticationOAuth, publicSettings.AuthenticationMethod)
|
||||
}
|
||||
if publicSettings.OAuthLoginURI != dummyOAuthLoginURI {
|
||||
t.Errorf("wrong OAuthLoginURI when SSO is switched on, want: %s, got: %s", dummyOAuthLoginURI, publicSettings.OAuthLoginURI)
|
||||
}
|
||||
if publicSettings.OAuthLogoutURI != dummyOAuthLogoutURI {
|
||||
t.Errorf("wrong OAuthLogoutURI, want: %s, got: %s", dummyOAuthLogoutURI, publicSettings.OAuthLogoutURI)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePublicSettingsWithoutSSO(t *testing.T) {
|
||||
setup()
|
||||
mockAppSettings.OAuthSettings.SSO = false
|
||||
publicSettings := generatePublicSettings(mockAppSettings)
|
||||
if publicSettings.AuthenticationMethod != portainer.AuthenticationOAuth {
|
||||
t.Errorf("wrong AuthenticationMethod, want: %d, got: %d", portainer.AuthenticationOAuth, publicSettings.AuthenticationMethod)
|
||||
}
|
||||
expectedOAuthLoginURI := dummyOAuthLoginURI + "&prompt=login"
|
||||
if publicSettings.OAuthLoginURI != expectedOAuthLoginURI {
|
||||
t.Errorf("wrong OAuthLoginURI when SSO is switched off, want: %s, got: %s", expectedOAuthLoginURI, publicSettings.OAuthLoginURI)
|
||||
}
|
||||
if publicSettings.OAuthLogoutURI != dummyOAuthLogoutURI {
|
||||
t.Errorf("wrong OAuthLogoutURI, want: %s, got: %s", dummyOAuthLogoutURI, publicSettings.OAuthLogoutURI)
|
||||
}
|
||||
}
|
||||
@@ -489,6 +489,8 @@ type (
|
||||
Scopes string `json:"Scopes"`
|
||||
OAuthAutoCreateUsers bool `json:"OAuthAutoCreateUsers"`
|
||||
DefaultTeamID TeamID `json:"DefaultTeamID"`
|
||||
SSO bool `json:"SSO"`
|
||||
LogoutURI string `json:"LogoutURI"`
|
||||
}
|
||||
|
||||
// Pair defines a key/value string pair
|
||||
@@ -1329,7 +1331,7 @@ const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.4.0"
|
||||
// DBVersion is the version number of the Portainer database
|
||||
DBVersion = 27
|
||||
DBVersion = 31
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
ComposeSyntaxMaxVersion = "3.9"
|
||||
// AssetsServerURL represents the URL of the Portainer asset server
|
||||
|
||||
@@ -1,4 +1,32 @@
|
||||
<div>
|
||||
<div
|
||||
><div class="col-sm-12 form-section-title">
|
||||
Single Sign-On
|
||||
</div>
|
||||
<!-- SSO -->
|
||||
<div class="form-group">
|
||||
<label for="use-sso" class="control-label col-sm-2 text-left" style="padding-top: 0;">
|
||||
Use SSO
|
||||
<portainer-tooltip position="bottom" message="When using SSO the OAuth provider is not forced to prompt for credentials."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<label class="switch"> <input id="use-sso" type="checkbox" ng-model="$ctrl.settings.SSO" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !SSO -->
|
||||
|
||||
<!-- HideInternalAuth -->
|
||||
<div class="form-group" ng-if="$ctrl.settings.SSO">
|
||||
<label for="hide-internal-auth" class="control-label col-sm-2 text-left" style="padding-top: 0;">
|
||||
Hide internal authentication prompt
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<label class="switch"> <input id="hide-internal-auth" type="checkbox" disabled /><i></i> </label>
|
||||
<span class="text-muted small" style="margin-left: 15px;">
|
||||
This feature is available in <a href="https://www.portainer.io/business-upsell?from=hide-internal-auth" target="_blank"> Portainer Business Edition</a>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !HideInternalAuth -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Automatic user provisioning
|
||||
</div>
|
||||
@@ -105,7 +133,18 @@
|
||||
<input type="text" class="form-control" id="oauth_redirect_uri" ng-model="$ctrl.settings.RedirectURI" placeholder="http://yourportainer.com/" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="oauth_logout_url" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Logout URL
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="URL used by Portainer to redirect the user to the OAuth provider in order to log the user out of the identity provider session."
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" id="oauth_logout_url" ng-model="$ctrl.settings.LogoutURI" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="oauth_user_identifier" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
User identifier
|
||||
|
||||
Reference in New Issue
Block a user