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 }