1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-25 08:19:40 +02:00

feat(app): highlight be provided value [EE-882] (#5703)

This commit is contained in:
Chaim Lev-Ari 2021-10-06 09:24:26 +03:00 committed by GitHub
parent 8421113d49
commit e60dbba93b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
175 changed files with 5627 additions and 1216 deletions

View file

@ -18,6 +18,7 @@ import (
"github.com/portainer/portainer/api/http/handler/file"
"github.com/portainer/portainer/api/http/handler/helm"
"github.com/portainer/portainer/api/http/handler/kubernetes"
"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"
@ -53,6 +54,7 @@ type Handler struct {
HelmTemplatesHandler *helm.Handler
KubernetesHandler *kubernetes.Handler
FileHandler *file.Handler
LDAPHandler *ldap.Handler
MOTDHandler *motd.Handler
RegistryHandler *registries.Handler
ResourceControlHandler *resourcecontrols.Handler
@ -189,6 +191,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"):

View file

@ -0,0 +1,53 @@
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)
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
}

View file

@ -1,4 +1,4 @@
package settings
package ldap
import (
"net/http"
@ -7,42 +7,43 @@ import (
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
)
type settingsLDAPCheckPayload struct {
type checkPayload struct {
LDAPSettings portainer.LDAPSettings
}
func (payload *settingsLDAPCheckPayload) Validate(r *http.Request) error {
func (payload *checkPayload) Validate(r *http.Request) error {
return nil
}
// @id SettingsLDAPCheck
// @id LDAPCheck
// @summary Test LDAP connectivity
// @description Test LDAP connectivity using LDAP details
// @description **Access policy**: administrator
// @tags settings
// @tags ldap
// @security jwt
// @accept json
// @param body body settingsLDAPCheckPayload true "details"
// @param body body checkPayload true "details"
// @success 204 "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /settings/ldap/check [put]
func (handler *Handler) settingsLDAPCheck(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload settingsLDAPCheckPayload
// @router /ldap/check [post]
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}
}
if (payload.LDAPSettings.TLSConfig.TLS || payload.LDAPSettings.StartTLS) && !payload.LDAPSettings.TLSConfig.TLSSkipVerify {
caCertPath, _ := handler.FileService.GetPathForTLSFile(filesystem.LDAPStorePath, portainer.TLSFileCA)
payload.LDAPSettings.TLSConfig.TLSCACertPath = caCertPath
settings := &payload.LDAPSettings
err = handler.prefillSettings(settings)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to fetch default settings", err}
}
err = handler.LDAPService.TestConnectivity(&payload.LDAPSettings)
err = handler.LDAPService.TestConnectivity(settings)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to connect to LDAP server", err}
}

View file

@ -5,7 +5,7 @@ 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/security"
)
@ -35,8 +35,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
bouncer.AdminAccess(httperror.LoggerHandler(h.settingsUpdate))).Methods(http.MethodPut)
h.Handle("/settings/public",
bouncer.PublicAccess(httperror.LoggerHandler(h.settingsPublic))).Methods(http.MethodGet)
h.Handle("/settings/authentication/checkLDAP",
bouncer.AdminAccess(httperror.LoggerHandler(h.settingsLDAPCheck))).Methods(http.MethodPut)
return h
}

View file

@ -29,6 +29,7 @@ import (
"github.com/portainer/portainer/api/http/handler/file"
"github.com/portainer/portainer/api/http/handler/helm"
kubehandler "github.com/portainer/portainer/api/http/handler/kubernetes"
"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"
@ -175,6 +176,11 @@ func (server *Server) Start() error {
var helmTemplatesHandler = helm.NewTemplateHandler(requestBouncer, server.HelmPackageManager)
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)
@ -255,6 +261,7 @@ func (server *Server) Start() error {
EndpointEdgeHandler: endpointEdgeHandler,
EndpointProxyHandler: endpointProxyHandler,
FileHandler: fileHandler,
LDAPHandler: ldapHandler,
HelmTemplatesHandler: helmTemplatesHandler,
KubernetesHandler: kubernetesHandler,
MOTDHandler: motdHandler,

View file

@ -1,11 +1,11 @@
package ldap
import (
"errors"
"fmt"
"strings"
ldap "github.com/go-ldap/ldap/v3"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
httperrors "github.com/portainer/portainer/api/http/errors"
@ -20,55 +20,28 @@ var (
// Service represents a service used to authenticate users against a LDAP/AD.
type Service struct{}
func searchUser(username string, conn *ldap.Conn, settings []portainer.LDAPSearchSettings) (string, error) {
var userDN string
found := false
usernameEscaped := ldap.EscapeFilter(username)
for _, searchSettings := range settings {
searchRequest := ldap.NewSearchRequest(
searchSettings.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(&%s(%s=%s))", searchSettings.Filter, searchSettings.UserNameAttribute, usernameEscaped),
[]string{"dn"},
nil,
)
// 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
}
if len(sr.Entries) == 1 {
found = true
userDN = sr.Entries[0].DN
break
}
func createConnection(settings *portainer.LDAPSettings) (*ldap.Conn, error) {
conn, err := createConnectionForURL(settings.URL, settings)
if err != nil {
return nil, errors.Wrap(err, "failed creating LDAP connection")
}
if !found {
return "", errUserNotFound
}
return userDN, nil
return conn, nil
}
func createConnection(settings *portainer.LDAPSettings) (*ldap.Conn, error) {
func createConnectionForURL(url string, settings *portainer.LDAPSettings) (*ldap.Conn, error) {
if settings.TLSConfig.TLS || settings.StartTLS {
config, err := crypto.CreateTLSConfigurationFromDisk(settings.TLSConfig.TLSCACertPath, settings.TLSConfig.TLSCertPath, settings.TLSConfig.TLSKeyPath, settings.TLSConfig.TLSSkipVerify)
if err != nil {
return nil, err
}
config.ServerName = strings.Split(settings.URL, ":")[0]
config.ServerName = strings.Split(url, ":")[0]
if settings.TLSConfig.TLS {
return ldap.DialTLS("tcp", settings.URL, config)
return ldap.DialTLS("tcp", url, config)
}
conn, err := ldap.Dial("tcp", settings.URL)
conn, err := ldap.Dial("tcp", url)
if err != nil {
return nil, err
}
@ -81,7 +54,7 @@ func createConnection(settings *portainer.LDAPSettings) (*ldap.Conn, error) {
return conn, nil
}
return ldap.Dial("tcp", settings.URL)
return ldap.Dial("tcp", url)
}
// AuthenticateUser is used to authenticate a user against a LDAP/AD.
@ -133,13 +106,157 @@ func (*Service) GetUserGroups(username string, settings *portainer.LDAPSettings)
return nil, err
}
userGroups := getGroups(userDN, connection, settings.GroupSearchSettings)
userGroups := getGroupsByUser(userDN, connection, settings.GroupSearchSettings)
return userGroups, nil
}
// SearchUsers searches for users with the specified settings
func (*Service) SearchUsers(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
}
}
users := map[string]bool{}
for _, searchSettings := range settings.SearchSettings {
searchRequest := ldap.NewSearchRequest(
searchSettings.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
searchSettings.Filter,
[]string{"dn", searchSettings.UserNameAttribute},
nil,
)
sr, err := connection.Search(searchRequest)
if err != nil {
return nil, err
}
for _, user := range sr.Entries {
username := user.GetAttributeValue(searchSettings.UserNameAttribute)
if username != "" {
users[username] = true
}
}
}
usersList := []string{}
for user := range users {
usersList = append(usersList, user)
}
return usersList, nil
}
// SearchGroups searches for groups with the specified settings
func (*Service) SearchGroups(settings *portainer.LDAPSettings) ([]portainer.LDAPUser, 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.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, 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
}
}
}
users := []portainer.LDAPUser{}
for username, groups := range userGroups {
groupList := []string{}
for group := range groups {
groupList = append(groupList, group)
}
user := portainer.LDAPUser{
Name: username,
Groups: groupList,
}
users = append(users, user)
}
return users, nil
}
func searchUser(username string, conn *ldap.Conn, settings []portainer.LDAPSearchSettings) (string, error) {
var userDN string
found := false
usernameEscaped := ldap.EscapeFilter(username)
for _, searchSettings := range settings {
searchRequest := ldap.NewSearchRequest(
searchSettings.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(&%s(%s=%s))", searchSettings.Filter, searchSettings.UserNameAttribute, usernameEscaped),
[]string{"dn"},
nil,
)
// 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
}
if len(sr.Entries) == 1 {
found = true
userDN = sr.Entries[0].DN
break
}
}
if !found {
return "", errUserNotFound
}
return userDN, nil
}
// Get a list of group names for specified user from LDAP/AD
func getGroups(userDN string, conn *ldap.Conn, settings []portainer.LDAPGroupSearchSettings) []string {
func getGroupsByUser(userDN string, conn *ldap.Conn, settings []portainer.LDAPGroupSearchSettings) []string {
groups := make([]string, 0)
userDNEscaped := ldap.EscapeFilter(userDN)
@ -179,9 +296,18 @@ func (*Service) TestConnectivity(settings *portainer.LDAPSettings) error {
}
defer connection.Close()
err = connection.Bind(settings.ReaderDN, settings.Password)
if err != nil {
return err
if !settings.AnonymousMode {
err = connection.Bind(settings.ReaderDN, settings.Password)
if err != nil {
return err
}
} else {
err = connection.UnauthenticatedBind("")
if err != nil {
return err
}
}
return nil
}

View file

@ -513,6 +513,12 @@ type (
AutoCreateUsers bool `json:"AutoCreateUsers" example:"true"`
}
// LDAPUser represents a LDAP user
LDAPUser struct {
Name string
Groups []string
}
// LicenseInformation represents information about an extension license
LicenseInformation struct {
LicenseKey string `json:"LicenseKey,omitempty"`
@ -1295,6 +1301,8 @@ type (
AuthenticateUser(username, password string, settings *LDAPSettings) error
TestConnectivity(settings *LDAPSettings) error
GetUserGroups(username string, settings *LDAPSettings) ([]string, error)
SearchGroups(settings *LDAPSettings) ([]LDAPUser, error)
SearchUsers(settings *LDAPSettings) ([]string, error)
}
// OAuthService represents a service used to authenticate users using OAuth