Compare commits

...

8 Commits

Author SHA1 Message Date
Felix Han
30fc71e382 feat(oauth): updated logout logic with logoutUrl. 2021-05-11 23:31:10 +12:00
Hui
50ed81daaa feat(DB): Add new migration func for SSO settings EE-613 2021-05-11 15:08:05 +12:00
ArrisLee
c75f3ff0aa dbversion update 2021-05-11 10:32:13 +12:00
ArrisLee
130f53cbe0 cleanup and make helper func for unit testing 2021-05-11 10:26:32 +12:00
fhanportainer
6bf3eb4afd feat(oauth): add sso, hide internal auth teaser and logout options. (#5039) 2021-05-10 14:48:37 +12:00
ArrisLee
b99cf4b3c8 migration func naming modification 2021-05-07 13:28:24 +12:00
ArrisLee
0a8d722aed update DBversion const 2021-05-07 13:09:06 +12:00
ArrisLee
16507ded13 add updateSettingsToDB28 func and test 2021-05-07 13:05:29 +12:00
7 changed files with 174 additions and 5 deletions

View 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)
}

View 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)
}
}

View 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
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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

View File

@@ -2,7 +2,7 @@ import angular from 'angular';
class LogoutController {
/* @ngInject */
constructor($async, $state, $transition$, Authentication, StateManager, Notifications, LocalStorage) {
constructor($async, $state, $transition$, $window, Authentication, StateManager, Notifications, LocalStorage, SettingsService) {
this.$async = $async;
this.$state = $state;
this.$transition$ = $transition$;
@@ -11,6 +11,7 @@ class LogoutController {
this.StateManager = StateManager;
this.Notifications = Notifications;
this.LocalStorage = LocalStorage;
this.SettingsService = SettingsService;
this.logo = this.StateManager.getState().application.logo;
this.logoutAsync = this.logoutAsync.bind(this);
@@ -24,11 +25,17 @@ class LogoutController {
async logoutAsync() {
const error = this.$transition$.params().error;
const performApiLogout = this.$transition$.params().performApiLogout;
const settings = await this.SettingsService.publicSettings();
try {
await this.Authentication.logout(performApiLogout);
} finally {
this.LocalStorage.storeLogoutReason(error);
this.$state.go('portainer.auth', { reload: true });
if (settings.OAuthLogoutURI) {
this.$window.location.href = settings.OAuthLogoutURI;
} else {
this.$state.go('portainer.auth', { reload: true });
}
}
}