2020-02-09 13:46:52 +00:00
package main
import (
2020-02-09 14:01:20 +00:00
"crypto/rand"
2020-02-09 14:44:18 +00:00
"crypto/tls"
2020-02-09 15:46:26 +00:00
"encoding/json"
"flag"
2020-02-09 16:52:48 +00:00
"fmt"
2020-02-09 14:44:18 +00:00
"html/template"
2020-02-09 15:46:26 +00:00
"io/ioutil"
"log"
"net/http"
"os"
"strings"
2020-02-09 13:46:52 +00:00
2020-02-09 14:44:18 +00:00
"github.com/go-ldap/ldap/v3"
2020-02-09 21:06:33 +00:00
"github.com/gorilla/mux"
2020-02-09 22:04:37 +00:00
"github.com/gorilla/sessions"
2020-02-09 13:46:52 +00:00
)
2020-02-09 14:01:20 +00:00
type ConfigFile struct {
HttpBindAddr string ` json:"http_bind_addr" `
LdapServerAddr string ` json:"ldap_server_addr" `
2020-02-09 15:46:26 +00:00
LdapTLS bool ` json:"ldap_tls" `
2020-02-09 17:03:10 +00:00
2023-02-08 15:46:13 +00:00
BaseDN string ` json:"base_dn" `
UserBaseDN string ` json:"user_base_dn" `
UserNameAttr string ` json:"user_name_attr" `
GroupBaseDN string ` json:"group_base_dn" `
GroupNameAttr string ` json:"group_name_attr" `
MailingBaseDN string ` json:"mailing_list_base_dn" `
MailingNameAttr string ` json:"mailing_list_name_attr" `
MailingGuestsBaseDN string ` json:"mailing_list_guest_user_base_dn" `
2020-02-14 20:58:34 +00:00
2020-02-14 17:57:25 +00:00
InvitationBaseDN string ` json:"invitation_base_dn" `
InvitationNameAttr string ` json:"invitation_name_attr" `
InvitedMailFormat string ` json:"invited_mail_format" `
InvitedAutoGroups [ ] string ` json:"invited_auto_groups" `
2020-02-09 17:03:10 +00:00
2020-02-14 20:58:34 +00:00
WebAddress string ` json:"web_address" `
MailFrom string ` json:"mail_from" `
SMTPServer string ` json:"smtp_server" `
SMTPUsername string ` json:"smtp_username" `
SMTPPassword string ` json:"smtp_password" `
2020-02-10 14:26:02 +00:00
AdminAccount string ` json:"admin_account" `
2020-02-09 16:52:48 +00:00
GroupCanInvite string ` json:"group_can_invite" `
GroupCanAdmin string ` json:"group_can_admin" `
2021-07-29 22:04:17 +00:00
2023-04-19 13:07:46 +00:00
S3AdminEndpoint string ` json:"s3_admin_endpoint" `
S3AdminToken string ` json:"s3_admin_token" `
2023-04-18 20:45:04 +00:00
2021-08-16 13:30:14 +00:00
S3Endpoint string ` json:"s3_endpoint" `
S3AccessKey string ` json:"s3_access_key" `
S3SecretKey string ` json:"s3_secret_key" `
S3Region string ` json:"s3_region" `
S3Bucket string ` json:"s3_bucket" `
2020-02-09 14:01:20 +00:00
}
2020-02-09 13:46:52 +00:00
2023-09-25 08:27:49 +00:00
var fsServer = flag . NewFlagSet ( "server" , flag . ContinueOnError )
var configFlag = fsServer . String ( "config" , "./config.json" , "Configuration file path" )
2020-02-09 14:01:20 +00:00
2020-02-09 14:44:18 +00:00
var config * ConfigFile
const SESSION_NAME = "guichet_session"
2022-12-01 22:05:59 +00:00
var staticPath = "./static"
var templatePath = "./templates"
2020-02-09 15:46:26 +00:00
var store sessions . Store = nil
func readConfig ( ) ConfigFile {
2021-08-16 12:36:14 +00:00
// Default configuration values for certain fields
2020-02-09 14:01:20 +00:00
config_file := ConfigFile {
HttpBindAddr : ":9991" ,
2020-02-09 14:44:18 +00:00
LdapServerAddr : "ldap://127.0.0.1:389" ,
2020-02-14 20:58:34 +00:00
UserNameAttr : "uid" ,
GroupNameAttr : "gid" ,
InvitationNameAttr : "cn" ,
InvitedAutoGroups : [ ] string { } ,
2020-02-09 14:01:20 +00:00
}
2020-02-10 14:26:02 +00:00
_ , err := os . Stat ( * configFlag )
2020-02-09 14:01:20 +00:00
if os . IsNotExist ( err ) {
2021-08-16 12:36:14 +00:00
log . Fatalf ( "Could not find Guichet configuration file at %s. Please create this file, for example starting with config.json.example and customizing it for your deployment." , * configFlag )
2020-02-09 14:01:20 +00:00
}
if err != nil {
log . Fatal ( err )
}
bytes , err := ioutil . ReadFile ( * configFlag )
if err != nil {
log . Fatal ( err )
}
err = json . Unmarshal ( bytes , & config_file )
if err != nil {
log . Fatal ( err )
}
return config_file
2020-02-09 13:46:52 +00:00
}
2022-12-01 22:05:59 +00:00
func getTemplate ( name string ) * template . Template {
2023-04-19 13:07:46 +00:00
return template . Must ( template . New ( "layout.html" ) . Funcs ( template . FuncMap {
"contains" : strings . Contains ,
} ) . ParseFiles (
templatePath + "/layout.html" ,
templatePath + "/" + name ,
) )
2022-12-01 22:05:59 +00:00
}
2020-02-09 13:46:52 +00:00
func main ( ) {
2023-09-25 08:27:49 +00:00
if len ( os . Args ) < 2 {
server ( os . Args [ 1 : ] )
return
}
switch os . Args [ 1 ] {
case "cli" :
cliMain ( os . Args [ 2 : ] )
case "server" :
server ( os . Args [ 2 : ] )
default :
log . Println ( "Usage: guichet [server|cli] --help" )
os . Exit ( 1 )
}
}
2020-02-09 13:46:52 +00:00
2023-09-25 08:27:49 +00:00
func server ( args [ ] string ) {
log . Println ( "Starting Guichet Server" )
fsServer . Parse ( args )
2020-02-09 14:44:18 +00:00
config_file := readConfig ( )
config = & config_file
2020-02-10 14:26:02 +00:00
session_key := make ( [ ] byte , 32 )
n , err := rand . Read ( session_key )
if err != nil || n != 32 {
log . Fatal ( err )
}
store = sessions . NewCookieStore ( session_key )
2020-02-09 14:01:20 +00:00
2020-02-09 21:06:33 +00:00
r := mux . NewRouter ( )
r . HandleFunc ( "/" , handleHome )
r . HandleFunc ( "/logout" , handleLogout )
2021-08-16 13:30:14 +00:00
2023-09-15 16:25:37 +00:00
r . HandleFunc ( "/api/unstable/garage/bucket/{bucket}" , handleAPIGarageBucket )
2023-09-15 12:32:44 +00:00
2020-02-09 21:06:33 +00:00
r . HandleFunc ( "/profile" , handleProfile )
r . HandleFunc ( "/passwd" , handlePasswd )
2021-08-16 13:30:14 +00:00
r . HandleFunc ( "/picture/{name}" , handleDownloadPicture )
2021-07-29 22:04:17 +00:00
2021-08-16 14:27:20 +00:00
r . HandleFunc ( "/directory/search" , handleDirectorySearch )
2021-07-21 19:21:46 +00:00
r . HandleFunc ( "/directory" , handleDirectory )
2023-04-19 13:07:46 +00:00
r . HandleFunc ( "/garage/key" , handleGarageKey )
r . HandleFunc ( "/garage/website" , handleGarageWebsiteList )
r . HandleFunc ( "/garage/website/new" , handleGarageWebsiteNew )
r . HandleFunc ( "/garage/website/b/{bucket}" , handleGarageWebsiteInspect )
2023-04-18 17:37:51 +00:00
2020-02-14 17:57:25 +00:00
r . HandleFunc ( "/invite/new_account" , handleInviteNewAccount )
r . HandleFunc ( "/invite/send_code" , handleInviteSendCode )
2020-02-14 20:58:34 +00:00
r . HandleFunc ( "/invitation/{code}" , handleInvitationCode )
2020-02-14 17:57:25 +00:00
2020-02-09 21:06:33 +00:00
r . HandleFunc ( "/admin/users" , handleAdminUsers )
r . HandleFunc ( "/admin/groups" , handleAdminGroups )
2023-02-08 12:11:43 +00:00
r . HandleFunc ( "/admin/mailing" , handleAdminMailing )
r . HandleFunc ( "/admin/mailing/{id}" , handleAdminMailingList )
2020-02-09 21:06:33 +00:00
r . HandleFunc ( "/admin/ldap/{dn}" , handleAdminLDAP )
2020-02-09 22:04:27 +00:00
r . HandleFunc ( "/admin/create/{template}/{super_dn}" , handleAdminCreate )
2020-02-09 17:28:42 +00:00
2022-12-01 22:05:59 +00:00
staticfiles := http . FileServer ( http . Dir ( staticPath ) )
2020-02-09 21:06:33 +00:00
r . Handle ( "/static/{file:.*}" , http . StripPrefix ( "/static/" , staticfiles ) )
2020-02-09 14:44:18 +00:00
2020-02-10 14:26:02 +00:00
log . Printf ( "Starting HTTP server on %s" , config . HttpBindAddr )
2020-02-10 14:26:02 +00:00
err = http . ListenAndServe ( config . HttpBindAddr , logRequest ( r ) )
2020-02-09 13:46:52 +00:00
if err != nil {
log . Fatal ( "Cannot start http server: " , err )
}
}
2020-02-09 14:01:20 +00:00
2020-02-09 14:44:18 +00:00
type LoginInfo struct {
Username string
2020-02-09 15:46:26 +00:00
DN string
2020-02-09 14:44:18 +00:00
Password string
2020-02-09 15:46:26 +00:00
}
2020-02-09 14:44:18 +00:00
2020-02-09 16:35:16 +00:00
type LoginStatus struct {
2020-02-09 16:52:48 +00:00
Info * LoginInfo
conn * ldap . Conn
2020-02-09 16:35:16 +00:00
UserEntry * ldap . Entry
2020-02-11 23:13:24 +00:00
CanAdmin bool
CanInvite bool
2020-02-09 16:35:16 +00:00
}
2020-02-14 20:58:34 +00:00
func ( login * LoginStatus ) WelcomeName ( ) string {
ret := login . UserEntry . GetAttributeValue ( "givenname" )
if ret == "" {
ret = login . UserEntry . GetAttributeValue ( "displayname" )
}
if ret == "" {
ret = login . Info . Username
}
return ret
}
2020-02-09 14:44:18 +00:00
func logRequest ( handler http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
log . Printf ( "%s %s %s\n" , r . RemoteAddr , r . Method , r . URL )
handler . ServeHTTP ( w , r )
} )
}
2020-02-09 16:35:16 +00:00
func checkLogin ( w http . ResponseWriter , r * http . Request ) * LoginStatus {
2020-02-10 14:26:02 +00:00
var login_info * LoginInfo
2020-02-09 14:44:18 +00:00
session , err := store . Get ( r , SESSION_NAME )
2020-02-10 14:26:02 +00:00
if err == nil {
username , ok := session . Values [ "login_username" ]
password , ok2 := session . Values [ "login_password" ]
user_dn , ok3 := session . Values [ "login_dn" ]
if ok && ok2 && ok3 {
login_info = & LoginInfo {
DN : user_dn . ( string ) ,
Username : username . ( string ) ,
Password : password . ( string ) ,
}
}
2020-02-09 15:46:26 +00:00
}
2020-02-09 14:44:18 +00:00
2020-02-10 14:26:02 +00:00
if login_info == nil {
2020-02-09 16:35:16 +00:00
login_info = handleLogin ( w , r )
if login_info == nil {
return nil
}
2020-02-09 14:44:18 +00:00
}
2020-02-09 16:35:16 +00:00
l := ldapOpen ( w )
if l == nil {
return nil
}
err = l . Bind ( login_info . DN , login_info . Password )
if err != nil {
delete ( session . Values , "login_username" )
delete ( session . Values , "login_password" )
delete ( session . Values , "login_dn" )
err = session . Save ( r , w )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return nil
}
return checkLogin ( w , r )
}
2020-02-10 14:26:02 +00:00
loginStatus := & LoginStatus {
2020-02-10 14:26:02 +00:00
Info : login_info ,
conn : l ,
2020-02-10 14:26:02 +00:00
}
requestKind := "(objectClass=organizationalPerson)"
2020-02-11 23:13:24 +00:00
if strings . EqualFold ( login_info . DN , config . AdminAccount ) {
2020-02-10 14:26:02 +00:00
requestKind = "(objectclass=*)"
}
2020-02-09 16:35:16 +00:00
searchRequest := ldap . NewSearchRequest (
login_info . DN ,
ldap . ScopeBaseObject , ldap . NeverDerefAliases , 0 , 0 , false ,
2020-02-10 14:26:02 +00:00
requestKind ,
2021-08-16 13:30:14 +00:00
[ ] string {
"dn" ,
"displayname" ,
"givenname" ,
"sn" ,
"mail" ,
"memberof" ,
"description" ,
2023-04-19 13:07:46 +00:00
"garage_s3_access_key" ,
2021-08-16 13:30:14 +00:00
FIELD_NAME_DIRECTORY_VISIBILITY ,
FIELD_NAME_PROFILE_PICTURE ,
} ,
2020-02-09 16:35:16 +00:00
nil )
sr , err := l . Search ( searchRequest )
2020-02-09 16:52:48 +00:00
if err != nil {
2020-02-09 16:35:16 +00:00
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return nil
}
if len ( sr . Entries ) != 1 {
2020-02-10 14:26:02 +00:00
http . Error ( w , fmt . Sprintf ( "Unable to find entry for %s" , login_info . DN ) , http . StatusInternalServerError )
2020-02-09 16:35:16 +00:00
return nil
}
2020-02-10 14:26:02 +00:00
loginStatus . UserEntry = sr . Entries [ 0 ]
2020-02-11 23:13:24 +00:00
loginStatus . CanAdmin = strings . EqualFold ( loginStatus . Info . DN , config . AdminAccount )
loginStatus . CanInvite = false
for _ , attr := range loginStatus . UserEntry . Attributes {
if strings . EqualFold ( attr . Name , "memberof" ) {
for _ , group := range attr . Values {
if config . GroupCanInvite != "" && strings . EqualFold ( group , config . GroupCanInvite ) {
loginStatus . CanInvite = true
}
if config . GroupCanAdmin != "" && strings . EqualFold ( group , config . GroupCanAdmin ) {
loginStatus . CanAdmin = true
}
}
}
}
2020-02-10 14:26:02 +00:00
return loginStatus
2020-02-09 14:44:18 +00:00
}
func ldapOpen ( w http . ResponseWriter ) * ldap . Conn {
l , err := ldap . DialURL ( config . LdapServerAddr )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return nil
}
if config . LdapTLS {
err = l . StartTLS ( & tls . Config { InsecureSkipVerify : true } )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return nil
}
}
return l
}
2020-02-09 14:01:20 +00:00
// Page handlers ----
2020-02-09 16:52:48 +00:00
type HomePageData struct {
2020-02-14 20:58:34 +00:00
Login * LoginStatus
BaseDN string
2020-02-09 16:52:48 +00:00
}
2020-02-09 14:01:20 +00:00
func handleHome ( w http . ResponseWriter , r * http . Request ) {
2022-12-01 22:05:59 +00:00
templateHome := getTemplate ( "home.html" )
2020-02-09 15:46:26 +00:00
2020-02-09 14:44:18 +00:00
login := checkLogin ( w , r )
if login == nil {
return
}
2020-02-10 14:26:02 +00:00
data := & HomePageData {
2020-02-14 20:58:34 +00:00
Login : login ,
BaseDN : config . BaseDN ,
2020-02-10 14:26:02 +00:00
}
templateHome . Execute ( w , data )
2020-02-09 14:44:18 +00:00
}
2020-02-09 15:46:26 +00:00
func handleLogout ( w http . ResponseWriter , r * http . Request ) {
session , err := store . Get ( r , SESSION_NAME )
if err != nil {
2020-02-14 17:57:25 +00:00
session , _ = store . New ( r , SESSION_NAME )
2020-02-09 15:46:26 +00:00
}
delete ( session . Values , "login_username" )
delete ( session . Values , "login_password" )
delete ( session . Values , "login_dn" )
err = session . Save ( r , w )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
http . Redirect ( w , r , "/" , http . StatusFound )
}
2020-02-09 17:03:10 +00:00
type LoginFormData struct {
Username string
2020-02-12 15:02:42 +00:00
WrongUser bool
WrongPass bool
2020-02-09 17:03:10 +00:00
ErrorMessage string
}
2023-09-15 11:59:48 +00:00
func buildUserDN ( username string ) string {
user_dn := fmt . Sprintf ( "%s=%s,%s" , config . UserNameAttr , username , config . UserBaseDN )
if strings . EqualFold ( username , config . AdminAccount ) {
user_dn = username
}
return user_dn
}
2020-02-09 14:44:18 +00:00
func handleLogin ( w http . ResponseWriter , r * http . Request ) * LoginInfo {
2022-12-01 22:05:59 +00:00
templateLogin := getTemplate ( "login.html" )
2020-02-09 15:46:26 +00:00
2020-02-09 14:44:18 +00:00
if r . Method == "GET" {
templateLogin . Execute ( w , LoginFormData { } )
return nil
} else if r . Method == "POST" {
r . ParseForm ( )
username := strings . Join ( r . Form [ "username" ] , "" )
2020-02-09 15:46:26 +00:00
password := strings . Join ( r . Form [ "password" ] , "" )
2023-09-15 11:59:48 +00:00
user_dn := buildUserDN ( username )
2020-02-09 14:44:18 +00:00
l := ldapOpen ( w )
if l == nil {
return nil
}
2020-02-09 15:46:26 +00:00
err := l . Bind ( user_dn , password )
2020-02-09 14:44:18 +00:00
if err != nil {
2020-02-12 15:02:42 +00:00
data := & LoginFormData {
Username : username ,
}
if ldap . IsErrorWithCode ( err , ldap . LDAPResultInvalidCredentials ) {
data . WrongPass = true
} else if ldap . IsErrorWithCode ( err , ldap . LDAPResultNoSuchObject ) {
data . WrongUser = true
} else {
data . ErrorMessage = err . Error ( )
}
templateLogin . Execute ( w , data )
2020-02-09 14:44:18 +00:00
return nil
}
// Successfully logged in, save it to session
session , err := store . Get ( r , SESSION_NAME )
if err != nil {
2020-02-10 14:26:02 +00:00
session , _ = store . New ( r , SESSION_NAME )
2020-02-09 14:44:18 +00:00
}
2020-02-09 15:46:26 +00:00
session . Values [ "login_username" ] = username
session . Values [ "login_password" ] = password
session . Values [ "login_dn" ] = user_dn
err = session . Save ( r , w )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return nil
}
return & LoginInfo {
DN : user_dn ,
Username : username ,
Password : password ,
}
2020-02-09 14:44:18 +00:00
} else {
http . Error ( w , "Unsupported method" , http . StatusBadRequest )
return nil
}
2020-02-09 14:01:20 +00:00
}