feat(ldap): auto admin group mapping EE-994
This commit is contained in:
21
api/bolt/migrator/migrate_dbversion32.go
Normal file
21
api/bolt/migrator/migrate_dbversion32.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_dbversion32_test.go
Normal file
52
api/bolt/migrator/migrate_dbversion32_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 TestMigrateStackEntryPoint(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,36 @@ func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.
|
||||
return handler.authenticateInternal(w, user, password)
|
||||
}
|
||||
|
||||
if ldapSettings.AdminAutoPopulate {
|
||||
userGroups, err := handler.LDAPService.GetUserGroups(user.Username, ldapSettings)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{
|
||||
StatusCode: http.StatusUnprocessableEntity,
|
||||
Message: "Failed to retrieve user groups from LDAP server",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
adminGroupsMap := make(map[string]bool)
|
||||
for _, adminGroup := range ldapSettings.AdminGroups {
|
||||
adminGroupsMap[adminGroup] = true
|
||||
}
|
||||
|
||||
for _, userGroup := range userGroups {
|
||||
if adminGroupsMap[userGroup] {
|
||||
user.Role = portainer.AdministratorRole
|
||||
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,
|
||||
}
|
||||
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.addUserIntoTeams(user, ldapSettings)
|
||||
if err != nil {
|
||||
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
|
||||
@@ -100,7 +134,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 +143,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 +151,31 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
|
||||
Role: portainer.StandardUserRole,
|
||||
}
|
||||
|
||||
if ldapSettings.AdminAutoPopulate {
|
||||
userGroups, err := handler.LDAPService.GetUserGroups(username, ldapSettings)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{
|
||||
StatusCode: http.StatusUnprocessableEntity,
|
||||
Message: "Failed to retrieve user groups from LDAP server",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
adminGroupsMap := make(map[string]bool)
|
||||
for _, adminGroup := range ldapSettings.AdminGroups {
|
||||
adminGroupsMap[adminGroup] = true
|
||||
}
|
||||
|
||||
for _, userGroup := range userGroups {
|
||||
if adminGroupsMap[userGroup] {
|
||||
user.Role = portainer.AdministratorRole
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
47
api/http/handler/ldap/ldap_groups.go
Normal file
47
api/http/handler/ldap/ldap_groups.go
Normal file
@@ -0,0 +1,47 @@
|
||||
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
|
||||
}
|
||||
|
||||
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})
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -138,6 +138,67 @@ func (*Service) GetUserGroups(username string, settings *portainer.LDAPSettings)
|
||||
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)
|
||||
}
|
||||
|
||||
return groups, 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)
|
||||
|
||||
@@ -1190,6 +1190,7 @@ type (
|
||||
AuthenticateUser(username, password string, settings *LDAPSettings) error
|
||||
TestConnectivity(settings *LDAPSettings) error
|
||||
GetUserGroups(username string, settings *LDAPSettings) ([]string, error)
|
||||
SearchAdminGroups(settings *LDAPSettings) ([]string, error)
|
||||
}
|
||||
|
||||
// OAuthService represents a service used to authenticate users using OAuth
|
||||
@@ -1348,7 +1349,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
|
||||
|
||||
Reference in New Issue
Block a user