Compare commits

...

22 Commits

Author SHA1 Message Date
ArrisLee
8cda372e38 resolve conflict 2021-10-08 15:17:53 +13:00
ArrisLee
339a44e43d update unit test 2021-08-12 11:30:50 +12:00
ArrisLee
c3a62797d7 cleanups 2021-08-12 09:09:57 +12:00
ArrisLee
fbfe81b3a1 update error message 2021-08-11 17:30:52 +12:00
ArrisLee
fba55db143 cleanup 2021-08-11 17:24:59 +12:00
ArrisLee
aa9400e9dc resolve conflics 2021-08-11 10:48:56 +12:00
ArrisLee
002649c81a backport changes from EE 2021-08-11 03:01:53 +12:00
Felix Han
204542f073 sorting lowercase first 2021-08-08 12:14:18 +12:00
Felix Han
521cb19fbc moved sorting group items from back end to front end 2021-08-05 14:30:58 +12:00
Felix Han
290844ffee PR feedback updates 2021-07-29 23:45:31 +12:00
Felix Han
d87aa4ca23 replaced por-switch-field with plain html 2021-07-29 14:14:18 +12:00
ArrisLee
dfb01215ad resolve conflicts 2021-07-21 22:57:51 +12:00
Felix Han
6a7343179f change assign admin group text color based on group length 2021-07-20 17:05:29 +12:00
ArrisLee
7ae5cb21cb resolve conflict 2021-07-19 13:01:31 +12:00
Hui
e03ede1dfb feat(ldap): update get user groups func EE-1148 (#5321)
* feat(ldap): update get user groups func

* cleanup
2021-07-19 09:25:38 +12:00
ArrisLee
0972ae534f Merge branch 'feat/EE-568/admin-auto-population-ldap' of github.com:portainer/portainer into feat/EE-568/admin-auto-population-ldap 2021-07-19 00:07:01 +12:00
ArrisLee
9da73e1594 sort admin groups 2021-07-19 00:02:01 +12:00
fhanportainer
aaa9c0f096 feat(settings): added auto populate admin group section in LDAP (#5280)
* feat(settings): added auto populate admin group section  in LDAP

* moved admin groups API call to newly created ldap service

* preload admin groups

* disable admin auto populate when not selecting any groups
2021-07-18 22:34:07 +12:00
ArrisLee
5474bfae5b update payload validator 2021-07-18 15:34:27 +12:00
ArrisLee
6d7a5da1f2 update handler struct to support ldap 2021-07-16 12:10:56 +12:00
Hui
5c774141f4 feat(ldap): auto admin group mapping EE-994 2021-07-16 11:26:06 +12:00
Hui
3eebe3a08a feat(ldap): update LDAP settings for auto admin population EE-992 (#5282)
* update LDAP settings for auto admin population

* cleanup
2021-07-14 14:51:45 +12:00
24 changed files with 961 additions and 428 deletions

View File

@@ -34,10 +34,13 @@ func (store *Store) Init() error {
AutoCreateUsers: true,
TLSConfig: portainer.TLSConfiguration{},
SearchSettings: []portainer.LDAPSearchSettings{
portainer.LDAPSearchSettings{},
{},
},
GroupSearchSettings: []portainer.LDAPGroupSearchSettings{
portainer.LDAPGroupSearchSettings{},
{},
},
AdminGroupSearchSettings: []portainer.LDAPGroupSearchSettings{
{},
},
},
OAuthSettings: portainer.OAuthSettings{},

View File

@@ -1,6 +1,6 @@
package migrator
import "github.com/portainer/portainer/api"
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) updateSettingsToVersion13() error {
legacySettings, err := m.settingsService.Settings()
@@ -10,7 +10,7 @@ func (m *Migrator) updateSettingsToVersion13() error {
legacySettings.LDAPSettings.AutoCreateUsers = false
legacySettings.LDAPSettings.GroupSearchSettings = []portainer.LDAPGroupSearchSettings{
portainer.LDAPGroupSearchSettings{},
{},
}
return m.settingsService.UpdateSettings(legacySettings)

View File

@@ -21,7 +21,13 @@ func (m *Migrator) migrateDBVersionToDB32() error {
return err
}
if err := m.updateVolumeResourceControlToDB32(); err != nil {
err = m.updateAdminGroupSearchSettingsToDB32()
if err != nil {
return err
}
err = m.updateVolumeResourceControlToDB32()
if err != nil {
return err
}
@@ -228,6 +234,17 @@ func findResourcesToUpdateForDB32(dockerID string, volumesData map[string]interf
}
}
func (m *Migrator) updateAdminGroupSearchSettingsToDB32() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
if legacySettings.LDAPSettings.AdminGroupSearchSettings == nil {
legacySettings.LDAPSettings.AdminGroupSearchSettings = []portainer.LDAPGroupSearchSettings{}
}
return m.settingsService.UpdateSettings(legacySettings)
}
func (m *Migrator) kubeconfigExpiryToDB32() error {
settings, err := m.settingsService.Settings()
if err != nil {

View File

@@ -0,0 +1,37 @@
package migrator
import (
"path"
"testing"
"time"
"github.com/boltdb/bolt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/portainer/portainer/api/bolt/settings"
"github.com/stretchr/testify/assert"
)
func TestMigrateAdminGroupSearchSettings(t *testing.T) {
databasePath := path.Join(t.TempDir(), "portainer-ee-mig-32.db")
dbConn, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second})
assert.NoError(t, err, "failed to init DB connection")
defer dbConn.Close()
internalDBConn := &internal.DbConnection{DB: dbConn}
settingsService, err := settings.NewService(internalDBConn)
assert.NoError(t, err, "failed to init settings service")
settingsService.UpdateSettings(&portainer.Settings{})
m := &Migrator{
db: dbConn,
settingsService: settingsService,
}
err = m.updateAdminGroupSearchSettingsToDB32()
assert.NoError(t, err, "failed to update settings")
updatedSettings, err := settingsService.Settings()
assert.NoError(t, err, "failed to fetch updated settings")
assert.NotNil(t, updatedSettings.LDAPSettings.AdminGroupSearchSettings)
}

View File

@@ -1,11 +1,12 @@
package auth
import (
"errors"
"log"
"net/http"
"strings"
errors "github.com/pkg/errors"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -53,28 +54,33 @@ func (handler *Handler) authenticate(w http.ResponseWriter, r *http.Request) *ht
var payload authenticatePayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve settings from the database", Err: err}
}
u, err := handler.DataStore.User().UserByUsername(payload.Username)
if err != nil && err != bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a user with the specified username from the database", Err: err}
}
if err == bolterrors.ErrObjectNotFound && (settings.AuthenticationMethod == portainer.AuthenticationInternal || settings.AuthenticationMethod == portainer.AuthenticationOAuth) {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
return &httperror.HandlerError{StatusCode: http.StatusUnprocessableEntity, Message: "Invalid credentials", Err: httperrors.ErrUnauthorized}
}
if settings.AuthenticationMethod == portainer.AuthenticationLDAP {
if u == nil && settings.LDAPSettings.AutoCreateUsers {
return handler.authenticateLDAPAndCreateUser(w, payload.Username, payload.Password, &settings.LDAPSettings)
} else if u == nil && !settings.LDAPSettings.AutoCreateUsers {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
if u == nil {
if settings.LDAPSettings.AutoCreateUsers || settings.LDAPSettings.AdminAutoPopulate {
return handler.authenticateLDAPAndCreateUser(w, payload.Username, payload.Password, &settings.LDAPSettings)
}
return &httperror.HandlerError{
StatusCode: http.StatusUnprocessableEntity,
Message: "Invalid credentials",
Err: httperrors.ErrUnauthorized,
}
}
return handler.authenticateLDAP(w, u, payload.Password, &settings.LDAPSettings)
}
@@ -88,18 +94,57 @@ func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.
return handler.authenticateInternal(w, user, password)
}
if ldapSettings.AdminAutoPopulate {
isLDAPAdmin, err := isLDAPAdmin(user.Username, handler.LDAPService, ldapSettings)
if err != nil {
return &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to search and match LDAP admin groups",
Err: err,
}
}
if isLDAPAdmin && user.Role != portainer.AdministratorRole {
if err := handler.updateUserRole(user, portainer.AdministratorRole); err != nil {
return &httperror.HandlerError{
StatusCode: http.StatusUnprocessableEntity,
Message: "Failed to assign admin role to the user",
Err: err,
}
}
}
if !isLDAPAdmin && user.Role == portainer.AdministratorRole {
if err := handler.updateUserRole(user, portainer.StandardUserRole); err != nil {
return &httperror.HandlerError{
StatusCode: http.StatusUnprocessableEntity,
Message: "Failed to remove admin role from the user",
Err: err,
}
}
}
}
err = handler.addUserIntoTeams(user, ldapSettings)
if err != nil {
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
}
err = handler.AuthorizationService.UpdateUsersAuthorizations()
if err != nil {
return &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to update user authorizations",
Err: err,
}
}
return handler.writeToken(w, user)
}
func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portainer.User, password string) *httperror.HandlerError {
err := handler.CryptoService.CompareHashAndData(user.Password, password)
if err != nil {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
return &httperror.HandlerError{StatusCode: http.StatusUnprocessableEntity, Message: "Invalid credentials", Err: httperrors.ErrUnauthorized}
}
return handler.writeToken(w, user)
@@ -108,7 +153,7 @@ func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portai
func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, username, password string, ldapSettings *portainer.LDAPSettings) *httperror.HandlerError {
err := handler.LDAPService.AuthenticateUser(username, password, ldapSettings)
if err != nil {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", err}
return &httperror.HandlerError{StatusCode: http.StatusUnprocessableEntity, Message: "Invalid credentials", Err: err}
}
user := &portainer.User{
@@ -116,9 +161,23 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
Role: portainer.StandardUserRole,
}
if ldapSettings.AdminAutoPopulate {
isLDAPAdmin, err := isLDAPAdmin(username, handler.LDAPService, ldapSettings)
if err != nil {
return &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to search and match LDAP admin groups",
Err: err,
}
}
if isLDAPAdmin {
user.Role = portainer.AdministratorRole
}
}
err = handler.DataStore.User().CreateUser(user)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist user inside the database", Err: err}
}
err = handler.addUserIntoTeams(user, ldapSettings)
@@ -126,6 +185,16 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
}
err = handler.AuthorizationService.UpdateUsersAuthorizations()
if err != nil {
return &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to update user authorizations",
Err: err,
}
}
return handler.writeToken(w, user)
}
@@ -148,7 +217,7 @@ func (handler *Handler) addUserIntoTeams(user *portainer.User, settings *portain
return err
}
userGroups, err := handler.LDAPService.GetUserGroups(user.Username, settings)
userGroups, err := handler.LDAPService.GetUserGroups(user.Username, settings, false)
if err != nil {
return err
}
@@ -181,6 +250,36 @@ func (handler *Handler) addUserIntoTeams(user *portainer.User, settings *portain
return nil
}
func isLDAPAdmin(username string, ldapService portainer.LDAPService, ldapSettings *portainer.LDAPSettings) (bool, error) {
//get groups the user belongs to
userGroups, err := ldapService.GetUserGroups(username, ldapSettings, true)
if err != nil {
return false, errors.Wrap(err, "failed to retrieve user groups from LDAP server")
}
//convert the AdminGroups recorded in LDAP Settings to a map
adminGroupsMap := make(map[string]bool)
for _, adminGroup := range ldapSettings.AdminGroups {
adminGroupsMap[adminGroup] = true
}
//check if any of the user groups matches the admin group records
for _, userGroup := range userGroups {
if adminGroupsMap[userGroup] {
return true, nil
}
}
return false, nil
}
func (handler *Handler) updateUserRole(user *portainer.User, role portainer.UserRole) error {
user.Role = role
if err := handler.DataStore.User().UpdateUser(user.ID, user); err != nil {
return errors.New("Unable to update user role inside the database")
}
return nil
}
func teamExists(teamName string, ldapGroups []string) bool {
for _, group := range ldapGroups {
if strings.ToLower(group) == strings.ToLower(teamName) {

View File

@@ -0,0 +1,34 @@
package auth
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
)
func TestIsLDAPAdmin_Match(t *testing.T) {
ldapService := testhelpers.NewLDAPService()
mockLDAPSettings := &portainer.LDAPSettings{
AdminGroups: []string{"manager", "stuff"},
}
isLDAPAdmin, err := isLDAPAdmin("username", ldapService, mockLDAPSettings)
assert.NoError(t, err)
assert.Equal(t, true, isLDAPAdmin)
}
func TestIsLDAPAdmin_NotMatch(t *testing.T) {
ldapService := testhelpers.NewLDAPService()
mockLDAPSettings := &portainer.LDAPSettings{
AdminGroups: []string{"admin", "manager"},
}
isLDAPAdmin, err := isLDAPAdmin("username", ldapService, mockLDAPSettings)
assert.NoError(t, err)
assert.Equal(t, false, isLDAPAdmin)
}

View File

@@ -5,10 +5,11 @@ import (
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
)
// Handler is the HTTP handler used to handle authentication operations.
@@ -21,6 +22,7 @@ type Handler struct {
OAuthService portainer.OAuthService
ProxyManager *proxy.Manager
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
AuthorizationService *authorization.Service
}
// NewHandler creates a handler to manage authentication operations.

View File

@@ -26,6 +26,10 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
h.Handle("/ldap/check",
bouncer.AdminAccess(httperror.LoggerHandler(h.ldapCheck))).Methods(http.MethodPost)
h.Handle("/ldap/admin-groups",
bouncer.AdminAccess(httperror.LoggerHandler(h.ldapAdminGroups))).Methods(http.MethodPost)
h.Handle("/ldap/test",
bouncer.AdminAccess(httperror.LoggerHandler(h.ldapTestLogin))).Methods(http.MethodPost)
return h
}

View File

@@ -1,6 +1,7 @@
package ldap
import (
"errors"
"net/http"
httperror "github.com/portainer/libhttp/error"
@@ -14,38 +15,31 @@ type checkPayload struct {
}
func (payload *checkPayload) Validate(r *http.Request) error {
if len(payload.LDAPSettings.URL) == 0 {
return errors.New("Invalid LDAP URL")
}
return nil
}
// @id LDAPCheck
// @summary Test LDAP connectivity
// @description Test LDAP connectivity using LDAP details
// @description **Access policy**: administrator
// @tags ldap
// @security jwt
// @accept json
// @param body body checkPayload true "details"
// @success 204 "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /ldap/check [post]
// POST request on /ldap/check
func (handler *Handler) ldapCheck(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload checkPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
settings := &payload.LDAPSettings
err = handler.prefillSettings(settings)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to fetch default settings", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to fetch default settings", Err: err}
}
err = handler.LDAPService.TestConnectivity(settings)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to connect to LDAP server", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to connect to LDAP server", Err: err}
}
return response.Empty(w)

View File

@@ -0,0 +1,60 @@
package ldap
import (
"errors"
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
)
type adminGroupsPayload struct {
LDAPSettings portainer.LDAPSettings
}
func (payload *adminGroupsPayload) Validate(r *http.Request) error {
if len(payload.LDAPSettings.URL) == 0 {
return errors.New("Invalid LDAP URL. Empty URL identified")
}
if len(payload.LDAPSettings.AdminGroupSearchSettings) == 0 {
return errors.New("Invalid AdminGroupSearchSettings. When 'AdminAutoPopulate' is true, at least one search settings is required")
}
return nil
}
// @id LDAPAdminGroups
// @summary Fetch LDAP admin groups
// @description Fetch LDAP admin groups from LDAP server based on AdminGroupSearchSettings
// @description **Access policy**: administrator
// @tags ldap
// @security jwt
// @accept json
// @produce json
// @param body body adminGroupsPayload true "LDAPSettings"
// @success 200 {array} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /ldap/admin-groups [post]
func (handler *Handler) ldapAdminGroups(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload adminGroupsPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
settings := &payload.LDAPSettings
err = handler.prefillSettings(settings)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to fetch default settings", Err: err}
}
groups, err := handler.LDAPService.SearchAdminGroups(settings)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to search admin groups", Err: err}
}
return response.JSON(w, groups)
}

View File

@@ -0,0 +1,54 @@
package ldap
import (
"errors"
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
httperrors "github.com/portainer/portainer/api/http/errors"
)
type testLoginPayload struct {
LDAPSettings portainer.LDAPSettings
Username string
Password string
}
type testLoginResponse struct {
Valid bool `json:"valid"`
}
func (payload *testLoginPayload) Validate(r *http.Request) error {
if len(payload.LDAPSettings.URL) == 0 {
return errors.New("Invalid LDAP URL")
}
return nil
}
// POST request on /ldap/test
func (handler *Handler) ldapTestLogin(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload testLoginPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
settings := &payload.LDAPSettings
err = handler.prefillSettings(settings)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to fetch default settings", err}
}
err = handler.LDAPService.AuthenticateUser(payload.Username, payload.Password, settings)
if err != nil && err != httperrors.ErrUnauthorized {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to test user authorization", err}
}
return response.JSON(w, &testLoginResponse{Valid: err != httperrors.ErrUnauthorized})
}

View File

@@ -66,6 +66,20 @@ func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
return errors.New("Invalid user session timeout")
}
}
if payload.AuthenticationMethod != nil && *payload.AuthenticationMethod == 2 {
if payload.LDAPSettings == nil {
return errors.New("Invalid LDAP Configuration")
}
if payload.LDAPSettings.URL == "" {
return errors.New("Invalid LDAP URL")
}
if payload.LDAPSettings.AdminAutoPopulate && len(payload.LDAPSettings.AdminGroupSearchSettings) == 0 {
return errors.New("Missing Admin group Search settings. when AdminAutoPopulate is true, at least one settings is required")
}
if !payload.LDAPSettings.AdminAutoPopulate && len(payload.LDAPSettings.AdminGroups) > 0 {
payload.LDAPSettings.AdminGroups = []string{}
}
}
if payload.KubeconfigExpiry != nil {
_, err := time.ParseDuration(*payload.KubeconfigExpiry)
if err != nil {
@@ -93,12 +107,12 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
var payload settingsUpdatePayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve the settings from the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve the settings from the database", Err: err}
}
if payload.AuthenticationMethod != nil {
@@ -151,7 +165,7 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval {
err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update snapshot interval", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to update snapshot interval", Err: err}
}
}
@@ -186,7 +200,7 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
err = handler.DataStore.Settings().UpdateSettings(settings)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist settings changes inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist settings changes inside the database", Err: err}
}
return response.JSON(w, settings)
@@ -211,7 +225,7 @@ func (handler *Handler) updateTLS(settings *portainer.Settings) *httperror.Handl
settings.LDAPSettings.TLSConfig.TLSCACertPath = ""
err := handler.FileService.DeleteTLSFiles(filesystem.LDAPStorePath)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove TLS files from disk", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to remove TLS files from disk", Err: err}
}
}
return nil

View File

@@ -103,6 +103,7 @@ func (server *Server) Start() error {
offlineGate := offlinegate.NewOfflineGate()
var authHandler = auth.NewHandler(requestBouncer, rateLimiter)
authHandler.AuthorizationService = server.AuthorizationService
authHandler.DataStore = server.DataStore
authHandler.CryptoService = server.CryptoService
authHandler.JWTService = server.JWTService

View File

@@ -0,0 +1,42 @@
package testhelpers
import (
portainer "github.com/portainer/portainer/api"
)
type ldapService struct{}
// NewLDAPService creates new mock for portainer.LDAPService
func NewLDAPService() *ldapService {
return &ldapService{}
}
// AuthenticateUser is used to authenticate a user against a LDAP/AD.
func (service *ldapService) AuthenticateUser(username, password string, settings *portainer.LDAPSettings) error {
return nil
}
// GetUserGroups is used to retrieve user groups from LDAP/AD.
func (service *ldapService) GetUserGroups(username string, settings *portainer.LDAPSettings, useAutoAdminSearchSettings bool) ([]string, error) {
if useAutoAdminSearchSettings {
return []string{"stuff", "operator"}, nil
}
return []string{"stuff"}, nil
}
// SearchGroups searches for groups with the specified settings
func (service *ldapService) SearchAdminGroups(settings *portainer.LDAPSettings) ([]string, error) {
return nil, nil
}
func (service *ldapService) TestConnectivity(settings *portainer.LDAPSettings) error {
return nil
}
func (service *ldapService) SearchGroups(settings *portainer.LDAPSettings) ([]portainer.LDAPUser, error) {
return []portainer.LDAPUser{}, nil
}
func (service *ldapService) SearchUsers(settings *portainer.LDAPSettings) ([]string, error) {
return []string{}, nil
}

View File

@@ -4,8 +4,9 @@ import (
"fmt"
"strings"
ldap "github.com/go-ldap/ldap/v3"
"github.com/pkg/errors"
ldap "github.com/go-ldap/ldap/v3"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
httperrors "github.com/portainer/portainer/api/http/errors"
@@ -87,7 +88,7 @@ func (*Service) AuthenticateUser(username, password string, settings *portainer.
}
// GetUserGroups is used to retrieve user groups from LDAP/AD.
func (*Service) GetUserGroups(username string, settings *portainer.LDAPSettings) ([]string, error) {
func (*Service) GetUserGroups(username string, settings *portainer.LDAPSettings, useAutoAdminSearchSettings bool) ([]string, error) {
connection, err := createConnection(settings)
if err != nil {
return nil, err
@@ -106,7 +107,78 @@ func (*Service) GetUserGroups(username string, settings *portainer.LDAPSettings)
return nil, err
}
userGroups := getGroupsByUser(userDN, connection, settings.GroupSearchSettings)
groupSearchSettings := settings.GroupSearchSettings
if useAutoAdminSearchSettings {
groupSearchSettings = settings.AdminGroupSearchSettings
}
userGroups := getGroupsByUser(userDN, connection, groupSearchSettings)
return userGroups, nil
}
// SearchAdminGroups searches for groups with the specified settings
func (*Service) SearchAdminGroups(settings *portainer.LDAPSettings) ([]string, error) {
userGroups, err := searchUserGroups(settings, true)
if err != nil {
return nil, errors.WithMessage(err, "failed searching user groups")
}
deduplicatedGroups := make(map[string]struct{})
for _, gs := range userGroups {
for _, group := range gs {
deduplicatedGroups[group] = struct{}{}
}
}
groups := make([]string, 0, len(deduplicatedGroups))
for group := range deduplicatedGroups {
groups = append(groups, group)
}
return groups, nil
}
func searchUserGroups(settings *portainer.LDAPSettings, useAutoAdminSearchSettings bool) (map[string][]string, error) {
connection, err := createConnection(settings)
if err != nil {
return nil, errors.WithMessage(err, "failed to esteblish an LDAP connection")
}
defer connection.Close()
if !settings.AnonymousMode {
if err := connection.Bind(settings.ReaderDN, settings.Password); err != nil {
return nil, errors.Wrap(err, "failed to bind an LDAP connection")
}
}
groupSearchSettings := settings.GroupSearchSettings
if useAutoAdminSearchSettings {
groupSearchSettings = settings.AdminGroupSearchSettings
}
userGroups := make(map[string][]string)
for _, searchSettings := range groupSearchSettings {
searchRequest := ldap.NewSearchRequest(
searchSettings.GroupBaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
searchSettings.GroupFilter,
[]string{"cn", searchSettings.GroupAttribute},
nil,
)
sr, err := connection.Search(searchRequest)
if err != nil {
return nil, errors.Wrap(err, "failed to perform a user groups search")
}
for _, entry := range sr.Entries {
members := entry.GetAttributeValues(searchSettings.GroupAttribute)
for _, username := range members {
userGroups[username] = append(userGroups[username], entry.GetAttributeValue("cn"))
}
}
}
return userGroups, nil
}

View File

@@ -510,7 +510,10 @@ type (
SearchSettings []LDAPSearchSettings `json:"SearchSettings"`
GroupSearchSettings []LDAPGroupSearchSettings `json:"GroupSearchSettings"`
// Automatically provision users and assign them to matching LDAP group names
AutoCreateUsers bool `json:"AutoCreateUsers" example:"true"`
AutoCreateUsers bool `json:"AutoCreateUsers" example:"true"`
AdminAutoPopulate bool `json:"AdminAutoPopulate" example:"true"`
AdminGroupSearchSettings []LDAPGroupSearchSettings `json:"AdminGroupSearchSettings"`
AdminGroups []string `json:"AdminGroups" example:"['manager','operator']"`
}
// LDAPUser represents a LDAP user
@@ -1300,7 +1303,8 @@ type (
LDAPService interface {
AuthenticateUser(username, password string, settings *LDAPSettings) error
TestConnectivity(settings *LDAPSettings) error
GetUserGroups(username string, settings *LDAPSettings) ([]string, error)
GetUserGroups(username string, settings *LDAPSettings, useAutoAdminSearchSettings bool) ([]string, error)
SearchAdminGroups(settings *LDAPSettings) ([]string, error)
SearchGroups(settings *LDAPSettings) ([]LDAPUser, error)
SearchUsers(settings *LDAPSettings) ([]string, error)
}

View File

@@ -13,6 +13,7 @@ angular
.constant('API_ENDPOINT_REGISTRIES', 'api/registries')
.constant('API_ENDPOINT_RESOURCE_CONTROLS', 'api/resource_controls')
.constant('API_ENDPOINT_SETTINGS', 'api/settings')
.constant('API_ENDPOINT_LDAP', 'api/ldap')
.constant('API_ENDPOINT_STACKS', 'api/stacks')
.constant('API_ENDPOINT_STATUS', 'api/status')
.constant('API_ENDPOINT_SUPPORT', 'api/support')

View File

@@ -21,436 +21,438 @@ async function initAuthentication(authManager, Authentication, $rootScope, $stat
return await Authentication.init();
}
angular.module('portainer.app', ['portainer.oauth', 'portainer.rbac', componentsModule, settingsModule, featureFlagModule, userActivityModule, 'portainer.shared.datatable']).config([
'$stateRegistryProvider',
function ($stateRegistryProvider) {
'use strict';
angular
.module('portainer.app', ['portainer.oauth', 'portainer.rbac', componentsModule, settingsModule, featureFlagModule, userActivityModule, 'portainer.shared.datatable'])
.config([
'$stateRegistryProvider',
function ($stateRegistryProvider) {
'use strict';
var root = {
name: 'root',
abstract: true,
onEnter: /* @ngInject */ function onEnter($async, StateManager, Authentication, Notifications, authManager, $rootScope, $state) {
return $async(async () => {
const appState = StateManager.getState();
if (!appState.loading) {
return;
}
try {
const loggedIn = await initAuthentication(authManager, Authentication, $rootScope, $state);
await StateManager.initialize();
if (!loggedIn && isTransitionRequiresAuthentication($state.transition)) {
$state.go('portainer.logout');
return Promise.reject('Unauthenticated');
}
} catch (err) {
Notifications.error('Failure', err, 'Unable to retrieve application settings');
throw err;
}
});
},
views: {
'sidebar@': {
templateUrl: './views/sidebar/sidebar.html',
controller: 'SidebarController',
},
},
resolve: {
featuresServiceInitialized: /* @ngInject */ function featuresServiceInitialized($async, featureService, Notifications) {
var root = {
name: 'root',
abstract: true,
onEnter: /* @ngInject */ function onEnter($async, StateManager, Authentication, Notifications, authManager, $rootScope, $state) {
return $async(async () => {
try {
await featureService.init();
} catch (e) {
Notifications.error('Failed initializing features service', e);
throw e;
}
});
},
},
};
var endpointRoot = {
name: 'endpoint',
url: '/:endpointId',
parent: 'root',
abstract: true,
resolve: {
endpoint: /* @ngInject */ function endpoint($async, $state, $transition$, EndpointService, Notifications) {
return $async(async () => {
try {
const endpointId = +$transition$.params().endpointId;
const endpoint = await EndpointService.endpoint(endpointId);
if ((endpoint.Type === 4 || endpoint.Type === 7) && !endpoint.EdgeID) {
$state.go('portainer.endpoints.endpoint', { id: endpoint.Id });
return;
}
return endpoint;
} catch (e) {
Notifications.error('Failed loading environment', e);
$state.go('portainer.home', {}, { reload: true });
const appState = StateManager.getState();
if (!appState.loading) {
return;
}
try {
const loggedIn = await initAuthentication(authManager, Authentication, $rootScope, $state);
await StateManager.initialize();
if (!loggedIn && isTransitionRequiresAuthentication($state.transition)) {
$state.go('portainer.logout');
return Promise.reject('Unauthenticated');
}
} catch (err) {
Notifications.error('Failure', err, 'Unable to retrieve application settings');
throw err;
}
});
},
},
};
var portainer = {
name: 'portainer',
parent: 'root',
abstract: true,
};
var account = {
name: 'portainer.account',
url: '/account',
views: {
'content@': {
templateUrl: './views/account/account.html',
controller: 'AccountController',
views: {
'sidebar@': {
templateUrl: './views/sidebar/sidebar.html',
controller: 'SidebarController',
},
},
},
};
var authentication = {
name: 'portainer.auth',
url: '/auth',
params: {
reload: false,
},
views: {
'content@': {
templateUrl: './views/auth/auth.html',
controller: 'AuthenticationController',
controllerAs: 'ctrl',
resolve: {
featuresServiceInitialized: /* @ngInject */ function featuresServiceInitialized($async, featureService, Notifications) {
return $async(async () => {
try {
await featureService.init();
} catch (e) {
Notifications.error('Failed initializing features service', e);
throw e;
}
});
},
},
'sidebar@': {},
},
};
const logout = {
name: 'portainer.logout',
url: '/logout',
params: {
error: '',
performApiLogout: false,
},
views: {
'content@': {
templateUrl: './views/logout/logout.html',
controller: 'LogoutController',
controllerAs: 'ctrl',
};
var endpointRoot = {
name: 'endpoint',
url: '/:endpointId',
parent: 'root',
abstract: true,
resolve: {
endpoint: /* @ngInject */ function endpoint($async, $state, $transition$, EndpointService, Notifications) {
return $async(async () => {
try {
const endpointId = +$transition$.params().endpointId;
const endpoint = await EndpointService.endpoint(endpointId);
if ((endpoint.Type === 4 || endpoint.Type === 7) && !endpoint.EdgeID) {
$state.go('portainer.endpoints.endpoint', { id: endpoint.Id });
return;
}
return endpoint;
} catch (e) {
Notifications.error('Failed loading environment', e);
$state.go('portainer.home', {}, { reload: true });
return;
}
});
},
},
'sidebar@': {},
},
};
};
var endpoints = {
name: 'portainer.endpoints',
url: '/endpoints',
views: {
'content@': {
templateUrl: './views/endpoints/endpoints.html',
controller: 'EndpointsController',
var portainer = {
name: 'portainer',
parent: 'root',
abstract: true,
};
var account = {
name: 'portainer.account',
url: '/account',
views: {
'content@': {
templateUrl: './views/account/account.html',
controller: 'AccountController',
},
},
},
};
};
var endpoint = {
name: 'portainer.endpoints.endpoint',
url: '/:id',
views: {
'content@': {
templateUrl: './views/endpoints/edit/endpoint.html',
controller: 'EndpointController',
var authentication = {
name: 'portainer.auth',
url: '/auth',
params: {
reload: false,
},
},
};
const endpointKubernetesConfiguration = {
name: 'portainer.endpoints.endpoint.kubernetesConfig',
url: '/configure',
views: {
'content@': {
templateUrl: '../kubernetes/views/configure/configure.html',
controller: 'KubernetesConfigureController',
controllerAs: 'ctrl',
views: {
'content@': {
templateUrl: './views/auth/auth.html',
controller: 'AuthenticationController',
controllerAs: 'ctrl',
},
'sidebar@': {},
},
},
};
var endpointCreation = {
name: 'portainer.endpoints.new',
url: '/new',
views: {
'content@': {
templateUrl: './views/endpoints/create/createendpoint.html',
controller: 'CreateEndpointController',
};
const logout = {
name: 'portainer.logout',
url: '/logout',
params: {
error: '',
performApiLogout: false,
},
},
};
var endpointAccess = {
name: 'portainer.endpoints.endpoint.access',
url: '/access',
views: {
'content@': {
templateUrl: './views/endpoints/access/endpointAccess.html',
controller: 'EndpointAccessController',
controllerAs: 'ctrl',
views: {
'content@': {
templateUrl: './views/logout/logout.html',
controller: 'LogoutController',
controllerAs: 'ctrl',
},
'sidebar@': {},
},
},
};
};
var groups = {
name: 'portainer.groups',
url: '/groups',
views: {
'content@': {
templateUrl: './views/groups/groups.html',
controller: 'GroupsController',
var endpoints = {
name: 'portainer.endpoints',
url: '/endpoints',
views: {
'content@': {
templateUrl: './views/endpoints/endpoints.html',
controller: 'EndpointsController',
},
},
},
};
};
var group = {
name: 'portainer.groups.group',
url: '/:id',
views: {
'content@': {
templateUrl: './views/groups/edit/group.html',
controller: 'GroupController',
var endpoint = {
name: 'portainer.endpoints.endpoint',
url: '/:id',
views: {
'content@': {
templateUrl: './views/endpoints/edit/endpoint.html',
controller: 'EndpointController',
},
},
},
};
};
var groupCreation = {
name: 'portainer.groups.new',
url: '/new',
views: {
'content@': {
templateUrl: './views/groups/create/creategroup.html',
controller: 'CreateGroupController',
const endpointKubernetesConfiguration = {
name: 'portainer.endpoints.endpoint.kubernetesConfig',
url: '/configure',
views: {
'content@': {
templateUrl: '../kubernetes/views/configure/configure.html',
controller: 'KubernetesConfigureController',
controllerAs: 'ctrl',
},
},
},
};
};
var groupAccess = {
name: 'portainer.groups.group.access',
url: '/access',
views: {
'content@': {
templateUrl: './views/groups/access/groupAccess.html',
controller: 'GroupAccessController',
var endpointCreation = {
name: 'portainer.endpoints.new',
url: '/new',
views: {
'content@': {
templateUrl: './views/endpoints/create/createendpoint.html',
controller: 'CreateEndpointController',
},
},
},
};
};
var home = {
name: 'portainer.home',
url: '/home',
views: {
'content@': {
templateUrl: './views/home/home.html',
controller: 'HomeController',
var endpointAccess = {
name: 'portainer.endpoints.endpoint.access',
url: '/access',
views: {
'content@': {
templateUrl: './views/endpoints/access/endpointAccess.html',
controller: 'EndpointAccessController',
controllerAs: 'ctrl',
},
},
},
};
};
var init = {
name: 'portainer.init',
abstract: true,
url: '/init',
views: {
'sidebar@': {},
},
};
const wizard = {
name: 'portainer.wizard',
url: '/wizard',
views: {
'content@': {
component: 'wizardView',
var groups = {
name: 'portainer.groups',
url: '/groups',
views: {
'content@': {
templateUrl: './views/groups/groups.html',
controller: 'GroupsController',
},
},
},
};
};
const wizardEndpoints = {
name: 'portainer.wizard.endpoints',
url: '/endpoints',
views: {
'content@': {
component: 'wizardEndpoints',
var group = {
name: 'portainer.groups.group',
url: '/:id',
views: {
'content@': {
templateUrl: './views/groups/edit/group.html',
controller: 'GroupController',
},
},
},
};
};
var initEndpoint = {
name: 'portainer.init.endpoint',
url: '/endpoint',
views: {
'content@': {
templateUrl: './views/init/endpoint/initEndpoint.html',
controller: 'InitEndpointController',
controllerAs: 'ctrl',
var groupCreation = {
name: 'portainer.groups.new',
url: '/new',
views: {
'content@': {
templateUrl: './views/groups/create/creategroup.html',
controller: 'CreateGroupController',
},
},
},
};
};
var initAdmin = {
name: 'portainer.init.admin',
url: '/admin',
views: {
'content@': {
templateUrl: './views/init/admin/initAdmin.html',
controller: 'InitAdminController',
var groupAccess = {
name: 'portainer.groups.group.access',
url: '/access',
views: {
'content@': {
templateUrl: './views/groups/access/groupAccess.html',
controller: 'GroupAccessController',
},
},
},
};
};
var registries = {
name: 'portainer.registries',
url: '/registries',
views: {
'content@': {
templateUrl: './views/registries/registries.html',
controller: 'RegistriesController',
var home = {
name: 'portainer.home',
url: '/home',
views: {
'content@': {
templateUrl: './views/home/home.html',
controller: 'HomeController',
},
},
},
};
};
var registry = {
name: 'portainer.registries.registry',
url: '/:id',
views: {
'content@': {
templateUrl: './views/registries/edit/registry.html',
controller: 'RegistryController',
var init = {
name: 'portainer.init',
abstract: true,
url: '/init',
views: {
'sidebar@': {},
},
},
};
};
const registryCreation = {
name: 'portainer.registries.new',
url: '/new',
views: {
'content@': {
component: 'createRegistry',
const wizard = {
name: 'portainer.wizard',
url: '/wizard',
views: {
'content@': {
component: 'wizardView',
},
},
},
};
};
var settings = {
name: 'portainer.settings',
url: '/settings',
views: {
'content@': {
templateUrl: './views/settings/settings.html',
controller: 'SettingsController',
const wizardEndpoints = {
name: 'portainer.wizard.endpoints',
url: '/endpoints',
views: {
'content@': {
component: 'wizardEndpoints',
},
},
},
};
};
var settingsAuthentication = {
name: 'portainer.settings.authentication',
url: '/auth',
views: {
'content@': {
templateUrl: './views/settings/authentication/settingsAuthentication.html',
controller: 'SettingsAuthenticationController',
var initEndpoint = {
name: 'portainer.init.endpoint',
url: '/endpoint',
views: {
'content@': {
templateUrl: './views/init/endpoint/initEndpoint.html',
controller: 'InitEndpointController',
controllerAs: 'ctrl',
},
},
},
};
};
var tags = {
name: 'portainer.tags',
url: '/tags',
views: {
'content@': {
templateUrl: './views/tags/tags.html',
controller: 'TagsController',
var initAdmin = {
name: 'portainer.init.admin',
url: '/admin',
views: {
'content@': {
templateUrl: './views/init/admin/initAdmin.html',
controller: 'InitAdminController',
},
},
},
};
};
var users = {
name: 'portainer.users',
url: '/users',
views: {
'content@': {
templateUrl: './views/users/users.html',
controller: 'UsersController',
var registries = {
name: 'portainer.registries',
url: '/registries',
views: {
'content@': {
templateUrl: './views/registries/registries.html',
controller: 'RegistriesController',
},
},
},
};
};
var user = {
name: 'portainer.users.user',
url: '/:id',
views: {
'content@': {
templateUrl: './views/users/edit/user.html',
controller: 'UserController',
var registry = {
name: 'portainer.registries.registry',
url: '/:id',
views: {
'content@': {
templateUrl: './views/registries/edit/registry.html',
controller: 'RegistryController',
},
},
},
};
};
var teams = {
name: 'portainer.teams',
url: '/teams',
views: {
'content@': {
templateUrl: './views/teams/teams.html',
controller: 'TeamsController',
const registryCreation = {
name: 'portainer.registries.new',
url: '/new',
views: {
'content@': {
component: 'createRegistry',
},
},
},
};
};
var team = {
name: 'portainer.teams.team',
url: '/:id',
views: {
'content@': {
templateUrl: './views/teams/edit/team.html',
controller: 'TeamController',
var settings = {
name: 'portainer.settings',
url: '/settings',
views: {
'content@': {
templateUrl: './views/settings/settings.html',
controller: 'SettingsController',
},
},
},
};
};
$stateRegistryProvider.register(root);
$stateRegistryProvider.register(endpointRoot);
$stateRegistryProvider.register(portainer);
$stateRegistryProvider.register(account);
$stateRegistryProvider.register(authentication);
$stateRegistryProvider.register(logout);
$stateRegistryProvider.register(endpoints);
$stateRegistryProvider.register(endpoint);
$stateRegistryProvider.register(endpointAccess);
$stateRegistryProvider.register(endpointCreation);
$stateRegistryProvider.register(endpointKubernetesConfiguration);
$stateRegistryProvider.register(groups);
$stateRegistryProvider.register(group);
$stateRegistryProvider.register(groupAccess);
$stateRegistryProvider.register(groupCreation);
$stateRegistryProvider.register(home);
$stateRegistryProvider.register(init);
$stateRegistryProvider.register(wizard);
$stateRegistryProvider.register(wizardEndpoints);
$stateRegistryProvider.register(initEndpoint);
$stateRegistryProvider.register(initAdmin);
$stateRegistryProvider.register(registries);
$stateRegistryProvider.register(registry);
$stateRegistryProvider.register(registryCreation);
$stateRegistryProvider.register(settings);
$stateRegistryProvider.register(settingsAuthentication);
$stateRegistryProvider.register(tags);
$stateRegistryProvider.register(users);
$stateRegistryProvider.register(user);
$stateRegistryProvider.register(teams);
$stateRegistryProvider.register(team);
},
]);
var settingsAuthentication = {
name: 'portainer.settings.authentication',
url: '/auth',
views: {
'content@': {
templateUrl: './views/settings/authentication/settingsAuthentication.html',
controller: 'SettingsAuthenticationController',
},
},
};
var tags = {
name: 'portainer.tags',
url: '/tags',
views: {
'content@': {
templateUrl: './views/tags/tags.html',
controller: 'TagsController',
},
},
};
var users = {
name: 'portainer.users',
url: '/users',
views: {
'content@': {
templateUrl: './views/users/users.html',
controller: 'UsersController',
},
},
};
var user = {
name: 'portainer.users.user',
url: '/:id',
views: {
'content@': {
templateUrl: './views/users/edit/user.html',
controller: 'UserController',
},
},
};
var teams = {
name: 'portainer.teams',
url: '/teams',
views: {
'content@': {
templateUrl: './views/teams/teams.html',
controller: 'TeamsController',
},
},
};
var team = {
name: 'portainer.teams.team',
url: '/:id',
views: {
'content@': {
templateUrl: './views/teams/edit/team.html',
controller: 'TeamController',
},
},
};
$stateRegistryProvider.register(root);
$stateRegistryProvider.register(endpointRoot);
$stateRegistryProvider.register(portainer);
$stateRegistryProvider.register(account);
$stateRegistryProvider.register(authentication);
$stateRegistryProvider.register(logout);
$stateRegistryProvider.register(endpoints);
$stateRegistryProvider.register(endpoint);
$stateRegistryProvider.register(endpointAccess);
$stateRegistryProvider.register(endpointCreation);
$stateRegistryProvider.register(endpointKubernetesConfiguration);
$stateRegistryProvider.register(groups);
$stateRegistryProvider.register(group);
$stateRegistryProvider.register(groupAccess);
$stateRegistryProvider.register(groupCreation);
$stateRegistryProvider.register(home);
$stateRegistryProvider.register(init);
$stateRegistryProvider.register(wizard);
$stateRegistryProvider.register(wizardEndpoints);
$stateRegistryProvider.register(initEndpoint);
$stateRegistryProvider.register(initAdmin);
$stateRegistryProvider.register(registries);
$stateRegistryProvider.register(registry);
$stateRegistryProvider.register(registryCreation);
$stateRegistryProvider.register(settings);
$stateRegistryProvider.register(settingsAuthentication);
$stateRegistryProvider.register(tags);
$stateRegistryProvider.register(users);
$stateRegistryProvider.register(user);
$stateRegistryProvider.register(teams);
$stateRegistryProvider.register(team);
},
]);
function isTransitionRequiresAuthentication(transition) {
const UNAUTHENTICATED_ROUTES = ['portainer.logout', 'portainer.auth'];

View File

@@ -0,0 +1,14 @@
angular.module('portainer.app').factory('LDAP', [
'$resource',
'API_ENDPOINT_LDAP',
function SettingsFactory($resource, API_ENDPOINT_LDAP) {
'use strict';
return $resource(
API_ENDPOINT_LDAP + '/:action',
{},
{
adminGroups: { method: 'POST', isArray: true, params: { action: 'admin-groups' } },
}
);
},
]);

View File

@@ -0,0 +1,28 @@
angular.module('portainer.app').factory('LDAPService', [
'$q',
'LDAP',
function LdapServiceFactory($q, LDAP) {
'use strict';
var service = {};
service.adminGroups = function (ldapSettings) {
var deferred = $q.defer();
LDAP.adminGroups({ ldapSettings })
.$promise.then(function success(data) {
const userGroups = data
.sort((a, b) => {
return a.toLowerCase() > b.toLowerCase() ? 1 : -1;
})
.map((name) => ({ name, selected: ldapSettings.AdminGroups && ldapSettings.AdminGroups.includes(name) ? true : false }));
deferred.resolve(userGroups);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve admin grous', err: err });
});
return deferred.promise;
};
return service;
},
]);

View File

@@ -23,6 +23,14 @@ export function buildLdapSettingsModel() {
GroupAttribute: '',
},
],
AdminGroupSearchSettings: [
{
GroupBaseDN: '',
GroupFilter: '',
GroupAttribute: '',
},
],
AdminAutoPopulate: false,
AutoCreateUsers: true,
};
}

View File

@@ -249,7 +249,7 @@ class AuthenticationController {
this.generateOAuthLoginURI();
return;
}
if(!this.logo){
if (!this.logo) {
await this.StateManager.initialize();
this.logo = this.StateManager.getState().application.logo;
}

View File

@@ -168,14 +168,14 @@
<div class="form-group">
<div class="col-sm-12">
<button
class="btn btn-sm btn-primary"
ngf-select
accept=".gz,.encrypted"
ngf-accept="'application/x-tar,application/x-gzip'"
ng-model="formValues.BackupFile"
auto-focus
>Select file</button
>
class="btn btn-sm btn-primary"
ngf-select
accept=".gz,.encrypted"
ngf-accept="'application/x-tar,application/x-gzip'"
ng-model="formValues.BackupFile"
auto-focus
>Select file</button
>
<span style="margin-left: 5px;">
{{ formValues.BackupFile.name }}
<i class="fa fa-times red-icon" ng-if="!formValues.BackupFile" aria-hidden="true"></i>

View File

@@ -7,6 +7,8 @@ import { buildLdapSettingsModel, buildAdSettingsModel } from '@/portainer/settin
angular.module('portainer.app').controller('SettingsAuthenticationController', SettingsAuthenticationController);
function SettingsAuthenticationController($q, $scope, $state, Notifications, SettingsService, FileUploadService, TeamService, LDAPService) {
const DEFAULT_GROUP_FILTER = '(objectClass=groupOfNames)';
$scope.state = {
uploadInProgress: false,
actionInProgress: false,
@@ -32,6 +34,7 @@ function SettingsAuthenticationController($q, $scope, $state, Notifications, Set
{ key: '6 months', value: `${24 * 30 * 6}h` },
{ key: '1 year', value: `${24 * 30 * 12}h` },
],
enableAssignAdminGroup: false,
};
$scope.formValues = {
@@ -42,6 +45,19 @@ function SettingsAuthenticationController($q, $scope, $state, Notifications, Set
adSettings: buildAdSettingsModel(),
ldapSettings: buildLdapSettingsModel(),
},
selectedAdminGroups: [],
};
$scope.groups = null;
$scope.searchAdminGroups = async function () {
const settings = {
...$scope.settings.LDAPSettings,
AdminGroupSearchSettings: $scope.settings.LDAPSettings.AdminGroupSearchSettings.map((search) => ({ ...search, GroupFilter: search.GroupFilter || DEFAULT_GROUP_FILTER })),
};
$scope.groups = await LDAPService.adminGroups(settings);
$scope.state.enableAssignAdminGroup = $scope.groups && $scope.groups.length > 0;
};
$scope.authOptions = [
@@ -120,6 +136,16 @@ function SettingsAuthenticationController($q, $scope, $state, Notifications, Set
$scope.saveSettings = function () {
const settings = angular.copy($scope.settings);
if ($scope.formValues.LDAPSettings.AdminAutoPopulate && $scope.formValues.selectedAdminGroups.length > 0) {
settings.LDAPSettings.AdminGroups = $scope.formValues.selectedAdminGroups.map((team) => team.name);
} else {
settings.LDAPSettings.AdminGroups = [];
}
if ($scope.formValues.selectedAdminGroups && $scope.formValues.selectedAdminGroups.length === 0) {
settings.LDAPSettings.AdminAutoPopulate = false;
}
const { settings: ldapSettings, uploadRequired, tlscaFile } = prepareLDAPSettings();
settings.LDAPSettings = ldapSettings;
$scope.state.uploadInProgress = uploadRequired;
@@ -201,6 +227,21 @@ function SettingsAuthenticationController($q, $scope, $state, Notifications, Set
return true;
}
async function prelodaAdminGroup() {
if ($scope.settings.LDAPSettings.AdminAutoPopulate && $scope.settings.LDAPSettings.AdminGroups && $scope.settings.LDAPSettings.AdminGroups.length > 0) {
const settings = {
...$scope.settings.LDAPSettings,
AdminGroupSearchSettings: $scope.settings.LDAPSettings.AdminGroupSearchSettings.map((search) => ({
...search,
GroupFilter: search.GroupFilter || '(objectClass=groupOfNames)',
})),
};
$scope.groups = await LDAPService.adminGroups(settings);
$scope.state.enableAssignAdminGroup = $scope.groups && $scope.groups.length > 0;
}
}
function initView() {
$q.all({
settings: SettingsService.settings(),
@@ -236,6 +277,8 @@ function SettingsAuthenticationController($q, $scope, $state, Notifications, Set
if (!settings.LDAPSettings.ServerType) {
settings.LDAPSettings.ServerType = 0;
}
prelodaAdminGroup();
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve application settings');