| @@ -6,11 +6,13 @@ go: | |||||
| - 1.4 | - 1.4 | ||||
| - tip | - tip | ||||
| sudo: false | |||||
| before_install: | |||||
| - sudo apt-get update -qq | |||||
| - sudo apt-get install -y libpam-dev | |||||
| script: go build -v | script: go build -v | ||||
| notifications: | notifications: | ||||
| email: | email: | ||||
| - u@gogs.io | - u@gogs.io | ||||
| slack: gophercn:o5pSanyTeNhnfYc3QnG0X7Wx | |||||
| slack: gophercn:o5pSanyTeNhnfYc3QnG0X7Wx | |||||
| @@ -619,6 +619,7 @@ auths.smtp_auth = SMTP Authorization Type | |||||
| auths.smtphost = SMTP Host | auths.smtphost = SMTP Host | ||||
| auths.smtpport = SMTP Port | auths.smtpport = SMTP Port | ||||
| auths.enable_tls = Enable TLS Encryption | auths.enable_tls = Enable TLS Encryption | ||||
| auths.pam_service_name = PAM Service Name | |||||
| auths.enable_auto_register = Enable Auto Registration | auths.enable_auto_register = Enable Auto Registration | ||||
| auths.tips = Tips | auths.tips = Tips | ||||
| auths.edit = Edit Authorization Setting | auths.edit = Edit Authorization Setting | ||||
| @@ -17,6 +17,7 @@ import ( | |||||
| "github.com/go-xorm/xorm" | "github.com/go-xorm/xorm" | ||||
| "github.com/gogits/gogs/modules/auth/ldap" | "github.com/gogits/gogs/modules/auth/ldap" | ||||
| "github.com/gogits/gogs/modules/auth/pam" | |||||
| "github.com/gogits/gogs/modules/log" | "github.com/gogits/gogs/modules/log" | ||||
| "github.com/gogits/gogs/modules/uuid" | "github.com/gogits/gogs/modules/uuid" | ||||
| ) | ) | ||||
| @@ -28,6 +29,7 @@ const ( | |||||
| PLAIN | PLAIN | ||||
| LDAP | LDAP | ||||
| SMTP | SMTP | ||||
| PAM | |||||
| ) | ) | ||||
| var ( | var ( | ||||
| @@ -39,12 +41,14 @@ var ( | |||||
| var LoginTypes = map[LoginType]string{ | var LoginTypes = map[LoginType]string{ | ||||
| LDAP: "LDAP", | LDAP: "LDAP", | ||||
| SMTP: "SMTP", | SMTP: "SMTP", | ||||
| PAM: "PAM", | |||||
| } | } | ||||
| // Ensure structs implemented interface. | // Ensure structs implemented interface. | ||||
| var ( | var ( | ||||
| _ core.Conversion = &LDAPConfig{} | _ core.Conversion = &LDAPConfig{} | ||||
| _ core.Conversion = &SMTPConfig{} | _ core.Conversion = &SMTPConfig{} | ||||
| _ core.Conversion = &PAMConfig{} | |||||
| ) | ) | ||||
| type LDAPConfig struct { | type LDAPConfig struct { | ||||
| @@ -74,6 +78,18 @@ func (cfg *SMTPConfig) ToDB() ([]byte, error) { | |||||
| return json.Marshal(cfg) | return json.Marshal(cfg) | ||||
| } | } | ||||
| type PAMConfig struct { | |||||
| ServiceName string // pam service (e.g. system-auth) | |||||
| } | |||||
| func (cfg *PAMConfig) FromDB(bs []byte) error { | |||||
| return json.Unmarshal(bs, &cfg) | |||||
| } | |||||
| func (cfg *PAMConfig) ToDB() ([]byte, error) { | |||||
| return json.Marshal(cfg) | |||||
| } | |||||
| type LoginSource struct { | type LoginSource struct { | ||||
| Id int64 | Id int64 | ||||
| Type LoginType | Type LoginType | ||||
| @@ -97,6 +113,10 @@ func (source *LoginSource) SMTP() *SMTPConfig { | |||||
| return source.Cfg.(*SMTPConfig) | return source.Cfg.(*SMTPConfig) | ||||
| } | } | ||||
| func (source *LoginSource) PAM() *PAMConfig { | |||||
| return source.Cfg.(*PAMConfig) | |||||
| } | |||||
| func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) { | func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) { | ||||
| if colName == "type" { | if colName == "type" { | ||||
| ty := (*val).(int64) | ty := (*val).(int64) | ||||
| @@ -105,6 +125,8 @@ func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) { | |||||
| source.Cfg = new(LDAPConfig) | source.Cfg = new(LDAPConfig) | ||||
| case SMTP: | case SMTP: | ||||
| source.Cfg = new(SMTPConfig) | source.Cfg = new(SMTPConfig) | ||||
| case PAM: | |||||
| source.Cfg = new(PAMConfig) | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -197,6 +219,13 @@ func UserSignIn(uname, passwd string) (*User, error) { | |||||
| return u, nil | return u, nil | ||||
| } | } | ||||
| log.Warn("Fail to login(%s) by SMTP(%s): %v", uname, source.Name, err) | log.Warn("Fail to login(%s) by SMTP(%s): %v", uname, source.Name, err) | ||||
| } else if source.Type == PAM { | |||||
| u, err := LoginUserPAMSource(nil, uname, passwd, | |||||
| source.Id, source.Cfg.(*PAMConfig), true) | |||||
| if err == nil { | |||||
| return u, nil | |||||
| } | |||||
| log.Warn("Fail to login(%s) by PAM(%s): %v", uname, source.Name, err) | |||||
| } | } | ||||
| } | } | ||||
| @@ -218,6 +247,8 @@ func UserSignIn(uname, passwd string) (*User, error) { | |||||
| return LoginUserLdapSource(u, u.LoginName, passwd, source.Id, source.Cfg.(*LDAPConfig), false) | return LoginUserLdapSource(u, u.LoginName, passwd, source.Id, source.Cfg.(*LDAPConfig), false) | ||||
| case SMTP: | case SMTP: | ||||
| return LoginUserSMTPSource(u, u.LoginName, passwd, source.Id, source.Cfg.(*SMTPConfig), false) | return LoginUserSMTPSource(u, u.LoginName, passwd, source.Id, source.Cfg.(*SMTPConfig), false) | ||||
| case PAM: | |||||
| return LoginUserPAMSource(u, u.LoginName, passwd, source.Id, source.Cfg.(*PAMConfig), false) | |||||
| } | } | ||||
| return nil, ErrUnsupportedLoginType | return nil, ErrUnsupportedLoginType | ||||
| } | } | ||||
| @@ -359,3 +390,33 @@ func LoginUserSMTPSource(u *User, name, passwd string, sourceId int64, cfg *SMTP | |||||
| err := CreateUser(u) | err := CreateUser(u) | ||||
| return u, err | return u, err | ||||
| } | } | ||||
| // Query if name/passwd can login against PAM | |||||
| // Create a local user if success | |||||
| // Return the same LoginUserPlain semantic | |||||
| func LoginUserPAMSource(u *User, name, passwd string, sourceId int64, cfg *PAMConfig, autoRegister bool) (*User, error) { | |||||
| if err := pam.PAMAuth(cfg.ServiceName, name, passwd); err != nil { | |||||
| if strings.Contains(err.Error(), "Authentication failure") { | |||||
| return nil, ErrUserNotExist | |||||
| } | |||||
| return nil, err | |||||
| } | |||||
| if !autoRegister { | |||||
| return u, nil | |||||
| } | |||||
| // fake a local user creation | |||||
| u = &User{ | |||||
| LowerName: strings.ToLower(name), | |||||
| Name: strings.ToLower(name), | |||||
| LoginType: PAM, | |||||
| LoginSource: sourceId, | |||||
| LoginName: name, | |||||
| IsActive: true, | |||||
| Passwd: passwd, | |||||
| Email: name, | |||||
| } | |||||
| err := CreateUser(u) | |||||
| return u, err | |||||
| } | |||||
| @@ -30,6 +30,7 @@ type AuthenticationForm struct { | |||||
| SMTPPort int `form:"smtp_port"` | SMTPPort int `form:"smtp_port"` | ||||
| TLS bool `form:"tls"` | TLS bool `form:"tls"` | ||||
| AllowAutoRegister bool `form:"allowautoregister"` | AllowAutoRegister bool `form:"allowautoregister"` | ||||
| PAMServiceName string | |||||
| } | } | ||||
| func (f *AuthenticationForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { | func (f *AuthenticationForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { | ||||
| @@ -0,0 +1,35 @@ | |||||
| // +build !windows | |||||
| // Copyright 2014 The Gogs Authors. All rights reserved. | |||||
| // Use of this source code is governed by a MIT-style | |||||
| // license that can be found in the LICENSE file. | |||||
| package pam | |||||
| import ( | |||||
| "errors" | |||||
| "github.com/msteinert/pam" | |||||
| ) | |||||
| func PAMAuth(serviceName, userName, passwd string) error { | |||||
| t, err := pam.StartFunc(serviceName, userName, func(s pam.Style, msg string) (string, error) { | |||||
| switch s { | |||||
| case pam.PromptEchoOff: | |||||
| return passwd, nil | |||||
| case pam.PromptEchoOn, pam.ErrorMsg, pam.TextInfo: | |||||
| return "", nil | |||||
| } | |||||
| return "", errors.New("Unrecognized PAM message style") | |||||
| }) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| if err = t.Authenticate(0); err != nil { | |||||
| return err | |||||
| } | |||||
| return nil | |||||
| } | |||||
| @@ -0,0 +1,15 @@ | |||||
| // +build windows | |||||
| // Copyright 2014 The Gogs Authors. All rights reserved. | |||||
| // Use of this source code is governed by a MIT-style | |||||
| // license that can be found in the LICENSE file. | |||||
| package pam | |||||
| import ( | |||||
| "errors" | |||||
| ) | |||||
| func PAMAuth(serviceName, userName, passwd string) error { | |||||
| return errors.New("PAM not supported") | |||||
| } | |||||
| @@ -753,10 +753,17 @@ function initAdmin() { | |||||
| if (v == 2) { | if (v == 2) { | ||||
| $('.ldap').toggleShow(); | $('.ldap').toggleShow(); | ||||
| $('.smtp').toggleHide(); | $('.smtp').toggleHide(); | ||||
| $('.pam').toggleHide(); | |||||
| } | } | ||||
| if (v == 3) { | if (v == 3) { | ||||
| $('.smtp').toggleShow(); | $('.smtp').toggleShow(); | ||||
| $('.ldap').toggleHide(); | $('.ldap').toggleHide(); | ||||
| $('.pam').toggleHide(); | |||||
| } | |||||
| if (v == 4) { | |||||
| $('.pam').toggleShow(); | |||||
| $('.smtp').toggleHide(); | |||||
| $('.ldap').toggleHide(); | |||||
| } | } | ||||
| }); | }); | ||||
| @@ -84,6 +84,10 @@ func NewAuthSourcePost(ctx *middleware.Context, form auth.AuthenticationForm) { | |||||
| Port: form.SMTPPort, | Port: form.SMTPPort, | ||||
| TLS: form.TLS, | TLS: form.TLS, | ||||
| } | } | ||||
| case models.PAM: | |||||
| u = &models.PAMConfig{ | |||||
| ServiceName: form.PAMServiceName, | |||||
| } | |||||
| default: | default: | ||||
| ctx.Error(400) | ctx.Error(400) | ||||
| return | return | ||||
| @@ -166,6 +170,10 @@ func EditAuthSourcePost(ctx *middleware.Context, form auth.AuthenticationForm) { | |||||
| Port: form.SMTPPort, | Port: form.SMTPPort, | ||||
| TLS: form.TLS, | TLS: form.TLS, | ||||
| } | } | ||||
| case models.PAM: | |||||
| config = &models.PAMConfig{ | |||||
| ServiceName: form.PAMServiceName, | |||||
| } | |||||
| default: | default: | ||||
| ctx.Error(400) | ctx.Error(400) | ||||
| return | return | ||||
| @@ -91,6 +91,12 @@ | |||||
| <label class="req" for="smtp_port">{{.i18n.Tr "admin.auths.smtpport"}}</label> | <label class="req" for="smtp_port">{{.i18n.Tr "admin.auths.smtpport"}}</label> | ||||
| <input class="ipt ipt-large ipt-radius {{if .Err_SmtpPort}}ipt-error{{end}}" id="smtp_port" name="smtp_port" value="{{.Source.SMTP.Port}}" /> | <input class="ipt ipt-large ipt-radius {{if .Err_SmtpPort}}ipt-error{{end}}" id="smtp_port" name="smtp_port" value="{{.Source.SMTP.Port}}" /> | ||||
| </div> | </div> | ||||
| {{else if eq $type 4}} | |||||
| <div class="field"> | |||||
| <label class="req" for="pam_service_name">{{.i18n.Tr "admin.auths.pam_service_name"}}</label> | |||||
| <input class="ipt ipt-large ipt-radius {{if .Err_PAMServiceName}}ipt-error{{end}}" id="pam_service_name" name="pam_service_name" value="{{.Source.PAM.ServiceName}}" /> | |||||
| </div> | |||||
| {{end}} | {{end}} | ||||
| <div class="field"> | <div class="field"> | ||||
| @@ -86,6 +86,12 @@ | |||||
| <input class="ipt ipt-large ipt-radius {{if .Err_SmtpPort}}ipt-error{{end}}" id="smtp_port" name="smtp_port" value="{{.smtp_port}}" /> | <input class="ipt ipt-large ipt-radius {{if .Err_SmtpPort}}ipt-error{{end}}" id="smtp_port" name="smtp_port" value="{{.smtp_port}}" /> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div class="pam hidden"> | |||||
| <div class="field"> | |||||
| <label class="req" for="pam_service_name">{{.i18n.Tr "admin.auths.pam_service_name"}}</label> | |||||
| <input class="ipt ipt-large ipt-radius {{if .Err_PAMServiceName}}ipt-error{{end}}" id="pam_service_name" name="pam_service_name" value="{{.pam_service_name}}" /> | |||||
| </div> | |||||
| </div> | |||||
| <div class="field"> | <div class="field"> | ||||
| <div class="smtp hidden"> | <div class="smtp hidden"> | ||||
| <label></label> | <label></label> | ||||