aboutsummaryrefslogblamecommitdiff
path: root/login.go
blob: 277e3aea2dfea54f3ceb2278e111169c45369892 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15














                                                                      



                                                                                                     
                                                                                                         
                                                                                                                               




























                                                                     



                                           
















                                                                                               

                       





































                                                                                   
                      

                      
 

                                                                           
                         



















                                                                                                                   
                                    





                                     
                                 

                                  

                                    
 
 



































                                                                          


                                    
                                                            
                                                           





























































                                                                                                              






































                                                                                           
package main

import (
	"crypto/tls"
	"errors"
	"fmt"
	"net/http"
	"strings"

	garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"
	"github.com/go-ldap/ldap/v3"
)

var (
	ErrNotAuthenticatedSession = fmt.Errorf("User has no session")
	ErrNotAuthenticatedBasic   = fmt.Errorf("User has not sent Authentication Basic information")
	ErrNotAuthenticated        = fmt.Errorf("User is not authenticated")
	ErrWrongLDAPCredentials    = fmt.Errorf("LDAP credentials are wrong")
	ErrLDAPServerUnreachable   = fmt.Errorf("Unable to open the LDAP server")
	ErrLDAPSearchInternalError = fmt.Errorf("LDAP Search of this user failed with an internal error")
	ErrLDAPSearchNotFound      = fmt.Errorf("User is authenticated but its associated data can not be found during search")
)

// --- Login Info ---
type LoginInfo struct {
	Username string
	Password string
}

func NewLoginInfoFromSession(r *http.Request) (*LoginInfo, error) {
	session, err := store.Get(r, SESSION_NAME)
	if err == nil {
		username, ok_user := session.Values["login_username"]
		password, ok_pwd := session.Values["login_password"]

		if ok_user && ok_pwd {
			loginInfo := &LoginInfo{
				Username: username.(string),
				Password: password.(string),
			}
			return loginInfo, nil
		}
	}

	return nil, errors.Join(ErrNotAuthenticatedSession, err)
}

func NewLoginInfoFromBasicAuth(r *http.Request) (*LoginInfo, error) {
	username, password, ok := r.BasicAuth()
	if ok {
		login_info := &LoginInfo{
			Username: username,
			Password: password,
		}

		return login_info, nil
	}
	return nil, ErrNotAuthenticatedBasic
}

func (li *LoginInfo) DN() string {
	user_dn := fmt.Sprintf("%s=%s,%s", config.UserNameAttr, li.Username, config.UserBaseDN)
	if strings.EqualFold(li.Username, config.AdminAccount) {
		user_dn = li.Username
	}

	return user_dn
}

// --- Login Status ---
type LoginStatus struct {
	Info *LoginInfo
	conn *ldap.Conn
}

func NewLoginStatus(r *http.Request, login_info *LoginInfo) (*LoginStatus, error) {
	l, err := NewLdapCon()
	if err != nil {
		return nil, err
	}

	err = l.Bind(login_info.DN(), login_info.Password)
	if err != nil {
		return nil, errors.Join(ErrWrongLDAPCredentials, err)
	}

	loginStatus := &LoginStatus{
		Info: login_info,
		conn: l,
	}
	return loginStatus, nil
}

func NewLdapCon() (*ldap.Conn, error) {
	l, err := ldap.DialURL(config.LdapServerAddr)
	if err != nil {
		return nil, errors.Join(ErrLDAPServerUnreachable, err)
	}

	if config.LdapTLS {
		err = l.StartTLS(&tls.Config{InsecureSkipVerify: true})
		if err != nil {
			return nil, errors.Join(ErrLDAPServerUnreachable, err)
		}
	}

	return l, nil
}

// --- Capabilities ---
type Capabilities struct {
	CanAdmin  bool
	CanInvite bool
}

func NewCapabilities(login *LoginStatus, entry *ldap.Entry) *Capabilities {
	// Initialize
	canAdmin := false
	canInvite := false

	// Special case for the "admin" account that is de-facto admin
	canAdmin = strings.EqualFold(login.Info.DN(), config.AdminAccount)

	// Check if this account is part of a group that give capabilities
	for _, attr := range entry.Attributes {
		if strings.EqualFold(attr.Name, "memberof") {
			for _, group := range attr.Values {
				if config.GroupCanInvite != "" && strings.EqualFold(group, config.GroupCanInvite) {
					canInvite = true
				}
				if config.GroupCanAdmin != "" && strings.EqualFold(group, config.GroupCanAdmin) {
					canAdmin = true
				}
			}
		}
	}

	return &Capabilities{
		CanAdmin:  canAdmin,
		CanInvite: canInvite,
	}
}

// --- Logged User ---
type LoggedUser struct {
	Login        *LoginStatus
	Entry        *ldap.Entry
	Capabilities *Capabilities
	Quota        *UserQuota
	s3key        *garage.KeyInfo
}

func NewLoggedUser(login *LoginStatus) (*LoggedUser, error) {
	requestKind := "(objectClass=organizationalPerson)"
	if strings.EqualFold(login.Info.DN(), config.AdminAccount) {
		requestKind = "(objectclass=*)"
	}

	searchRequest := ldap.NewSearchRequest(
		login.Info.DN(),
		ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false,
		requestKind,
		[]string{
			"dn",
			"displayname",
			"givenname",
			"sn",
			"mail",
			"memberof",
			"description",
			"garage_s3_access_key",
			FIELD_NAME_DIRECTORY_VISIBILITY,
			FIELD_NAME_PROFILE_PICTURE,
			FIELD_QUOTA_WEBSITE_SIZE_BURSTED,
			FIELD_QUOTA_WEBSITE_COUNT,
		},
		nil)

	sr, err := login.conn.Search(searchRequest)
	if err != nil {
		return nil, ErrLDAPSearchInternalError
	}

	if len(sr.Entries) != 1 {
		return nil, ErrLDAPSearchNotFound
	}
	entry := sr.Entries[0]

	lu := &LoggedUser{
		Login:        login,
		Entry:        entry,
		Capabilities: NewCapabilities(login, entry),
		Quota:        NewUserQuotaFromEntry(entry),
	}
	return lu, nil
}
func (lu *LoggedUser) WelcomeName() string {
	ret := lu.Entry.GetAttributeValue("givenname")
	if ret == "" {
		ret = lu.Entry.GetAttributeValue("displayname")
	}
	if ret == "" {
		ret = lu.Login.Info.Username
	}
	return ret
}
func (lu *LoggedUser) S3KeyInfo() (*garage.KeyInfo, error) {
	var err error
	var keyPair *garage.KeyInfo

	if lu.s3key == nil {
		keyID := lu.Entry.GetAttributeValue("garage_s3_access_key")
		if keyID == "" {
			// If there is no S3Key in LDAP, generate it...
			keyPair, err = grgCreateKey(lu.Login.Info.Username)
			if err != nil {
				return nil, err
			}
			modify_request := ldap.NewModifyRequest(lu.Login.Info.DN(), nil)
			modify_request.Replace("garage_s3_access_key", []string{*keyPair.AccessKeyId})
			// @FIXME compatibility feature for bagage (SFTP+webdav)
			// you can remove it once bagage will be updated to fetch the key from garage directly
			// or when bottin will be able to dynamically fetch it.
			modify_request.Replace("garage_s3_secret_key", []string{*keyPair.SecretAccessKey})
			err = lu.Login.conn.Modify(modify_request)
			if err != nil {
				return nil, err
			}
		} else {
			// There is an S3 key in LDAP, fetch its descriptor...
			keyPair, err = grgGetKey(keyID)
			if err != nil {
				return nil, err
			}
		}

		// Cache the keypair...
		lu.s3key = keyPair
	}

	return lu.s3key, nil
}

// --- Require User Check
func RequireUser(r *http.Request) (*LoggedUser, error) {
	var login_info *LoginInfo

	if li, err := NewLoginInfoFromSession(r); err == nil {
		login_info = li
	} else if li, err := NewLoginInfoFromBasicAuth(r); err == nil {
		login_info = li
	} else {
		return nil, ErrNotAuthenticated
	}

	loginStatus, err := NewLoginStatus(r, login_info)
	if err != nil {
		return nil, err
	}

	return NewLoggedUser(loginStatus)
}

func RequireUserHtml(w http.ResponseWriter, r *http.Request) *LoggedUser {
	user, err := RequireUser(r)

	if errors.Is(err, ErrNotAuthenticated) || errors.Is(err, ErrWrongLDAPCredentials) {
		http.Redirect(w, r, "/login", http.StatusFound)
		return nil
	}

	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return nil
	}

	return user
}

func RequireUserApi(w http.ResponseWriter, r *http.Request) *LoggedUser {
	user, err := RequireUser(r)

	if errors.Is(err, ErrNotAuthenticated) || errors.Is(err, ErrWrongLDAPCredentials) {
		http.Error(w, err.Error(), http.StatusUnauthorized)
		return nil
	}

	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return nil
	}

	return user
}