Compare commits
9 Commits
feat/EE-35
...
feat/EE-11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5fd787814 | ||
|
|
f577e6728e | ||
|
|
0972ae534f | ||
|
|
9da73e1594 | ||
|
|
aaa9c0f096 | ||
|
|
5474bfae5b | ||
|
|
6d7a5da1f2 | ||
|
|
5c774141f4 | ||
|
|
3eebe3a08a |
@@ -39,6 +39,9 @@ func (store *Store) Init() error {
|
||||
GroupSearchSettings: []portainer.LDAPGroupSearchSettings{
|
||||
portainer.LDAPGroupSearchSettings{},
|
||||
},
|
||||
AdminGroupSearchSettings: []portainer.LDAPGroupSearchSettings{
|
||||
portainer.LDAPGroupSearchSettings{},
|
||||
},
|
||||
},
|
||||
OAuthSettings: portainer.OAuthSettings{},
|
||||
|
||||
|
||||
21
api/bolt/migrator/migrate_dbversion31.go
Normal file
21
api/bolt/migrator/migrate_dbversion31.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package migrator
|
||||
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
func (m *Migrator) migrateDBVersionTo32() error {
|
||||
if err := m.migrateAdminGroupSearchSettings(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) migrateAdminGroupSearchSettings() 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)
|
||||
}
|
||||
52
api/bolt/migrator/migrate_dbversion31_test.go
Normal file
52
api/bolt/migrator/migrate_dbversion31_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
"github.com/portainer/portainer/api/bolt/settings"
|
||||
)
|
||||
|
||||
func TestMigrateAdminGroupSearchSettings(t *testing.T) {
|
||||
testingDBStorePath, _ = os.Getwd()
|
||||
testingDBFileName = "portainer-ee-mig-32.db"
|
||||
databasePath := path.Join(testingDBStorePath, testingDBFileName)
|
||||
dbConn, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second})
|
||||
if err != nil {
|
||||
t.Errorf("failed to init testing DB connection: %v", err)
|
||||
}
|
||||
defer dbConn.Close()
|
||||
defer os.Remove(testingDBFileName)
|
||||
internalDBConn := &internal.DbConnection{
|
||||
DB: dbConn,
|
||||
}
|
||||
settingsService, err := settings.NewService(internalDBConn)
|
||||
if err != nil {
|
||||
t.Errorf("failed to init testing settings service: %v", err)
|
||||
}
|
||||
dummySettingsObj := map[string]interface{}{
|
||||
"LogoURL": "example.com",
|
||||
}
|
||||
if err := internal.UpdateObject(internalDBConn, "settings", []byte("SETTINGS"), dummySettingsObj); err != nil {
|
||||
t.Errorf("failed to create mock settings: %v", err)
|
||||
}
|
||||
m := &Migrator{
|
||||
db: dbConn,
|
||||
settingsService: settingsService,
|
||||
}
|
||||
if err := m.migrateAdminGroupSearchSettings(); 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.LDAPSettings.AdminGroupSearchSettings == nil {
|
||||
t.Error("LDAP AdminGroupSearchSettings should not be nil")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -366,5 +366,12 @@ func (m *Migrator) Migrate() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.9.0
|
||||
if m.currentDBVersion < 32 {
|
||||
if err := m.migrateDBVersionTo32(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
||||
@@ -54,28 +54,32 @@ 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 {
|
||||
if u == nil && (settings.LDAPSettings.AutoCreateUsers || settings.LDAPSettings.AdminAutoPopulate) {
|
||||
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}
|
||||
} else if u == nil && !settings.LDAPSettings.AutoCreateUsers && !settings.LDAPSettings.AdminAutoPopulate {
|
||||
return &httperror.HandlerError{
|
||||
StatusCode: http.StatusUnprocessableEntity,
|
||||
Message: "Invalid credentials",
|
||||
Err: httperrors.ErrUnauthorized,
|
||||
}
|
||||
}
|
||||
return handler.authenticateLDAP(w, u, payload.Password, &settings.LDAPSettings)
|
||||
}
|
||||
@@ -89,6 +93,43 @@ func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.
|
||||
return handler.authenticateInternal(w, user, password)
|
||||
}
|
||||
|
||||
if ldapSettings.AdminAutoPopulate {
|
||||
matched, err := handler.isLDAPAdmin(user.Username, ldapSettings)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{
|
||||
StatusCode: http.StatusUnprocessableEntity,
|
||||
Message: "Failed to search and match LDAP admin groups",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
if matched && 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 !matched && 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := handler.DataStore.User().UpdateUser(user.ID, user); err != nil {
|
||||
return &httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Unable to update user role inside the database",
|
||||
Err: err,
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.addUserIntoTeams(user, ldapSettings)
|
||||
if err != nil {
|
||||
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
|
||||
@@ -100,7 +141,7 @@ func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.
|
||||
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)
|
||||
@@ -109,7 +150,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{
|
||||
@@ -117,9 +158,23 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
|
||||
Role: portainer.StandardUserRole,
|
||||
}
|
||||
|
||||
if ldapSettings.AdminAutoPopulate {
|
||||
matched, err := handler.isLDAPAdmin(username, ldapSettings)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{
|
||||
StatusCode: http.StatusUnprocessableEntity,
|
||||
Message: "Failed to search and match LDAP admin groups",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
if matched {
|
||||
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)
|
||||
@@ -190,6 +245,32 @@ func (handler *Handler) addUserIntoTeams(user *portainer.User, settings *portain
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) isLDAPAdmin(username string, ldapSettings *portainer.LDAPSettings) (bool, error) {
|
||||
adminUserGroups, err := handler.LDAPService.GetUserAdminGroups(username, ldapSettings)
|
||||
if err != nil {
|
||||
return false, errors.New("Failed to retrieve user groups from LDAP server")
|
||||
}
|
||||
adminGroupsMap := make(map[string]bool)
|
||||
for _, adminGroup := range ldapSettings.AdminGroups {
|
||||
adminGroupsMap[adminGroup] = true
|
||||
}
|
||||
|
||||
for _, userGroup := range adminUserGroups {
|
||||
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) {
|
||||
|
||||
37
api/http/handler/auth/authenticate_test.go
Normal file
37
api/http/handler/auth/authenticate_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
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) {
|
||||
|
||||
h := Handler{
|
||||
LDAPService: testhelpers.NewLDAPService(),
|
||||
}
|
||||
mockLDAPSettings := &portainer.LDAPSettings{
|
||||
AdminGroups: []string{"manager", "operator"},
|
||||
}
|
||||
|
||||
matched, err := h.isLDAPAdmin("username", mockLDAPSettings)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, matched)
|
||||
}
|
||||
|
||||
func TestIsLDAPAdmin_NotMatch(t *testing.T) {
|
||||
|
||||
h := Handler{
|
||||
LDAPService: testhelpers.NewLDAPService(),
|
||||
}
|
||||
mockLDAPSettings := &portainer.LDAPSettings{
|
||||
AdminGroups: []string{"admin", "operator"},
|
||||
}
|
||||
|
||||
matched, err := h.isLDAPAdmin("username", mockLDAPSettings)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, false, matched)
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/portainer/portainer/api/http/handler/endpointproxy"
|
||||
"github.com/portainer/portainer/api/http/handler/endpoints"
|
||||
"github.com/portainer/portainer/api/http/handler/file"
|
||||
"github.com/portainer/portainer/api/http/handler/ldap"
|
||||
"github.com/portainer/portainer/api/http/handler/motd"
|
||||
"github.com/portainer/portainer/api/http/handler/registries"
|
||||
"github.com/portainer/portainer/api/http/handler/resourcecontrols"
|
||||
@@ -49,6 +50,7 @@ type Handler struct {
|
||||
EndpointHandler *endpoints.Handler
|
||||
EndpointProxyHandler *endpointproxy.Handler
|
||||
FileHandler *file.Handler
|
||||
LDAPHandler *ldap.Handler
|
||||
MOTDHandler *motd.Handler
|
||||
RegistryHandler *registries.Handler
|
||||
ResourceControlHandler *resourcecontrols.Handler
|
||||
@@ -177,6 +179,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
default:
|
||||
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
|
||||
}
|
||||
case strings.HasPrefix(r.URL.Path, "/api/ldap"):
|
||||
http.StripPrefix("/api", h.LDAPHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/motd"):
|
||||
http.StripPrefix("/api", h.MOTDHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/registries"):
|
||||
|
||||
57
api/http/handler/ldap/handler.go
Normal file
57
api/http/handler/ldap/handler.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle LDAP search Operations
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore portainer.DataStore
|
||||
FileService portainer.FileService
|
||||
LDAPService portainer.LDAPService
|
||||
}
|
||||
|
||||
// NewHandler returns a new Handler
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (handler *Handler) prefillSettings(ldapSettings *portainer.LDAPSettings) error {
|
||||
if !ldapSettings.AnonymousMode && ldapSettings.Password == "" {
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ldapSettings.Password = settings.LDAPSettings.Password
|
||||
}
|
||||
|
||||
if (ldapSettings.TLSConfig.TLS || ldapSettings.StartTLS) && !ldapSettings.TLSConfig.TLSSkipVerify {
|
||||
caCertPath, err := handler.FileService.GetPathForTLSFile(filesystem.LDAPStorePath, portainer.TLSFileCA)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ldapSettings.TLSConfig.TLSCACertPath = caCertPath
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
46
api/http/handler/ldap/ldap_check.go
Normal file
46
api/http/handler/ldap/ldap_check.go
Normal file
@@ -0,0 +1,46 @@
|
||||
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 checkPayload struct {
|
||||
LDAPSettings portainer.LDAPSettings
|
||||
}
|
||||
|
||||
func (payload *checkPayload) Validate(r *http.Request) error {
|
||||
if len(payload.LDAPSettings.URL) == 0 {
|
||||
return errors.New("Invalid LDAP URL")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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{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}
|
||||
}
|
||||
|
||||
err = handler.LDAPService.TestConnectivity(settings)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to connect to LDAP server", Err: err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
60
api/http/handler/ldap/ldap_groups.go
Normal file
60
api/http/handler/ldap/ldap_groups.go
Normal 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 URLs. At least one URL is required")
|
||||
}
|
||||
if len(payload.LDAPSettings.AdminGroupSearchSettings) == 0 {
|
||||
return errors.New("Invalid AdminGroupSearchSettings. At least one search setting 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)
|
||||
}
|
||||
54
api/http/handler/ldap/ldap_test_login.go
Normal file
54
api/http/handler/ldap/ldap_test_login.go
Normal 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})
|
||||
|
||||
}
|
||||
@@ -52,6 +52,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("Invalid AdminGroupSearchSettings")
|
||||
}
|
||||
if !payload.LDAPSettings.AdminAutoPopulate && len(payload.LDAPSettings.AdminGroups) > 0 {
|
||||
payload.LDAPSettings.AdminGroups = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/portainer/portainer/api/http/handler/endpointproxy"
|
||||
"github.com/portainer/portainer/api/http/handler/endpoints"
|
||||
"github.com/portainer/portainer/api/http/handler/file"
|
||||
"github.com/portainer/portainer/api/http/handler/ldap"
|
||||
"github.com/portainer/portainer/api/http/handler/motd"
|
||||
"github.com/portainer/portainer/api/http/handler/registries"
|
||||
"github.com/portainer/portainer/api/http/handler/resourcecontrols"
|
||||
@@ -51,7 +52,7 @@ import (
|
||||
|
||||
// Server implements the portainer.Server interface
|
||||
type Server struct {
|
||||
AuthorizationService *authorization.Service
|
||||
AuthorizationService *authorization.Service
|
||||
BindAddress string
|
||||
AssetsPath string
|
||||
Status *portainer.Status
|
||||
@@ -155,6 +156,11 @@ func (server *Server) Start() error {
|
||||
|
||||
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
|
||||
|
||||
var ldapHandler = ldap.NewHandler(requestBouncer)
|
||||
ldapHandler.DataStore = server.DataStore
|
||||
ldapHandler.FileService = server.FileService
|
||||
ldapHandler.LDAPService = server.LDAPService
|
||||
|
||||
var motdHandler = motd.NewHandler(requestBouncer)
|
||||
|
||||
var registryHandler = registries.NewHandler(requestBouncer)
|
||||
@@ -229,6 +235,7 @@ func (server *Server) Start() error {
|
||||
EndpointEdgeHandler: endpointEdgeHandler,
|
||||
EndpointProxyHandler: endpointProxyHandler,
|
||||
FileHandler: fileHandler,
|
||||
LDAPHandler: ldapHandler,
|
||||
MOTDHandler: motdHandler,
|
||||
RegistryHandler: registryHandler,
|
||||
ResourceControlHandler: resourceControlHandler,
|
||||
|
||||
35
api/internal/testhelpers/ldap_service.go
Normal file
35
api/internal/testhelpers/ldap_service.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type ldapService struct{}
|
||||
|
||||
// NewGitService creates new mock for portainer.GitService.
|
||||
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) ([]string, error) {
|
||||
return []string{"stuff"}, nil
|
||||
}
|
||||
|
||||
func (service *ldapService) GetUserAdminGroups(username string, settings *portainer.LDAPSettings) ([]string, error) {
|
||||
return []string{"manager", "lead"}, 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
|
||||
}
|
||||
142
api/ldap/ldap.go
142
api/ldap/ldap.go
@@ -3,6 +3,7 @@ package ldap
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
ldap "github.com/go-ldap/ldap/v3"
|
||||
@@ -138,6 +139,109 @@ func (*Service) GetUserGroups(username string, settings *portainer.LDAPSettings)
|
||||
return userGroups, nil
|
||||
}
|
||||
|
||||
// GetUserGroups is used to retrieve user groups from LDAP/AD.
|
||||
func (*Service) GetUserAdminGroups(username string, settings *portainer.LDAPSettings) ([]string, error) {
|
||||
connection, err := createConnection(settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer connection.Close()
|
||||
|
||||
if !settings.AnonymousMode {
|
||||
err = connection.Bind(settings.ReaderDN, settings.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
userDN, err := searchUser(username, connection, settings.SearchSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userGroups := getGroupsByUser(userDN, connection, settings.AdminGroupSearchSettings)
|
||||
|
||||
return userGroups, nil
|
||||
}
|
||||
|
||||
// SearchGroups searches for groups with the specified settings
|
||||
func (*Service) SearchAdminGroups(settings *portainer.LDAPSettings) ([]string, error) {
|
||||
type groupSet map[string]bool
|
||||
|
||||
connection, err := createConnection(settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer connection.Close()
|
||||
|
||||
if !settings.AnonymousMode {
|
||||
err = connection.Bind(settings.ReaderDN, settings.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
userGroups := map[string]groupSet{}
|
||||
|
||||
for _, searchSettings := range settings.AdminGroupSearchSettings {
|
||||
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, err
|
||||
}
|
||||
|
||||
for _, entry := range sr.Entries {
|
||||
members := entry.GetAttributeValues(searchSettings.GroupAttribute)
|
||||
for _, username := range members {
|
||||
_, ok := userGroups[username]
|
||||
if !ok {
|
||||
userGroups[username] = groupSet{}
|
||||
}
|
||||
userGroups[username][entry.GetAttributeValue("cn")] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groupsMap := make(map[string]bool)
|
||||
|
||||
for _, groups := range userGroups {
|
||||
for group := range groups {
|
||||
groupsMap[group] = true
|
||||
}
|
||||
}
|
||||
|
||||
groups := make([]string, 0, len(groupsMap))
|
||||
for group := range groupsMap {
|
||||
groups = append(groups, group)
|
||||
}
|
||||
sort.Strings(groups)
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
// TestConnectivity is used to test a connection against the LDAP server using the credentials
|
||||
// specified in the LDAPSettings.
|
||||
func (*Service) TestConnectivity(settings *portainer.LDAPSettings) error {
|
||||
|
||||
connection, err := createConnection(settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer connection.Close()
|
||||
|
||||
err = connection.Bind(settings.ReaderDN, settings.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get a list of group names for specified user from LDAP/AD
|
||||
func getGroups(userDN string, conn *ldap.Conn, settings []portainer.LDAPGroupSearchSettings) []string {
|
||||
groups := make([]string, 0)
|
||||
@@ -169,19 +273,33 @@ func getGroups(userDN string, conn *ldap.Conn, settings []portainer.LDAPGroupSea
|
||||
return groups
|
||||
}
|
||||
|
||||
// TestConnectivity is used to test a connection against the LDAP server using the credentials
|
||||
// specified in the LDAPSettings.
|
||||
func (*Service) TestConnectivity(settings *portainer.LDAPSettings) error {
|
||||
// Get a list of group names for specified user from LDAP/AD
|
||||
func getGroupsByUser(userDN string, conn *ldap.Conn, settings []portainer.LDAPGroupSearchSettings) []string {
|
||||
groups := make([]string, 0)
|
||||
userDNEscaped := ldap.EscapeFilter(userDN)
|
||||
|
||||
connection, err := createConnection(settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer connection.Close()
|
||||
for _, searchSettings := range settings {
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
searchSettings.GroupBaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
fmt.Sprintf("(&%s(%s=%s))", searchSettings.GroupFilter, searchSettings.GroupAttribute, userDNEscaped),
|
||||
[]string{"cn"},
|
||||
nil,
|
||||
)
|
||||
|
||||
err = connection.Bind(settings.ReaderDN, settings.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
// Deliberately skip errors on the search request so that we can jump to other search settings
|
||||
// if any issue arise with the current one.
|
||||
sr, err := conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entry := range sr.Entries {
|
||||
for _, attr := range entry.Attributes {
|
||||
groups = append(groups, attr.Values[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
@@ -470,7 +470,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"`
|
||||
}
|
||||
|
||||
// LicenseInformation represents information about an extension license
|
||||
@@ -1187,6 +1190,8 @@ type (
|
||||
AuthenticateUser(username, password string, settings *LDAPSettings) error
|
||||
TestConnectivity(settings *LDAPSettings) error
|
||||
GetUserGroups(username string, settings *LDAPSettings) ([]string, error)
|
||||
GetUserAdminGroups(username string, settings *LDAPSettings) ([]string, error)
|
||||
SearchAdminGroups(settings *LDAPSettings) ([]string, error)
|
||||
}
|
||||
|
||||
// OAuthService represents a service used to authenticate users using OAuth
|
||||
@@ -1345,7 +1350,7 @@ const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.6.0"
|
||||
// DBVersion is the version number of the Portainer database
|
||||
DBVersion = 30
|
||||
DBVersion = 32
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
ComposeSyntaxMaxVersion = "3.9"
|
||||
// AssetsServerURL represents the URL of the Portainer asset server
|
||||
|
||||
@@ -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')
|
||||
|
||||
14
app/portainer/rest/ldap.js
Normal file
14
app/portainer/rest/ldap.js
Normal 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' } },
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
||||
24
app/portainer/services/api/ldapService.js
Normal file
24
app/portainer/services/api/ldapService.js
Normal file
@@ -0,0 +1,24 @@
|
||||
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.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;
|
||||
},
|
||||
]);
|
||||
@@ -364,6 +364,104 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- !group-search-settings -->
|
||||
|
||||
<!-- admin group-search-settings -->
|
||||
<div class="col-sm-12 form-section-title" style="float: initial;">
|
||||
Auto-populate team admins
|
||||
</div>
|
||||
|
||||
<div
|
||||
ng-repeat="config in formValues.LDAPSettings.AdminGroupSearchSettings | limitTo: (1 - formValues.LDAPSettings.AdminGroupSearchSettings)"
|
||||
style="display: block; margin-bottom: 10px;"
|
||||
>
|
||||
<div class="form-group" ng-if="$index > 0" style="margin-bottom: 10px;">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
Extra search configuration
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="ldap_admin_group_basedn_{{ $index }}" class="col-sm-4 col-md-2 control-label text-left">
|
||||
Group Base DN
|
||||
<portainer-tooltip position="bottom" message="The distinguished name of the element from which the LDAP server will search for groups."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-8 col-md-4">
|
||||
<input type="text" class="form-control" id="ldap_admin_group_basedn_{{ $index }}" ng-model="config.GroupBaseDN" placeholder="dc=ldap,dc=domain,dc=tld" />
|
||||
</div>
|
||||
|
||||
<label for="ldap_admin_group_att_{{ $index }}" class="col-sm-4 col-md-2 control-label text-left">
|
||||
Group Membership Attribute
|
||||
<portainer-tooltip position="bottom" message="LDAP attribute which denotes the group membership."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-8 col-md-4">
|
||||
<input type="text" class="form-control" id="ldap_admin_group_att_{{ $index }}" ng-model="config.GroupAttribute" placeholder="member" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ldap_admin_group_filter_{{ $index }}" class="col-sm-4 col-md-2 control-label text-left">
|
||||
Group Filter
|
||||
<portainer-tooltip position="bottom" message="The LDAP search filter used to select group elements, optional."></portainer-tooltip>
|
||||
</label>
|
||||
<div ng-class="{ 'col-sm-7 col-md-9': $index, 'col-sm-8 col-md-10': !$index }">
|
||||
<input type="text" class="form-control" id="ldap_admin_group_filter_{{ $index }}" ng-model="config.GroupFilter" placeholder="(objectClass=account)" />
|
||||
</div>
|
||||
<div class="col-sm-1" ng-if="$index > 0">
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeAdminGroupSearchConfiguration($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top: 10px;">
|
||||
<div class="col-sm-12">
|
||||
<button class="label label-default interactive" style="border: 0;" ng-click="addAdminGroupSearchConfiguration()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add group search configuration
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-sm-12" style="margin-top: 10px;">
|
||||
<button class="btn btm-sm btn-primary" type="button" ng-click="searchAdminGroups()">
|
||||
Fetch Admin Group(s)
|
||||
</button>
|
||||
<span ng-if="formValues.groups && formValues.groups.length === 0" style="margin-left: 30px;">
|
||||
<i class="fa fa-exclamation-triangle text-warning" aria-hidden="true"></i> No groups found</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
ng-model="formValues.LDAPSettings.AdminAutoPopulate"
|
||||
name="admin-auto-populate"
|
||||
label="Assign admin rights to group(s)"
|
||||
label-class="col-sm-2 text-muted small"
|
||||
disabled="groups === null || groups.length === 0"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="formValues.LDAPSettings.AdminAutoPopulate">
|
||||
<div class="col-sm-12">
|
||||
<label for="group-access" class="control-label text-left">
|
||||
Select Group(s)
|
||||
</label>
|
||||
<span
|
||||
isteven-multi-select
|
||||
ng-if="groups.length > 0 || groups.length > 1"
|
||||
input-model="groups"
|
||||
output-model="formValues.selectedAdminGroups"
|
||||
button-label="Name"
|
||||
item-label="Name"
|
||||
tick-property="selected"
|
||||
helper-elements="filter"
|
||||
search-property="Name"
|
||||
translation="{nothingSelected: 'Select one or more teams', search: 'Search...'}"
|
||||
style="margin-left: 20px;"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="isOauthEnabled()">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import _ from 'lodash-es';
|
||||
angular.module('portainer.app').controller('SettingsAuthenticationController', [
|
||||
'$q',
|
||||
'$scope',
|
||||
@@ -6,7 +7,10 @@ angular.module('portainer.app').controller('SettingsAuthenticationController', [
|
||||
'SettingsService',
|
||||
'FileUploadService',
|
||||
'TeamService',
|
||||
function ($q, $scope, $state, Notifications, SettingsService, FileUploadService, TeamService) {
|
||||
'LDAPService',
|
||||
function ($q, $scope, $state, Notifications, SettingsService, FileUploadService, TeamService, LDAPService) {
|
||||
const DEFAULT_GROUP_FILTER = '(objectClass=groupOfNames)';
|
||||
|
||||
$scope.state = {
|
||||
successfulConnectivityCheck: false,
|
||||
failedConnectivityCheck: false,
|
||||
@@ -35,6 +39,7 @@ angular.module('portainer.app').controller('SettingsAuthenticationController', [
|
||||
{ key: '6 months', value: `${24 * 30 * 6}h` },
|
||||
{ key: '1 year', value: `${24 * 30 * 12}h` },
|
||||
],
|
||||
enableAssignAdminGroup: false,
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
@@ -63,8 +68,32 @@ angular.module('portainer.app').controller('SettingsAuthenticationController', [
|
||||
GroupAttribute: '',
|
||||
},
|
||||
],
|
||||
AdminGroupSearchSettings: [
|
||||
{
|
||||
GroupBaseDN: '',
|
||||
GroupFilter: '',
|
||||
GroupAttribute: '',
|
||||
},
|
||||
],
|
||||
AdminAutoPopulate: false,
|
||||
AutoCreateUsers: true,
|
||||
},
|
||||
selectedAdminGroups: '',
|
||||
};
|
||||
|
||||
$scope.groups = [];
|
||||
|
||||
$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);
|
||||
|
||||
if ($scope.groups && $scope.groups.length > 0) {
|
||||
$scope.state.enableAssignAdminGroup = true;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.isOauthEnabled = function isOauthEnabled() {
|
||||
@@ -87,6 +116,14 @@ angular.module('portainer.app').controller('SettingsAuthenticationController', [
|
||||
$scope.formValues.LDAPSettings.GroupSearchSettings.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addAdminGroupSearchConfiguration = function () {
|
||||
$scope.formValues.LDAPSettings.GroupSearchSettings.push({ GroupBaseDN: '', GroupAttribute: '', GroupFilter: '' });
|
||||
};
|
||||
|
||||
$scope.removeAdminGroupSearchConfiguration = function (index) {
|
||||
$scope.formValues.LDAPSettings.GroupSearchSettings.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.LDAPConnectivityCheck = function () {
|
||||
var settings = angular.copy($scope.settings);
|
||||
var TLSCAFile = $scope.formValues.TLSCACert !== settings.LDAPSettings.TLSConfig.TLSCACert ? $scope.formValues.TLSCACert : null;
|
||||
@@ -123,6 +160,16 @@ angular.module('portainer.app').controller('SettingsAuthenticationController', [
|
||||
|
||||
$scope.saveSettings = function () {
|
||||
var settings = angular.copy($scope.settings);
|
||||
if ($scope.formValues.LDAPSettings.AdminAutoPopulate && $scope.formValues.selectedAdminGroups && $scope.formValues.selectedAdminGroups.length > 0) {
|
||||
settings.LDAPSettings.AdminGroups = _.map($scope.formValues.selectedAdminGroups, (team) => team.Name);
|
||||
} else {
|
||||
settings.LDAPSettings.AdminGroups = [];
|
||||
}
|
||||
|
||||
if ($scope.formValues.selectedAdminGroups && $scope.formValues.selectedAdminGroups.length === 0) {
|
||||
settings.LDAPSettings.AdminAutoPopulate = false;
|
||||
}
|
||||
|
||||
var TLSCAFile = $scope.formValues.TLSCACert !== settings.LDAPSettings.TLSConfig.TLSCACert ? $scope.formValues.TLSCACert : null;
|
||||
|
||||
if ($scope.formValues.LDAPSettings.AnonymousMode) {
|
||||
@@ -158,6 +205,23 @@ angular.module('portainer.app').controller('SettingsAuthenticationController', [
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if ($scope.groups && $scope.groups.length > 0) {
|
||||
$scope.state.enableAssignAdminGroup = true;
|
||||
}
|
||||
}
|
||||
function initView() {
|
||||
$q.all({
|
||||
settings: SettingsService.settings(),
|
||||
@@ -170,6 +234,7 @@ angular.module('portainer.app').controller('SettingsAuthenticationController', [
|
||||
$scope.formValues.LDAPSettings = settings.LDAPSettings;
|
||||
$scope.OAuthSettings = settings.OAuthSettings;
|
||||
$scope.formValues.TLSCACert = settings.LDAPSettings.TLSConfig.TLSCACert;
|
||||
prelodaAdminGroup();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve application settings');
|
||||
|
||||
Reference in New Issue
Block a user