aboutsummaryrefslogtreecommitdiff
path: root/login.go
diff options
context:
space:
mode:
Diffstat (limited to 'login.go')
-rw-r--r--login.go294
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
+}