diff options
Diffstat (limited to 'login.go')
-rw-r--r-- | login.go | 294 |
1 files changed, 294 insertions, 0 deletions
diff --git a/login.go b/login.go new file mode 100644 index 0000000..277e3ae --- /dev/null +++ b/login.go @@ -0,0 +1,294 @@ +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 +} |