forked from Deuxfleurs/guichet
294 lines
7 KiB
Go
294 lines
7 KiB
Go
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
|
|
}
|