From 34cec5c41a785a9697d5c2a6533ca19680ba5189 Mon Sep 17 00:00:00 2001 From: Jeroen Massar Date: Thu, 16 Feb 2017 12:43:17 +0100 Subject: [PATCH 01/39] Introduce IndexedKeySet This solves having a map being passed in several places, making the code more readable and it also removes code duplication for generating the final output from the keyset. Switched to SHA256, as MD5 has been deprecated for any kind of use a long time ago. (and the overhead with current cpu's is so minimal that we really will not notice it). --- lib/group.go | 9 +++++---- lib/ml.go | 14 +++++++------- lib/pgp/keyset.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++ lib/user.go | 5 +++-- ui/group.go | 10 +++------- ui/ml.go | 12 ++++-------- ui/user.go | 14 ++++---------- 7 files changed, 75 insertions(+), 38 deletions(-) create mode 100644 lib/pgp/keyset.go diff --git a/lib/group.go b/lib/group.go index 076f9da..f16c131 100644 --- a/lib/group.go +++ b/lib/group.go @@ -2,6 +2,7 @@ package pitchfork import ( "errors" + pfpgp "trident.li/pitchfork/lib/pgp" ) const ( @@ -23,7 +24,7 @@ type PfGroup interface { Select(ctx PfCtx, group_name string, perms Perm) (err error) GetGroups(ctx PfCtx, username string) (groups []PfGroupMember, err error) GetGroupsAll() (groups []PfGroupMember, err error) - GetKeys(ctx PfCtx, keyset map[[16]byte][]byte) (err error) + GetKeys(ctx PfCtx, keyset *pfpgp.IndexedKeySet) (err error) IsMember(user string) (ismember bool, isadmin bool, out PfMemberState, err error) ListGroupMembersTot(search string) (total int, err error) ListGroupMembers(search string, username string, offset int, max int, nominated bool, inclhidden bool, exact bool) (members []PfGroupMember, err error) @@ -226,7 +227,7 @@ func (grp *PfGroupS) GetGroupsAll() (members []PfGroupMember, err error) { } // GetKeys returns the keyfile for a group -func (grp *PfGroupS) GetKeys(ctx PfCtx, keyset map[[16]byte][]byte) (err error) { +func (grp *PfGroupS) GetKeys(ctx PfCtx, keyset *pfpgp.IndexedKeySet) (err error) { var ml PfML mls, err := ml.ListWithUser(ctx, grp, ctx.SelectedUser()) if err != nil { @@ -251,8 +252,8 @@ func (grp *PfGroupS) GetKeys(ctx PfCtx, keyset map[[16]byte][]byte) (err error) mlist := ctx.SelectedML() - /* Get the ML List Key */ - err = mlist.GetKey(ctx, keyset) + /* Get the ML List Keys */ + err = mlist.GetKeys(ctx, keyset) if err != nil { return err } diff --git a/lib/ml.go b/lib/ml.go index 50d80d1..cd9bdc5 100755 --- a/lib/ml.go +++ b/lib/ml.go @@ -2,7 +2,6 @@ package pitchfork import ( - "crypto/md5" "errors" pfpgp "trident.li/pitchfork/lib/pgp" @@ -307,7 +306,7 @@ func (ml *PfML) ListWithUser(ctx PfCtx, grp PfGroup, user PfUser) (mls []PfML, e } // GetKeys returns the keys for a given mailinglist. -func (ml *PfML) GetKey(ctx PfCtx, keyset map[[16]byte][]byte) (err error) { +func (ml *PfML) GetKeys(ctx PfCtx, keyset *pfpgp.IndexedKeySet) (err error) { var key string q := "SELECT COALESCE(pubkey, '') " + @@ -322,11 +321,11 @@ func (ml *PfML) GetKey(ctx PfCtx, keyset map[[16]byte][]byte) (err error) { /* Only append a list key when it exists */ if key != "" { - keyset[md5.Sum([]byte(key))] = []byte(key) + keyset.Add(key) } /* List active members/collect keys */ - err = ListKeys(ctx, keyset, ml.GroupName, ml.ListName) + err = ml.ListKeys(ctx, keyset) if err != nil { return } @@ -402,7 +401,7 @@ func ml_member_list(ctx PfCtx, args []string) (err error) { } // ListKeys returns the keys for a mailinglist. -func ListKeys(ctx PfCtx, keyset map[[16]byte][]byte, gr_name string, ml_name string) (err error) { +func (ml *PfML) ListKeys(ctx PfCtx, keyset *pfpgp.IndexedKeySet) (err error) { q := "SELECT me.keyring " + "FROM member_email me, " + "member_mailinglist ml, " + @@ -415,7 +414,7 @@ func ListKeys(ctx PfCtx, keyset map[[16]byte][]byte, gr_name string, ml_name str " AND ml.trustgroup = $1 " + " AND ml.lhs = $2" - rows, err := DB.Query(q, gr_name, ml_name) + rows, err := DB.Query(q, ml.GroupName, ml.ListName) if err != nil { return } @@ -429,7 +428,8 @@ func ListKeys(ctx PfCtx, keyset map[[16]byte][]byte, gr_name string, ml_name str if err != nil { return } - keyset[md5.Sum([]byte(key))] = []byte(key) + + keyset.Add(key) } return } diff --git a/lib/pgp/keyset.go b/lib/pgp/keyset.go new file mode 100644 index 0000000..964c249 --- /dev/null +++ b/lib/pgp/keyset.go @@ -0,0 +1,49 @@ +package pfpgp + +import ( + "crypto/sha256" +) + +// The index is SHA256 hashed, thus the index is 16 bytes wide. +const IndexedKeySetHashSize = sha256.Size + +// IndexedKeySet is used to sort PGP keys and add them in a +// unique way to avoid keys to be listed multiple times. +// +// We SHA256 each key to only allow a key to be added once. +type IndexedKeySet struct { + keys map[[IndexedKeySetHashSize]byte][]byte +} + +// NewIndexedKeySet creates a new IndexedKeySet. +func NewIndexedKeySet() *IndexedKeySet { + return &IndexedKeySet{make(map[[IndexedKeySetHashSize]byte][]byte)} +} + +// Add can be used to add a key to the IndexedKeySet. +// +// The 'key' provided is a full ASCII formatted PGP PUBLIC KEY block. +// +// A SHA256 is used to hash the key to avoid duplicates of the exact same key. +func (keyset *IndexedKeySet) Add(key string) { + bkey := []byte(key) + + keyset.keys[sha256.Sum256(bkey)] = bkey +} + +// ToBytes converts the keyset into ASCII output. +// +// Unix-style newlines (LF-only) are added in between +// the keys to keep them separate in the output. +func (keyset *IndexedKeySet) ToBytes() (output []byte) { + // Add each key in the set + for k := range keyset.keys { + // Add the key + output = append(output, keyset.keys[k][:]...) + + // Add a \n (LF) + output = append(output, byte(0x0a)) + } + + return +} diff --git a/lib/user.go b/lib/user.go index 3bc83d7..f354497 100755 --- a/lib/user.go +++ b/lib/user.go @@ -9,6 +9,7 @@ import ( "time" "github.com/pborman/uuid" + pfpgp "trident.li/pitchfork/lib/pgp" ) // Standardized error messages @@ -61,7 +62,7 @@ type PfUser interface { SharedGroups(ctx PfCtx, otheruser PfUser) (ok bool, err error) GetImage(ctx PfCtx) (img []byte, err error) GetHideEmail() (hide_email bool) - GetKeys(ctx PfCtx, keyset map[[16]byte][]byte) (err error) + GetKeys(ctx PfCtx, keyset *pfpgp.IndexedKeySet) (err error) GetDetails() (details []PfUserDetail, err error) GetLanguages() (languages []PfUserLanguage, err error) Get(what string) (val string, err error) @@ -437,7 +438,7 @@ func (user *PfUserS) GetHideEmail() (hide_email bool) { return user.Hide_email } -func (user *PfUserS) GetKeys(ctx PfCtx, keyset map[[16]byte][]byte) (err error) { +func (user *PfUserS) GetKeys(ctx PfCtx, keyset *pfpgp.IndexedKeySet) (err error) { groups, err := user.GetGroups(ctx) if err != nil { return diff --git a/ui/group.go b/ui/group.go index d1d5512..9262dfd 100755 --- a/ui/group.go +++ b/ui/group.go @@ -4,6 +4,7 @@ import ( "strconv" pf "trident.li/pitchfork/lib" + pfpgp "trident.li/pitchfork/lib/pgp" ) // h_group_add handles group additions @@ -280,8 +281,7 @@ func H_group_member_profile(cui PfUI) { // h_group_pgp_keys returns a file containing the group's PGP keys func h_group_pgp_keys(cui PfUI) { - var output []byte - keyset := make(map[[16]byte][]byte) + keyset := pfpgp.NewIndexedKeySet() grp := cui.SelectedGroup() err := grp.GetKeys(cui, keyset) @@ -291,13 +291,9 @@ func h_group_pgp_keys(cui PfUI) { return } + output := keyset.ToBytes() fname := grp.GetGroupName() + ".asc" - for k := range keyset { - output = append(output, keyset[k][:]...) - output = append(output, byte(0x0a)) - } - cui.SetContentType("application/pgp-keys") cui.SetFileName(fname) cui.SetExpires(60) diff --git a/ui/ml.go b/ui/ml.go index 446eac7..d06e272 100755 --- a/ui/ml.go +++ b/ui/ml.go @@ -4,6 +4,7 @@ import ( "strconv" pf "trident.li/pitchfork/lib" + pfpgp "trident.li/pitchfork/lib/pgp" ) // h_ml_new allows creation of a new Mailinglist @@ -59,23 +60,18 @@ func h_ml_new(cui PfUI) { // h_ml_pgp returns the PGP key of a group func h_ml_pgp(cui PfUI) { - var output []byte - keyset := make(map[[16]byte][]byte) + keyset := pfpgp.NewIndexedKeySet() grp := cui.SelectedGroup() ml := cui.SelectedML() - err := ml.GetKey(cui, keyset) + err := ml.GetKeys(cui, keyset) if err != nil { H_error(cui, StatusNotFound) return } - for k := range keyset { - output = append(output, keyset[k][:]...) - output = append(output, byte(0x0a)) - } - + output := keyset.ToBytes() fname := grp.GetGroupName() + "-" + ml.ListName + ".asc" cui.SetContentType("application/pgp-keys") diff --git a/ui/user.go b/ui/user.go index 5098b8d..3f97b3e 100755 --- a/ui/user.go +++ b/ui/user.go @@ -6,6 +6,7 @@ import ( "trident.li/keyval" pf "trident.li/pitchfork/lib" + pfpgp "trident.li/pitchfork/lib/pgp" ) // h_user_username handles the username modification page @@ -212,25 +213,18 @@ func h_user_index(cui PfUI) { // h_user_pgp_keys returns the PGP keys of all the groups of a user func h_user_pgp_keys(cui PfUI) { - var err error - var output []byte - - keyset := make(map[[16]byte][]byte) + keyset := pfpgp.NewIndexedKeySet() user := cui.SelectedUser() - err = user.GetKeys(cui, keyset) + err := user.GetKeys(cui, keyset) if err != nil { /* Temp redirect to unknown */ H_NoAccess(cui) return } + output := keyset.ToBytes() fname := user.GetUserName() + ".asc" - for k := range keyset { - output = append(output, keyset[k][:]...) - output = append(output, byte(0x0a)) - } - cui.SetContentType("application/pgp-keys") cui.SetFileName(fname) cui.SetExpires(60) From 7a6c5a04cd103ff8e14346de28f4e2d5c514beca Mon Sep 17 00:00:00 2001 From: Jeroen Massar Date: Thu, 16 Feb 2017 12:56:18 +0100 Subject: [PATCH 02/39] Typo fix: Wether -> Whether --- lib/ctx.go | 4 ++-- lib/user.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ctx.go b/lib/ctx.go index 9a6fb2c..34b5e19 100755 --- a/lib/ctx.go +++ b/lib/ctx.go @@ -772,7 +772,7 @@ func (ctx *PfCtxS) IsLoggedIn() bool { } // IsGroupMember can be used to check if the selected user -// is a member of the selected group and wether the user +// is a member of the selected group and whether the user // can see the group. func (ctx *PfCtxS) IsGroupMember() bool { if !ctx.HasSelectedUser() { @@ -798,7 +798,7 @@ func (ctx *PfCtxS) IsGroupMember() bool { return true } - /* Normal group users, it depends on wether they can see them */ + /* Normal group users, it depends on whether they can see them */ return state.can_see } diff --git a/lib/user.go b/lib/user.go index f354497..8550791 100755 --- a/lib/user.go +++ b/lib/user.go @@ -101,7 +101,7 @@ type PfUserS struct { Telephone string `label:"Telephone" pftype:"tel" pfset:"self" pfget:"user_view" pfcol:"tel_info" hint:"The phone number where to contact the user using voice messages"` Airport string `label:"Airport" min:"3" max:"3" pfset:"self" pfget:"user_view" hint:"Closest airport for this user"` Biography string `label:"Biography" pftype:"text" pfset:"self" pfget:"user_view" pfcol:"bio_info" hint:"Biography for this user"` - IsSysadmin bool `label:"System Administrator" pfset:"sysadmin" pfget:"group_admin" pfskipfailperm:"yes" pfcol:"sysadmin" hint:"Wether the user is a System Administrator"` + IsSysadmin bool `label:"System Administrator" pfset:"sysadmin" pfget:"group_admin" pfskipfailperm:"yes" pfcol:"sysadmin" hint:"Whether the user is a System Administrator"` CanBeSysadmin bool `label:"Can Be System Administrator" pfset:"nobody" pfget:"nobody" pfskipfailperm:"yes" pfcol:"sysadmin" hint:"If the user can toggle between Regular and SysAdmin usermode"` LoginAttempts int `label:"Number of failed Login Attempts" pfset:"self,group_admin" pfget:"group_admin" pfskipfailperm:"yes" pfcol:"login_attempts" hint:"How many failed login attempts have been registered"` No_email bool `label:"Email Disabled" pfset:"sysadmin" pfget:"self,group_admin" pfskipfailperm:"yes" hint:"Email address is disabled due to SMTP errors"` From 3672b4fd7b01d1a131da84a6621721dd379e845f Mon Sep 17 00:00:00 2001 From: Jeroen Massar Date: Thu, 16 Feb 2017 12:56:52 +0100 Subject: [PATCH 03/39] Typo fix: Overriden -> Overridden --- ui/login.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/login.go b/ui/login.go index bdf030f..3bd1175 100755 --- a/ui/login.go +++ b/ui/login.go @@ -85,7 +85,7 @@ func h_loginui(cui PfUI, msg string, err error) { pp, err = cui.(*PfUIS).f_uiloginoverride(cui, &p) if err != nil { - cui.Errf("Overriden Login failed: %s", err.Error()) + cui.Errf("Overridden Login failed: %s", err.Error()) H_error(cui, StatusInternalServerError) } } else { From b6fd5d08d5048ab7aefe4292062f634d800e54c1 Mon Sep 17 00:00:00 2001 From: Jeroen Massar Date: Thu, 16 Feb 2017 14:42:05 +0100 Subject: [PATCH 04/39] Typo fix: occured -> occurred --- ui/cmd.go | 4 ++-- ui/ui.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/cmd.go b/ui/cmd.go index 37c5388..b6b02f8 100644 --- a/ui/cmd.go +++ b/ui/cmd.go @@ -17,7 +17,7 @@ func h_api(cui PfUI) { err = cui.Cmd(cui.GetPath()) if err != nil { - cui.OutLn("An error occured: %s", err.Error()) + cui.OutLn("An error occurred: %s", err.Error()) } } @@ -50,7 +50,7 @@ func h_cli(cui PfUI) { } if err != nil { - out += "An error occured: " + out += "An error occurred: " out += err.Error() + "\n" } diff --git a/ui/ui.go b/ui/ui.go index b52d0eb..be0fb09 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -564,7 +564,7 @@ func (cui *PfUIS) NoSubs() bool { // so that it can be called by test functions. // // It returns the ip address as a net.IP and as a string -// along with an error if any occured, which should be rare. +// along with an error if any occurred, which should be rare. // // The incoming xff is sanitized to make it more standardized. // This as some separate by space, others by comma while the From 647ccba47799200098776b24b365e80238a00002 Mon Sep 17 00:00:00 2001 From: Jeroen Massar Date: Tue, 14 Mar 2017 15:02:27 +0100 Subject: [PATCH 05/39] Add iptrk_max, jwt_timeout and loginattempts_max configuration settings, instead of having them hardcoded --- lib/cfg.go | 110 +++++++++++++++++++++++++++++++-------------------- lib/setup.go | 4 +- lib/user.go | 2 +- 3 files changed, 70 insertions(+), 46 deletions(-) diff --git a/lib/cfg.go b/lib/cfg.go index d956c40..70e7b83 100755 --- a/lib/cfg.go +++ b/lib/cfg.go @@ -5,6 +5,7 @@ import ( "bufio" "encoding/json" "errors" + "fmt" "net" "os" "strings" @@ -12,48 +13,51 @@ import ( // PfConfig contains the configuration details for the system, as loaded from the configuration file type PfConfig struct { - Conf_root string `` /* From command line option or default setting */ - File_roots []string `json:"file_roots"` /* Where we look for files */ - Var_root string `json:"var_root"` /* Where variable files are stored */ - Tmp_roots []string `json:"tmp_roots"` /* Templates */ - LogFile string `json:"logfile"` /* Where to write our log file (with logrotate support) */ - Token_prv interface{} `` // Private portion of the JWT Token - Token_pub interface{} `` // Public portion of the JWT Token - UserAgent string `json:"useragent"` // The HTTP and SMTP/Email user agent to use when contacting other servers - CSS []string `json:"css"` // The CSS files to load (HTML meta header) - Javascript []string `json:"javascript"` // The javascript libraries to load (HTML meta header) - CSP string `json:"csp"` // The Content-Security-Protection HTTP header we include in our output - XFF []string `json:"xff_trusted_cidr"` // The CIDR prefixes that are trusted X-Forwarded-For networks - XFFc []*net.IPNet `` // Cached parsed version of X-Forward-For configuration - Db_host string `json:"db_host"` // The database hostname - Db_port string `json:"db_port"` // The database port - Db_name string `json:"db_name"` // The database name - Db_user string `json:"db_user"` // The database user - Db_pass string `json:"db_pass"` // The database password - Db_ssl_mode string `json:"db_ssl_mode"` // The database SSL mode (require|ignore) - Db_admin_db string `json:"db_admin_db"` // The database name used for administrative actions - Db_admin_user string `json:"db_admin_user"` // The database user used for administrative actions - Db_admin_pass string `json:"db_admin_pass"` // The database password used for administrative actions - Nodename string `json:"nodename"` // Name of this node (typically matches the hostname and automatically set by program) - Http_host string `json:"http_host"` // The Host on which we serve HTTP - Http_port string `json:"http_port"` // The port on which we serve HTTP - JWT_prv string `json:"jwt_key_prv"` // Private portion of the JWT Token - JWT_pub string `json:"jwt_key_pub"` // Public portion of the JWT Token - Application interface{} `json:"application"` // Application specific configuration see GetAppConfig() / GetAppConfigBool() - Username_regexp string `json:"username_regexp"` // Regular expression for filtering/rejecting usernames - UserHomeLinks bool `json:"user_home_links"` // If User Home Links are active - SMTP_host string `json:"smtp_host"` // SMTP Host to use for outbound emails - SMTP_port string `json:"smtp_port"` // SMTP Port to use for outbound emails - SMTP_SSL string `json:"smtp_ssl"` // Whether to require SSL for outbound emails (ignore|require) - Msg_mon_from string `json:"msg_monitor_from"` // Email address used for From: for monitoring messages (messages module) - Msg_mon_to string `json:"msg_monitor_to"` // Email address used for To: for monitoring messages (messages module) - TimeFormat string `json:"timeformat"` // Time Format - DateFormat string `json:"dateformat"` // Date Format - PW_WeakDicts []string `json:"pw_weakdicts"` // List of filenames containing password dictionaries - CFG_UserMinLen string `json:"username_min_length"` // Minimum Username length - CFG_UserExample string `json:"username_example"` // Username Example - TransDefault string `json:"translation_default"` // Translation - Default Language - TransLanguages []string `json:"translation_languages"` // Translation - Available Languages + Conf_root string `` /* From command line option or default setting */ + File_roots []string `json:"file_roots"` /* Where we look for files */ + Var_root string `json:"var_root"` /* Where variable files are stored */ + Tmp_roots []string `json:"tmp_roots"` /* Templates */ + LogFile string `json:"logfile"` /* Where to write our log file (with logrotate support) */ + Token_prv interface{} `` // Private portion of the JWT Token + Token_pub interface{} `` // Public portion of the JWT Token + UserAgent string `json:"useragent"` // The HTTP and SMTP/Email user agent to use when contacting other servers + CSS []string `json:"css"` // The CSS files to load (HTML meta header) + Javascript []string `json:"javascript"` // The javascript libraries to load (HTML meta header) + CSP string `json:"csp"` // The Content-Security-Protection HTTP header we include in our output + XFF []string `json:"xff_trusted_cidr"` // The CIDR prefixes that are trusted X-Forwarded-For networks + XFFc []*net.IPNet `` // Cached parsed version of X-Forward-For configuration + Db_host string `json:"db_host"` // The database hostname + Db_port string `json:"db_port"` // The database port + Db_name string `json:"db_name"` // The database name + Db_user string `json:"db_user"` // The database user + Db_pass string `json:"db_pass"` // The database password + Db_ssl_mode string `json:"db_ssl_mode"` // The database SSL mode (require|ignore) + Db_admin_db string `json:"db_admin_db"` // The database name used for administrative actions + Db_admin_user string `json:"db_admin_user"` // The database user used for administrative actions + Db_admin_pass string `json:"db_admin_pass"` // The database password used for administrative actions + Nodename string `json:"nodename"` // Name of this node (typically matches the hostname and automatically set by program) + Http_host string `json:"http_host"` // The Host on which we serve HTTP + Http_port string `json:"http_port"` // The port on which we serve HTTP + JWT_prv string `json:"jwt_key_prv"` // Private portion of the JWT Token + JWT_pub string `json:"jwt_key_pub"` // Public portion of the JWT Token + Application interface{} `json:"application"` // Application specific configuration see GetAppConfig() / GetAppConfigBool() + Username_regexp string `json:"username_regexp"` // Regular expression for filtering/rejecting usernames + UserHomeLinks bool `json:"user_home_links"` // If User Home Links are active + SMTP_host string `json:"smtp_host"` // SMTP Host to use for outbound emails + SMTP_port string `json:"smtp_port"` // SMTP Port to use for outbound emails + SMTP_SSL string `json:"smtp_ssl"` // Whether to require SSL for outbound emails (ignore|require) + Msg_mon_from string `json:"msg_monitor_from"` // Email address used for From: for monitoring messages (messages module) + Msg_mon_to string `json:"msg_monitor_to"` // Email address used for To: for monitoring messages (messages module) + TimeFormat string `json:"timeformat"` // Time Format + DateFormat string `json:"dateformat"` // Date Format + PW_WeakDicts []string `json:"pw_weakdicts"` // List of filenames containing password dictionaries + CFG_UserMinLen string `json:"username_min_length"` // Minimum Username length + CFG_UserExample string `json:"username_example"` // Username Example + TransDefault string `json:"translation_default"` // Translation - Default Language + TransLanguages []string `json:"translation_languages"` // Translation - Available Languages + IPTrkMax int `json:"iptrk_max"` /* Maximum IPTrk count, before being locked out */ + JWTTimeout int `json:"jwt_timeout"` /* JWT Timeout in minutes */ + LoginAttemptsMax int `json:"loginattempts_max"` /* Maximum Login attempts (tracked and checked per-account) */ } /* SMTP_SSL = ignore | require */ @@ -143,6 +147,9 @@ func (cfg *PfConfig) Load(toolname string, confroot string) (err error) { /* Defaults */ Config.Conf_root = confroot Config.UserHomeLinks = true + Config.IPTrkMax = 100 + Config.JWTTimeout = 30 + Config.LoginAttemptsMax = 5 /* Open the configuration file */ fn := Config.Conf_root + toolname + ".conf" @@ -271,7 +278,24 @@ func (cfg *PfConfig) Load(toolname string, confroot string) (err error) { Config.DateFormat = "2006-01-02" } - /* Check that the configuration is sane */ + /* Verify IPtrk count is minimum value */ + if Config.IPTrkMax <= 1 { + err = fmt.Errorf("iptrk_max set to %d but that would lock everybody out after one failed attempt, minimum is 1", Config.IPTrkMax) + return + } + + /* Verify JWT is at least long enough for people to be logged in for a bit */ + if Config.JWTTimeout < 5 { + err = fmt.Errorf("jwt_timeout set to %d which is too short for a useable session", Config.JWTTimeout) + return + } + + if Config.LoginAttemptsMax < 1 { + err = fmt.Errorf("loginattempts_max set to %d which would mean nobody could ever login, please configure it above 1", Config.LoginAttemptsMax) + return + } + + /* Check that XFF are sane & pre-parse it */ for _, x := range Config.XFF { var xc *net.IPNet diff --git a/lib/setup.go b/lib/setup.go index 5b99584..0c9bbcb 100755 --- a/lib/setup.go +++ b/lib/setup.go @@ -49,10 +49,10 @@ func Setup(toolname string, confroot string, verbosedb bool, app_schema_version // Starts starts background services. func Starts() { /* Start IP Tracker -- against brute force login attempts */ - Iptrk_start(5, 10*time.Hour, "1 hour") + Iptrk_start(Config.IPTrkMax, 10*time.Hour, "1 hour") /* Start JWT Invalidation caching/clearing */ - JwtInv_start(30 * time.Minute) + JwtInv_start(time.Duration(Config.JWTTimeout) * time.Minute) } // Stops stops background services, should be matching and thus deferred after a Starts() call. diff --git a/lib/user.go b/lib/user.go index fb52b3d..70107e9 100755 --- a/lib/user.go +++ b/lib/user.go @@ -585,7 +585,7 @@ func (user *PfUserS) CheckAuth(ctx PfCtx, username string, password string, twof return } - if user.LoginAttempts > 5 { + if user.LoginAttempts > Config.LoginAttemptsMax { err = errors.New("Too many login attempts for this account") return } From c28237028a024bd1e6491a8237c899956ed9503e Mon Sep 17 00:00:00 2001 From: Jeroen Massar Date: Wed, 15 Mar 2017 09:38:40 +0100 Subject: [PATCH 06/39] Only belongs in trident, thus moved there --- share/templates/misc/recover.tmpl | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 share/templates/misc/recover.tmpl diff --git a/share/templates/misc/recover.tmpl b/share/templates/misc/recover.tmpl deleted file mode 100644 index 8c82f00..0000000 --- a/share/templates/misc/recover.tmpl +++ /dev/null @@ -1,7 +0,0 @@ -{{template "inc/header.tmpl" .}} - - {{ .Intro }} - - {{ pfform .UI .Recover . true }} - -{{template "inc/footer.tmpl" .}} From 904fc8b461b7bc6a05681d4688a6fc407e2ff144 Mon Sep 17 00:00:00 2001 From: wesdawg Date: Thu, 29 Jun 2017 22:57:57 -0400 Subject: [PATCH 07/39] Fix PGP download link --- share/templates/user/email/edit.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/templates/user/email/edit.tmpl b/share/templates/user/email/edit.tmpl index a37633d..4b903a2 100644 --- a/share/templates/user/email/edit.tmpl +++ b/share/templates/user/email/edit.tmpl @@ -82,7 +82,7 @@ Unverified - +
Key ID:{{ .Email.PgpKeyID }}
Expires: {{ fmt_time .Email.PgpKeyExpire }}
Download:{{ .Email.PgpKeyID }}.asc
Download:{{ .Email.PgpKeyID }}.asc
{{ else }} Not defined From 64c1066a30af690149f1e8455cff3eca43e65dcd Mon Sep 17 00:00:00 2001 From: wesdawg Date: Fri, 1 Sep 2017 23:07:05 -0400 Subject: [PATCH 08/39] Update ui.go Fix typo --- ui/ui.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/ui.go b/ui/ui.go index 8d71e99..91fdbc8 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -1556,11 +1556,11 @@ func (cui *PfUIS) HandleFormS(cmd string, autoop bool, args []string, obj interf if msg != "" { msg += ", " } - msg += strconv.Itoa(nomods) + " fields where not modified" + msg += strconv.Itoa(nomods) + " fields were not modified" } if msg == "" { - msg = "No fields where modified" + msg = "No fields were modified" } return From bf9e9993b3b7f7d1c6fa00ba27f359c948c4648d Mon Sep 17 00:00:00 2001 From: Chris Morrow Date: Tue, 12 Sep 2017 05:03:24 +0000 Subject: [PATCH 09/39] For tick/1961 - add ARF files as possible uploads to the wiki. --- lib/file.go | 1 + 1 file changed, 1 insertion(+) mode change 100755 => 100644 lib/file.go diff --git a/lib/file.go b/lib/file.go old mode 100755 new mode 100644 index e854a7a..37a877a --- a/lib/file.go +++ b/lib/file.go @@ -302,6 +302,7 @@ func file_mimetype(path string) (mt string, err error) { /* Quick lookup of our own to guarantee that these types are supported */ types := map[string]string{ + "arf": "application/octet-stream", "doc": "application/msword", "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "html": "text/html", From d136de646f64cfffc8c27fa4365040cb108180e1 Mon Sep 17 00:00:00 2001 From: Jeroen Massar Date: Wed, 15 Mar 2017 09:38:40 +0100 Subject: [PATCH 10/39] Only belongs in trident, thus moved there --- share/templates/misc/recover.tmpl | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 share/templates/misc/recover.tmpl diff --git a/share/templates/misc/recover.tmpl b/share/templates/misc/recover.tmpl deleted file mode 100644 index 8c82f00..0000000 --- a/share/templates/misc/recover.tmpl +++ /dev/null @@ -1,7 +0,0 @@ -{{template "inc/header.tmpl" .}} - - {{ .Intro }} - - {{ pfform .UI .Recover . true }} - -{{template "inc/footer.tmpl" .}} From 8e93731d9b727f445d596e29eab137577dd5952b Mon Sep 17 00:00:00 2001 From: Ben April Date: Sun, 24 Sep 2017 11:20:08 -0400 Subject: [PATCH 11/39] Bump changelog --- debian/changelog | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/debian/changelog b/debian/changelog index 13938e2..0ac8b0b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,17 @@ +pitchfork (1.9.5) stable; urgency=medium + + * #82 Documentation work. + * #84 Page_Show -> PageShow. + * #88 SQL Cleanup. + * #72 Add dep for pitchfork-data package. + * (PF59) Make iptrk, jet timeout and login attempts config variables. + * (T97) Group Member Vouch overview shows wrong affiliation. + * (T93) Add a note in UI and email about the characters used in the recovery token + * (PF Merge 163) Typo Fix by wesdawg + * (PF Merge 160) Fix PGP download link by wesdawg + + -- Ben April Sun, 24 Sep 2017 00:46:15 -0400 + pitchfork (1.9.4) stable; urgency=medium * (PF65) Update login min length to be a config value along with example username. From 15bf5de342da5877447c298ed5761d2c135d3c41 Mon Sep 17 00:00:00 2001 From: Chris Morrow Date: Tue, 12 Sep 2017 05:03:24 +0000 Subject: [PATCH 12/39] For tick/1961 - add ARF files as possible uploads to the wiki. --- lib/file.go | 1 + 1 file changed, 1 insertion(+) mode change 100755 => 100644 lib/file.go diff --git a/lib/file.go b/lib/file.go old mode 100755 new mode 100644 index a87c739..bda5cfb --- a/lib/file.go +++ b/lib/file.go @@ -320,6 +320,7 @@ func file_mimetype(path string) (mt string, err error) { /* Quick lookup of our own to guarantee that these types are supported */ types := map[string]string{ + "arf": "application/octet-stream", "doc": "application/msword", "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "html": "text/html", From 2cfc3962bd6ce57d5455df604d9d450fe6e9fbae Mon Sep 17 00:00:00 2001 From: Ben April Date: Sun, 24 Sep 2017 13:52:23 -0400 Subject: [PATCH 13/39] Bump changelog --- debian/changelog | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/changelog b/debian/changelog index 0ac8b0b..baf464a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -9,6 +9,7 @@ pitchfork (1.9.5) stable; urgency=medium * (T93) Add a note in UI and email about the characters used in the recovery token * (PF Merge 163) Typo Fix by wesdawg * (PF Merge 160) Fix PGP download link by wesdawg + * Support ARF files. -- Ben April Sun, 24 Sep 2017 00:46:15 -0400 From 2f0bff339c3ac6009b59ca76f3e68daa19a4abc9 Mon Sep 17 00:00:00 2001 From: Jeroen Massar Date: Fri, 10 Feb 2017 06:48:37 +0100 Subject: [PATCH 14/39] Indentation fixups --- lib/template.go | 8 ++++---- share/webroot/css/form.css | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/template.go b/lib/template.go index d686a02..91fdb06 100644 --- a/lib/template.go +++ b/lib/template.go @@ -67,19 +67,19 @@ func tmp_pager_more(cur int, max int) int { /* Variable size pager function. */ func tmp_var_pager_less_ok(page int, cur int) bool { - return cur >= page + return cur >= page } func tmp_var_pager_less(page int, cur int) int { - return cur - page + return cur - page } func tmp_var_pager_more_ok(page int, cur int, max int) bool { - return cur < (max - page) + return cur < (max - page) } func tmp_var_pager_more(page int, cur int, max int) int { - return cur + page + return cur + page } func tmp_group_home_link(ctx PfCtx, groupname string, username string, fullname string) template.HTML { diff --git a/share/webroot/css/form.css b/share/webroot/css/form.css index c9a7804..c5f1a73 100644 --- a/share/webroot/css/form.css +++ b/share/webroot/css/form.css @@ -25,7 +25,7 @@ list-style-type : none; padding : .5em; position : relative; - clear : both; + clear : both; } .styled_form ul li ul @@ -444,7 +444,7 @@ textarea.console input.search, input.search:active, input.search:focus { - background : white url(/gfx/search.png) no-repeat 98% center; + background : white url(/gfx/search.png) no-repeat 98% center; background-size : 2em 2em; } @@ -492,7 +492,7 @@ table form.styled_form input table form.styled_form fieldset ul { - min-width : 200px; + min-width : 200px; } table form.styled_form fieldset ul li From 26aa62984916dbb7e8df4de3db22b3618d906c14 Mon Sep 17 00:00:00 2001 From: Jeroen Massar Date: Tue, 14 Feb 2017 14:41:45 +0100 Subject: [PATCH 15/39] Ensure that the confroot path ends in a slash --- lib/cfg.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/cfg.go b/lib/cfg.go index 33a23e3..04becc3 100755 --- a/lib/cfg.go +++ b/lib/cfg.go @@ -100,12 +100,16 @@ func (cfg *PfConfig) Load(toolname string, confroot string) (err error) { Errf("Could not determine working directory: %s", err.Error()) return } + Dbgf("Running from: %s", wd) if confroot == "" { - confroot = "/etc/" + toolname + "/" + confroot = "/etc/" + toolname } + /* Ensure that the confroot path ends in a slash */ + confroot = URL_EnsureSlash(confroot) + /* Defaults */ Config.Conf_root = confroot Config.UserHomeLinks = true From e17f99016dd7d9bfdb43870dd3a376ee9416c1c8 Mon Sep 17 00:00:00 2001 From: Jeroen Massar Date: Wed, 15 Feb 2017 17:53:26 +0100 Subject: [PATCH 16/39] The big documentation run This includes some minor functionname changes & cleanups --- cmd/cli/cli.go | 114 +++--- doc.go | 368 ++++++++++++++++++ lib/app.go | 15 + lib/cfg.go | 137 +++++-- lib/ctx.go | 540 ++++++++++++++++++-------- lib/db.go | 341 ++++++++++++----- lib/detail.go | 9 + lib/diff.go | 5 + lib/doc.go | 8 + lib/file.go | 97 +++-- lib/file_search.go | 3 + lib/file_test.go | 2 + lib/group.go | 60 ++- lib/groupmember.go | 23 ++ lib/helper_test.go | 1 + lib/iptrk.go | 26 +- lib/iptrk_test.go | 8 + lib/jwt.go | 13 +- lib/jwt_invalid.go | 58 +-- lib/jwt_invalid_test.go | 4 + lib/language.go | 10 +- lib/mail.go | 14 +- lib/main_test.go | 2 + lib/mainmenu.go | 2 + lib/menu.go | 22 +- lib/messages.go | 53 ++- lib/messages_test.go | 3 + lib/misc.go | 268 +++++++++++-- lib/misc_test.go | 62 +++ lib/ml.go | 43 ++- lib/oauth2.go | 6 + lib/pgp/pgp.go | 5 + lib/pw.go | 32 +- lib/pw_dict.go | 18 +- lib/pw_dict_test.go | 3 + lib/search.go | 49 ++- lib/setup.go | 14 +- lib/struct.go | 318 ++++++++++++--- lib/struct_test.go | 61 +++ lib/system.go | 49 ++- lib/template.go | 150 +++++--- lib/template_test.go | 54 +++ lib/translate.go | 2 + lib/user.go | 122 ++++-- lib/user_2fa.go | 99 ++++- lib/user_detail.go | 18 +- lib/user_email.go | 89 ++++- lib/user_events.go | 29 ++ lib/user_language.go | 20 +- lib/wiki.go | 33 ++ lib/wiki_export.go | 13 +- lib/wiki_import.go | 19 +- lib/wiki_rnd.go | 39 +- lib/wiki_search.go | 5 + ui/cmd.go | 4 +- ui/doc.go | 8 + ui/error.go | 20 +- ui/file.go | 11 + ui/form.go | 131 ++++++- ui/form_csrf.go | 58 +-- ui/group.go | 16 + ui/iptrk.go | 1 + ui/link.go | 16 +- ui/log.go | 40 +- ui/login.go | 5 + ui/logout.go | 1 + ui/main_test.go | 3 + ui/menu.go | 35 +- ui/messages.go | 6 + ui/ml.go | 9 + ui/oauth2.go | 20 +- ui/qr.go | 2 +- ui/root.go | 8 +- ui/root_test.go | 1 + ui/search.go | 1 + ui/setup.go | 19 +- ui/static.go | 9 +- ui/system.go | 6 + ui/ui.go | 681 +++++++++++++++++++++++++++------ ui/ui_test.go | 33 ++ ui/ui_xff_test.go | 10 +- ui/urltest/closablerecorder.go | 3 + ui/urltest/urltest.go | 2 +- ui/user.go | 19 + ui/user_2fa.go | 24 +- ui/user_email.go | 13 + ui/wellknown.go | 2 + ui/wiki.go | 15 + 88 files changed, 3879 insertions(+), 911 deletions(-) create mode 100644 doc.go create mode 100644 lib/doc.go create mode 100644 lib/struct_test.go create mode 100644 lib/template_test.go create mode 100644 ui/doc.go create mode 100644 ui/ui_test.go diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index bf92616..2da751c 100755 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -1,35 +1,31 @@ -/* - * Trident Pitchfork CLI - Tickly (tcli) - * - * This is effectively a HTTP client for Pitchfork's daemon. - * All requests are sent over HTTP, there is no access directly to anything. - * - * This client also serves as an example on how to talk to the Trident API. - * - * tcli stores a token in ~/. for retaining the logged-in state. - * - * Custom environment variables: - * - Select a custom token file with: - * ${env_token}=/other/path/to/tokenfile - * This is useful if you want to have multiple identities - * or want to keep a token around that has the sysadmin bit set - * - * - Enable verbosity with: - * ${env_verbose}= - * - * - Disable verbosity with - * ${env_verbose}=off - * or unset the environment variable - */ +// Trident Pitchfork CLI - Tickly (tcli) +// +// This is effectively a HTTP client for Pitchfork's daemon. +// All requests are sent over HTTP, there is no access directly to anything. +// +// This client also serves as an example on how to talk to the Trident API. +// +// tcli stores a token in ~/. for retaining the logged-in state. +// +// Custom environment variables: +// - Select a custom token file with: +// ${env_token}=/other/path/to/tokenfile +// This is useful if you want to have multiple identities +// or want to keep a token around that has the sysadmin bit set +// +// - Enable verbosity with: +// ${env_verbose}= +// +// - Disable verbosity with +// ${env_verbose}=off +// or unset the environment variable package pf_cmd_cli import ( - "errors" "flag" "fmt" "io/ioutil" - "net/http" "os" "os/user" @@ -37,24 +33,35 @@ import ( cc "trident.li/pitchfork/cmd/cli/cmd" ) -var g_isverbose = false +// Whether verbosity is enabled +var verbosity = false -func terr(str ...interface{}) { - fmt.Print("--> ") - fmt.Println(str...) -} - -func verb(str ...interface{}) { - if g_isverbose { +// outputVerbose is used to print out verbose messages +func outputVerbose(str ...interface{}) { + if verbosity { fmt.Print("~~~ ") fmt.Println(str...) } } +// output is used to print out actual output. +// +// We wrap the fmt.Print function thus allowing +// us easier to find where output() is happening +// and possibly to later implement redirection of +// the output to other output channels or prefix +// extra details to the output, eg a timestamp. func output(str ...interface{}) { fmt.Print(str...) } +// output_err is used for printing errors +func output_err(str ...interface{}) { + fmt.Print("--> ") + fmt.Println(str...) +} + +// token_read is used to read a token from a file into a string func token_read(filename string) (token string) { tokenb, err := ioutil.ReadFile(filename) if err != nil { @@ -64,18 +71,27 @@ func token_read(filename string) (token string) { return string(tokenb) } +// token_store is used to store a token string into a file func token_store(filename string, token string) { err := ioutil.WriteFile(filename, []byte(token), 0600) if err != nil { - terr("Error while storing token in " + filename + ": " + err.Error()) + output_err("Error while storing token in " + filename + ": " + err.Error()) } } -func http_redir(req *http.Request, via []*http.Request) error { - terr("Redirected connection, this should never happen!") - return errors.New("I don't want to be redirected!") -} - +// CLI is the big call that applications call to implement a CLI towards pitchfork +// +// It configures verbosity of the output functions. +// Tries to locate and load an existing stored cookie. +// Determines the location of the daemon's HTTP interface. +// And finally used CLICmd() to execute the command. +// +// Args are the arguments coming from the shell. +// token_name is the name of the token when send to the HTTP server. +// env_token is the name of the environment variable where a token can be found. +// env_verbose is the name of the environment variable that indicates the verbosity level. +// env_server is the name of the environment variable that indicates the location of our HTTP server. +// default_server is the URL of the default HTTP server. func CLI(token_name string, env_token string, env_verbose string, env_server string, default_server string) { var tokenfile string var server string @@ -90,8 +106,10 @@ func CLI(token_name string, env_token string, env_verbose string, env_server str /* Determine verbosity -- based on environment or flag */ verb_env := os.Getenv(env_verbose) - if verb_env == "on" || verbose { - g_isverbose = true + if (verb_env != "" && verb_env != "off") || verbose { + verbosity = true + } else { + verbosity = false } /* @@ -115,7 +133,7 @@ func CLI(token_name string, env_token string, env_verbose string, env_server str token := token_read(tokenfile) if token != "" { - verb("Read existing token") + outputVerbose("Read existing token") } /* Use the server from the flag? */ @@ -140,13 +158,13 @@ func CLI(token_name string, env_token string, env_verbose string, env_server str for readarg > 0 { fd := int(os.Stdin.Fd()) if !terminal.IsTerminal(fd) { - terr("Terminal is not a TTY") + output_err("Terminal is not a TTY") os.Exit(1) } else { fmt.Print("Hidden argument: ") txt, err := terminal.ReadPassword(fd) if err != nil { - terr("Could not read argument: " + err.Error()) + output_err("Could not read argument: " + err.Error()) os.Exit(1) } @@ -159,7 +177,7 @@ func CLI(token_name string, env_token string, env_verbose string, env_server str newtoken, rc, err := cc.CLICmd(args, token, server, verb, output) if err != nil { - terr("Error: " + err.Error()) + output_err("Error: " + err.Error()) /* Set a non-0 exit code when something failed */ if rc == 0 { @@ -168,14 +186,14 @@ func CLI(token_name string, env_token string, env_verbose string, env_server str } else { /* Unauthorized? Then kill the token */ if newtoken == "" && token != "" { - verb("Unauthorized, destroy old token") + outputVerbose("Unauthorized, destroy old token") os.Remove(tokenfile) } else if newtoken != "" && newtoken != token { - verb("Storing new token") + outputVerbose("Storing new token") token_store(tokenfile, newtoken) } - verb("Done") + outputVerbose("Done") } os.Exit(rc) diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..1beb860 --- /dev/null +++ b/doc.go @@ -0,0 +1,368 @@ +/* + +Package pitchfork is a Golang framework for secure communication platforms. + +Typically one will include the lib/ (pitchfork) and ui/ (pitchforkui) subpackages. + +Website: https://trident.li/ + +License: Apache 2.0 (See LICENSE file) + +# Pitchfork Applications + +Pitchfork Applications serve a way to expose Pitchfork functionality as an application. +These applications are located in the 'cmd' directory. + +Applications that use Pitchfork typically implement their own wrappers around the CLI, Setup and Serve calls in cmd/cli, cmd/setup, and cmd/serve respectively. + +These application specific version can then pass in custom callbacks to be called etc. + +## Server (the daemon) + +The Server (cmd/server/server.go) code has a large Serve() call which loads the +configuration file (as passed or as a environment variable as defined by the application) +sets up the HTTP web server and then starts serving requests. +See cmd/server/server.go for more details. + +This Serve() calls gets called from the 'server'/'daemon' utility of the application. + +Typically the daemon will be exposed behind a Nginx HTTP proxy that only serves content using HTTPS. + +## CLI utility + +CLI (cmd/cli/cli.go) implements a CLI tool for the application that allows shell based +CLI access to the daemon. + +The CLI call effectively parses the arguments and optional environment variables and uses +a, typically loopback-based, HTTP request to send the command to the daemon's HTTP server. + +The request comes in as a HTTP request in the daemon (H_root() as mentioned above), but as +it is targeted at the /cli/ URL, gets handled by the h_cli() function in ui/cmd.go. +This function in turn uses the CmdOut function of the context to execute and then return +the command in a buffer, outputting it back to the HTTP caller and thus the cli tool. + +The 'system batch' command can be used to run batch scripts. + +## Setup utility + +The setup utility (cmd/setup/setup.go) does direct database access, thus bypassing most permissions. On a normal system it is restricted to be run only by the 'root' user as the configuration +file containing the database credentials can only be accessed by users in the applications group. + +The setup utility allows setting up the database, upgrading the schemas when there is a new one +but also to add an initial or new user and changing passwords of users without knowing the +original password (as it does direct database access). + +The setup utility also exposes a sudo command that can be used to run CLI commands as any given user. + +## Wikiexport utility + +The wikiexport utility can be used to export a FosWiki wiki, archiving it in a single zipfile. +That zip file can then be transported to another host, where it can be imported into the Pitchfork Wiki system. + +# Configuration file + +The configuration file loaded by the server/daemon and the setup tool is a JSON file +but with comments, lines starting with a hash ('#') interleaved for clarification. + +See lib/cfg.go for more details and the contents of the configuration file. + +# Details + +## File structure and Function naming + +File names are prefixed depending on their location in the (menu) system. + +Function names are prefixed depending on their location in the codebase. + +For example, functions related to a User are located in {lib|ui}/user.go. +The CLI entry points and menus and background functions are named user_*(), while the UI equivalents are h_user_*(). + +CLI functions are marked with '(CLI)' in the comment above the function to indicate that it is a entry point directly from a menu. + +The h_ or H_ prefix used for UI functions indicates that it is called from the HTTP handler. +UI helper functions, those not called directly from UI menu are not prefixed with the h_ prefix as they are not handlers. + +There are a few deviations from this naming scheme, but these are primarily functions in the lib/misc.go file and as that indicates contains a miscellaneous set of functions. + +A function ending in an 'f' is typically a printf-style 'formatted' function that accepts a +format along with a variable amount of arguments. + +A function ending in an 'A' is typically a 'Advanced' function or a recursive function: it gets called from the function without the A but provides more arguments that are not always commonly needed. + +### Shared resources + +The share/ directory contains shared resources that pitchfork can use. + +The file_roots configuration option allows specifying one or more of these file_roots. +When trying to open a file each file_root is searched in order and the first file found +will be used. + +This allows one to override a file by placing the edition that needs to override it +in the earliest file_root. + +An application will thus typically put it's own file_root first in the configuration +and order the pitchfork share directory last. + +#### dbschemas + +share/dbschemas/ contains the database schemas, primarily used during setup and upgrade. + +Each schema is versioned and allows upgrading from that version to the next edition. +The 'portal_schema_version' key in the 'schema_metadata' table keeps track which current +version of the schema is applied. + +The DB_*.psql files contain the System (Pitchfork) database schemas. + +The APP_*.psql files, which are located in the application's share/dbschemas/ directory contain the Application databas schema. + +test_data.psql contains testing data, that developers can use for testing. + +#### pwdicts + +The share/pwdicts directory contains password dictionaries. + +These password dictionaries are used to detect weak passwords and reject those passwords from being used. + +The files (ending in .txt), contain a password per line. Lines starting with a hash ('#') are treated as comments and ignored when the password dictionaries are loaded into Pitchfork. + +#### rendered + +The share/rendered directly is analogous to the webroot directory, except that it provides the resources used for wiki_export. + +It contains the static files needed for visualizing a rendered page. + +The complete directory is recursively copied into the target directory specified as the target for wiki_export. + +The Pitchfork share/rendered directory is empty as otherwise those files would be copied too into what is an application defined set of files. + +#### setup + +The share/setup directory contains CLI batch files with file extension ```.cli```. These can be used to configure the system by executing the contained Pitchfork CLI commands. + +The batch files are plain text files with per each line a CLI command. + +Each setup filename is in the format of APP_setup_???.cli where the ??? indicates a version number. + +The current version of the application setup version is tracked in the database table schema_metadata under the app_setup_version key. +It can be controlled with the ```system appsetup set``` command and retrieved with the ```system appsetup get``` command. + +The CLI command ```system appsetup upgrade``` will only run the batch files that have not been run previously yet thus allowing a system to upgrade itself to the latest edition it is aware of. + +The setup directory might also contain relevant files for performing a setup. For example Markdown (.md) files can be stored in the directory for puproses of using them during setup. One can then run the following command: + +``` +group wiki system updatef /PageName "Setup added" APP_setup_x.PageName.md +``` + +This will update from the file named APP_setup_x.PageName.md the /PageName path of the wiki of the system group, logging that it was "Setup added". + +The ```system appsetup upgrade``` command changes the internal 'current working directory' (see man getcwd(3)) to the setup files directory so that files without a full path are loaded from there. + +#### templates + +The share/templates directory contains Golang Templates (https://golang.org/pkg/text/template/) +which are typically used by the UI system of Pitchfork. + +All templates are loaded at the start of the application. + +The structure of the share/templates + +#### webroot + +The share/webroot contains files to be served using the static file serving ability of Pitchfork. + +The subdirectories are 'css' for CSS files, 'gfx' for images, and 'js' for Javascript files. + +The favicon.ico is also located here, as it can be statically served. +robots-ok.txt and robots.txt are served depending on the Robots setting in the system preferences, the first is served when indexing by robots is allowed, the second is served when robots are denied. + +The files in these directories are statically served. Any file placed in them is thus directly accessible and are not checked for permissions. These directories do not generate a listing though, thus getting an index from them is not possible. + +## Context + +The context (ctx on the lib/cli level, cui on the UI level), retains per-request details that various parts of the code might need, eg for knowing if and which user is logged in and what groups they belong to. + +It also enables overriding of creation of NewUser, NewGroup and Menus allowing applications to extend those objects with more properties without modifying the Pitchfork code base. + +### Selecting Users, Groups, Mailinglist, Email and other objects + +Throughout the code various functions affect a user, a group or other objects. + +These objects can be selected for active comparison using the SelectUser, SelectGroup, SelectML, SelectEmail functions in context. + +The selection functions require as an argument a set of permissions that have to be fulfilled by the active user for being able to select that object. + +When the object is selected it can be retrieved with an equivalent Selected* function at which point it can be further used for processing. + +The major usecase of the Select is that one part of the code can select for instance a user, and another part will automatically be able to use that user for it's purposes, thus avoiding the need to pass it around as a spar parameter, which would not otherwise easily/nicely be possible with the menu functions. + +One major use for the selected objects is permission tests. + +# Permissions + +The context retains the details (username, groups, etc) about the logged in user. + +Based on those, and other, properties retained in Ctx, permission decisions can be made. + +The ctx's CheckPerms() (lib/ctx.go) function can be used for checking permissions and describes all the permissions that are available. + +Per example, a commonly used permission is simply 'user' + +The permissions are used throughout Pitchfork. + +In menu's, both CLI and UI they are used to determine if a given entry is available for the selected user in the context. + +In structures, handled by struct.go, a pfset and/or pfget tag can be set to indicate the permissions for that specific field. If a context does not have access to a field, that field will not be visible to that user. + +For instance: +``` +type Example struct { + Field string `label:"Field" pfget:"user" pfget:"sysadmin"` +} +``` +Would mean that a user can retrieve the field, but it requires a sysadmin privilege to set it. + +The core permission code is located in lib/ctx.go with CheckPerm and lib/struct.go for anything that tests structure related permissions. + +Permissions in pfget/pfset tag can be specified separated by commas to specify multiple permissions that would be acceptible to satisfy the permission check. +Perm's FromString function in lib/ctx handles this conversion from textual edition of a permission to the binary Perm that is used throughout. + +## Sysadmin Privilege + +The sysadmin privilege is gained by having the sysadmin flag set in the user's table. This can be toggled using the CLI by executing 'user set sysadmin true|false' or using the user configuration UI. Of course it requires sysadmin privileges to toggle. + +User's posessing the sysadmin bit are not directly a sysadmin when they login. +They first have to call swapadmin to swap from normal user to a sysadmin. + +This allows a user account to have sysadmin privileges but from login to act like a normal user, till it is chosen to elevate these priveleges. + +## Permission Debugging / Tracing + +The functions PDbg and PDbgf contain a code level debug switch that can be flipped from false to true to enable output of the decisions being made regarding permissions in the ctx.CheckPerms() call. + +When all Pitchfork permissions have been checked CheckPerms() calls the Application Permissions check, if defined. + +# Menu structures + +Access to menu entries is protected by the Pitchfork permission system. If none of the permissions is satisfied, then the menu is not visible +or accessible for the given caller. PERM_HIDDEN is used to indicate that a menu entry is not visible in CLI help output or in the menu structures +in the webinterface. + +The menu override functions can use AddPerms/DelPerms/Remove functions of the menu objects to change permissions, remove functions or replace functions. + +The special pf.PERM_NOSUBS permission notes that only the bare URL (eg: /login/) is accepted for that menu path, any arguments (eg '/login/subpath') results in a 404. +This is primarily a defense against crawlers that attempt to try all possible combinatations of paths, while we have no unique paths under that URL. +Especially useful for a crawler that mistakingly tries to keep on recursing, eg '/login/login/login/...'). + +## CLI + +For the CLI the root of the menu is in pitchfork:lib/mainmenu.go as defined in MainMenu. Functions can be followed from there +to find out which CLI command maps to which function. Alternatively, typically the function name will map directly to a filename. +Eg the 'system' command is to be found in pitchfork:lib/system.go. + +## UI + +For the UI the root of the menu is in H_root (pitchfork:ui/root.go) which matches the HTTP root (/) and is where Go net/http's sends the request the first time into Pitchfork. + +The Main menu is used for URL processing and finding which function should be called for it. The submenu is only used for visual appearance. + +# Adding new functionality / writing applications + +Pitchfork is a CLI centric application: the actual functionality typically resides in the CLI. This is done so that the CLI gets first attention and also to make permissions to actual actions all in a central location: in the CLI. + +To add new functionality it is typically advised to start with the CLI function. One can then call the CLI function from the UI. + +This way the task can be performed through both the UI and the CLI. +Testing most portions of the UI can then also be automated by calling the CLI functions which perform the same function, minus the HTML rendering and HTTP Form parsing, which Go handles. + +Any accidental mistakes at the UI level regarding permissions is irrelevant as the CLI is the real gatekeeper to an action to be performed. + +# Translation Support + +Translation is fully supported by pfform and friends, though not by the CLI menu system. + +Every label from an object passed to pfform() is checked for a Translate() function and when that function is +present, it is called along with the label and the target language. The resulting string is used then as the label or hint for the given string. + +# IP Tracking (IPTrk) + +To avoid repeated login attempts in attempt to guess a username/password combination, the system has an +IP Tracking module (pitchfork:src/iptrk.go) that tracks the amount of failures per IP address. + +User's thus can be locked out when they attempt to login too many times. + +Hence, do check 'iptrk list' for the user's IP address when they are unable to login. +'iptrk remove can be used to remove an individual IP address and 'iptrk flush' can be used to flush the table. +The UI equivalent management interface can be found under the System menu (/system/iptrk/). + +# ModOpts + +The Pitchfork system has three modules that have Module Options (ModOpts): messages (lib/messages.go), file (lib/file.go) and wiki (lib/wiki.go). + +These module options configure the module so that it prefixes a path to the data it stores, thus allowing it to be used in multiple locations. +It also configures the URLPath so that it can be exposed in the CLI and UI interfaces in multiple places. + +# Testing + +We include tests using the go [testing](https://golang.org/pkg/testing/) framework. + +### Running Tests + +Running tests requires that two environment variables are set: +``` +export PITCHFORK_TOOLNAME=trident +export PITCHFORK_CONFROOT=/Users/jeroen/git/trident/tconf/ +``` +These specify the name of the tool (and thus the name of the configfile) +and the directory where the config file and related files are loaded from. + +One can then run the tests with: +``` +make tests +``` +or verbosely: +``` +make vtests +``` + +or manually with: +``` +go test trident.li/pitchfork/lib -v +go test trident.li/pitchfork/ui -v +``` +One can also run tests individually by specifying a filter, eg: +``` +go test trident.li/pitchfork/lib -v -run IPtrk +``` +which would run only the iptrk related tests. + +The argument to ```-run``` is a regexp, ```AB[CD]``` would for instance match functions named +```Test_ABC``` + ```Test_ABD```. + +See also the top of the *._test.go files for the simple cut&paste variants. + +## CLI Tests + +Either make mini tests for the exact functions. +Or Call pf.Cmd() passing the various that need to be done. +Error or return body can then be checked. + +## UI Tests + +Pitchfork's URLTest module (```ui/urltest/```) contains the URL_Test() function that +accepts a URLTest structure that allows passing in various variables that act as the +request or as the response checks. One can do positive and negative checks with it. + +Passing a set Username in the test causes a cookie to be created for that user and thus +it automatically looks like one is logged in as that user. + +# Common abbreviations + +CLI = Command Line Interface +Ctx = Context +Cui = Context for User Interface +ModOpts = Pitchfork Module Options +UI = User Interface + +*/ +package pitchfork diff --git a/lib/app.go b/lib/app.go index 130fd8c..6677a4c 100644 --- a/lib/app.go +++ b/lib/app.go @@ -1,10 +1,23 @@ +// Pitchfork app (Application) specific configuration package pitchfork +// AppName is the default Application Name var AppName = "Pitchfork" + +// AppVersion is the default application version (typically set by compiler options) var AppVersion = "unconfigured" + +// AppCopyright details the copyright details of the application +// Set generally by the SetAppDetails call. var AppCopyright = "" + +// AppWebsite is used for indicating the home location of the website +// Set generally by the SetAppDetails call. var AppWebsite = "" +// SetAppDetails configures application details. +// +// The server and setup utility call this to configure these values. func SetAppDetails(name string, ver string, copyright string, website string) { AppName = name AppVersion = ver @@ -12,10 +25,12 @@ func SetAppDetails(name string, ver string, copyright string, website string) { AppWebsite = website } +// AppVersionStr returns the Applicatication's version string. func AppVersionStr() string { return AppVersion } +// VersionText returns the Applications version text including copyright details. func VersionText() string { t := AppName + "\n" + "Version: " + AppVersion + "\n" diff --git a/lib/cfg.go b/lib/cfg.go index 04becc3..d956c40 100755 --- a/lib/cfg.go +++ b/lib/cfg.go @@ -1,3 +1,4 @@ +// Pitchfork cfg is used for all configuration elements loaded from the .conf file package pitchfork import ( @@ -9,55 +10,62 @@ import ( "strings" ) +// PfConfig contains the configuration details for the system, as loaded from the configuration file type PfConfig struct { - Conf_root string `` /* From command line option or default setting */ - File_roots []string `json:"file_roots"` /* Where we look for files */ - Var_root string `json:"var_root"` /* Where variable files are stored */ - Tmp_roots []string `json:"tmp_roots"` /* Templates */ - LogFile string `json:"logfile"` /* Where to write our log file (with logrotate support) */ - Token_prv interface{} `` - Token_pub interface{} `` - UserAgent string `json:"useragent"` - CSS []string `json:"css"` - Javascript []string `json:"javascript"` - CSP string `json:"csp"` - XFF []string `json:"xff_trusted_cidr"` - XFFc []*net.IPNet `` - Db_host string `json:"db_host"` - Db_port string `json:"db_port"` - Db_name string `json:"db_name"` - Db_user string `json:"db_user"` - Db_pass string `json:"db_pass"` - Db_ssl_mode string `json:"db_ssl_mode"` - Db_admin_db string `json:"db_admin_db"` - Db_admin_user string `json:"db_admin_user"` - Db_admin_pass string `json:"db_admin_pass"` - Nodename string `json:"nodename"` - Http_host string `json:"http_host"` - Http_port string `json:"http_port"` - JWT_prv string `json:"jwt_key_prv"` - JWT_pub string `json:"jwt_key_pub"` - Application interface{} `json:"application"` - Username_regexp string `json:"username_regexp"` - UserHomeLinks bool `json:"user_home_links"` - SMTP_host string `json:"smtp_host"` - SMTP_port string `json:"smtp_port"` - SMTP_SSL string `json:"smtp_ssl"` - Msg_mon_from string `json:"msg_monitor_from"` - Msg_mon_to string `json:"msg_monitor_to"` - TimeFormat string `json:"timeformat"` - DateFormat string `json:"dateformat"` - PW_WeakDicts []string `json:"pw_weakdicts"` - CFG_UserMinLen string `json:"username_min_length"` - CFG_UserExample string `json:"username_example"` - TransDefault string `json:"translation_default"` - TransLanguages []string `json:"translation_languages"` + Conf_root string `` /* From command line option or default setting */ + File_roots []string `json:"file_roots"` /* Where we look for files */ + Var_root string `json:"var_root"` /* Where variable files are stored */ + Tmp_roots []string `json:"tmp_roots"` /* Templates */ + LogFile string `json:"logfile"` /* Where to write our log file (with logrotate support) */ + Token_prv interface{} `` // Private portion of the JWT Token + Token_pub interface{} `` // Public portion of the JWT Token + UserAgent string `json:"useragent"` // The HTTP and SMTP/Email user agent to use when contacting other servers + CSS []string `json:"css"` // The CSS files to load (HTML meta header) + Javascript []string `json:"javascript"` // The javascript libraries to load (HTML meta header) + CSP string `json:"csp"` // The Content-Security-Protection HTTP header we include in our output + XFF []string `json:"xff_trusted_cidr"` // The CIDR prefixes that are trusted X-Forwarded-For networks + XFFc []*net.IPNet `` // Cached parsed version of X-Forward-For configuration + Db_host string `json:"db_host"` // The database hostname + Db_port string `json:"db_port"` // The database port + Db_name string `json:"db_name"` // The database name + Db_user string `json:"db_user"` // The database user + Db_pass string `json:"db_pass"` // The database password + Db_ssl_mode string `json:"db_ssl_mode"` // The database SSL mode (require|ignore) + Db_admin_db string `json:"db_admin_db"` // The database name used for administrative actions + Db_admin_user string `json:"db_admin_user"` // The database user used for administrative actions + Db_admin_pass string `json:"db_admin_pass"` // The database password used for administrative actions + Nodename string `json:"nodename"` // Name of this node (typically matches the hostname and automatically set by program) + Http_host string `json:"http_host"` // The Host on which we serve HTTP + Http_port string `json:"http_port"` // The port on which we serve HTTP + JWT_prv string `json:"jwt_key_prv"` // Private portion of the JWT Token + JWT_pub string `json:"jwt_key_pub"` // Public portion of the JWT Token + Application interface{} `json:"application"` // Application specific configuration see GetAppConfig() / GetAppConfigBool() + Username_regexp string `json:"username_regexp"` // Regular expression for filtering/rejecting usernames + UserHomeLinks bool `json:"user_home_links"` // If User Home Links are active + SMTP_host string `json:"smtp_host"` // SMTP Host to use for outbound emails + SMTP_port string `json:"smtp_port"` // SMTP Port to use for outbound emails + SMTP_SSL string `json:"smtp_ssl"` // Whether to require SSL for outbound emails (ignore|require) + Msg_mon_from string `json:"msg_monitor_from"` // Email address used for From: for monitoring messages (messages module) + Msg_mon_to string `json:"msg_monitor_to"` // Email address used for To: for monitoring messages (messages module) + TimeFormat string `json:"timeformat"` // Time Format + DateFormat string `json:"dateformat"` // Date Format + PW_WeakDicts []string `json:"pw_weakdicts"` // List of filenames containing password dictionaries + CFG_UserMinLen string `json:"username_min_length"` // Minimum Username length + CFG_UserExample string `json:"username_example"` // Username Example + TransDefault string `json:"translation_default"` // Translation - Default Language + TransLanguages []string `json:"translation_languages"` // Translation - Available Languages } /* SMTP_SSL = ignore | require */ var Config PfConfig +// GetAppConfig gets an application configuration variable (string). +// +// Application configuration values are stored in the 'application' section +// of the application's configuration. +// +// returns a string. func (cfg *PfConfig) GetAppConfig(varname string) (out string) { out = "" @@ -76,6 +84,12 @@ func (cfg *PfConfig) GetAppConfig(varname string) (out string) { return } +// GetAppConfig gets an application configuration variable (boolean). +// +// Application configuration values are stored in the 'application' section +// of the application's configuration. +// +// returns a boolean. func (cfg *PfConfig) GetAppConfigBool(varname string) (out bool) { out = false @@ -94,6 +108,22 @@ func (cfg *PfConfig) GetAppConfigBool(varname string) (out bool) { return } +// Load loads the application configuration for the given +// toolname and from the optionally provided configroot. +// +// The configuration file is a JSON file, interjected with +// comment lines indicated as such as they start with a has ('#') symbol. +// +// Before running the file through the JSON parser we strip these +// comment lines, thus allowing it to be parsed. +// +// The PfConfig structure contains all possible entries. +// +// See doc/conf/example.conf for an example configuration file. +// +// The variables in the 'application' section do not have direct +// accessors in PfConfig, but can be retrieved using the GetAppConfig() +// and GetAppConfigBool() functions. func (cfg *PfConfig) Load(toolname string, confroot string) (err error) { wd, err := os.Getwd() if err != nil { @@ -184,48 +214,59 @@ func (cfg *PfConfig) Load(toolname string, confroot string) (err error) { return } + // Default to the toolname if Config.Db_name == "" { Config.Db_name = toolname } + // Default to the toolname if Config.Db_user == "" { Config.Db_user = toolname } + // The default name for the JWT private key file if Config.JWT_prv == "" { Config.JWT_prv = "jwt.prv" } + // The default name for the JWT public key file if Config.JWT_pub == "" { Config.JWT_pub = "jwt.pub" } + // Minimal CSS configuration if none configured if len(Config.CSS) == 0 { Config.CSS = []string{"style", "form"} } + // Minimal CSP configuration if none configured if Config.CSP == "" { Config.CSP = "default-src 'self'; img-src 'self' data:" } + // Default the regular expression for usernames if Config.Username_regexp == "" { Config.Username_regexp = "^[a-z][a-z0-9]*$" } + // Ensure that SMTP parameters are configured if Config.SMTP_host == "" || Config.SMTP_port == "" || Config.SMTP_SSL == "" { err = errors.New("Please configure the SMTP parameters (smtp_host, smtp_port, smtp_ssl)") return } + // Make sure the SMTP_SSL option is either require or ignore if Config.SMTP_SSL != "require" && Config.SMTP_SSL != "ignore" { err = errors.New("Configuration variable 'smtp_ssl' is not set to 'require' or 'ignore' but '" + Config.SMTP_SSL + "'") return } + // Default Time format (yyyy-mm-dd HH:MM) if Config.TimeFormat == "" { Config.TimeFormat = "2006-01-02 15:04" } + // Default Date format (yyyy-mm-dd) if Config.DateFormat == "" { Config.DateFormat = "2006-01-02" } @@ -244,15 +285,27 @@ func (cfg *PfConfig) Load(toolname string, confroot string) (err error) { Config.XFFc = append(Config.XFFc, xc) } + // Load the private key for JWT Tokens err = cfg.Token_LoadPrv() if err != nil { return } + // Load the public key for JWT Tokens err = cfg.Token_LoadPub() if err != nil { return } + // Ensure that a default language is configured + if cfg.TransDefault == "" { + cfg.TransDefault = "en-US" + } + + // Ensure that the Translanguages setting has content too + if len(cfg.TransLanguages) == 0 { + cfg.TransLanguages = []string{"en-US.json"} + } + return } diff --git a/lib/ctx.go b/lib/ctx.go index deab68a..9a6fb2c 100755 --- a/lib/ctx.go +++ b/lib/ctx.go @@ -1,3 +1,4 @@ +// Pitchfork ctx defines the context that is passed through Pitchfork pertaining primarily to the logged in, selected user/group package pitchfork import ( @@ -12,41 +13,47 @@ import ( i18n "github.com/nicksnyder/go-i18n/i18n" ) +// ErrLoginIncorrect is used when a login is incorrect, this to hide more specific reasons var ErrLoginIncorrect = errors.New("Login incorrect") +// PfNewUserI, NewGroupI, PfMenuI, PfAppPermsI, PfPostBecomeI are function definitions to allow overriding of these functions by application code type PfNewUserI func() (user PfUser) type PfNewGroupI func() (user PfGroup) type PfMenuI func(ctx PfCtx, menu *PfMenu) type PfAppPermsI func(ctx PfCtx, what string, perms Perm) (final bool, ok bool, err error) type PfPostBecomeI func(ctx PfCtx) -type PfCreds struct { - sel_user PfUser /* Selected User */ - sel_group PfGroup /* Selected Group */ -} - +// PfModOptsI is the interface that is implemented by PfModOptsS allowing the latter to be extended with more details type PfModOptsI interface { IsModOpts() bool } +// PfModOptsS is the base structure used to impleent PfModOptsI type PfModOptsS struct { - /* CLI command prefix, eg 'group wiki' */ + // CLI command prefix, eg 'group wiki' Cmdpfx string - /* URL prefix, typically System_Get().PublicURL() */ + // URL prefix, typically System_Get().PublicURL() URLpfx string - /* Path Root */ + // Path Root Pathroot string - /* URL root, inside the hostname, eg '/group/name/wiki/' */ + // URL root, inside the hostname, eg '/group/name/wiki/' URLroot string } +// IsModOpts is a simple fakeish function to cause PfModOptsS to be of type PfModOptsI +// as it requires this function to be present, which other structures will not satisfy. func (m PfModOptsS) IsModOpts() bool { return true } +// PfModOpts can be used to easily initialize a PfModOptsS. +// +// The arguments match the variables in the PfModOpts structure. +// +// The function ensures that the web_root ends in a slash ('/'). func PfModOpts(ctx PfCtx, cmdpfx string, path_root string, web_root string) PfModOptsS { urlpfx := System_Get().PublicURL @@ -55,12 +62,16 @@ func PfModOpts(ctx PfCtx, cmdpfx string, path_root string, web_root string) PfMo return PfModOptsS{cmdpfx, urlpfx, path_root, web_root} } -/* Context Interface, allowing it to be extended */ +// PfCtx is the Context Interface. +// +// PfCtxS is the default implementation. +// +// This interface is primarily intended to allow extension by an application. + +// See the individual functions in PfCtxS for per function details. type PfCtx interface { GetAbort() <-chan bool SetAbort(abort <-chan bool) - StoreCreds() (creds PfCreds) - RestoreCreds(creds PfCreds) SetTx(tx *Tx) GetTx() (tx *Tx) Err(message string) @@ -92,9 +103,6 @@ type PfCtx interface { CanBeSysAdmin() bool SwapSysAdmin() bool IsSysAdmin() bool - ConvertPerms(str string) (perm Perm, err error) - IsPerm(perms Perm, perm Perm) bool - IsPermSet(perms Perm, perm Perm) bool CheckPerms(what string, perms Perm) (ok bool, err error) CheckPermsT(what string, permstr string) (ok bool, err error) TheUser() (user PfUser) @@ -103,14 +111,11 @@ type PfCtx interface { SelectedGroup() (grp PfGroup) SelectedML() (ml PfML) SelectedEmail() (email PfUserEmail) - SelectedUser2FA() (tfa PfUser2FA) HasSelectedUser() bool HasSelectedGroup() bool HasSelectedML() bool SelectMe() SelectUser(username string, perms Perm) (err error) - SelectUser2FA(id int, perms Perm) (err error) - SelectGroupA(grp PfGroup, gr_name string, perms Perm) (err error) SelectGroup(gr_name string, perms Perm) (err error) SelectML(ml_name string, perms Perm) (err error) SelectEmail(email string) (err error) @@ -134,32 +139,35 @@ type PfCtx interface { SetLanguage(name string) GetTfunc() i18n.TranslateFunc + // User and Group creation overrides NewUser() (user PfUser) NewUserI() (i interface{}) NewGroup() (user PfGroup) NewGroupI() (i interface{}) - /* Menu Overrides */ + // Menu Overrides MenuOverride(menu *PfMenu) - /* menu.go */ + // Menu Related (menu.go) Menu(args []string, menu PfMenu) (err error) WalkMenu(args []string) (menu *PfMEntry, err error) Cmd(args []string) (err error) CmdOut(cmd string, args []string) (msg string, err error) Batch(filename string) (err error) - /* appdata */ + // Application Data SetAppData(data interface{}) GetAppData() interface{} } +// SessionClaims describe claims for a session type SessionClaims struct { JWTClaims UserDesc string `json:"userdesc"` IsSysAdmin bool `json:"issysadmin"` } +// PfCtxS is the default implementation of PfCtx type PfCtxS struct { abort <-chan bool /* Abort the request */ status int /* HTTP Status code */ @@ -178,7 +186,6 @@ type PfCtxS struct { language string /* User's chosen language (TODO: Allow user to select it) */ tfunc i18n.TranslateFunc /* Translation function populated with current language */ sel_user PfUser /* Selected User */ - sel_user_2fa *PfUser2FA /* Selected User 2FA */ sel_group PfGroup /* Selected Group */ sel_ml *PfML /* Selected Mailing List */ sel_email *PfUserEmail /* Selected User email address */ @@ -190,24 +197,46 @@ type PfCtxS struct { f_appperms PfAppPermsI /* Application Permission Check */ f_postbecome PfPostBecomeI /* Post Become() */ - /* Unbuffered Output */ - outunbuf_fun string /* Function name that handles unbuffered output */ - outunbuf_obj ObjFuncI /* Object where the function lives */ + // Unbuffered Output */ + outunbuf_fun string // Function name that handles unbuffered output */ + outunbuf_obj ObjFuncI // Object where the function lives */ - /* Database internal */ - db_Tx *Tx + // Database internal + db_Tx *Tx // Used for database transactions - /* Menu internal values */ - menu_walkonly bool - menu_args []string - menu_menu *PfMEntry + // Menu internal values (menu.go) + menu_walkonly bool // Set to 'true' to indicate that only walk, do not execute; used for figuring out what arguments are needed + menu_args []string // Which arguments are currently requested + menu_menu *PfMEntry // Current menu entry being attempted /* Application Data */ - appdata interface{} + appdata interface{} // Application specific data } +// PfNewCtx allows overriding the NewCtx function, thus allowing extending PfCtx type PfNewCtx func() PfCtx +// NewPfCtx is used to initialize a new Pitchfork Context. +// +// The various arguments are all to provide the ability to replace +// standard Pitchfork functions with application specific ones that +// likely extends the Pitchfork functionality or that carry extra details. +// +// newuser is used as a function for creating new users. +// +// newgroup is used as a function for creating new groups. +// +// menuoverride is used as a function to override menu entries. +// +// appperms is used as a function to verify application specific permissions. +// +// postbecome is used as a callback after a user has changed (eg when logging in). +// +// All overrides are optional, and will be defaulted to the Pitchfork versions +// when they are provided as 'nil'. +// +// NewPfCtx is called from the constructors of PfUI and, except for testing +// should rarely be called directly as the context is already handed to a function. func NewPfCtx(newuser PfNewUserI, newgroup PfNewGroupI, menuoverride PfMenuI, appperms PfAppPermsI, postbecome PfPostBecomeI) PfCtx { if newuser == nil { newuser = NewPfUserA @@ -219,7 +248,7 @@ func NewPfCtx(newuser PfNewUserI, newgroup PfNewGroupI, menuoverride PfMenuI, ap tfunc, err := i18n.Tfunc(Config.TransDefault) if err != nil { - panic(err.Error()) + tfunc = nil } return &PfCtxS{f_newuser: newuser, @@ -228,84 +257,134 @@ func NewPfCtx(newuser PfNewUserI, newgroup PfNewGroupI, menuoverride PfMenuI, ap language: Config.TransDefault, mode_buffered: true, tfunc: tfunc} } +// GetAbort is used to retrieve the abort channel (as used/passed-down from the HTTP handler) +// +// This channel is used to indicate, by the HTTP library, that the HTTP client has +// disconnected and that the request can be aborted as the client will never receive +// the answer of the query. +// +// Used amongst others by the search infrastructure. func (ctx *PfCtxS) GetAbort() <-chan bool { return ctx.abort } +// SetAbort is used to set the abort channel (as used/passed-down from the HTTP handler). +// +// SetAbort is called from H_root() to configure the abort channel as passed down +// from the Golang HTTP package. func (ctx *PfCtxS) SetAbort(abort <-chan bool) { ctx.abort = abort } +// GetLanguage is used to retrieve the user-selected language setting +// +// The returned string is in the form of a RFC2616 Accept-Language header. +// Typically it will be 'en-us', or sometimes 'de', 'de-DE', 'de-CH' or 'es'. func (ctx *PfCtxS) GetLanguage() string { return ctx.language } +// SetLanguage accepts a RFC2616 style Accept-Language string +// it then uses that information to determine the best language +// to return. func (ctx *PfCtxS) SetLanguage(name string) { ctx.language = name tfunc, err := i18n.Tfunc(name, Config.TransDefault) if err != nil { + // XXX: Handle properly, this crashes the goproc based on invalid Accept-Language header + // The panic might expose information to the enduser panic(err.Error()) } ctx.tfunc = tfunc } +// GetTfunc returns the translation function func (ctx *PfCtxS) GetTfunc() i18n.TranslateFunc { return ctx.tfunc } +// SetAppData can be used to set the appdata of a context. +// +// Typically this is used by an application's edition of a context to store +// itself in the pitchfork context. This given that Golang does not support +// polymorphism and thus needs a place to hide the full version of itself. func (ctx *PfCtxS) SetAppData(appdata interface{}) { ctx.appdata = appdata } +// GetAppData is used for getting application specific data inside the context. +// +// Typically this is used by an application's edition of a context to retrieve +// itself from the pitchfork context. This given that Golang does not support +// polymorphism and it needs to retrieve itself from the embedded edition of itself. func (ctx *PfCtxS) GetAppData() interface{} { return ctx.appdata } -func (ctx *PfCtxS) StoreCreds() (creds PfCreds) { - creds.sel_user = ctx.sel_user - creds.sel_group = ctx.sel_group - return -} - -func (ctx *PfCtxS) RestoreCreds(creds PfCreds) { - ctx.sel_user = creds.sel_user - ctx.sel_group = creds.sel_group -} - +// NewUser causes a new PfUser (or extended edition) to be created. +// +// The override for NewUser, as configured at Ctx creation time is used +// thus allowing the application specific Ctx to be returned. func (ctx *PfCtxS) NewUser() PfUser { return ctx.f_newuser() } +// NewUserI is like NewUser() but returns a generic interface */ func (ctx *PfCtxS) NewUserI() interface{} { return ctx.f_newuser() } +// NewGroup causes a new PfGroup to be created by calling the +// application defined edition of a NewGroup function. func (ctx *PfCtxS) NewGroup() PfGroup { return ctx.f_newgroup() } +// NewGroupI is like NewGroup() but returns a generic interface func (ctx *PfCtxS) NewGroupI() interface{} { return ctx.f_newgroup() } +// MenuOverride is called before a menu is further processed, +// allowing entries to be modified by calling the callback. +// +// As noted, it is an optional override. func (ctx *PfCtxS) MenuOverride(menu *PfMenu) { if ctx.f_menuoverride != nil { ctx.f_menuoverride(ctx, menu) } } +// SetTx is used by the database code to select the current transaction func (ctx *PfCtxS) SetTx(tx *Tx) { ctx.db_Tx = tx } +// GetTx is used by the database code to get the current transaction func (ctx *PfCtxS) GetTx() (tx *Tx) { return ctx.db_Tx } +// GetRemote retrieves the remote address of the user/connection. +// +// The address is a IPv4 or IPv6 textual representation. func (ctx *PfCtxS) GetRemote() (remote string) { return ctx.remote } +// SetClient is used for configuring the client IP, remote address and Full User Agent strings. +// +// Typically not called from an application, but from cui's SetClientIP() +// which in turn gets called from the H_root. +// +// The clientip is a pre-parsed IP address and XFF-filtered hops. +// +// Remote contains the full IP address string (including X-Forwarded-For hops). +// +// Fullua contains the HTTP User-Agent header. +// +// This function sets the variables of the Ctx (client_ip, remote) and parses +// the Fullua (Full User-Agent) variable, storing the details in Ctx. func (ctx *PfCtxS) SetClient(clientip net.IP, remote string, fullua string) { ctx.client_ip = clientip ctx.remote = remote @@ -328,71 +407,98 @@ func (ctx *PfCtxS) SetClient(clientip net.IP, remote string, fullua string) { } } +// GetClientIP is used to get the client's IP address func (ctx *PfCtxS) GetClientIP() net.IP { return ctx.client_ip } +// GetUserAgent is used for retrieving the parsed User Agent; see also SetClient() func (ctx *PfCtxS) GetUserAgent() (string, string, string) { return ctx.ua_full, ctx.ua_browser, ctx.ua_os } +// SelectObject is used by the struct code (lib/struct.go) to set the +// object that it wants to keep track of during parsing. func (ctx *PfCtxS) SelectObject(obj *interface{}) { ctx.sel_obj = obj } +// SelectedObject is used by the struct code to retrieve +// the object it is currently parsing. func (ctx *PfCtxS) SelectedObject() (obj *interface{}) { return ctx.sel_obj } +// SetModOpts allows setting the options for the wiki and file modules func (ctx *PfCtxS) SetModOpts(opts PfModOptsI) { ctx.mod_opts = opts } +// GetModOpts allows getting the options for the wiki and file modules func (ctx *PfCtxS) GetModOpts() (opts interface{}) { return ctx.mod_opts } +// Perm is used for storing the OR value of permissions +// +// Note: Keep in sync with permnames && ui/ui (convenience for all the menus there). +// +// It is used as a bitfield, hence multiple perms are possible by ORing them together. +// Check access using the CheckPerms() function. +// +// The perms use the context's sel_{user|group|ml|*} variables to check if those permissions match. +// +// Being a SysAdmin overrides almost all permissions! +// +// Change the 'false' in PDbg to 'true' to see what permission decisions are being made. +// +// Application permissions are fully handled by the application. +// See the CheckPerms function for more details. type Perm uint64 -/* - * Note: Keep in sync with permnames && ui/ui (convienence for all the menus there) - * - * It is used as a bitfield, hence multiple perms are possible - * Check access using the CheckPerms() function - * - * The perms use the sel_{user|group|ml} vars to compare against - * - * Note: Being sysadmin overrides almost all permissions! - * - * Change the 'false' in PDbgf to 'true' to see what permission - * decisions are being made. - */ +// PERM_* define the permissions in the system. +// +// Each permission tests as true when the given condition is met. +// See the per permission desciption for what condition they test for. +// +// The permissions are listed from weak (NONE) to strong (NOBODY). +// +// Permissions can be ORed together, the strongest are tested first. +// +// Not all combinations will make sense. eg combining PERM_GUEST|PERM_USER +// means that both not-loggedin and loggedin users have access, at which +// point the check can just be replaced with PERM_NONE. +// +// Application permissions our application specific. +// +// The PERM_'s marked 'Flag' are not used for checking permissions +// but used for modifying the behavior of a menu entry. const ( - PERM_NOTHING Perm = 0 /* Nothing / empty permissions */ - PERM_NONE Perm = 1 << iota /* No access bits needed (unauthenticated) */ - PERM_GUEST /* Not authenticated */ - PERM_USER /* User (authenticated) */ - PERM_USER_SELF /* User when they selected themselves */ - PERM_USER_NOMINATE /* User when doing nomination */ - PERM_USER_VIEW /* User when just trying to view */ - PERM_GROUP_MEMBER /* Member of the group */ - PERM_GROUP_ADMIN /* Admin of the group */ - PERM_GROUP_WIKI /* Group has Wiki section enabled */ - PERM_GROUP_FILE /* Group has File section enabled */ - PERM_GROUP_CALENDAR /* Group has Calendar section enabled */ - PERM_SYS_ADMIN /* System Administrator */ - PERM_SYS_ADMIN_CAN /* Can be a System Administrator */ - PERM_CLI /* When CLI is enabled */ - PERM_API /* When API is enabled */ - PERM_OAUTH /* When OAUTH is enabled */ - PERM_LOOPBACK /* Connected from Loopback */ - PERM_HIDDEN /* Option is hidden */ - PERM_NOCRUMB /* Don't add a crumb for this menu */ - PERM_NOSUBS /* No sub menus for this menu entry */ - PERM_NOBODY /* Nobody has access */ - - /* Application permissions */ + PERM_NOTHING Perm = 0 // Nothing / empty permissions, primarily used for initialization, should not be found in code as it indicates that the Permission was not configured and thus should normally not be used + PERM_NONE Perm = 1 << iota // No permissions needed (authenticated or unauthenticated is okay), typically combined with the a Flag like PERM_HIDDEN or PERM_NOSUBS + PERM_GUEST // Tests that the user is not authenticated: The user is a Guest of the system; does not accept authenticated sessions + PERM_USER // Tests that the user is logged in: the user has authenticated + PERM_USER_SELF // Tests that the selected user matches the logged in user + PERM_USER_NOMINATE // Tests that the user can nominate the selected user + PERM_USER_VIEW // Tests that the user can view the selected user + PERM_GROUP_MEMBER // Tests that the selected user is an active member of the selected group that can see the group + PERM_GROUP_ADMIN // Tests that the selected user is an Admin of the selected group + PERM_GROUP_WIKI // Tests that the selected Group has the Wiki section enabled + PERM_GROUP_FILE // Tests that the selected Group has the File section enabled + PERM_GROUP_CALENDAR // Tests that the selected Group has the Calendar section enabled + PERM_SYS_ADMIN // Tests that the user is a System Administrator + PERM_SYS_ADMIN_CAN // Can be a System Administrator + PERM_CLI // Tests when the CLI option is enabled in system settings + PERM_API // Tests when the API option is enabled in system settings + PERM_OAUTH // Tests when the OAUTH option is enabled in system settings + PERM_LOOPBACK // Tests that the connection comes from loopback (127.0.0.1 / ::1 as the Client/Remote IP address) + PERM_HIDDEN // Flag: The menu option is hidden + PERM_NOCRUMB // Flag: Don't add a crumb for this menu + PERM_NOSUBS // Flag: No sub menus for this menu entry. See the NoSubs function for more details. + PERM_NOBODY // Absolutely nobody has access (highest priority, first checked) + + // Application permissions - defined by the application PERM_APP_0 PERM_APP_1 PERM_APP_2 @@ -405,9 +511,10 @@ const ( PERM_APP_9 ) +// permnames contains the human readable names matching the permissions var permnames []string -/* String init */ +// init is used to initialize permnames and verify that they are correct, at least in count func init() { permnames = []string{ "nothing", @@ -443,6 +550,7 @@ func init() { "app_9", } + // Verify that the correct amount of permissions is present max := uint64(1 << uint64(len(permnames))) if max != uint64(PERM_APP_9) { fmt.Printf("Expected %d got %d\n", max, PERM_APP_9) @@ -450,66 +558,102 @@ func init() { } } +// Shortcutted commonly used HTTP error codes const ( StatusOK = 200 StatusUnauthorized = 401 ) +// Debug is a Global Debug flag, used primarily for determining if debug messages should be output. Typically toggled by flags var Debug = false -/* Constructor */ +// Init is the "constructor" for Pitchfork Contexts func (ctx *PfCtxS) Init() (err error) { - /* Default HTTP status */ + // Default HTTP status ctx.status = StatusOK - /* Default Shell Return Code to 0 */ + // Default Shell Return Code to 0 ctx.returncode = 0 return err } +// SetStatus can be used by a h_* function to set the status of the context. +// +// The status typically matches a HTTP error (eg StatusNotFound from golang HTTP library). +// +// The final status is flushed out during UI's Flush() time. +// +// The status code is tracked in lib instead of the UI layer to allow a generic +// status code system inside Pitchfork. func (ctx *PfCtxS) SetStatus(code int) { ctx.status = code } +// GetStatus can be used to get the status of the context. +// +// Typically only called by UI Flush(), but in theory could be used +// by an application/function to check the current error code too. func (ctx *PfCtxS) GetStatus() (code int) { return ctx.status } +// SetReturnCode is used by the CLI edition of tools to return a Shell Return Code. func (ctx *PfCtxS) SetReturnCode(rc int) { ctx.returncode = rc } +// GetReturnCode is used by the CLI edition of tools to fetch the set Shell Return Code. +// +// During UI Flush() this error code is fetched and when not-0 reported as X-ReturnCode. func (ctx *PfCtxS) GetReturnCode() (rc int) { return ctx.returncode } +// GetLoc returns where in the CLI menu system our code is located (XXX: Improve naming). +// +// This function is typically called by MenuOverrides so that they can determine +// where they are and thus what they might want to change. func (ctx *PfCtxS) GetLoc() string { return ctx.loc } +// GetLastPart is used to get the last portion of the location (XXX: Improve naming). func (ctx *PfCtxS) GetLastPart() string { fa := strings.Split(ctx.loc, " ") return fa[len(fa)-1] } +// Become can be used to become the given user. +// +// The context code that logs in a user uses this. +// This can be used for a 'sudo' type mechanism as is cmd/setup/sudo.go. +// +// After changing users, the PostBecome function is called when configured. +// This allows an application to for instance update state or other such +// properties when the user changes. +// +// Use sparingly and after properly checking permissions to see if +// the user is really supposed to be able to become that user. func (ctx *PfCtxS) Become(user PfUser) { - /* Use the details from the user */ + // Use the details from the user ctx.user = user - /* Select one-self */ + // Select one-self ctx.sel_user = user - /* Post Become() hook? */ + // Post Become() hook if configured if ctx.f_postbecome != nil { ctx.f_postbecome(ctx) } } +// GetToken retrieves the authentication token (JWT) provided by the user, if any func (ctx *PfCtxS) GetToken() (tok string) { return ctx.token } +// NewToken causes a new JWT websession token to be generated for loggedin users func (ctx *PfCtxS) NewToken() (err error) { if !ctx.IsLoggedIn() { return errors.New("Not authenticated") @@ -517,25 +661,31 @@ func (ctx *PfCtxS) NewToken() (err error) { theuser := ctx.TheUser() - /* Set some claims */ + // Set some claims ctx.token_claims.UserDesc = theuser.GetFullName() ctx.token_claims.IsSysAdmin = theuser.IsSysAdmin() username := theuser.GetUserName() - /* Create the token */ + // Create the token token := Token_New("websession", username, TOKEN_EXPIRATIONMINUTES, &ctx.token_claims) - /* Sign and get the complete encoded token as a string */ + // Sign and get the complete encoded token as a string ctx.token, err = token.Sign() if err != nil { - /* Invalid token when something went wrong */ + // Invalid token when something went wrong ctx.token = "" } return } +// LoginToken can be used to log in using a token. +// +// It takes a JWT encoded as a string. +// It returns a boolean indicating if the token is going to expire soon +// (and thus indicating that a new token should be sent out to the user) +// and/or an error to indicate failure. func (ctx *PfCtxS) LoginToken(tok string) (expsoon bool, err error) { /* No valid token */ ctx.token = "" @@ -571,6 +721,10 @@ func (ctx *PfCtxS) LoginToken(tok string) (expsoon bool, err error) { return expsoon, nil } +// Login can be used to login using a username, password +// and optionally, when configured, a twofactor code. +// +// A userevent is logged when this function was succesful. func (ctx *PfCtxS) Login(username string, password string, twofactor string) (err error) { user := ctx.NewUser() @@ -593,6 +747,10 @@ func (ctx *PfCtxS) Login(username string, password string, twofactor string) (er return nil } +// Logout can be used to log the authenticated user out of the system. +// +// The JWT token that was previously in use is added to the JWT Invalidated list +// thus denying the further use of that token. func (ctx *PfCtxS) Logout() { if ctx.token != "" { Jwt_invalidate(ctx.token, &ctx.token_claims) @@ -604,6 +762,7 @@ func (ctx *PfCtxS) Logout() { ctx.token_claims = SessionClaims{} } +// IsLoggedIn can be used to check if the context has a properly logged in user. func (ctx *PfCtxS) IsLoggedIn() bool { if ctx.user == nil { return false @@ -612,6 +771,9 @@ func (ctx *PfCtxS) IsLoggedIn() bool { return true } +// IsGroupMember can be used to check if the selected user +// is a member of the selected group and wether the user +// can see the group. func (ctx *PfCtxS) IsGroupMember() bool { if !ctx.HasSelectedUser() { return false @@ -640,6 +802,8 @@ func (ctx *PfCtxS) IsGroupMember() bool { return state.can_see } +// IAmGroupAdmin can be used to ask if the logged in user +// is a groupadmin of the selected group. func (ctx *PfCtxS) IAmGroupAdmin() bool { if !ctx.IsLoggedIn() { return false @@ -660,6 +824,7 @@ func (ctx *PfCtxS) IAmGroupAdmin() bool { return isadmin } +// IAmGroupMember can be used to check if the logged in user is a groupmember func (ctx *PfCtxS) IAmGroupMember() bool { if !ctx.IsLoggedIn() { return false @@ -676,6 +841,7 @@ func (ctx *PfCtxS) IAmGroupMember() bool { return ismember } +// GroupHasWiki can be used to check if the selected group has a wiki module enabled func (ctx *PfCtxS) GroupHasWiki() bool { if !ctx.HasSelectedGroup() { return false @@ -684,6 +850,7 @@ func (ctx *PfCtxS) GroupHasWiki() bool { return ctx.sel_group.HasWiki() } +// GroupHasFile can be used to check if the selected group has a file module enabled func (ctx *PfCtxS) GroupHasFile() bool { if !ctx.HasSelectedGroup() { return false @@ -692,6 +859,7 @@ func (ctx *PfCtxS) GroupHasFile() bool { return ctx.sel_group.HasFile() } +// GroupHasCalendar can be used to check if the selected group has a calendar module enabled func (ctx *PfCtxS) GroupHasCalendar() bool { if !ctx.HasSelectedGroup() { return false @@ -700,6 +868,7 @@ func (ctx *PfCtxS) GroupHasCalendar() bool { return ctx.sel_group.HasCalendar() } +// CanBeSysAdmin returns whether the loggedin user can become a sysadmin. func (ctx *PfCtxS) CanBeSysAdmin() bool { if !ctx.IsLoggedIn() { return false @@ -714,6 +883,7 @@ func (ctx *PfCtxS) CanBeSysAdmin() bool { return true } +// SwapSysAdmin swaps a user's privilege between normal user and sysadmin. func (ctx *PfCtxS) SwapSysAdmin() bool { /* Not logged, can't be SysAdmin */ if !ctx.IsLoggedIn() { @@ -734,6 +904,13 @@ func (ctx *PfCtxS) SwapSysAdmin() bool { return true } +// IsSysAdmin indicates if the current user is a sysadmin +// and has swapped to it, see SwapSysAdmin. +// +// The SAR (System Administation Restrictions) are checked. +// When the SAR is enabled/configured, one can only become/be +// a sysadmin when coming from the correct IP address as +// configured in th SAR list. func (ctx *PfCtxS) IsSysAdmin() bool { if !ctx.IsLoggedIn() { return false @@ -767,7 +944,15 @@ func (ctx *PfCtxS) IsSysAdmin() bool { return false } -func (ctx *PfCtxS) ConvertPerms(str string) (perm Perm, err error) { +// FromString can be used to parse a string into a Perm object. +// +// str can be in the formats: +// perm1 +// perm1,perm2 +// perm1,perm2,perm3 +// +// When an unknown permission is encountered, this function will return an error. +func (perm Perm) FromString(str string) (err error) { str = strings.ToLower(str) perm = PERM_NOTHING @@ -791,14 +976,19 @@ func (ctx *PfCtxS) ConvertPerms(str string) (perm Perm, err error) { } if !found { - return PERM_NOTHING, errors.New("Unknown permission: '" + pm + "'") + err = errors.New("Unknown permission: '" + pm + "'") + return } break } - return perm, nil + err = nil + return } +// String returns the string representation of a Perm. +// +// This can be used for in for instance debug output. func (perm Perm) String() (str string) { for i := 0; i < len(permnames); i++ { @@ -818,19 +1008,28 @@ func (perm Perm) String() (str string) { return str } -func (ctx *PfCtxS) IsPerm(perms Perm, perm Perm) bool { +/* IsPerm returns whether the provided Perm is the same Perm as given */ +func (perm Perm) IsPerm(perms Perm) bool { return perms == perm } -func (ctx *PfCtxS) IsPermSet(perms Perm, perm Perm) bool { +/* IsSet checks if the perm is in the given set of Perms */ +func (perm Perm) IsSet(perms Perm) bool { return perms&perm > 0 } -/* - * Multiple permissions can be specified - * thus test from least to most to see - * if any of them allows access - */ +// CheckPerms can verify if the given permissions string is valied for the provided Perms. +// +// One of multiple permissions can be specified by OR-ing the permissions together +// thus test from least to most to see if any of them allows access. +// +// To debug permissions, toggle the code-level switch in PDbg and PDbgf(). +// +// Application permissions are tested at the end when all pitchfork permissions +// still allow it to proceed. +// +// The what parameter indicates the piece of code wanting to see the permissions +// verified, this thus primarily serves as a debug help. func (ctx *PfCtxS) CheckPerms(what string, perms Perm) (ok bool, err error) { /* No error yet */ sys := System_Get() @@ -856,23 +1055,19 @@ func (ctx *PfCtxS) CheckPerms(what string, perms Perm) (ok bool, err error) { } /* Nobody? */ - if ctx.IsPermSet(perms, PERM_NOBODY) { + if perms.IsSet(PERM_NOBODY) { ctx.PDbgf(what, perms, "Nobody") return false, errors.New("Nobody is allowed") } - if ctx.IsPerm(perms, PERM_NOBODY) { - panic("EHMM") - } - /* No permissions? */ - if ctx.IsPerm(perms, PERM_NOTHING) { + if perms.IsPerm(PERM_NOTHING) { ctx.PDbgf(what, perms, "Nothing") return true, nil } /* CLI when enabled and user is authenticated */ - if ctx.IsPermSet(perms, PERM_CLI) { + if perms.IsSet(PERM_CLI) { ctx.PDbgf(what, perms, "CLI") if ctx.IsLoggedIn() && sys.CLIEnabled { ctx.PDbgf(what, perms, "CLI - Enabled") @@ -883,7 +1078,7 @@ func (ctx *PfCtxS) CheckPerms(what string, perms Perm) (ok bool, err error) { } /* Loopback calls can always access the API (for tcli) */ - if ctx.IsPermSet(perms, PERM_API) { + if perms.IsSet(PERM_API) { ctx.PDbgf(what, perms, "API") if sys.APIEnabled { ctx.PDbgf(what, perms, "API - Enabled") @@ -894,7 +1089,7 @@ func (ctx *PfCtxS) CheckPerms(what string, perms Perm) (ok bool, err error) { } /* Is OAuth enabled? */ - if ctx.IsPermSet(perms, PERM_OAUTH) { + if perms.IsSet(PERM_OAUTH) { ctx.PDbgf(what, perms, "OAuth") if sys.OAuthEnabled { ctx.PDbgf(what, perms, "OAuth - Enabled") @@ -905,7 +1100,7 @@ func (ctx *PfCtxS) CheckPerms(what string, perms Perm) (ok bool, err error) { } /* Loopback? */ - if ctx.IsPermSet(perms, PERM_LOOPBACK) { + if perms.IsSet(PERM_LOOPBACK) { ctx.PDbgf(what, perms, "Loopback") if ctx.client_ip.IsLoopback() { ctx.PDbgf(what, perms, "Is Loopback") @@ -916,7 +1111,7 @@ func (ctx *PfCtxS) CheckPerms(what string, perms Perm) (ok bool, err error) { } /* User must not be authenticated */ - if ctx.IsPermSet(perms, PERM_GUEST) { + if perms.IsSet(PERM_GUEST) { ctx.PDbgf(what, perms, "Guest") if !ctx.IsLoggedIn() { ctx.PDbgf(what, perms, "Guest - Not Logged In") @@ -928,7 +1123,7 @@ func (ctx *PfCtxS) CheckPerms(what string, perms Perm) (ok bool, err error) { } /* User has to have selected themselves */ - if ctx.IsPermSet(perms, PERM_USER_SELF) { + if perms.IsSet(PERM_USER_SELF) { ctx.PDbgf(what, perms, "User Self") if ctx.IsLoggedIn() { ctx.PDbgf(what, perms, "User Self - Logged In") @@ -951,7 +1146,7 @@ func (ctx *PfCtxS) CheckPerms(what string, perms Perm) (ok bool, err error) { } /* User has to have selected themselves */ - if ctx.IsPermSet(perms, PERM_USER_VIEW) { + if perms.IsSet(PERM_USER_VIEW) { ctx.PDbgf(what, perms, "User View") if ctx.IsLoggedIn() { ctx.PDbgf(what, perms, "User View - Logged In") @@ -981,7 +1176,7 @@ func (ctx *PfCtxS) CheckPerms(what string, perms Perm) (ok bool, err error) { } /* User has to be a group member + Wiki enabled */ - if ctx.IsPermSet(perms, PERM_GROUP_WIKI) { + if perms.IsSet(PERM_GROUP_WIKI) { ctx.PDbgf(what, perms, "Group Wiki?") if ctx.GroupHasWiki() { ctx.PDbgf(what, perms, "HasWiki - ok") @@ -997,7 +1192,7 @@ func (ctx *PfCtxS) CheckPerms(what string, perms Perm) (ok bool, err error) { } /* User has to be a group member + File enabled */ - if ctx.IsPermSet(perms, PERM_GROUP_FILE) { + if perms.IsSet(PERM_GROUP_FILE) { ctx.PDbgf(what, perms, "Group File?") if ctx.GroupHasFile() { ctx.PDbgf(what, perms, "HasFile - ok") @@ -1013,7 +1208,7 @@ func (ctx *PfCtxS) CheckPerms(what string, perms Perm) (ok bool, err error) { } /* User has to be a group member + Calendar enabled */ - if ctx.IsPermSet(perms, PERM_GROUP_CALENDAR) { + if perms.IsSet(PERM_GROUP_CALENDAR) { ctx.PDbgf(what, perms, "Group Calendar?") if ctx.GroupHasCalendar() { ctx.PDbgf(what, perms, "HasCalendar - ok") @@ -1029,7 +1224,7 @@ func (ctx *PfCtxS) CheckPerms(what string, perms Perm) (ok bool, err error) { } /* No permissions needed */ - if ctx.IsPermSet(perms, PERM_NONE) { + if perms.IsSet(PERM_NONE) { ctx.PDbgf(what, perms, "None") /* Always succeeds */ return true, nil @@ -1055,7 +1250,7 @@ func (ctx *PfCtxS) CheckPerms(what string, perms Perm) (ok bool, err error) { err = errors.New("Not a SysAdmin") /* User has to be authenticated */ - if ctx.IsPermSet(perms, PERM_USER) { + if perms.IsSet(PERM_USER) { ctx.PDbgf(what, perms, "User?") if ctx.IsLoggedIn() { ctx.PDbgf(what, perms, "User - Logged In") @@ -1066,7 +1261,7 @@ func (ctx *PfCtxS) CheckPerms(what string, perms Perm) (ok bool, err error) { } /* User has to be a group admin */ - if ctx.IsPermSet(perms, PERM_GROUP_ADMIN) { + if perms.IsSet(PERM_GROUP_ADMIN) { ctx.PDbgf(what, perms, "Group admin?") if ctx.IAmGroupAdmin() { ctx.PDbgf(what, perms, "Group admin - ok") @@ -1077,7 +1272,7 @@ func (ctx *PfCtxS) CheckPerms(what string, perms Perm) (ok bool, err error) { } /* User has to be a group member */ - if ctx.IsPermSet(perms, PERM_GROUP_MEMBER) { + if perms.IsSet(PERM_GROUP_MEMBER) { ctx.PDbgf(what, perms, "Group member?") if ctx.IsGroupMember() { ctx.PDbgf(what, perms, "Group member - ok") @@ -1088,7 +1283,7 @@ func (ctx *PfCtxS) CheckPerms(what string, perms Perm) (ok bool, err error) { } /* User wants to nominate somebody (even themselves) */ - if ctx.IsPermSet(perms, PERM_USER_NOMINATE) { + if perms.IsSet(PERM_USER_NOMINATE) { ctx.PDbgf(what, perms, "User Nominate") if ctx.IsLoggedIn() { ctx.PDbgf(what, perms, "User Nominate - Logged In") @@ -1105,7 +1300,7 @@ func (ctx *PfCtxS) CheckPerms(what string, perms Perm) (ok bool, err error) { } /* Can the user become a SysAdmin? */ - if ctx.IsPermSet(perms, PERM_SYS_ADMIN_CAN) { + if perms.IsSet(PERM_SYS_ADMIN_CAN) { if ctx.IsLoggedIn() { ctx.PDbgf(what, perms, "Sys Admin Can - Logged In") if ctx.CanBeSysAdmin() { @@ -1139,8 +1334,13 @@ func (ctx *PfCtxS) CheckPerms(what string, perms Perm) (ok bool, err error) { return false, err } +// CheckPermsT can be used to check a Textual version of permissions. +// +// Used when the caller has the textual representation of the permissions. func (ctx *PfCtxS) CheckPermsT(what string, permstr string) (ok bool, err error) { - perms, err := ctx.ConvertPerms(permstr) + var perms Perm + + err = perms.FromString(permstr) if err != nil { return } @@ -1148,59 +1348,64 @@ func (ctx *PfCtxS) CheckPermsT(what string, permstr string) (ok bool, err error) return ctx.CheckPerms(what, perms) } +// TheUser returns the currently selected user func (ctx *PfCtxS) TheUser() (user PfUser) { /* Return a copy, not a reference */ return ctx.user } +// SelectedSelf checks if the logged in user and the selected user are the same. func (ctx *PfCtxS) SelectedSelf() bool { return ctx.IsLoggedIn() && ctx.HasSelectedUser() && ctx.user.GetUserName() == ctx.sel_user.GetUserName() } +// SelectedUser returns the selected user. func (ctx *PfCtxS) SelectedUser() (user PfUser) { /* Return a copy, not a reference */ return ctx.sel_user } +// SelectedGroup returns the selected group. func (ctx *PfCtxS) SelectedGroup() (grp PfGroup) { /* Return a copy, not a reference */ return ctx.sel_group } +// SelectedML returns the selected mailinglist. func (ctx *PfCtxS) SelectedML() (ml PfML) { /* Return a copy, not a reference */ return *ctx.sel_ml } +// SelectedEmail returns the selected email address. func (ctx *PfCtxS) SelectedEmail() (email PfUserEmail) { /* Return a copy, not a reference */ return *ctx.sel_email } -func (ctx *PfCtxS) SelectedUser2FA() (tfa PfUser2FA) { - /* Return a copy, not a reference */ - return *ctx.sel_user_2fa -} - +// HasSelectedUser returns whether a user was selected. func (ctx *PfCtxS) HasSelectedUser() bool { return ctx.sel_user != nil } +// HasSelectedGroup returns whether a group was selected. func (ctx *PfCtxS) HasSelectedGroup() bool { return ctx.sel_group != nil } +// HasSelectedML returns whether a mailinglist was selected. func (ctx *PfCtxS) HasSelectedML() bool { return ctx.sel_ml != nil } +// SelectMe caused the user to select themselves. func (ctx *PfCtxS) SelectMe() { ctx.sel_user = ctx.user } -/* This creates a PfUser */ +// SelectUser selects the user if the given permissions are matched. func (ctx *PfCtxS) SelectUser(username string, perms Perm) (err error) { ctx.PDbgf("PfCtxS::SelectUser", perms, "%q", username) @@ -1228,33 +1433,11 @@ func (ctx *PfCtxS) SelectUser(username string, perms Perm) (err error) { return } -func (ctx *PfCtxS) SelectUser2FA(id int, perms Perm) (err error) { - ctx.PDbgf("SelectUser2FA", perms, "%d", id) - - /* Nothing to select, always works */ - if id == 0 { - ctx.sel_user_2fa = nil - return nil - } - - /* No user selected, no 2FA selected */ - if !ctx.HasSelectedUser() { - ctx.sel_user_2fa = nil - return nil - } - - ctx.sel_user_2fa = NewPfUser2FA() - err = ctx.sel_user_2fa.Select(ctx, id, perms) - if err != nil { - ctx.sel_user_2fa = nil - } - - return -} - -/* Unless SysAdmin one cannot select a group one is not a member of */ -func (ctx *PfCtxS) SelectGroupA(grp PfGroup, gr_name string, perms Perm) (err error) { - ctx.PDbgf("SelectGroupA", perms, "%q", gr_name) +// SelectGroup selects the group, depending on the permission bits provided. +// +// After succesfully selecting, SelectedGroup can be used to retrieve the group. +func (ctx *PfCtxS) SelectGroup(gr_name string, perms Perm) (err error) { + ctx.PDbgf("SelectGroup", perms, "%q", gr_name) /* Nothing to select */ if gr_name == "" { @@ -1262,7 +1445,7 @@ func (ctx *PfCtxS) SelectGroupA(grp PfGroup, gr_name string, perms Perm) (err er return nil } - ctx.sel_group = grp + ctx.sel_group = ctx.NewGroup() err = ctx.sel_group.Select(ctx, gr_name, perms) if err != nil { ctx.sel_group = nil @@ -1271,10 +1454,7 @@ func (ctx *PfCtxS) SelectGroupA(grp PfGroup, gr_name string, perms Perm) (err er return } -func (ctx *PfCtxS) SelectGroup(gr_name string, perms Perm) (err error) { - return ctx.SelectGroupA(ctx.NewGroup(), gr_name, perms) -} - +// SelectML selects a mailinglist depending on the permissions of the logged in user func (ctx *PfCtxS) SelectML(ml_name string, perms Perm) (err error) { ctx.PDbgf("SelectUserML", perms, "%q", ml_name) @@ -1298,6 +1478,9 @@ func (ctx *PfCtxS) SelectML(ml_name string, perms Perm) (err error) { return } +// SelectEmail selects an email address. +// +// Users can only select their own email addresses (PERM_USER_SELF). func (ctx *PfCtxS) SelectEmail(email string) (err error) { perms := PERM_USER_SELF @@ -1329,41 +1512,53 @@ func (ctx *PfCtxS) SelectEmail(email string) (err error) { return } +// Err allows printing error messages (syslog/stdout) with details from the context. func (ctx *PfCtxS) Err(message string) { ErrA(1, message) } +// Errf allows printing formatted error messages (syslog/stdout) with details from the context. func (ctx *PfCtxS) Errf(format string, a ...interface{}) { ErrA(1, format, a...) } +// Log allows printing log messages (syslog/stdout) with details from the context func (ctx *PfCtxS) Log(message string) { LogA(1, message) } +// Logf allows printing formatted log messages (syslog/stdout) with details from the context func (ctx *PfCtxS) Logf(format string, a ...interface{}) { LogA(1, format, a...) } +// Dbg allows printing debug messages (syslog/stdout) with details from the context func (ctx *PfCtxS) Dbg(message string) { DbgA(1, message) } +// Dbgf allows printing formatted debug messages (syslog/stdout) with details from the context func (ctx *PfCtxS) Dbgf(format string, a ...interface{}) { DbgA(1, format, a...) } +// PDbgf is used for permission debugging. +// +// It needs to be enabled with a Code level Debug option. +// Change the 'false' to 'true' and every permission decision will be listed. +// Remember: sysadmin overrules most permissions, thus test with normal user. func (ctx *PfCtxS) PDbgf(what string, perm Perm, format string, a ...interface{}) { - /* - * Code level Debug option - * Change the 'false' to 'true' and every permission decision will be listed - * Remember: sysadmin overrules most permissions, thus test with normal user - */ if false { ctx.Dbgf("Perms(\""+what+"\"/"+strconv.Itoa(int(perm))+"): "+format, a...) } } +// Out can be used to print a line to the output for the context (CLI or HTTP). +// +// When buffering is disabled, the txt is directly forwarded to a special +// direct output function. +// +// When buffering is enabled, the txt is accumulatd in the output buffer. func (ctx *PfCtxS) Out(txt string) { if !ctx.mode_buffered { /* Call the function that takes care of Direct output */ @@ -1377,14 +1572,20 @@ func (ctx *PfCtxS) Out(txt string) { } } +// Outf can be used to let the Out string be formatted first. func (ctx *PfCtxS) Outf(format string, a ...interface{}) { ctx.Out(fmt.Sprintf(format, a...)) } +// OutLn ensure that the Out outputted message ends in a newline func (ctx *PfCtxS) OutLn(format string, a ...interface{}) { ctx.Outf(format+"\n", a...) } +// SetOutUnbuffered causes the Out* functions to become unbuffered. +// +// The object and function passed in are then later used for calling +// and acually performing the output of the txt with the Out() function. func (ctx *PfCtxS) SetOutUnbuffered(obj interface{}, fun string) { objtrail := []interface{}{obj} ok, obji := ObjHasFunc(objtrail, fun) @@ -1396,6 +1597,7 @@ func (ctx *PfCtxS) SetOutUnbuffered(obj interface{}, fun string) { ctx.outunbuf_fun = fun } +// OutBuffered causes the Out* functions to become buffered. func (ctx *PfCtxS) OutBuffered(on bool) { if !on && ctx.outunbuf_fun == "" { panic("Can't enable buffered mode without unbuffered function") @@ -1404,10 +1606,12 @@ func (ctx *PfCtxS) OutBuffered(on bool) { ctx.mode_buffered = on } +// IsBuffered can be used to check if output is being buffered or directly outputted. func (ctx *PfCtxS) IsBuffered() bool { return ctx.mode_buffered } +// Buffered can be used to return the buffered string. func (ctx *PfCtxS) Buffered() (o string) { o = ctx.output ctx.output = "" diff --git a/lib/db.go b/lib/db.go index ab8f41f..6eee629 100644 --- a/lib/db.go +++ b/lib/db.go @@ -1,3 +1,4 @@ +// Pitchfork db (Database layer) it primarily extends golang's database/sql to provide convience functions, automatic reconnects etc package pitchfork import ( @@ -12,76 +13,85 @@ import ( "strings" ) +// ErrNoRows can be used as a shortcut to check that no rows where returned var ErrNoRows = sql.ErrNoRows +// DB_AndOr is used to provide SQL contruction capabilities specifying either an AND or an OR type DB_AndOr int +// DB_OP_AND and DB_OP_OR are the two current Database Operands const ( - DB_OP_AND DB_AndOr = iota - DB_OP_OR + DB_OP_AND DB_AndOr = iota // SQL AND + DB_OP_OR // SQL OR ) +// DB_Op is used to provide a operand: LIKE, ILIKE, EQ, NE, LE, GE type DB_Op int const ( - DB_OP_LIKE DB_Op = iota - DB_OP_ILIKE - DB_OP_EQ - DB_OP_NE - DB_OP_LE - DB_OP_GE + DB_OP_LIKE DB_Op = iota // LIKE match * + DB_OP_ILIKE // ILIKE match + DB_OP_EQ // EQual match + DB_OP_NE // Not Equal match + DB_OP_LE // Less than or Equal match + DB_OP_GE // Greater than or Equal match ) +// PfDB stores the information for a Database connection type PfDB struct { - sql *sql.DB - version int - appversion int - username string - verbosity bool - silence bool + sql *sql.DB // The golang SQL object + version int // The version of the system schema + appversion int // The version of the application schema + username string // The username usd for the connection + verbosity bool // Whether the database should be verbose and log all queries to stdout/syslog + silence bool // Whether the database code should be mostly silent } +// PfQuery stores the information for a query, used primary for setup and compound instructions type PfQuery struct { - query string - desc string - failok bool - failstr string + query string // The SQL query + desc string // Description of the query + failok bool // If failure of executing this query is okay + failstr string // What the failure message is when the query fails } +// Tx wraps a golang SQL transaction - primarily avoiding the need to import database/sql type Tx struct { *sql.Tx } +// Rows wraps a golang SQL Rows return - this to keep the query, parameters and result together, handy for logging errors type Rows struct { - q string - p []interface{} - rows *sql.Rows - db *PfDB + q string // The SQL Query + p []interface{} // The SQL Query Parameters + rows *sql.Rows // The returned rows + db *PfDB // The database object that executed the query } +// Row is a singular version of Rows type Row struct { - q string - p []interface{} - row *sql.Row - db *PfDB + q string // The SQL Query + p []interface{} // The SQL Query Parameters + row *sql.Row // The returned row + db *PfDB // The database object that executed the query } -/* Global database variable - there can only be one */ +// Global database variable - there can only be one var DB PfDB +// DB_Init is used to initialize the database func DB_Init(verbosity bool) { DB.Init(verbosity) } +// DB_SetAppVersion is used to inform the system what the expected Application Database schema version is func DB_SetAppVersion(ver int) { DB.SetAppVersion(ver) } -/* - * Check for a Unique Violation - * - * "duplicate key value violates unique constraint" - */ +// DB_IsPQErrorConstraint checks if an error is a PostgreSQL Unique Violation +// +// "duplicate key value violates unique constraint" func DB_IsPQErrorConstraint(err error) bool { /* Attempt to cast to a libpq error */ pqerr, ok := err.(*pq.Error) @@ -97,6 +107,9 @@ func DB_IsPQErrorConstraint(err error) bool { return false } +// Init is used to initialize a Database object. +// +// It is used to enable/disable verbose database messages. func (db *PfDB) Init(verbosity bool) { db.sql = nil @@ -110,43 +123,39 @@ func (db *PfDB) Init(verbosity bool) { db.verbosity = verbosity } +// SetAppVersion is used to indicate what application schema version is expected func (db *PfDB) SetAppVersion(version int) { db.appversion = version } -func (db *PfDB) Silence(braaf bool) { - db.silence = braaf -} - -func (db *PfDB) Verb(message string) { - if db.verbosity { - OutA(Where(2) + " DB." + message) - } +// Silence can be used to enable the silenced mode +func (db *PfDB) Silence(silence_enabled bool) { + db.silence = silence_enabled } -func (db *PfDB) Verbf(format string, arg ...interface{}) { +// outVerbosef can be used to verbosely output a formatted database message; code-location details are added +func (db *PfDB) outVerbosef(format string, arg ...interface{}) { if db.verbosity { - OutA(Where(2)+" DB."+format, arg...) + outf(Where(2)+" DB."+format, arg...) } } -func (db *PfDB) Err(message string) { - OutA(Where(2) + " DB." + message) -} - -func (db *PfDB) Errf(format string, arg ...interface{}) { - OutA(Where(2)+" DB."+format, arg...) +// Errf can be used to output a formatted database error - code-location details are added +func (db *PfDB) outErrorf(format string, arg ...interface{}) { + outf(Where(2)+" DB."+format, arg...) } +// ToNullString can be used to easily convert a string into a sql.NullString object func ToNullString(s string) sql.NullString { return sql.NullString{String: s, Valid: s != ""} } +// ToNullInt64 can be used to easily convert a int64 into a sql.NullInt64 object func ToNullInt64(v int64) sql.NullInt64 { return sql.NullInt64{Int64: v, Valid: true} } -/* Connect to the database */ +// connect is used internally to cause a connection to be made for the database func (db *PfDB) connect(dbname string, host string, port string, username string, password string) (err error) { /* Already connected, then don't do it again */ if db.sql != nil { @@ -180,7 +189,7 @@ func (db *PfDB) connect(dbname string, host string, port string, username string /* Don't require SSL */ str += "sslmode=" + Config.Db_ssl_mode + " " - db.Verbf("connect: %s", str) + db.outVerbosef("connect: %s", str) /* "postgres" here is the driver */ db.sql, err = sql.Open("postgres", str) @@ -196,6 +205,7 @@ func (db *PfDB) connect(dbname string, host string, port string, username string return err } +// disconnect is used internally to force close a database connection func (db *PfDB) disconnect() { if db.sql != nil { db.sql.Close() @@ -203,35 +213,65 @@ func (db *PfDB) disconnect() { } } +// Connect_def is used to connect to the default database. +// +// This is the standard database used by the application. +// +// This function is normally called from the Query/Exec related functions. +// though it can be called to prime the connectivity and to check +// that the database can be connected to. func (db *PfDB) Connect_def() (err error) { return db.connect(Config.Db_name, Config.Db_host, Config.Db_port, Config.Db_user, Config.Db_pass) } +// connect_pg is internally used to connect to the 'administrative' database (typically template0) +// +// This function is normally called from the database upgrade functions as the administrative +// database can be used to modify the schema of the database. func (db *PfDB) connect_pg(dbname string) (err error) { db.disconnect() return db.connect(dbname, Config.Db_host, Config.Db_port, Config.Db_admin_user, Config.Db_admin_pass) } +// TxBegin is used to start a SQL Transaction. +// +// After this multiple Query/Exec's can be performed till +// a TxRollBack() or TxCommit() are called which causes +// all the intermediary SQL commands to be ignored/forgotten +// or with a TxCommit finalized into the database. +// +// There should always be a matching TxRollback() or TxCommit() +// otherwise all the intermdiary queries done will never be +// actually performed and applied to the database. +// +// The transaction is local to the given Ctx. func (db *PfDB) TxBegin(ctx PfCtx) (err error) { err = db.Connect_def() if err != nil { return err } - var stx *sql.Tx - stx, err = db.sql.Begin() + // XXX: Verify that we are not already in a Tx + // if we are in a Tx, increase a recursion counter + // so that nested Tx's are possible + + var tx *sql.Tx + tx, err = db.sql.Begin() if err != nil { ctx.SetTx(nil) - db.Errf("TxBegin() failed: %s", err.Error()) + db.outErrorf("TxBegin() failed: %s", err.Error()) } else { - ctx.SetTx(&Tx{stx}) - db.Verb("TxBegin()") + ctx.SetTx(&Tx{tx}) + db.outVerbosef("TxBegin(%v)", tx) } return err } +// TxRollback is used to abort/rollback a SQL Transaction. +// +// Called after a TxBegin when the transaction needs a rollback. func (db *PfDB) TxRollback(ctx PfCtx) { tx := ctx.GetTx() if tx == nil { @@ -241,9 +281,9 @@ func (db *PfDB) TxRollback(ctx PfCtx) { err := tx.Rollback() if err != nil { - db.Errf("TxRollback() failed: %s", err.Error()) + db.outErrorf("TxRollback() failed: %s", err.Error()) } else { - db.Verb("TxRollback() Ok") + db.outVerbosef("TxRollback(%v) Ok", tx) } /* No Transaction anymore */ @@ -251,6 +291,10 @@ func (db *PfDB) TxRollback(ctx PfCtx) { return } +// TxCommit is used to commit a SQL Transaction +// +// Called after a TxBegin and other SQL commands +// to indicate that the transaction needs to be commited. func (db *PfDB) TxCommit(ctx PfCtx) (err error) { tx := ctx.GetTx() if tx == nil { @@ -262,18 +306,33 @@ func (db *PfDB) TxCommit(ctx PfCtx) (err error) { ctx.SetTx(nil) if err != nil { - db.Verbf("TxCommit() %s", err.Error()) + db.outVerbosef("TxCommit() %s", err.Error()) } else { - db.Verb("TxCommit() Ok") + db.outVerbosef("TxCommit() Ok") } return } +// QI is used to Quote an Identifier +// +// Typically parameters should be used where possible. +// +// Unfortunately tablenames cannot be parameterized +// at which point this comes into play. func (db *PfDB) QI(name string) string { return pq.QuoteIdentifier(name) } +// IsSelect is used as a check to see if a SQL query is a +// SELECT statement and thus non modifying, primarily used +// to check whether audittxt is needed or not. +// +// This is a very simple test and primarily is used +// to protect against accidental programmer mistakes. +// +// Note that a smart programmer can bypass this, but as +// they are code-level already... func (db *PfDB) IsSelect(query string) (ok bool) { if len(query) >= 6 && query[0:6] != "SELECT" { return false @@ -282,6 +341,13 @@ func (db *PfDB) IsSelect(query string) (ok bool) { return true } +// audit causes a audit message to be recorded with formatting based on the given parameters. +// +// The ctx is primarily used for determining which user/group is performing the action. +// The audittxt is a short message that gets logged describing the changes being made. +// Placeholders like $1, $2, $3 can be used to reference the arguments given. +// The query is the SQL query being performed. +// The args contain zero or more arguments that are referenced from in the placeholder. func (db *PfDB) audit(ctx PfCtx, audittxt string, query string, args ...interface{}) (err error) { /* * No context is available when using tsetup @@ -303,7 +369,7 @@ func (db *PfDB) audit(ctx PfCtx, audittxt string, query string, args ...interfac /* Format the Audit String */ logmsg, aerr := db.formatQuery(audittxt, args...) if aerr != nil { - ctx.Errf("DB.audit: Could not format audit string: '%s': %s", audittxt, aerr.Error()) + db.outErrorf("DB.audit: Could not format audit string: '%s': %s", audittxt, aerr.Error()) return } @@ -341,7 +407,7 @@ func (db *PfDB) audit(ctx PfCtx, audittxt string, query string, args ...interfac if err != nil { if Debug { - db.Errf("exec(%s)[%v] audit error: %s", query, args, err.Error()) + db.outErrorf("exec(%s)[%v] audit error: %s", query, args, err.Error()) } err = errors.New("Auditing error, please check the logs") @@ -350,7 +416,20 @@ func (db *PfDB) audit(ctx PfCtx, audittxt string, query string, args ...interfac return } -/* Wrapper functions, ensuring database is connected */ +// Query is used to make a SQL Query. It is primarily a wrapper function that ensures the database is connected +// One or more results will be returned from this function. +// +// The 'query' argument consists of a full SQL argument, with placeholders ($1, $2, $3, etc) +// for the arguments. The 'args' passed in are zero or more arguments that match these placeholders. +// Using the placeholders ensures that no SQL-escaping can happen. +// +// Only SELECT queries should be using this function, as there is no audittxt and thus a query that modifies +// the database cannot be logged properly. +// +// Example Query: +// SELECT column FROM table WHERE id = $1 +// +// See also: Exec(), QueryRow() func (db *PfDB) Query(query string, args ...interface{}) (trows *Rows, err error) { var rows *sql.Rows @@ -365,12 +444,12 @@ func (db *PfDB) Query(query string, args ...interface{}) (trows *Rows, err error return nil, err } - db.Verbf("QueryA: %s %#v", query, args) + db.outVerbosef("QueryA: %s %#v", query, args) rows, err = db.sql.Query(query, args...) if err != nil { - db.Errf("Query(%s)[%#v] error: %s", query, args, err.Error()) + db.outErrorf("Query(%s)[%#v] error: %s", query, args, err.Error()) /* When in debug mode, dump & exit, so we can trace it */ if Debug { @@ -384,6 +463,14 @@ func (db *PfDB) Query(query string, args ...interface{}) (trows *Rows, err error return &Rows{query, args, rows, db}, err } +// queryrow is used to query for a row providing an audittxt. +// +// This is a DB internal function. +// +// queryrow always only returns one result. This can thus be used +// when one knows there is only one result, when one limits the +// result to be only one result, or when using the RETURNING +// option of PostgreSQL to return a newly INSERTed column. func (db *PfDB) queryrow(ctx PfCtx, audittxt string, query string, args ...interface{}) (trow *Row) { var row *sql.Row @@ -393,7 +480,7 @@ func (db *PfDB) queryrow(ctx PfCtx, audittxt string, query string, args ...inter return nil } - /* Transaction already in progress? */ + /* Transaction already in progress? (XXX: support nested Tx, see TxBegin) */ local_tx := false if audittxt != "" && ctx != nil && ctx.GetTx() == nil { /* Create a local one */ @@ -405,7 +492,7 @@ func (db *PfDB) queryrow(ctx PfCtx, audittxt string, query string, args ...inter } if db.verbosity && !db.silence { - db.Verbf("QueryRow: %s [%v]", query, args) + db.outVerbosef("QueryRow: %s [%v]", query, args) } row = db.sql.QueryRow(query, args...) @@ -426,12 +513,16 @@ func (db *PfDB) queryrow(ctx PfCtx, audittxt string, query string, args ...inter return &Row{query, args, row, db} } -/* Query for a Row, without Audittxt; use with care */ +// QueryRowNA queries for a row without audit (NO = No Audit). +// +// This should be used sparingly, mostly only for situations +// where auditting or other very transient changes are happening +// that do not belong in an auditlog forever. func (db *PfDB) QueryRowNA(query string, args ...interface{}) (trow *Row) { return db.queryrow(nil, "", query, args...) } -/* Query for a Row, with an Audittxt for situations where the query is an INSERT/UPDATE with RETURNING */ +// QueryRowA queries for a row, with an Audittxt for situations where the query is an INSERT/UPDATE with RETURNING func (db *PfDB) QueryRowA(ctx PfCtx, audittxt string, query string, args ...interface{}) (trow *Row) { if audittxt == "" && !db.IsSelect(query) { @@ -442,11 +533,12 @@ func (db *PfDB) QueryRowA(ctx PfCtx, audittxt string, query string, args ...inte return db.queryrow(ctx, audittxt, query, args...) } -/* Query for a Row, SELECT() only; thus no audittxt needed as nothing changes */ +// QueryRow queries for a row, SELECT() only; thus no audittxt needed as nothing changes func (db *PfDB) QueryRow(query string, args ...interface{}) (trow *Row) { return db.QueryRowA(nil, "", query, args...) } +// Scan can be sued to parse the results of one of the QueryRow functions func (rows *Rows) Scan(args ...interface{}) (err error) { err = rows.rows.Scan(args...) @@ -455,7 +547,7 @@ func (rows *Rows) Scan(args ...interface{}) (err error) { break case err != nil: - rows.db.Errf("Rows.Scan(%s)[%v] error: %s", rows.q, rows.p, err.Error()) + rows.db.outErrorf("Rows.Scan(%s)[%v] error: %s", rows.q, rows.p, err.Error()) break default: @@ -466,16 +558,19 @@ func (rows *Rows) Scan(args ...interface{}) (err error) { return err } +// Next steps to the next row in a result set; returns false when no more rows are available func (rows *Rows) Next() bool { return rows.rows.Next() } +// Close closes the resultset; typically called from a 'defer' func (rows *Rows) Close() { if rows != nil && rows.rows != nil { rows.rows.Close() } } +// Scan causes the row to be scanned and returned func (row *Row) Scan(args ...interface{}) (err error) { if row.row == nil { return ErrNoRows @@ -488,7 +583,7 @@ func (row *Row) Scan(args ...interface{}) (err error) { break case err != nil: - row.db.Errf("Row.Scan(%s)[%v] error: %s", row.q, row.p, err.Error()) + row.db.outErrorf("Row.Scan(%s)[%v] error: %s", row.q, row.p, err.Error()) break default: @@ -499,6 +594,7 @@ func (row *Row) Scan(args ...interface{}) (err error) { return } +// formatQuery is an internal call that replaces arguments in the right place, used primarily for audit string creation. func (db *PfDB) formatQuery(q string, args ...interface{}) (str string, err error) { str = "" @@ -554,7 +650,9 @@ func (db *PfDB) formatQuery(q string, args ...interface{}) (str string, err erro return } -/* PfCtx contains selected User & group, the changed object matches these */ +// exec is an internal function for executing a query. +// +// PfCtx contains selected User & group, the changed object matches these func (db *PfDB) exec(ctx PfCtx, report bool, affected int64, query string, args ...interface{}) (err error) { err = db.Connect_def() if err != nil { @@ -564,23 +662,23 @@ func (db *PfDB) exec(ctx PfCtx, report bool, affected int64, query string, args var res sql.Result if ctx != nil && ctx.GetTx() != nil { - db.Verbf("exec(%s) Tx args: %v", query, args) + db.outVerbosef("exec(%s) Tx args: %v", query, args) res, err = ctx.GetTx().Exec(query, args...) } else { - db.Verbf("exec(%s) args: %v", query, args) + db.outVerbosef("exec(%s) args: %v", query, args) res, err = db.sql.Exec(query, args...) } /* When in debug mode, dump & exit, so we can trace it */ if err != nil && Debug { - db.Errf("exec(%s)[%v] error: %s", query, args, err.Error()) + db.outErrorf("exec(%s)[%v] error: %s", query, args, err.Error()) debug.PrintStack() - db.Errf("exec(%s) error: %s", query, err.Error()) + db.outErrorf("exec(%s) error: %s", query, err.Error()) os.Exit(-1) } if report && err != nil { - db.Errf("exec(%s)[%v] error: %s", query, args, err.Error()) + db.outErrorf("exec(%s)[%v] error: %s", query, args, err.Error()) /* * Callers should never show raw SQL error messages @@ -612,7 +710,7 @@ func (db *PfDB) exec(ctx PfCtx, report bool, affected int64, query string, args * * TODO: when all code has been properly tested, change this to returning an error */ - db.Errf("exec(%s)[%#v] expected %d row(s) changed, but %d changed", query, args, affected, chg) + db.outErrorf("exec(%s)[%#v] expected %d row(s) changed, but %d changed", query, args, affected, chg) return } } @@ -620,7 +718,9 @@ func (db *PfDB) exec(ctx PfCtx, report bool, affected int64, query string, args return } -/* Exec that does not require audittxt */ +// execA is an internal function for executing a query. +// +// It handles Transactions. func (db *PfDB) execA(ctx PfCtx, audittxt string, affected int64, query string, args ...interface{}) (err error) { /* Transaction already in progress? */ local_tx := false @@ -661,11 +761,16 @@ func (db *PfDB) execA(ctx PfCtx, audittxt string, affected int64, query string, return } +// ExecNA is an Exec with No Audit +// +// Use sparingly, see QueryRowNA for details func (db *PfDB) ExecNA(affected int64, query string, args ...interface{}) (err error) { return db.execA(nil, "", affected, query, args...) } -/* Exec() with forced requirement for audit message */ +// Exec with forced requirement for audit message; used for SQL queries that do not return rows. +// +// When just querying (SELECT) and thus not modifying data one can use the Query() and QueryRow() functions. func (db *PfDB) Exec(ctx PfCtx, audittxt string, affected int64, query string, args ...interface{}) (err error) { if audittxt == "" { panic("db.Exec() given no audittxt") @@ -674,6 +779,14 @@ func (db *PfDB) Exec(ctx PfCtx, audittxt string, affected int64, query string, a return db.execA(ctx, audittxt, affected, query, args...) } +// Increase is a shortcut function to increase the integer value of a column in a database. +// +// The field to increase is identified by the 'what' argument. +// The database table is identified using the 'table' argument. +// The column of the table identified using the 'ident' argument. +// +// A custom audittxt can be provided or otherwise the function will +// generate a audittxt that is logged alongside the changing of the value. func (db *PfDB) Increase(ctx PfCtx, audittxt, table string, ident string, what string) (err error) { if audittxt == "" { audittxt = "Increased " + table + "." + what @@ -687,6 +800,7 @@ func (db *PfDB) Increase(ctx PfCtx, audittxt, table string, ident string, what s return } +// set is an internal function for updating given fields of a table. func (db *PfDB) set(ctx PfCtx, audittxt string, obj interface{}, table string, idents map[string]string, what string, val interface{}) (updated bool, err error) { var args []interface{} @@ -708,6 +822,13 @@ func (db *PfDB) set(ctx PfCtx, audittxt string, obj interface{}, table string, i return } +// UpdateFieldMulti allows updating multiple fields, specified by the idents map in one go. +// +// The UpdateFieldMulti takes any kind of object and updates the rows identified +// by the 'idents', in the database table given with 'table', the db field named 'what' +// with value 'val'. +// +// Permissions can optionally be ignored by specifying checkperms = false. func (db *PfDB) UpdateFieldMulti(ctx PfCtx, obj interface{}, idents map[string]string, table string, what string, val string, checkperms bool) (updated bool, err error) { var ftype string var fname string @@ -774,24 +895,32 @@ func (db *PfDB) UpdateFieldMulti(ctx PfCtx, obj interface{}, idents map[string]s return } +// UpdateFieldNP can be used to update a single field in a table, NP = NoPermissionsCheck. +// +// The UpdateField set of functions take any kind of object and update the row identified +// by the 'ident', in the database table given with 'table', the db field named 'what' +// with value 'val'. func (db *PfDB) UpdateFieldNP(ctx PfCtx, obj interface{}, ident string, table string, what string, val string) (updated bool, err error) { idents := make(map[string]string) idents["ident"] = ident return db.UpdateFieldMulti(ctx, obj, idents, table, what, val, false) } +// UpdateField can be used to update a single field in a table, permissions are checked func (db *PfDB) UpdateField(ctx PfCtx, obj interface{}, ident string, table string, what string, val string) (updated bool, err error) { idents := make(map[string]string) idents["ident"] = ident return db.UpdateFieldMulti(ctx, obj, idents, table, what, val, true) } +// UpdateFieldMsg can be used to update a field providing a message whether the update was successful or not func (db *PfDB) UpdateFieldMsg(ctx PfCtx, obj interface{}, ident string, table string, what string, val string) (err error) { idents := make(map[string]string) idents["ident"] = ident return db.UpdateFieldMultiMsg(ctx, obj, idents, table, what, val) } +// UpdateFieldMultiMsg is used to update one or more fields and outputting a message indicating success or not func (db *PfDB) UpdateFieldMultiMsg(ctx PfCtx, obj interface{}, set map[string]string, table string, what string, val string) (err error) { var updated bool @@ -809,6 +938,7 @@ func (db *PfDB) UpdateFieldMultiMsg(ctx PfCtx, obj interface{}, set map[string]s return } +// GetSchemaVersion returns the schema version func (db *PfDB) GetSchemaVersion() (version int, err error) { q := "SELECT value " + "FROM schema_metadata " + @@ -819,6 +949,7 @@ func (db *PfDB) GetSchemaVersion() (version int, err error) { return } +// GetAppSchemaVersion returns the application schema version func (db *PfDB) GetAppSchemaVersion() (version int, err error) { q := "SELECT value " + "FROM schema_metadata " + @@ -829,7 +960,7 @@ func (db *PfDB) GetAppSchemaVersion() (version int, err error) { return } -/* Checks that our schema version is matching what we expect */ +// Check checks that our schema version is matching what we expect returning a message about the status func (db *PfDB) Check() (msg string, err error) { msg = "" @@ -891,6 +1022,7 @@ func (db *PfDB) Check() (msg string, err error) { return } +// SizeReport returns the top num list of table sorted by descending size func (db *PfDB) SizeReport(num int) (sizes [][]string, err error) { sizes = nil @@ -926,6 +1058,7 @@ func (db *PfDB) SizeReport(num int) (sizes [][]string, err error) { return } +// QueryFix is used to quickly replace <>, <> and <> in queries with their real values -- used primarily by setup functions func (db *PfDB) QueryFix(q string) (f string) { f = q f = strings.Replace(f, "<>", db.QI(Config.Db_name), -1) @@ -935,10 +1068,10 @@ func (db *PfDB) QueryFix(q string) (f string) { } /* - * Execute series of queries while replacing variables + * queries execute series of queries while replacing variables. * - * These queries are *NOT* audit-logged - * Only code that should call this are DB upgrade scripts + * These queries are *NOT* audit-logged. + * Only code that should call this are DB upgrade scripts. */ func (db *PfDB) queries(f string, qs []PfQuery) (err error) { for _, o := range qs { @@ -970,6 +1103,7 @@ func (db *PfDB) queries(f string, qs []PfQuery) (err error) { return } +// Cleanup_psql connects to Postgresql and DROPS our database and user, thus preparing for re-setup -- used by setup for developers func (db *PfDB) Cleanup_psql() (err error) { f := "Cleanup_psql" @@ -999,6 +1133,10 @@ func (db *PfDB) Cleanup_psql() (err error) { return } +// Fix_Perms ensures that the PostgreSQL permissions are correctly configured for our database. +// +// During updates/upgrades or due to manual intervention some of these grants might disappear. +// Fix_Perms ensures that grants are properly intact. func (db *PfDB) Fix_Perms() (err error) { var qs = []PfQuery{ /* @@ -1030,6 +1168,7 @@ func (db *PfDB) Fix_Perms() (err error) { return } +// Setup_psql creates the PostgeSQL specifics permissions, languag, user and database func (db *PfDB) Setup_psql() (err error) { var qs = []PfQuery{ {"CREATE LANGUAGE plpgsql", @@ -1067,6 +1206,7 @@ func (db *PfDB) Setup_psql() (err error) { return } +// Setup_DB configures the database and upgrades it where needed. func (db *PfDB) Setup_DB() (err error) { /* Connect to the *tool* database as the postgres user */ err = DB.connect_pg(Config.Db_name) @@ -1091,7 +1231,7 @@ func (db *PfDB) Setup_DB() (err error) { } /* - * "Execute" a .psql file with SQL commands + * executeFile can be used to * "Execute" a .psql file with SQL commands. * * These queries are *NOT* audit-logged * Only code that should call this are DB upgrade scripts @@ -1167,7 +1307,10 @@ func (db *PfDB) executeFile(schemafilename string) (err error) { return } -/* Upgrade from schema in database to latest version by executing the relevant files */ +// upgradedb is an internal function used to upgrade the database schema. +// +// Upgrade from schema in database to latest version by executing the relevant files. +// Both system (systemdb = true) or applcation schema can be upgraded. func (db *PfDB) upgradedb(systemdb bool, systemver int) (err error) { var ver int var name string @@ -1244,15 +1387,17 @@ func (db *PfDB) upgradedb(systemdb bool, systemver int) (err error) { return } +// Upgrade upgrades the system database to the current required schema version func (db *PfDB) Upgrade() (err error) { return db.upgradedb(true, 0) } +// AppUpgrade upgrades the application database to the current required schema version func (db *PfDB) AppUpgrade() (err error) { return db.upgradedb(false, db.appversion) } -/* Simple query builder */ +// Q_AddArg is part of the Simple query builder - it adds a argument to the given query string and argument list func (db *PfDB) Q_AddArg(q *string, args *[]interface{}, arg interface{}) { if arg != nil { *args = append(*args, arg) @@ -1261,6 +1406,18 @@ func (db *PfDB) Q_AddArg(q *string, args *[]interface{}, arg interface{}) { *q += "$" + strconv.Itoa((len(*args))) + " " } +// Q_AddWhere is part of the Simple query builder. +// +// It adds a 'where' argument to the given query string and argument list, optionally using WHERE/AND/OR to tie it in. +// +// q = existing query string to append to +// args = existing argument array +// str = the column to match +// op = the operand to use for comparing the column +// arg = what to compare the column agains +// and = whether to 'AND' the WHERE clause +// multi = whether multiple 'AND's are concatenated +// argoffset = how many args where used before and thus not part of the 'where' clause func (db *PfDB) Q_AddWhere(q *string, args *[]interface{}, str string, op string, arg interface{}, and bool, multi bool, argoffset int) { if len(*args) <= argoffset { *q += " WHERE " @@ -1282,30 +1439,32 @@ func (db *PfDB) Q_AddWhere(q *string, args *[]interface{}, str string, op string db.Q_AddArg(q, args, arg) } +// Q_AddMultiClose is used to end a previously opened multi-and/or construct. func (db *PfDB) Q_AddMultiClose(q *string) { *q += ")" } +// Q_AddWhereOpAnd is used to add a "... AND str op arg" construct. func (db *PfDB) Q_AddWhereOpAnd(q *string, args *[]interface{}, str string, op string, arg interface{}) { db.Q_AddWhere(q, args, str, op, arg, true, false, 0) } +// Q_AddWhereOpAnd is used to add a "... AND str = arg" construct. func (db *PfDB) Q_AddWhereAnd(q *string, args *[]interface{}, str string, arg interface{}) { db.Q_AddWhere(q, args, str, "=", arg, true, false, 0) } +// Q_AddWhereOr is used to add a "... OR str = arg" construct. func (db *PfDB) Q_AddWhereOr(q *string, args *[]interface{}, str string, arg interface{}) { db.Q_AddWhere(q, args, str, "=", arg, false, false, 0) } +// Q_AddWhereAndN is used to add a "... AND str = arg" construct, not adding the argument to args. func (db *PfDB) Q_AddWhereAndN(q *string, args *[]interface{}, str string) { db.Q_AddWhere(q, args, str, "=", nil, true, false, 0) } +// Q_AddWhereOrN is used to add a "... OR str = arg" construct, not adding the argument to args. func (db *PfDB) Q_AddWhereOrN(q *string, args *[]interface{}, str string) { db.Q_AddWhere(q, args, str, "=", nil, false, false, 0) } - -func NI64(n int64) sql.NullInt64 { - return sql.NullInt64{Int64: n, Valid: true} -} diff --git a/lib/detail.go b/lib/detail.go index 1935bbf..64e974b 100644 --- a/lib/detail.go +++ b/lib/detail.go @@ -1,3 +1,4 @@ +// Pitchfork detail manages user's details package pitchfork import ( @@ -5,16 +6,19 @@ import ( "strings" ) +// PfDetail contains the type and displayname of a detail type PfDetail struct { Type string DisplayName string } +// ToString returns a string describing the detail func (td *PfDetail) ToString() (out string) { out = td.Type + " " + td.DisplayName return } +// DetailType returns the type based on a string func DetailType(detail string) (out string) { out = detail @@ -26,6 +30,7 @@ func DetailType(detail string) (out string) { return } +// DetailCheck checks if a detail is a valid detail func DetailCheck(detail string) (err error) { /* Verify that detail is a valid detail */ details, err := DetailList() @@ -44,6 +49,7 @@ func DetailCheck(detail string) (err error) { return } +// DetailList returns a list of possible details func DetailList() (details []PfDetail, err error) { q := "SELECT " + "type, " + @@ -72,6 +78,7 @@ func DetailList() (details []PfDetail, err error) { return } +// detail_new creates a new detail (CLI) func detail_new(ctx PfCtx, args []string) (err error) { type_name := args[0] type_descr := args[1] @@ -92,6 +99,7 @@ func detail_new(ctx PfCtx, args []string) (err error) { return } +// detail_list lists all possible details (CLI) func detail_list(ctx PfCtx, args []string) (err error) { details, err := DetailList() if err != nil { @@ -107,6 +115,7 @@ func detail_list(ctx PfCtx, args []string) (err error) { return } +// detail_menu provides the CLI menu for the details (CLI) func detail_menu(ctx PfCtx, args []string) (err error) { menu := NewPfMenu([]PfMEntry{ {"list", detail_list, 0, -1, nil, PERM_USER, "List details types"}, diff --git a/lib/diff.go b/lib/diff.go index f112e7c..39e1c37 100644 --- a/lib/diff.go +++ b/lib/diff.go @@ -1,3 +1,4 @@ +// Pitchfork diff is a simple diff abstration layer package pitchfork import ( @@ -5,12 +6,14 @@ import ( "strings" ) +// PfDiff contains the common portion, left and right differences type PfDiff struct { Common string Left string Right string } +// diff_do causes a diff to be made using difflib (internal) func diff_do(a string, b string) (diff []difflib.DiffRecord) { tA := strings.Split(a, "\n") tB := strings.Split(b, "\n") @@ -18,6 +21,7 @@ func diff_do(a string, b string) (diff []difflib.DiffRecord) { return difflib.Diff(tA, tB) } +// Diff_Out takes two strings and outputs the differences func Diff_Out(ctx PfCtx, a string, b string) { df := diff_do(a, b) @@ -38,6 +42,7 @@ func Diff_Out(ctx PfCtx, a string, b string) { } } +// DoDiff generates the difference between two strings, returning the result in a PfDiff func DoDiff(a string, b string) (diff []PfDiff) { df := diff_do(a, b) diff --git a/lib/doc.go b/lib/doc.go new file mode 100644 index 0000000..006a4ae --- /dev/null +++ b/lib/doc.go @@ -0,0 +1,8 @@ +/* +Pitchfork is a Golang framework for secure communication platforms. + +The pitchfork 'lib' is the core of Pitchfork. + +Website: https://trident.li/ +*/ +package pitchfork diff --git a/lib/file.go b/lib/file.go index e854a7a..a87c739 100755 --- a/lib/file.go +++ b/lib/file.go @@ -1,3 +1,4 @@ +// Pitchfork file is a file management module package pitchfork import ( @@ -15,15 +16,21 @@ import ( "time" ) +// File_Perms_Dir is the permission mask used for new directories. const File_Perms_Dir os.FileMode = 0700 + +// File_Perms_File is the permission mask used for new files. const File_Perms_File os.FileMode = 0600 +// ErrFilePathExists is he error returned when a path/file already exists. var ErrFilePathExists = errors.New("Path already exists") +// PfFileOpts is used as the ModRoot for file operations type PfFileOpts struct { PfModOptsS } +// File_GetModOpts is used to fetch the ModOpts out of the Context func File_GetModOpts(ctx PfCtx) PfFileOpts { mopts := ctx.GetModOpts() if mopts == nil { @@ -42,15 +49,18 @@ func File_GetModOpts(ctx PfCtx) PfFileOpts { return output } +// File_ModOpts should be used to set the default ModOpts func File_ModOpts(ctx PfCtx, cmdpfx string, path_root string, web_root string) { ctx.SetModOpts(PfFileOpts{PfModOpts(ctx, cmdpfx, path_root, web_root)}) } +// file_ApplyModOpts applies the module options to a path func file_ApplyModOpts(ctx PfCtx, path string) string { mopts := File_GetModOpts(ctx) return URL_Append(mopts.Pathroot, path) } +// PfFile contains a single entry describing a file along with all details type PfFile struct { File_id int `pfcol:"id" pftable:"file"` Path string `pfcol:"path" pftable:"file_namespace"` @@ -68,11 +78,13 @@ type PfFile struct { FullFileName string /* Not in the DB, see ApplyModOpts() */ } +// PfFileResult is used by the search interface for returning details about a file. type PfFileResult struct { Path string Snippet string } +// File_RevisionMax can be used to return the maximum revision of a given file. func File_RevisionMax(ctx PfCtx, path string) (total int, err error) { q := "SELECT COUNT(*) " + "FROM file_rev r " + @@ -87,6 +99,7 @@ func File_RevisionMax(ctx PfCtx, path string) (total int, err error) { return total, err } +// File_RevisionList can be used to retrieve the revisions for a file. func File_RevisionList(ctx PfCtx, path string, offset int, max int) (revs []PfFile, err error) { revs = nil var rows *Rows @@ -143,6 +156,7 @@ func File_RevisionList(ctx PfCtx, path string, offset int, max int) (revs []PfFi return } +// File_ChildPagesMax can be used to retrieve the number of files that are childs of the given path. func File_ChildPagesMax(ctx PfCtx, path string) (total int, err error) { var args []interface{} @@ -165,6 +179,7 @@ func File_ChildPagesMax(ctx PfCtx, path string) (total int, err error) { return } +// File_ChildPagesList can be used to retrieve the files that are childs of the given path. func File_ChildPagesList(ctx PfCtx, path string, offset int, max int) (paths []PfFile, err error) { paths = nil @@ -230,6 +245,7 @@ func File_ChildPagesList(ctx PfCtx, path string, offset int, max int) (paths []P return } +// PathOffset calculates the Path Offset func PathOffset(file_path string, dir_path string) (count int) { delta := strings.Replace(file_path, dir_path, "", 1) tpl := len(delta) - 1 @@ -239,6 +255,7 @@ func PathOffset(file_path string, dir_path string) (count int) { return strings.Count(delta, "/") } +// Fetch retrieves the details about a given path and optionally revision func (file *PfFile) Fetch(ctx PfCtx, path string, rev string) (err error) { path, err = file_chk_path(path) if err != nil { @@ -272,7 +289,7 @@ func (file *PfFile) Fetch(ctx PfCtx, path string, rev string) (err error) { return } -/* Add some stuff we do not store in the DB but are useful */ +// ApplyModOpts adds some details we do not store in the DB but are useful to have pre-generated. func (file *PfFile) ApplyModOpts(ctx PfCtx) { mopts := File_GetModOpts(ctx) root := mopts.Pathroot @@ -290,6 +307,7 @@ func (file *PfFile) ApplyModOpts(ctx PfCtx) { return } +// file_mimetype attempts in a very simple way to determine the mimetype of a file. func file_mimetype(path string) (mt string, err error) { /* TODO: We should use libmagic or so here, and then reject incorrect extensions */ ext := strings.ToLower(fp.Ext(path)) @@ -330,6 +348,7 @@ func file_mimetype(path string) (mt string, err error) { return } +// File_path_is_dir checks if a path is a directory or not. func File_path_is_dir(path string) (is_dir bool) { pl := len(path) @@ -340,6 +359,7 @@ func File_path_is_dir(path string) (is_dir bool) { return false } +// file_chk_path verifies that a path is sane, returning the filtered result, or error if unfixable. func file_chk_path(in string) (out string, err error) { /* Require something at least */ if in == "" { @@ -395,17 +415,15 @@ func file_chk_path(in string) (out string, err error) { return } -/* - * Generate random filename, using the real filename at the end. - * - * This way we ensure unique names but also allow the file - * to found again if it has to happen that somebody wants - * to go through all the raw files on the disk. - * - * We need unique names as the path can appear in multiple paths - * and have different files, while on the flipside the same - * file might be called differently in different paths. - */ +// file_path_to_local generates a random filename, using the real filename at the end. +// +// This way we ensure unique names but also allow the file +// to found again if it has to happen that somebody wants +// to go through all the raw files on the disk. +// +// We need unique names as the path can appear in multiple paths +// and have different files, while on the flipside the same +// file might be called differently in different paths. func file_path_to_local(path string) (local string, err error) { var pw PfPass var loops int @@ -451,6 +469,7 @@ func file_path_to_local(path string) (local string, err error) { return } +// file_dirname returns the physical edition of the directory func file_dirname(filename string) (dname string) { /* The root of our files storage */ dname = Config.Var_root + "files/" @@ -458,14 +477,17 @@ func file_dirname(filename string) (dname string) { return } +// file_filename returns the physical edition of the file func file_filename(filename string, rev int) (fname string) { fname = file_dirname(filename) fname += filename + ".r" + strconv.Itoa(rev) return } -const file_hash_chunk = 8192 // we settle for 8KB -/* SHA512 over a file */ +// file_hash_chunk configures our hash size: 8 KiB +const file_hash_chunk = 8192 + +// file_hash performs a SHA512 over a file. func file_hash(filename string) (hashstr string, err error) { file, err := os.Open(filename) if err != nil { @@ -494,6 +516,7 @@ func file_hash(filename string) (hashstr string, err error) { return } +// file_store stores the given file on disk and in our database. func file_store(ctx PfCtx, filename string, file_id int, rev int, file io.Reader) (err error) { var out *os.File var size int64 @@ -568,6 +591,7 @@ func file_store(ctx PfCtx, filename string, file_id int, rev int, file io.Reader return } +// file_add_entry adds the given file to the database, generating in-between directories till the root of the modroot. func file_add_entry(ctx PfCtx, ftype string, mimetype string, path string, description string, url string) (filename string, file_id int, rev int, err error) { var f PfFile @@ -670,6 +694,7 @@ func file_add_entry(ctx PfCtx, ftype string, mimetype string, path string, descr return } +// file_add_dir adds a directory to the tree (CLI) func file_add_dir(ctx PfCtx, args []string) (err error) { var f PfFile path := args[0] @@ -704,6 +729,7 @@ func file_add_dir(ctx PfCtx, args []string) (err error) { return } +// File_add_url adds a URL to the filetree (CLI) func File_add_url(ctx PfCtx, args []string) (err error) { path := args[0] description := args[1] @@ -723,9 +749,7 @@ func File_add_url(ctx PfCtx, args []string) (err error) { return } -/* - * Used by the UI directly and also CLI - */ +// File_add_file adds a file to the database (also used by UI directly, due to streaming of file) func File_add_file(ctx PfCtx, path string, description string, file io.Reader) (err error) { var filename string var file_id int @@ -766,13 +790,13 @@ func File_add_file(ctx PfCtx, path string, description string, file io.Reader) ( return } -/* - * !! CLI only !! - * - * Don't call through UI as it takes a local filename, don't want to read /etc/passwd ;) - * - * This is also why the function is marked as PERM_SYS_ADMIN - */ +// File_add_localfile adds a file from the local filesystem to the database. +// +// !! CLI only !! +// +// Don't call through UI as it takes a local filename, don't want to read /etc/passwd ;) +// +// This is also why the function is marked as PERM_SYS_ADMIN. func File_add_localfile(ctx PfCtx, args []string) (err error) { var file *os.File @@ -881,7 +905,9 @@ func File_add_localfile(ctx PfCtx, args []string) (err error) { return } -/* Called directly by UI and also CLI (TODO) */ +// File_Update updates an existing file with a new version. +// +// Called directly by UI and also CLI (TODO). func File_Update(ctx PfCtx, path string, desc string, changemsg string, file *os.File) (err error) { var rev int @@ -941,13 +967,13 @@ func File_Update(ctx PfCtx, path string, desc string, changemsg string, file *os return } -/* - * !! CLI only !! - * - * Don't call through UI as it takes a local filename, don't want to read /etc/passwd ;) - * - * This is also why the function is marked as PERM_SYS_ADMIN - */ +// file_update updates a file. +// +// !! CLI only !! +// +// Don't call through UI as it takes a local filename, don't want to read /etc/passwd ;) +// +// This is also why the function is marked as PERM_SYS_ADMIN func file_update(ctx PfCtx, args []string) (err error) { var file *os.File @@ -970,6 +996,7 @@ func file_update(ctx PfCtx, args []string) (err error) { return } +// file_get retrieves details of a file func file_get(ctx PfCtx, args []string) (err error) { path := args[0] @@ -997,6 +1024,7 @@ func file_get(ctx PfCtx, args []string) (err error) { return } +// file_list lists the details of a file (CLI) func file_list(ctx PfCtx, args []string) (err error) { path := args[0] @@ -1027,6 +1055,7 @@ func file_list(ctx PfCtx, args []string) (err error) { return } +// file_move moves a file around, can be used for renaming too (CLI) func file_move(ctx PfCtx, args []string) (err error) { mopts := File_GetModOpts(ctx) root := mopts.Pathroot @@ -1103,6 +1132,7 @@ func file_move(ctx PfCtx, args []string) (err error) { return nil } +// File_delete removes a file from the tree, optionally including all children. func File_delete(ctx PfCtx, path string, children bool) (cnt int, err error) { cnt = 0 mopts := File_GetModOpts(ctx) @@ -1160,6 +1190,7 @@ func File_delete(ctx PfCtx, path string, children bool) (cnt int, err error) { return } +// file_delete removes a file (CLI) func file_delete(ctx PfCtx, args []string) (err error) { path := args[0] children := args[1] @@ -1177,6 +1208,7 @@ func file_delete(ctx PfCtx, args []string) (err error) { return } +// file_copy copies a file (CLI) func file_copy(ctx PfCtx, args []string) (err error) { mopts := File_GetModOpts(ctx) root := mopts.Pathroot @@ -1254,6 +1286,7 @@ func file_copy(ctx PfCtx, args []string) (err error) { return nil } +// File_menu is the entry point of the file module CLI, called after setting the ModOpts (CLI) func File_menu(ctx PfCtx, args []string) (err error) { var menu = NewPfMenu([]PfMEntry{ {"add_dir", file_add_dir, 2, 2, []string{"path", "description"}, PERM_USER, "Add a directory"}, diff --git a/lib/file_search.go b/lib/file_search.go index eaa67fa..41f38c7 100644 --- a/lib/file_search.go +++ b/lib/file_search.go @@ -1,5 +1,8 @@ package pitchfork +// File_search provides a search interface for searching files. +// +// It searches for the given text in the description and the path name. func File_search(ctx PfCtx, pathroot string, c chan PfSearchResult, search string, abort <-chan bool) (err error) { /* XXX: Namespace limiter (Groups) */ q := "SELECT n.path, n.path, r.description " + diff --git a/lib/file_test.go b/lib/file_test.go index 61ab6f6..54d75d7 100644 --- a/lib/file_test.go +++ b/lib/file_test.go @@ -4,6 +4,7 @@ import ( "testing" ) +// TestPathOffset tests the File module's pathOffset function. func TestPathOffset(t *testing.T) { tsts := []struct { obj_path string @@ -31,6 +32,7 @@ func TestPathOffset(t *testing.T) { } +// TestFileChkPath tests the File module's file_chk_path() function. func TestFileChkPath(t *testing.T) { tsts := []struct { path string diff --git a/lib/group.go b/lib/group.go index 0022e4c..68e7380 100644 --- a/lib/group.go +++ b/lib/group.go @@ -9,6 +9,7 @@ const ( GROUP_STATE_BLOCKED = "blocked" ) +// PfGroup exposes the functions available for modifying Pitchfork Groups type PfGroup interface { String() string GetGroupName() string @@ -34,6 +35,7 @@ type PfGroup interface { GetVcards() (vcard string, err error) } +// PfGroupS is the standard implementation for a Pitchfork Group type PfGroupS struct { GroupName string `label:"Group Name" pfset:"nobody" pfget:"group_member" pfcol:"ident"` GroupDesc string `label:"Description" pfcol:"descr" pfset:"group_admin"` @@ -44,6 +46,7 @@ type PfGroupS struct { Button string `label:"Update Group" pftype:"submit"` } +// PfMemberState provides details about the state of a member of a group type PfMemberState struct { ident string can_login bool @@ -54,35 +57,42 @@ type PfMemberState struct { hidden bool } -/* Should not be directly called, use ctx or cui.NewGroup() instead */ +// NewPfGroup can be used to create a new group - should not be directly called, use ctx or cui.NewGroup() instead. func NewPfGroup() PfGroup { return &PfGroupS{} } +// String returns the name of the group func (grp *PfGroupS) String() string { return grp.GroupName } +// GetGroupName gets the name of the group. func (grp *PfGroupS) GetGroupName() string { return grp.GroupName } +// GetGroupDesc gets the description of a group. func (grp *PfGroupS) GetGroupDesc() string { return grp.GroupDesc } +// HasWiki returns if the group has a wiki configured. func (grp *PfGroupS) HasWiki() bool { return grp.Has_Wiki } +// HasFile returns if the group has a file module configured. func (grp *PfGroupS) HasFile() bool { return grp.Has_File } +// HasCalendar returns if the group has a calendar configured. func (grp *PfGroupS) HasCalendar() bool { return grp.Has_Calendar } +// fetch retrieves a group by name from the database. func (grp *PfGroupS) fetch(group_name string, nook bool) (err error) { /* Make sure the name is mostly sane */ group_name, err = Chk_ident("Group Name", group_name) @@ -104,11 +114,13 @@ func (grp *PfGroupS) fetch(group_name string, nook bool) (err error) { return } +// Refresh refreshes details about a group from the database. func (grp *PfGroupS) Refresh() (err error) { err = grp.fetch(grp.GroupName, false) return } +// Exists returns whether a group exists or not. func (grp *PfGroupS) Exists(group_name string) (exists bool) { err := grp.fetch(group_name, true) if err == ErrNoRows { @@ -118,6 +130,7 @@ func (grp *PfGroupS) Exists(group_name string) (exists bool) { return true } +// Select selects a group and returns it, depending on existence and permissions. func (grp *PfGroupS) Select(ctx PfCtx, group_name string, perms Perm) (err error) { err = grp.fetch(group_name, false) if err != nil { @@ -144,10 +157,9 @@ func (grp *PfGroupS) Select(ctx PfCtx, group_name string, perms Perm) (err error return } -/* - * Return the set of groups that the username is connected to - * If active is set nominations will also appear - */ +// GetGroups returns the set of groups that username is connected to. +// +// If active is set nominations will also appear. func (grp *PfGroupS) GetGroups(ctx PfCtx, username string) (members []PfGroupMember, err error) { var rows *Rows members = nil @@ -181,6 +193,7 @@ func (grp *PfGroupS) GetGroups(ctx PfCtx, username string) (members []PfGroupMem return } +// GetGroupsAll returns all groups. func (grp *PfGroupS) GetGroupsAll() (members []PfGroupMember, err error) { members = nil @@ -212,6 +225,7 @@ func (grp *PfGroupS) GetGroupsAll() (members []PfGroupMember, err error) { return } +// GetKeys returns the keyfile for a group func (grp *PfGroupS) GetKeys(ctx PfCtx, keyset map[[16]byte][]byte) (err error) { var ml PfML mls, err := ml.ListWithUser(ctx, grp, ctx.SelectedUser()) @@ -248,6 +262,7 @@ func (grp *PfGroupS) GetKeys(ctx PfCtx, keyset map[[16]byte][]byte) (err error) return } +// IsMember tests if the given username is a member of the group. func (grp *PfGroupS) IsMember(user string) (ismember bool, isadmin bool, out PfMemberState, err error) { ismember = false isadmin = false @@ -283,6 +298,7 @@ func (grp *PfGroupS) IsMember(user string) (ismember bool, isadmin bool, out PfM return } +// ListGroupMembersTot gets the total number of members matching the given search string. func (grp *PfGroupS) ListGroupMembersTot(search string) (total int, err error) { q := "SELECT COUNT(*) " + "FROM member_trustgroup mt " + @@ -305,7 +321,10 @@ func (grp *PfGroupS) ListGroupMembersTot(search string) (total int, err error) { return total, err } -/* Note: This implementation does not use the 'username' variable, but other implementations might */ +// GetMembers returns the members of a group based on search parameters and username +// +// TODO need to allow admins to see hidden users (blocked) +// Note: This implementation does not use the 'username' variable, but other implementations might func (grp *PfGroupS) ListGroupMembers(search string, username string, offset int, max int, nominated bool, inclhidden bool, exact bool) (members []PfGroupMember, err error) { var rows *Rows @@ -383,6 +402,7 @@ func (grp *PfGroupS) ListGroupMembers(search string, username string, offset int return } +// Add_default_mailinglists adds the default mailing lists to a group. func (grp *PfGroupS) Add_default_mailinglists(ctx PfCtx) (err error) { mls := make(map[string]string) mls["admin"] = "Group Administration" @@ -401,6 +421,7 @@ func (grp *PfGroupS) Add_default_mailinglists(ctx PfCtx) (err error) { return } +// group_add adds a group to the system. func group_add(ctx PfCtx, args []string) (err error) { var group_name string @@ -458,6 +479,7 @@ func group_add(ctx PfCtx, args []string) (err error) { return } +// group_remove removes a group from the system. func group_remove(ctx PfCtx, args []string) (err error) { q := "DELETE FROM trustgroup " + "WHERE ident = $1" @@ -469,6 +491,7 @@ func group_remove(ctx PfCtx, args []string) (err error) { return } +// group_list provides a way to list all the groups in a system. func group_list(ctx PfCtx, args []string) (err error) { grp := ctx.NewGroup() user := ctx.TheUser().GetUserName() @@ -498,6 +521,7 @@ func group_list(ctx PfCtx, args []string) (err error) { return } +// group_member_list lists the members sof a group. func group_member_list(ctx PfCtx, args []string) (err error) { grp := ctx.SelectedGroup() tmembers, err := grp.ListGroupMembers("", ctx.TheUser().GetUserName(), 0, 0, false, ctx.IAmGroupAdmin(), false) @@ -513,6 +537,7 @@ func group_member_list(ctx PfCtx, args []string) (err error) { return } +// group_member_auto_ml automatically subscribes users to the default mailinglists of a group. func group_member_auto_ml(ctx PfCtx, user PfUser) (err error) { var rows *Rows @@ -556,6 +581,7 @@ func group_member_auto_ml(ctx PfCtx, user PfUser) (err error) { return } +// Member_add adds a member to the group. func (grp *PfGroupS) Member_add(ctx PfCtx) (err error) { var email PfUserEmail @@ -601,11 +627,13 @@ func (grp *PfGroupS) Member_add(ctx PfCtx) (err error) { return } +// group_member_add is the CLI interface for adding a member to a group. func group_member_add(ctx PfCtx, args []string) (err error) { grp := ctx.SelectedGroup() return grp.Member_add(ctx) } +// Member_remove removes a member from a group. func (grp *PfGroupS) Member_remove(ctx PfCtx) (err error) { user := ctx.SelectedUser() @@ -635,11 +663,13 @@ func (grp *PfGroupS) Member_remove(ctx PfCtx) (err error) { return } +// group_member_remove is the CLI interface for removing a member from a group. (CLI) func group_member_remove(ctx PfCtx, args []string) (err error) { grp := ctx.SelectedGroup() return grp.Member_remove(ctx) } +// Member_set_state changes the state for a member of a group. func (grp *PfGroupS) Member_set_state(ctx PfCtx, state string) (err error) { user := ctx.SelectedUser() @@ -664,21 +694,25 @@ func (grp *PfGroupS) Member_set_state(ctx PfCtx, state string) (err error) { return } +// group_member_approve approves a member of a group func group_member_approve(ctx PfCtx, args []string) (err error) { grp := ctx.SelectedGroup() return grp.Member_set_state(ctx, GROUP_STATE_APPROVED) } +// group_member_block blocks a member of a group func group_member_block(ctx PfCtx, args []string) (err error) { grp := ctx.SelectedGroup() return grp.Member_set_state(ctx, GROUP_STATE_BLOCKED) } +// group_member_unblock unblocks a member by approving them again func group_member_unblock(ctx PfCtx, args []string) (err error) { /* Returns state to 'approved' */ return group_member_approve(ctx, args) } +// Member_set_admin sets/unsets the admin bit of a member func (grp *PfGroupS) Member_set_admin(ctx PfCtx, isadmin bool) (err error) { if !ctx.IAmGroupAdmin() { err = errors.New("Not a group admin") @@ -701,6 +735,7 @@ func (grp *PfGroupS) Member_set_admin(ctx PfCtx, isadmin bool) (err error) { return } +// GetVcards returns the vcards for the members of a group func (grp *PfGroupS) GetVcards() (vcard string, err error) { members, err := grp.ListGroupMembers("", "", 0, 0, false, false, false) if err != nil { @@ -724,16 +759,19 @@ func (grp *PfGroupS) GetVcards() (vcard string, err error) { return } +// group_member_promote promotes a member func group_member_promote(ctx PfCtx, args []string) (err error) { grp := ctx.SelectedGroup() return grp.Member_set_admin(ctx, true) } +// group_member_demote demotes a member func group_member_demote(ctx PfCtx, args []string) (err error) { grp := ctx.SelectedGroup() return grp.Member_set_admin(ctx, false) } +// group_member is the CLI menu for group member manipulation func group_member(ctx PfCtx, args []string) (err error) { var menu = NewPfMenu([]PfMEntry{ {"list", group_member_list, 1, 1, []string{"group"}, PERM_GROUP_MEMBER, "List members of this group"}, @@ -772,6 +810,7 @@ func group_member(ctx PfCtx, args []string) (err error) { return } +// group_set_xxx configures a property of a group func group_set_xxx(ctx PfCtx, args []string) (err error) { /* * args[.] == what, dropped by ctx.Menu() @@ -786,6 +825,7 @@ func group_set_xxx(ctx PfCtx, args []string) (err error) { return } +// group_sget sets or gets a property of a group func group_sget(ctx PfCtx, args []string, fun PfFunc) (err error) { grp := ctx.NewGroup() @@ -819,14 +859,17 @@ func group_sget(ctx PfCtx, args []string, fun PfFunc) (err error) { return } +// group_set allows setting a property of a group func group_set(ctx PfCtx, args []string) (err error) { return group_sget(ctx, args, group_set_xxx) } +// group_get allows retrieving a property of a group func group_get(ctx PfCtx, args []string) (err error) { return group_sget(ctx, args, nil) } +// Group_FileMod sets the ModOpts for a group's File Module. func Group_FileMod(ctx PfCtx) { grp := ctx.SelectedGroup() grpname := grp.GetGroupName() @@ -835,6 +878,7 @@ func Group_FileMod(ctx PfCtx) { File_ModOpts(ctx, "group file "+grpname, "/group/"+grpname, "/group/"+grpname+"/file") } +// group_file provides CLI access to a group's File Module (CLI). func group_file(ctx PfCtx, args []string) (err error) { grname := args[0] @@ -849,6 +893,7 @@ func group_file(ctx PfCtx, args []string) (err error) { return File_menu(ctx, args[1:]) } +// Group_WikiMod sets the ModOpts for a group's Wiki Module func Group_WikiMod(ctx PfCtx) { grp := ctx.SelectedGroup() grpname := grp.GetGroupName() @@ -857,6 +902,7 @@ func Group_WikiMod(ctx PfCtx) { Wiki_ModOpts(ctx, "group wiki "+grpname, "/group/"+grpname, "/group/"+grpname+"/wiki") } +// group_wiki provides access to the group's wiki. (CLI) func group_wiki(ctx PfCtx, args []string) (err error) { grname := args[0] @@ -871,6 +917,7 @@ func group_wiki(ctx PfCtx, args []string) (err error) { return Wiki_menu(ctx, args[1:]) } +// group_vcards returns the vcards of a group. func group_vcards(ctx PfCtx, args []string) (err error) { grname := args[0] @@ -891,6 +938,7 @@ func group_vcards(ctx PfCtx, args []string) (err error) { return } +// group_menu is the CLI access for Group configuration and details. (CLI) func group_menu(ctx PfCtx, args []string) (err error) { menu := NewPfMenu([]PfMEntry{ {"add", group_add, 1, 1, []string{"group"}, PERM_SYS_ADMIN, "Add a new group"}, diff --git a/lib/groupmember.go b/lib/groupmember.go index ee51e0e..d797a73 100644 --- a/lib/groupmember.go +++ b/lib/groupmember.go @@ -1,5 +1,6 @@ package pitchfork +// PfGroupMember provides an interface for modifying a Group Member type PfGroupMember interface { SQL_Selects() string SQL_Froms() string @@ -23,6 +24,7 @@ type PfGroupMember interface { GetAirport() string } +// PfGroupMemberS is the implementation of a PfGroupMember type PfGroupMemberS struct { UserName string FullName string @@ -41,10 +43,12 @@ type PfGroupMemberS struct { Airport string } +// NewPfGroupMember creates a new PfGroupMember func NewPfGroupMember() PfGroupMember { return &PfGroupMemberS{} } +// SQL_Selects returns the SQL SELECT statement needed to query common properties func (grpm *PfGroupMemberS) SQL_Selects() (q string) { return "SELECT " + "m.ident, " + @@ -64,6 +68,7 @@ func (grpm *PfGroupMemberS) SQL_Selects() (q string) { "m.airport" } +// SQL_Froms returns the SQL FROM portion func (grpm *PfGroupMemberS) SQL_Froms() string { return "FROM member_trustgroup mt " + "INNER JOIN trustgroup grp ON (mt.trustgroup = grp.ident) " + @@ -72,6 +77,7 @@ func (grpm *PfGroupMemberS) SQL_Froms() string { "INNER JOIN member_email me ON (me.email = mt.email)" } +// SQL_Scan scans a Row for GroupMembers func (grpm *PfGroupMemberS) SQL_Scan(rows *Rows) (err error) { return rows.Scan( &grpm.UserName, @@ -91,6 +97,7 @@ func (grpm *PfGroupMemberS) SQL_Scan(rows *Rows) (err error) { &grpm.Airport) } +// Set configures properties of a groupmember func (grpm *PfGroupMemberS) Set(groupname, groupdesc, username, fullname, affiliation string, groupadmin bool, groupstate string, cansee bool, email, pgpkey_id, entered, activity, telephone, sms, airport string) { grpm.GroupName = groupname grpm.GroupDesc = groupdesc @@ -109,66 +116,82 @@ func (grpm *PfGroupMemberS) Set(groupname, groupdesc, username, fullname, affili grpm.Airport = airport } +// GetGroupName returns the name of the group. func (grpm *PfGroupMemberS) GetGroupName() string { return grpm.GroupName } +// returns the group description. func (grpm *PfGroupMemberS) GetGroupDesc() string { return grpm.GroupDesc } +// GetUserName returns the username of the groupmember. func (grpm *PfGroupMemberS) GetUserName() string { return grpm.UserName } +// GetFullName gets the full name of the groupmember. func (grpm *PfGroupMemberS) GetFullName() string { return grpm.FullName } +// Returns the email address of a groupmember. func (grpm *PfGroupMemberS) GetEmail() string { return grpm.Email } +// GetAffiliation gets the affiliation of a group member func (grpm *PfGroupMemberS) GetAffiliation() string { return grpm.Affiliation } +// GetGroupAdmin sets the group admin bit func (grpm *PfGroupMemberS) GetGroupAdmin() bool { return grpm.GroupAdmin } +// GetGroupState gets the state of a group func (grpm *PfGroupMemberS) GetGroupState() string { return grpm.GroupState } +// Returns the CanSee attribute of a groupmember. func (grpm *PfGroupMemberS) GetGroupCanSee() bool { return grpm.GroupCanSee } +// Returns the PGPKeyID of a groupmember. func (grpm *PfGroupMemberS) GetPGPKeyID() string { return grpm.PGPKeyID } +// Returns whether the user has a PGP key. func (grpm *PfGroupMemberS) HasPGP() bool { return grpm.PGPKeyID != "" } +// Returns the 'entered' attribute of a groupmember. func (grpm *PfGroupMemberS) GetEntered() string { return grpm.Entered } +// Returns the last activity of a groupmember. func (grpm *PfGroupMemberS) GetActivity() string { return grpm.Activity } +// Returns the Telephone number of a groupmember. func (grpm *PfGroupMemberS) GetTel() string { return grpm.Tel } +// Returns the SMS number of a groupmember. func (grpm *PfGroupMemberS) GetSMS() string { return grpm.SMS } +// Returns the Airport code of a groupmember. func (grpm *PfGroupMemberS) GetAirport() string { return grpm.Airport } diff --git a/lib/helper_test.go b/lib/helper_test.go index 6d2ff3a..75ee847 100644 --- a/lib/helper_test.go +++ b/lib/helper_test.go @@ -1,5 +1,6 @@ package pitchfork +// testingctx is a helper function for testing to set up a Pitchfork Context for testing. func testingctx() PfCtx { return NewPfCtx(nil, nil, nil, nil, nil) } diff --git a/lib/iptrk.go b/lib/iptrk.go index 28a83a6..995f701 100644 --- a/lib/iptrk.go +++ b/lib/iptrk.go @@ -1,3 +1,4 @@ +// Pitchfork iptrk (IP address Track) is used to track IP addresses - eg for repeated false password entries. package pitchfork /* @@ -11,6 +12,7 @@ import ( "time" ) +// IPtrkEntry keeps a entry for the IP tracking type IPtrkEntry struct { Blocked bool IP string @@ -19,17 +21,19 @@ type IPtrkEntry struct { Last time.Time } +// IPtrkS is the command structure used to communicate with the goprocess that handles database entries. type IPtrkS struct { cmd string ip string chn chan bool } -var IPtrk_Max int -var IPtrk chan IPtrkS -var IPtrk_done chan bool -var IPtrk_running bool +var IPtrk_Max int // The Maximum amount of entries allowed per IP +var IPtrk chan IPtrkS // The channel used for communication with the goproc +var IPtrk_done chan bool // Signals whether the process is 'done' and should end +var IPtrk_running bool // Whether the goproc is running +// iptrk_add indicates a hit for the given IP address, returning whether it is blocked or not. func iptrk_add(ip string) (ret bool) { ret = true @@ -84,6 +88,7 @@ func iptrk_add(ip string) (ret bool) { return } +// iptrk_expire expires entries from the tracking database for the given time interval. func iptrk_expire(t string) bool { Dbgf("Expiring") @@ -97,6 +102,7 @@ func iptrk_expire(t string) bool { return true } +// iptrk_flush flushes entries for a given address or all when not provided. func iptrk_flush(ip string) bool { var err error @@ -118,7 +124,7 @@ func iptrk_flush(ip string) bool { return true } -/* Go routine that manages the ip tracking */ +// iptrk_rtn is the Go routine that manages the actual IP Tracking reading from the command channel so that queries are serialized. func iptrk_rtn(timeoutchk time.Duration, expire string) { IPtrk_running = true @@ -170,6 +176,7 @@ func iptrk_rtn(timeoutchk time.Duration, expire string) { IPtrk_done <- true } +// iptrk_cmd can be used to send commands to the iptrk_rtn goproc. func iptrk_cmd(cmd string, ip string) (ret bool) { /* Create result channel */ chn := make(chan bool) @@ -181,6 +188,7 @@ func iptrk_cmd(cmd string, ip string) (ret bool) { return } +// Iptrk_count can be request if a IP has been blocked. func Iptrk_count(ip string) (limited bool) { if IPtrk_running { limited = iptrk_cmd("add", ip) @@ -191,6 +199,7 @@ func Iptrk_count(ip string) (limited bool) { return } +// Iptrk_start starts the goproc used for serializing queries. func Iptrk_start(max int, timeoutchk time.Duration, expire string) { IPtrk = make(chan IPtrkS, 1000) IPtrk_done = make(chan bool) @@ -199,6 +208,7 @@ func Iptrk_start(max int, timeoutchk time.Duration, expire string) { go iptrk_rtn(timeoutchk, expire) } +// Iptrk_stop stops the goproc for serializing queries. func Iptrk_stop() { if !IPtrk_running { return @@ -211,6 +221,7 @@ func Iptrk_stop() { <-IPtrk_done } +// Iptrk_reset flushes the database for a given IP or all entries. func Iptrk_reset(ip string) (ret bool) { if IPtrk_running { ret = iptrk_cmd("flush", ip) @@ -220,6 +231,7 @@ func Iptrk_reset(ip string) (ret bool) { return } +// IPtrk_List provides an interface to listing the entries in the database. func IPtrk_List(ctx PfCtx) (ts []IPtrkEntry, err error) { q := "SELECT " + "ip, count, entered, last " + @@ -252,6 +264,7 @@ func IPtrk_List(ctx PfCtx) (ts []IPtrkEntry, err error) { return } +// iptrk_list is the CLI interface for the listing of IPs in the database. func iptrk_list(ctx PfCtx, args []string) (err error) { ts, err := IPtrk_List(ctx) @@ -279,12 +292,14 @@ func iptrk_list(ctx PfCtx, args []string) (err error) { return } +// iptrk_flushcmd is the CLI interface for flushing the database. func iptrk_flushcmd(ctx PfCtx, args []string) (err error) { Iptrk_reset("") ctx.OutLn("IPtrk flushed") return } +// iptrk_remove is the CLI interface for removing an IP from the database. func iptrk_remove(ctx PfCtx, args []string) (err error) { ip := args[0] @@ -303,6 +318,7 @@ func iptrk_remove(ctx PfCtx, args []string) (err error) { return } +// iptrk_menu is the CLI menu for IPtrk. func iptrk_menu(ctx PfCtx, args []string) (err error) { menu := NewPfMenu([]PfMEntry{ {"list", iptrk_list, 0, 0, nil, PERM_SYS_ADMIN, "List the contents of the IPtrk tables"}, diff --git a/lib/iptrk_test.go b/lib/iptrk_test.go index 32cffcd..30f8e7c 100644 --- a/lib/iptrk_test.go +++ b/lib/iptrk_test.go @@ -1,3 +1,4 @@ +// Pitchfork IPTrk Testing package pitchfork /* @@ -12,6 +13,7 @@ import ( "time" ) +// addip adds an IP to the database and checks if that gets blocked or not. func addip(t *testing.T, ip string, notlim bool) { lim := Iptrk_count(ip) @@ -34,6 +36,7 @@ func addip(t *testing.T, ip string, notlim bool) { return } +// SixthShouldFail checks if the 6th entry fails as it is then blocked. func SixthShouldFail(t *testing.T, ip string) { max := 5 @@ -53,14 +56,17 @@ func SixthShouldFail(t *testing.T, ip string) { addip(t, ip, false) } +// TestIPTtrkSixthShouldFail_v4 test for IPv4 address blocking. func TestIPTtrkSixthShouldFail_v4(t *testing.T) { SixthShouldFail(t, "192.0.2.4") } +// TestIPTtrkSixthShouldFail_v4 test for IPv6 address blocking. func TestIPtrkSixthShouldFail_v6(t *testing.T) { SixthShouldFail(t, "2001:db8::6") } +// TestIPtrkMix_v4v6 tests combo IPv4/IPv6 address blocking. func TestIPtrkMix_v4v6(t *testing.T) { max := 5 ip4 := "192.1.2.4" @@ -83,6 +89,7 @@ func TestIPtrkMix_v4v6(t *testing.T) { addip(t, ip6, false) } +// TestIPtrkFlush tests that flushing works. func TestIPtrkFlush(t *testing.T) { max := 5 ip6 := "2001:db8::6" @@ -113,6 +120,7 @@ func TestIPtrkFlush(t *testing.T) { addip(t, ip6, true) } +// TestIPtrkExpire tests that expiring works (takes a bit of time as it actually waits :) ). func TestIPtrkExpire(t *testing.T) { max := 5 ip6 := "2001:db8::6" diff --git a/lib/jwt.go b/lib/jwt.go index 0de527b..ee9be73 100755 --- a/lib/jwt.go +++ b/lib/jwt.go @@ -1,7 +1,6 @@ +// Pitchfork JWT wrapper module -- JWT helpers between Pitchfork and dgrijalva's JWT library. package pitchfork -/* JWT helpers between Pitchfork and dgrijalva's JWT library */ - import ( "errors" "fmt" @@ -13,27 +12,33 @@ import ( /* Default Token expiration time */ const TOKEN_EXPIRATIONMINUTES = 20 +// Token but wrapped for ease of use. type Token struct { *jwt.Token } +// GetClaims gets the claims from a token. func (tok *Token) GetClaims() JWTClaimI { return tok.Token.Claims.(JWTClaimI) } +// JWTClaimI wraps the claims for ease of use. type JWTClaimI interface { jwt.Claims GetJWTClaims() *JWTClaims } +// JWTClaims provides a standard set of claims. type JWTClaims struct { jwt.StandardClaims } +// GetJWTClaims returns the claims. func (jwtc *JWTClaims) GetJWTClaims() *JWTClaims { return jwtc } +// Token_New creates a new JWT token with provides arguments. func Token_New(ttype string, username string, expmins time.Duration, claims JWTClaimI) (token *Token) { now := time.Now() @@ -49,11 +54,13 @@ func Token_New(ttype string, username string, expmins time.Duration, claims JWTC return } +// Sign signs a token using our private key. func (token *Token) Sign() (tok string, err error) { tok, err = token.SignedString(Config.Token_prv) return } +// Token_Parse parses a token from a string, requiring a type and certain claims. func Token_Parse(tok string, ttype string, claims JWTClaimI) (expsoon bool, err error) { expsoon = false @@ -114,6 +121,7 @@ func Token_Parse(tok string, ttype string, claims JWTClaimI) (expsoon bool, err return } +// Token_LoadPrv loads our JWT private key. func (cfg *PfConfig) Token_LoadPrv() (err error) { var pem []byte @@ -134,6 +142,7 @@ func (cfg *PfConfig) Token_LoadPrv() (err error) { return } +// Token_LoadPub loads our JWT public key. func (cfg *PfConfig) Token_LoadPub() (err error) { var pem []byte diff --git a/lib/jwt_invalid.go b/lib/jwt_invalid.go index 05d6d86..73fb93d 100644 --- a/lib/jwt_invalid.go +++ b/lib/jwt_invalid.go @@ -1,13 +1,13 @@ +// Pitchfork's Invalid JWT tracker. package pitchfork -/* - * Note: jwt_invalid uses non-audit versions of DB queries, otherwise we would generate double traffic - * - * Invalid, but not expired-yet, tokens are stored in SQL. - * - * A in-go cache exists keeping a LRU of valid+invalid tokens - * to avoid hitting SQL all the time - */ +// Note: jwt_invalid uses non-audit versions of DB queries, +// otherwise we would generate double traffic (actual + audit). +// +// Invalid, but not expired-yet, tokens are stored in SQL. +// +// A in-go cache exists keeping a LRU of valid+invalid tokens +// to avoid hitting SQL all the time import ( "container/list" @@ -15,8 +15,10 @@ import ( "time" ) +// Memory Cached list of maximum 512 invalid tokens. const JWT_INVALID_CACHE_MAX = 512 +// jwtinvs structure contains the details for an invalid JWT token type jwtinvs struct { item *list.Element key string @@ -24,19 +26,20 @@ type jwtinvs struct { expiration int64 } -var jwtinv_cache map[string]jwtinvs -var jwtinv_list *list.List -var jwtinv_exit chan bool -var jwtinv_done chan bool -var jwtinv_running bool -var jwtinv_mutex = &sync.Mutex{} +var jwtinv_cache map[string]jwtinvs // Cache of invalid JWT items +var jwtinv_list *list.List // The sorted list of invalid JWT items +var jwtinv_exit chan bool // If the goproc has to exit +var jwtinv_done chan bool // If the goproc is done +var jwtinv_running bool // If the goproc is running +var jwtinv_mutex = &sync.Mutex{} // Synchronization mutex to avoid clashes +// init initializes the JWT Invalid details func init() { jwtinv_cache = make(map[string]jwtinvs) jwtinv_list = list.New() } -/* Removes items that have expired. */ +// jwtinv_expire removes items that have expired func jwtinv_expire() (err error) { jwtinv_mutex.Lock() defer jwtinv_mutex.Unlock() @@ -60,6 +63,7 @@ func jwtinv_expire() (err error) { return } +// jwtInvalid_rtn is the routine used for managing the invalid items func jwtInvalid_rtn(timeoutchk time.Duration) { jwtinv_running = true @@ -87,6 +91,7 @@ func jwtInvalid_rtn(timeoutchk time.Duration) { jwtinv_done <- true } +// JwtInv_start starts the goproc func JwtInv_start(timeoutchk time.Duration) { jwtinv_exit = make(chan bool) jwtinv_done = make(chan bool) @@ -94,6 +99,7 @@ func JwtInv_start(timeoutchk time.Duration) { go jwtInvalid_rtn(timeoutchk) } +// JwtInv_stop stops the goproc func JwtInv_stop() { if !jwtinv_running { return @@ -106,10 +112,10 @@ func JwtInv_stop() { <-jwtinv_done } -/* - * Mutex should be held when calling this - * not for calling directly, used by Jwt_invalidate() + Jwt_isinvalidated() - */ +// jwtinv_cache_add adds an item to the cache. +// +// Mutex should be held when calling this. +// not for calling directly, used by Jwt_invalidate() + Jwt_isinvalidated(). func jwtinv_cache_add(tok string, isvalid bool, claims JWTClaimI) { jwtc := claims.GetJWTClaims() isval := jwtinvs{nil, tok, isvalid, jwtc.ExpiresAt} @@ -126,10 +132,9 @@ func jwtinv_cache_add(tok string, isvalid bool, claims JWTClaimI) { } } -/* - * Mutex should be held when calling this - * not for calling directly - */ +// jwtinv_cache_del removes an item from the cache. +// +// Mutex should be held when calling this not for calling directly. func jwtinv_cache_del(tok string) { isval, ok := jwtinv_cache[tok] if !ok { @@ -141,6 +146,7 @@ func jwtinv_cache_del(tok string) { delete(jwtinv_cache, tok) } +// Jwt_invalidate invalidates a token. func Jwt_invalidate(tok string, claims JWTClaimI) { jwtc := claims.GetJWTClaims() @@ -180,6 +186,7 @@ func Jwt_invalidate(tok string, claims JWTClaimI) { jwtinv_cache_add(tok, false, claims) } +// Jwt_isinvalidated checks if a token is invalidated. func Jwt_isinvalidated(tok string, claims JWTClaimI) (invalid bool) { /* Invalid by default */ invalid = true @@ -213,20 +220,23 @@ func Jwt_isinvalidated(tok string, claims JWTClaimI) (invalid bool) { return } -/* Hooks for test code */ +// JwtInv_test_cache_len - test code hook. func JwtInv_test_cache_len() int { return len(jwtinv_cache) } +// JwtInv_test_cache_len - test code hook. func JwtInv_test_list_len() int { return jwtinv_list.Len() } +// JwtInv_test_iscached - test code hook. func JwtInv_test_iscached(tok string) (ok bool) { _, ok = jwtinv_cache[tok] return } +// JwtInv_test_expire - test code hook. func JwtInv_test_expire() (before int, after int, err error) { jwtinv_mutex.Lock() diff --git a/lib/jwt_invalid_test.go b/lib/jwt_invalid_test.go index 9594af8..3ba6843 100644 --- a/lib/jwt_invalid_test.go +++ b/lib/jwt_invalid_test.go @@ -1,3 +1,4 @@ +// Pitchfork JWTInvalid testing package pitchfork /* @@ -13,10 +14,12 @@ import ( "time" ) +// TestClaims are simple test claims. type TestClaims struct { JWTClaims } +// jwtinv_test tests if a token is invalid. func jwtinv_test(t *testing.T, n int, mins time.Duration) (tok string, claims *TestClaims) { tname := fmt.Sprintf("token%d", n) claims = &TestClaims{} @@ -31,6 +34,7 @@ func jwtinv_test(t *testing.T, n int, mins time.Duration) (tok string, claims *T return } +// TestJWTInvalidate tests whether invalidation works. func TestJWTInvalidate(t *testing.T) { off := 100 tok1, claims1 := jwtinv_test(t, 1, 10) diff --git a/lib/language.go b/lib/language.go index 01c708e..ee2adc2 100644 --- a/lib/language.go +++ b/lib/language.go @@ -1,15 +1,19 @@ +// Pitchfork User Language settings package pitchfork +// The language name. type PfLanguage struct { - Name string - Code string + Name string // Name of the language + Code string // Language code ('en', 'de', etc) in ISO 639-1 } +// ToString displays the name of the language. func (tl *PfLanguage) ToString() (out string) { out = tl.Code + "\t" + tl.Name return } +// LanguageList lists the possible languages. func LanguageList() (languages []PfLanguage, err error) { q := "SELECT " + "name, " + @@ -37,6 +41,7 @@ func LanguageList() (languages []PfLanguage, err error) { return } +// language_list lists the possible languages (CLI). func language_list(ctx PfCtx, args []string) (err error) { languages, err := LanguageList() if err != nil { @@ -52,6 +57,7 @@ func language_list(ctx PfCtx, args []string) (err error) { return } +// language_menu provides the CLI menu for languages (CLI). func language_menu(ctx PfCtx, args []string) (err error) { menu := NewPfMenu([]PfMEntry{ {"list", language_list, 0, -1, nil, PERM_USER, "List details types"}, diff --git a/lib/mail.go b/lib/mail.go index 5412066..c1a9ee7 100644 --- a/lib/mail.go +++ b/lib/mail.go @@ -1,3 +1,4 @@ +// Pitchfork Mail sending interface package pitchfork import ( @@ -6,9 +7,13 @@ import ( "net/smtp" ) +// Carriage Return (CR) + Line Feed (LF) as every line in an email should end with it (RFC822/RFC5322). const CRLF = "\r\n" -/* TODO: Simple version, replace with internally queued edition later */ +// mailA provides a way to send an email with various customizations. +// Suggested to use Mail() instead. +// +// TODO: This is the very simple version, replace with internally queued edition later. func mailA(ctx PfCtx, src_name string, src string, dst_name []string, dst []string, prefix bool, subject string, body string, regards bool, footer string, sysfooter bool) (err error) { if len(dst) != len(dst_name) { err = errors.New("Mismatch length in dst_name and dst options") @@ -153,7 +158,9 @@ func mailA(ctx PfCtx, src_name string, src string, dst_name []string, dst []stri return } -/* Wrapper around the real mailA() function so we can handle errors in a single place */ +// Mail allows one to send an email with various parameters. +// +// This is a wrapper around the real mailA() function so we can handle errors in a single place. func Mail(ctx PfCtx, src_name string, src string, dst_name string, dst string, prefix bool, subject string, body string, regards bool, footer string, sysfooter bool) (err error) { err = mailA(ctx, src_name, src, []string{dst_name}, []string{dst}, prefix, subject, body, regards, footer, sysfooter) if err != nil { @@ -164,6 +171,7 @@ func Mail(ctx PfCtx, src_name string, src string, dst_name string, dst string, p return } +// MailM is like Mail() but allows for multiple recipients to be specified. func MailM(ctx PfCtx, src_name string, src string, dst_name []string, dst []string, prefix bool, subject string, body string, regards bool, footer string, sysfooter bool) (err error) { err = mailA(ctx, src_name, src, dst_name, dst, prefix, subject, body, regards, footer, sysfooter) if err != nil { @@ -174,6 +182,7 @@ func MailM(ctx PfCtx, src_name string, src string, dst_name []string, dst []stri return } +// Mail_VerifyEmail causes an Email Verification Request to be sent to the given target. func Mail_VerifyEmail(ctx PfCtx, email PfUserEmail, verifycode string) (err error) { sys := System_Get() subject := "Email Verification Request" @@ -217,6 +226,7 @@ func Mail_VerifyEmail(ctx PfCtx, email PfUserEmail, verifycode string) (err erro return } +// Mail_PasswordChanged send a "Password changed" message to the intended recipient. func Mail_PasswordChanged(ctx PfCtx, email PfUserEmail) (err error) { sys := System_Get() subject := "Password changed" diff --git a/lib/main_test.go b/lib/main_test.go index 5f5f777..45e3ca2 100644 --- a/lib/main_test.go +++ b/lib/main_test.go @@ -1,3 +1,4 @@ +// Pitchfork's TestMain for testing code package pitchfork_test /* @@ -13,6 +14,7 @@ import ( pftst "trident.li/pitchfork/lib/test" ) +// TestMain sets up and tears down the testing environment. func TestMain(m *testing.M) { pftst.Test_setup() diff --git a/lib/mainmenu.go b/lib/mainmenu.go index 8a68dd5..cd06869 100644 --- a/lib/mainmenu.go +++ b/lib/mainmenu.go @@ -1,5 +1,7 @@ +// Pitchfork's Main Menu package pitchfork +// MainMenu is the Main CLI Menu for Pitchfork. var MainMenu = NewPfMenu([]PfMEntry{ {"user", user_menu, 0, -1, nil, PERM_NONE, "User commands"}, {"group", group_menu, 0, -1, nil, PERM_USER, "Group commands"}, diff --git a/lib/menu.go b/lib/menu.go index 68f7b68..1e96bb8 100755 --- a/lib/menu.go +++ b/lib/menu.go @@ -1,3 +1,4 @@ +// Pitchfork CLI Menu handling code package pitchfork import ( @@ -12,13 +13,16 @@ import ( "sync" ) -/* Mutex needed to only allow one batch script at a time */ +// Mutex needed to only allow one batch script at a time. var batchmutex = &sync.Mutex{} +// Standard error message for commands that are not known. const ERR_UNKNOWN_CMDPFX = "Unknown command: " +// PfFunc is used as a prototype for all CLI menu functions. type PfFunc func(ctx PfCtx, args []string) (err error) +// PfMEntry is a CLI menu entry with all the possible parameters. type PfMEntry struct { Cmd string Fun PfFunc @@ -29,22 +33,27 @@ type PfMEntry struct { Desc string } +// PfMenu is a series of PfMEntry and thus make up a menu. type PfMenu struct { M []PfMEntry } +// NewPfMenu creates a new PfMenu. func NewPfMenu(m []PfMEntry) PfMenu { return PfMenu{M: m} } +// NewPfMEntry creates a new entry for a menu. func NewPfMEntry(Cmd string, Fun PfFunc, Args_min int, Args_max int, Args []string, Perms Perm, Desc string) PfMEntry { return PfMEntry{Cmd, Fun, Args_min, Args_max, Args, Perms, Desc} } +// Add allows one to add a entry to a menu, use in combo with NewPfMEntry(). func (menu *PfMenu) Add(m ...PfMEntry) { menu.M = append(menu.M, m...) } +// Replace replaces the function of a menu thus allowing it to be overridden. func (menu *PfMenu) Replace(cmd string, fun PfFunc) { for i, m := range menu.M { if m.Cmd == cmd { @@ -54,6 +63,7 @@ func (menu *PfMenu) Replace(cmd string, fun PfFunc) { } } +// Remove removes an entry from a menu, thus allowing it to be forcefully disabled. func (menu *PfMenu) Remove(cmd string) { for i, m := range menu.M { if cmd == m.Cmd { @@ -63,7 +73,7 @@ func (menu *PfMenu) Remove(cmd string) { } } -/* Or new permissions into it, useful to mark a menu item hidden */ +// AddPerms OR's new permissions into the permissions, useful to mark a menu item hidden. func (menu *PfMenu) AddPerms(cmd string, perms Perm) { for i, m := range menu.M { if cmd == m.Cmd { @@ -73,6 +83,7 @@ func (menu *PfMenu) AddPerms(cmd string, perms Perm) { } } +// DelPerms removes (AND-OR) permissions from a permission entry. func (menu *PfMenu) DelPerms(cmd string, perms Perm) { for i, m := range menu.M { if cmd == m.Cmd { @@ -82,6 +93,7 @@ func (menu *PfMenu) DelPerms(cmd string, perms Perm) { } } +// SetPerms changes the permissions of an entry. func (menu *PfMenu) SetPerms(cmd string, perms Perm) { for i, m := range menu.M { if cmd == m.Cmd { @@ -91,6 +103,7 @@ func (menu *PfMenu) SetPerms(cmd string, perms Perm) { } } +// Menu either provides help about a given menu or executes the given command given enough parameters are provided. func (ctx *PfCtxS) Menu(args []string, menu PfMenu) (err error) { err = nil ok := false @@ -222,6 +235,7 @@ func (ctx *PfCtxS) Menu(args []string, menu PfMenu) (err error) { return } +// ErrIsUnknownCommand checks if an error undicates an unknown command func ErrIsUnknownCommand(err error) bool { s := err.Error() sl := len(s) @@ -229,12 +243,14 @@ func ErrIsUnknownCommand(err error) bool { return sl > el && s[:el] == ERR_UNKNOWN_CMDPFX } +// Cmd simply executes a command by calling the correct menu entry. func (ctx *PfCtxS) Cmd(args []string) (err error) { ctx.loc = "" return ctx.Menu(args, MainMenu) } +// CmdOut executes a command, buffering the output of the command and returning it. func (ctx *PfCtxS) CmdOut(cmd string, args []string) (msg string, err error) { cmds := []string{} if cmd != "" { @@ -248,6 +264,7 @@ func (ctx *PfCtxS) CmdOut(cmd string, args []string) (msg string, err error) { return } +// Batch executes a batch of commands from the given file. func (ctx *PfCtxS) Batch(filename string) (err error) { /* Only allow one batch at a time */ batchmutex.Lock() @@ -331,6 +348,7 @@ func (ctx *PfCtxS) Batch(filename string) (err error) { return } +// WalkMenu searching for the entry related to a given command, returning the entry instead of executing it. func (ctx *PfCtxS) WalkMenu(args []string) (menu *PfMEntry, err error) { ctx.menu_menu = nil ctx.menu_walkonly = true diff --git a/lib/messages.go b/lib/messages.go index 03bc298..84fd6fb 100755 --- a/lib/messages.go +++ b/lib/messages.go @@ -1,3 +1,4 @@ +// Pitchfork Messages module package pitchfork import ( @@ -12,9 +13,10 @@ import ( "time" ) -/* The separator between the message IDs */ +// Msg_sep defines the separator between the message IDs const Msg_sep = "/" +// PfMsgOpts describes the options for Messages Module type PfMsgOpts struct { PfModOptsS @@ -28,6 +30,7 @@ type PfMsgOpts struct { Title string } +// PfMessage describes a single Message type PfMessage struct { Id int `pfcol:"id"` Path string `pfcol:"path"` @@ -42,6 +45,7 @@ type PfMessage struct { /* TODO: extra properties: locked, hidden, etc */ } +// Msg_Props defines the SQL for fetching all the properties of a message var Msg_Props = "" + "SELECT msg.id, msg.path, msg.depth, msg.title, " + "msg.plaintext, msg.html, msg.entered, m.ident, m.descr, " + @@ -50,7 +54,9 @@ var Msg_Props = "" + "INNER JOIN member m ON msg.member = m.ident " + "LEFT OUTER JOIN msg_read ON msg.id = msg_read.id AND msg_read.member = $1" -/* We ignore effective root for this as that should always be valid */ +// Msg_PathValid checks if a given message path is valid. +// +// We ignore effective root for this as that should always be valid. func Msg_PathValid(ctx PfCtx, path *string) (err error) { /* * Verify that it is a valid path @@ -77,14 +83,16 @@ func Msg_PathValid(ctx PfCtx, path *string) (err error) { return } +// MsgType defines the type of message defined using MSGTYPE_*. type MsgType uint const ( - MSGTYPE_SECTION MsgType = iota - MSGTYPE_THREAD - MSGTYPE_MESSAGE + MSGTYPE_SECTION MsgType = iota // Message is a Section + MSGTYPE_THREAD // Message is a Thread + MSGTYPE_MESSAGE // Message is a Message ) +// Msg_GetModOpts retrieves the message options func Msg_GetModOpts(ctx PfCtx) PfMsgOpts { mopts := ctx.GetModOpts() if mopts == nil { @@ -94,10 +102,12 @@ func Msg_GetModOpts(ctx PfCtx) PfMsgOpts { return mopts.(PfMsgOpts) } +// Msg_ModOpts configures the Message module options func Msg_ModOpts(ctx PfCtx, cmdpfx string, path_root string, web_root string, thread_depth int, title string) { ctx.SetModOpts(PfMsgOpts{PfModOpts(ctx, cmdpfx, path_root, web_root), thread_depth, title}) } +// Msg_PathType returns the type of a path (MSGTYPE_*) func Msg_PathType(ctx PfCtx, path string) MsgType { pd := Msg_PathDepth(ctx, path) - Msg_ModPathDepth(ctx) @@ -112,24 +122,24 @@ func Msg_PathType(ctx PfCtx, path string) MsgType { return MSGTYPE_MESSAGE } -/* - * Calculate the depth of a path - * A depth of 0 is the root (/) - */ +// Msg_PathDepth returns the depth of a path. +// +// The depth of 0 is for the root (/) - relative to the ModOpts. func Msg_PathDepth(ctx PfCtx, path string) (depth int) { mopts := Msg_GetModOpts(ctx) return strings.Count(mopts.Pathroot+path, Msg_sep) - 1 } -/* - * The Module Root's should never have a trailing '/' - * Hence why we do not substract here compared to Msg_PathDepth() - */ +// Msg_ModPathDepth returns the depth of the ModPath. +// +// The Module Root's should never have a trailing '/'. +// Hence why we do not substract here compared to Msg_PathDepth(). func Msg_ModPathDepth(ctx PfCtx) (depth int) { mopts := Msg_GetModOpts(ctx) return strings.Count(mopts.Pathroot, Msg_sep) } +// Msg_MarkSeen marks a message as seen. func Msg_MarkSeen(ctx PfCtx, msg PfMessage) (err error) { if msg.Seen.Valid { err = errors.New("Already marked as seen") @@ -142,6 +152,7 @@ func Msg_MarkSeen(ctx PfCtx, msg PfMessage) (err error) { return } +// Msg_MarkNew marks as message as new. func Msg_MarkNew(ctx PfCtx, msg PfMessage) (err error) { if !msg.Seen.Valid { err = errors.New("Already marked as new") @@ -153,6 +164,7 @@ func Msg_MarkNew(ctx PfCtx, msg PfMessage) (err error) { return } +// Msg_GetThread retrieves a Thread based on the given path and other parameters. func Msg_GetThread(ctx PfCtx, path string, mindepth int, maxdepth int, offset int, max int) (msgs []PfMessage, err error) { msgs = nil var rows *Rows @@ -240,6 +252,7 @@ func Msg_GetThread(ctx PfCtx, path string, mindepth int, maxdepth int, offset in return } +// Msg_get retrieves a message based on the given path. func Msg_Get(ctx PfCtx, path string) (msg PfMessage, err error) { var html string @@ -276,6 +289,7 @@ func Msg_Get(ctx PfCtx, path string) (msg PfMessage, err error) { return } +// Msg_Create_With_User creates a new message noting that it was posted by the given user. func Msg_Create_With_User(ctx PfCtx, user PfUser, path string, title string, plaintext string, notify bool) (newpath string, err error) { /* How deep is it? */ depth := Msg_PathDepth(ctx, path) @@ -345,11 +359,13 @@ func Msg_Create_With_User(ctx PfCtx, user PfUser, path string, title string, pla return } +// Msg_Create creates a new message. func Msg_Create(ctx PfCtx, path string, title string, plaintext string, notify bool) (newpath string, err error) { newpath, err = Msg_Create_With_User(ctx, ctx.TheUser(), path, title, plaintext, notify) return } +// Msg_Post posts a new message. func Msg_Post(ctx PfCtx, path string, title string, plaintext string) (newpath string, err error) { err = Msg_PathValid(ctx, &path) if err != nil { @@ -378,6 +394,7 @@ func Msg_Post(ctx PfCtx, path string, title string, plaintext string) (newpath s return Msg_Create(ctx, path, title, plaintext, true) } +// msg_list lists the messages in the given path (CLI). func msg_list(ctx PfCtx, args []string) (err error) { path := args[0] @@ -398,6 +415,7 @@ func msg_list(ctx PfCtx, args []string) (err error) { return } +// msg_get returns the property of the message for the given path (CLI). func msg_get(ctx PfCtx, args []string) (err error) { path := args[0] prop := args[1] @@ -437,6 +455,7 @@ func msg_get(ctx PfCtx, args []string) (err error) { return } +// msg_show shows the message for the given path (CLI). func msg_show(ctx PfCtx, args []string) (err error) { path := args[0] @@ -478,6 +497,7 @@ func msg_show(ctx PfCtx, args []string) (err error) { return } +// msg_post is used to post a message - the path will be generated with a sequence number (CLI). func msg_post(ctx PfCtx, args []string) (err error) { path := args[0] title := args[1] @@ -487,6 +507,7 @@ func msg_post(ctx PfCtx, args []string) (err error) { return } +// msg_create is used to create a new message (CLI). func msg_create(ctx PfCtx, args []string) (err error) { path := args[0] title := args[1] @@ -502,6 +523,7 @@ func msg_create(ctx PfCtx, args []string) (err error) { return } +// msg_import_id returns the import ID from a string. func msg_import_id(txt string) (id string, err error) { i := strings.Index(txt, ")") if i == -1 { @@ -513,6 +535,7 @@ func msg_import_id(txt string) (id string, err error) { return } +// msg_subject returns the string from a import id. func msg_subject(txt string) (subject string, err error) { id, err := msg_import_id(txt) if err != nil { @@ -523,6 +546,7 @@ func msg_subject(txt string) (subject string, err error) { return } +// msg_import imports a exported wiki (CLI). func msg_import(ctx PfCtx, args []string) (err error) { rootpath := args[0] fn := args[1] @@ -671,6 +695,7 @@ func msg_import(ctx PfCtx, args []string) (err error) { return } +// msg_mark marks a message with a flag (CLI). func msg_mark(ctx PfCtx, args []string) (err error) { path := args[0] mark := args[1] @@ -703,6 +728,7 @@ func msg_mark(ctx PfCtx, args []string) (err error) { return } +// msg_purge purges a path from the system (CLI). func msg_purge(ctx PfCtx, args []string) (err error) { path := args[0] @@ -724,6 +750,7 @@ func msg_purge(ctx PfCtx, args []string) (err error) { return } +// Msg_menu is the Message Module's menu (CLI). func Msg_menu(ctx PfCtx, args []string) (err error) { menu := NewPfMenu([]PfMEntry{ {"list", msg_list, 1, 1, []string{"path"}, PERM_USER, "List messages in a given path"}, diff --git a/lib/messages_test.go b/lib/messages_test.go index 00b2da3..62288fe 100644 --- a/lib/messages_test.go +++ b/lib/messages_test.go @@ -1,3 +1,4 @@ +// Pitchfork Messages testing functions package pitchfork /* @@ -9,6 +10,7 @@ import ( "testing" ) +// test for Message paths validity func test(t *testing.T, path string, succeed bool) { /* Fake Ctx */ ctx := testingctx() @@ -43,6 +45,7 @@ func test(t *testing.T, path string, succeed bool) { return } +// TestMsg_PathValid tests if a given message path is valid/invalid func TestMsg_PathValid(t *testing.T) { /* Positive tests */ test(t, "/", true) diff --git a/lib/misc.go b/lib/misc.go index 21ba2a4..2dabadb 100644 --- a/lib/misc.go +++ b/lib/misc.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "errors" "fmt" + val "github.com/asaskevich/govalidator" "github.com/disintegration/imaging" "image" _ "image/gif" @@ -27,8 +28,23 @@ import ( "time" ) +// LogLocation determines if we log locations. Code-level debug option. var LogLocation = false +// where_strippath strips the path returned by Where() +// to a minimum so that log lines remain short and mostly unique. +// +// This is used to avoid overly long debug code paths in debug messages. +// +// The path is the path leading towards the source code file. +// +// The workdir is the current working directory, typically where +// a developer has source code checked out. +// +// The gopath is the value of the GOPATH environment path where gocode is stored. +// +// Effectively the code removes the workdir or the gopath/src from the provided path. +// This thus leaves either the developer's source directory or the hostname + packagename. func where_strippath(path string, workdir string, gopath string) (strippedpath string) { wl := len(workdir) fl := len(path) @@ -49,6 +65,23 @@ func where_strippath(path string, workdir string, gopath string) (strippedpath s return path } +// Where returns the string representation of where the +// code currently is (returns file + line number) +// +// The off parameter can be used to provide an offset into the stack. +// This is useful for the case where it is known that the caller of +// the Where function is already called by another function. +// Thus avoiding the reporting of an intermediary function instead +// of the location one would really want to see. +// +// hops in the stack, which are inserted due +// to golang interface wrappers, are unrolled so that the actual +// location of the source code is returned. +// +// It uses where_strippath to clean the path and keep it +// as short as possible. +// +// The Where function is primarily used for debug output. func Where(off int) string { file := "" line := 0 @@ -68,6 +101,10 @@ func Where(off int) string { return file + ":" + strconv.Itoa(line) } +// LogLoc returns the Where path's location. +// +// LogLoc, short for LogLocation, is used to include +// the Where path for logging purposes. func LogLoc(off int, pfx string) string { if !LogLocation { if pfx != "" { @@ -93,15 +130,14 @@ func LogLoc(off int, pfx string) string { return s } -/* - * Encode non-ASCII chars in URL-encoded format - * This to make sure control-codes etc do not end up in output - * - * We encode in URL-encoded format, but we do not escape HTML chars - * anything rendering those should handle that part properly. - * - * Note: this might not be unicode friendly - */ +// OutEsc encodes non-ASCII chars in URL-encoded format. +// +// This to make sure control-codes etc do not end up in output. +// +// We encode in URL-encoded format, but we do not escape HTML chars +// anything rendering those should handle that part properly. +// +// Note: this might not be exactly unicode friendly. func OutEsc(str string) (nstr string) { nstr = "" for i := 0; i < len(str); i++ { @@ -119,8 +155,16 @@ func OutEsc(str string) (nstr string) { return } -/* Silly golang log functions just ignore errors... thus do our own */ -func OutA(format string, a ...interface{}) { +// outf is used to output a string to the log output. +// +// One reason this exists is because standard golang log functions just ignore errors... +// thus do our own so that we know when logging fails. +// +// Failed log messags are printed to standard out, which would indeed +// in the case of a daemon still end up in /dev/null. But in debug mode +// one typically does not run the daemon in the background and would +// thus at least have a chance of seeing these messages. +func outf(format string, a ...interface{}) { str := fmt.Sprintf(format, a...) /* Escape odd chars */ @@ -133,34 +177,50 @@ func OutA(format string, a ...interface{}) { } } -/* - * Logging - centralized and to avoid importing log everywhere - * Might extend this with extra params to make per-context tracing possible - */ +// Logging - centralized and to avoid importing log everywhere. +// TODO: Might extend this with extra params to make per-context tracing possible. + +// ErrA logs an error message with variable arguments and formatting them, specifying an offset for the where location. func ErrA(off int, format string, a ...interface{}) { - OutA(LogLoc(1+off, "Error")+format, a...) + outf(LogLoc(1+off, "Error")+format, a...) } +// Err logs a error message as a pure string func Err(message string) { - ErrA(1, message) + ErrA(1, "%s", message) } +// Errf logs an error message with variable arguments and formatting them. func Errf(format string, a ...interface{}) { ErrA(1, format, a...) } +// LogA logs a message with variable arguments and formatting them, specifying an offset for the where location. func LogA(off int, format string, a ...interface{}) { - OutA(LogLoc(1+off, "")+format, a...) + outf(LogLoc(1+off, "")+format, a...) } +// Log logs a message as a pure string. func Log(message string) { - LogA(1, message) + LogA(1, "%s", message) } +// Logf logs an error message with variable arguments and formatting them. func Logf(format string, a ...interface{}) { LogA(1, format, a...) } +// DbgA formats a debug level log message. +// +// Debug output only happens when the Debug flag is enabled. +// +// The function logs the filename, shorted using the Where function, +// and the line number in that file, along with the function name. +// The formatted message is concatenated behind that. +// +// The offset is used to indicate that offset hops should +// be ignored in the stack, this to skip over the debug +// code and thus be able to report the real caller. func DbgA(off int, format string, a ...interface{}) { if !Debug { return @@ -172,19 +232,33 @@ func DbgA(off int, format string, a ...interface{}) { runtime.Callers(2+off, pc) f := runtime.FuncForPC(pc[0]) - OutA("Debug("+where+") "+f.Name()+" "+format, a...) + outf("Debug("+where+") "+f.Name()+" "+format, a...) } +// Dbg logs a debug message as a pure string. +// +// DbgA details what is further logged. func Dbg(message string) { DbgA(1, message) } +// Dbgf logs a debug message accepting formatting. +// +// DbgA details what is further logged. func Dbgf(format string, a ...interface{}) { DbgA(1, format, a...) } -/* IsTrue() defaults to false if the string is not known */ +// IsTrue returns a boolean indicating whether the +// string was true or false; it defaults to false +// if the string is not known to be a variant of true. +// +// Recognized as true are the world: 'yes', 'false' and 'on'. +// +// Both uppercase and lowercase variants are supported. func IsTrue(val string) (b bool) { + val = strings.ToLower(val) + if val == "yes" || val == "true" || val == "on" { b = true } else { @@ -194,6 +268,7 @@ func IsTrue(val string) (b bool) { return b } +// YesNo returns a textual yes/no depending if the input was true or false func YesNo(b bool) (val string) { if b == true { return "yes" @@ -202,16 +277,38 @@ func YesNo(b bool) (val string) { return "no" } +// NormalizeBoolean returns yes or no depending on the input being a variety of yes/true/on or no/false/off. +// +// It uses IsTrue and then YesNo to normalize the string, see those functions for more details. func NormalizeBoolean(val string) string { return YesNo(IsTrue(val)) } -/* Parse the string (obeying quoting) */ +// SplitArgs splits a string into components obeying quoting. +// +// Given a string str it splits the string into pieces that are returned +// but it allows quotes to surround parts. +// +// This is effectively the same behavior as most UNIX shells. func SplitArgs(str string) (args []string) { r := regexp.MustCompile("'.+'|\".+\"|\\S+") return r.FindAllString(str, -1) } +// Daemon replicates the daemon(2) system call causing the goprocess to run in the background +// +// The process is forked using fork(2). +// Umask is forced to 0. +// setsid(2) is usd to create a new session process group. +// Optionally (nochdir flag being non-zero) the working directory is changed to the root (/). +// Optionally (noclose flag being non-zero) the standard input/output/error file handles +// are closed. +// +// After calling Daemon the parent process will have exit'ed. +// The child process will continue on to run the rest of the go program. +// +// Note that the use of syscalls and forking is not fully supported under Golang +// but it has proven to work without issues over the last several years. func Daemon(nochdir int, noclose int) (err error) { var ret uintptr var ret2 uintptr @@ -264,6 +361,12 @@ func Daemon(nochdir int, noclose int) (err error) { return nil } +// SetUID changes the system's User and Groups ID, thus giving up on previous privileges +// +// SetUID effectively calls the setuid(2) and setgid(2) calls. +// +// This is to be run together with the Daemon process to drop privileges when the +// daemon was originally started as the root (uid = 0) user. func SetUID(username string) (err error) { var u *user.User var uid int @@ -292,10 +395,14 @@ func SetUID(username string) (err error) { return nil } +// GetPID returns the Process ID (PID) of the current process. +// +// It uses the getpid(2) call to do this and is primarily a wrapper for that call. func GetPID() (pid string) { return strconv.Itoa(syscall.Getpid()) } +// StorePID stores a PID into a file named filename. func StorePID(filename string, pid string) { err := ioutil.WriteFile(filename, []byte(pid), 0644) if err != nil { @@ -303,6 +410,10 @@ func StorePID(filename string, pid string) { } } +// SortKeys sorts a map of strings indexed by strings into an array of strings. +// +// It returns an array of keys, that are sorted, which then can be used as +// a sorted index into the table that was passed in. func SortKeys(tbl map[string]string) (keys []string) { for k := range tbl { keys = append(keys, k) @@ -311,16 +422,29 @@ func SortKeys(tbl map[string]string) (keys []string) { return } +// Hex converts a sequence of bytes into a human readable Hex string. +// +// This is primarily a convience function for readability. func Hex(data []byte) string { return fmt.Sprintf("%x", data) } +// HashIt SHA256-hashes the input string. +// +// The result is returned as a Hex-encoded string. func HashIt(input string) (hash string) { h := sha256.New() h.Write([]byte(input)) return Hex(h.Sum(nil)) } +// Fullname_to_ident creates a username from a fullname. +// +// Examples of conversion: +// Ben April -> benapril4125 +// Jeroen Massar -> jeroenmassar1242 +// Joe St'Sauver -> joestsauver7346 +// Eric Ziegast -> ericziegast3734 func Fullname_to_ident(name string) (out string, err error) { /* Force lower case */ out = strings.ToLower(name) @@ -337,6 +461,7 @@ func Fullname_to_ident(name string) (out string, err error) { return } +// Chk_ident verifies if a given input string matches our Username_regexp rules. func Chk_ident(name string, in string) (out string, err error) { /* Require something at least */ if in == "" { @@ -358,6 +483,20 @@ func Chk_ident(name string, in string) (out string, err error) { return } +// Chk_email can be used to check validity of the format of an email address. +func Chk_email(address string) bool { + return val.IsEmail(address) +} + +// Image_resize resizes an image from a file reader to the given dimensions +// and re-encodes it as a PNG. +// +// The maxsize parameter is specified as height 'x' width, eg '123x456'. +// +// The resizing takes care of possible embedded stego and also ensures +// that the image is the size we want it in. +// The re-encoding as PNG ensures that the image data does not contain +// nefarious instructions from the original image. func Image_resize(file io.Reader, maxsize string) (bits []byte, err error) { var im image.Image @@ -386,12 +525,26 @@ func Image_resize(file io.Reader, maxsize string) (bits []byte, err error) { } +// Rand_number returns a random number. +// +// The golang random is used, seeding it with a nano second of the current timestamp. +// +// The number is restricted by the maximum number provided as a parameter. +// +// Callers using this should not expect perfectly cryptographically secure numbers. func Rand_number(max int) int { s1 := rand.NewSource(time.Now().UnixNano()) r1 := rand.New(s1) return r1.Intn(max) } +// ToUTF8 converts a string from IOS8859_1 to UTF-8. +// +// This is primarily useful for copy&pasted text from Word documents. +// as otherwise the backticks are all magic. +// +// The input is a ISO8859_1 based string. +// Output is a standard Golang string which is UTF-8 based. func ToUTF8(iso8859_1_buf []byte) string { buf := make([]rune, len(iso8859_1_buf)) for i, b := range iso8859_1_buf { @@ -400,6 +553,13 @@ func ToUTF8(iso8859_1_buf []byte) string { return string(buf) } +// CopyFile duplicates a file from one location to another +// +// It overwrites files in the destination when they exist there. +// +// Permissions are copied from the source file to the destination file. +// +// The verbose option can be used to let it note which file it copied. func CopyFile(ctx PfCtx, verbose bool, source string, dest string) (err error) { srcf, err := os.Open(source) if err != nil { @@ -434,6 +594,26 @@ func CopyFile(ctx PfCtx, verbose bool, source string, dest string) (err error) { return } +// CopyDir recursively duplicates a directory. +// +// It recursively walks the source directory replicating it into +// the target directory. +// +// Files/directories starting with a '.' (typically used for 'hidden' files) +// are skipped from the copying process. Thus allowing .gitignore files +// to exist in the directory without being exposed to the destination directory. +// +// The context is used for outputting details about the copying process. +// The verbose option can be used for making the output a bit more verbose +// detailing primarily which directory it is copying and which files and +// directories are being ignored while copying. +// +// Logically the source file must exist and the destination must be writeable. +// +// Permissions for directories and files are copied to the destination. +// +// Any encountered error will break off the copy, leaving in +// the destination the progress made till that point. func CopyDir(ctx PfCtx, verbose bool, source string, dest string) (err error) { if verbose { ctx.OutLn("Copying Directory %s -> %s", source, dest) @@ -463,20 +643,36 @@ func CopyDir(ctx PfCtx, verbose bool, source string, dest string) (err error) { } for _, obj := range objs { - srcfptr := source + "/" + obj.Name() - dstfptr := dest + "/" + obj.Name() + n := obj.Name() + + /* Ignore names that are empty */ + if len(n) == 0 { + continue + } + + /* The source and destination filenames */ + srcfptr := source + "/" + n + dstfptr := dest + "/" + n + + /* Ignore names that begin with a dot (.) */ + if n[0] == '.' { + if verbose { + ctx.OutLn("Ignoring %q", srcfptr) + } + continue + } if obj.IsDir() { /* Copy sub-directories recursively */ err = CopyDir(ctx, verbose, srcfptr, dstfptr) if err != nil { - fmt.Println(err) + return } } else { /* File Copies */ err = CopyFile(ctx, verbose, srcfptr, dstfptr) if err != nil { - fmt.Println(err) + return } } @@ -485,6 +681,16 @@ func CopyDir(ctx PfCtx, verbose bool, source string, dest string) (err error) { return } +// ThisFunc returns the name of the current function. +// +// This is primarily useful in combination with the TrackTime function. +// +// A typical usage: +// ``` +// defer pf.TrackTime(pf.TrackStart(), pf.ThisFunc()+":Time Check") +// ``` +// This causes a timer to start, noting the function name in the final report. +// When the function returns the time spent since the above will be reported. func ThisFunc() string { pc := make([]uintptr, 10) runtime.Callers(2, pc) @@ -492,16 +698,21 @@ func ThisFunc() string { return f.Name() } +// TrackStart returns the current time. +// +// To be used in combo with TrackTime(). func TrackStart() time.Time { return time.Now() } +// TrackTime returns and logs the difference between the start (TrackStart) and end of the time. func TrackTime(start time.Time, name string) (elapsed time.Duration) { elapsed = time.Since(start) DbgA(1, "%s took %s", name, elapsed) return } +// Fmt_Time formats a time in standard time output format or returns 'never' when the time is zero. func Fmt_Time(t time.Time) string { if t.IsZero() { return "never" @@ -510,6 +721,7 @@ func Fmt_Time(t time.Time) string { return t.Format(Config.TimeFormat) } +// ErrIsDisconnect returns true when the error is a disconnection error. func ErrIsDisconnect(err error) bool { neterr, ok := err.(net.Error) @@ -520,7 +732,7 @@ func ErrIsDisconnect(err error) bool { return false } -/* Ensure that a URL ends in a slash */ +// URL_EnsureSlash ensures that a URL ends in a slash. func URL_EnsureSlash(url string) string { if len(url) == 0 || url[len(url)-1] != '/' { url += "/" @@ -529,7 +741,7 @@ func URL_EnsureSlash(url string) string { return url } -/* Append two parts of a URL together, adding a '/' in the middle where needed */ +// URL_Append append two parts of a URL together, adding a '/' in the middle where needed. func URL_Append(url1 string, url2 string) (url string) { url1 = strings.TrimSpace(url1) url2 = strings.TrimSpace(url2) diff --git a/lib/misc_test.go b/lib/misc_test.go index b3054ad..37956e6 100644 --- a/lib/misc_test.go +++ b/lib/misc_test.go @@ -1,3 +1,4 @@ +// Pitchfork Misc Testing. package pitchfork /* @@ -9,6 +10,7 @@ import ( "testing" ) +// test_where tests the where_strippath() function func test_where(t *testing.T, path string, workdir string, gopath string, expected string) { result := where_strippath(path, workdir, gopath) @@ -19,6 +21,7 @@ func test_where(t *testing.T, path string, workdir string, gopath string, expect } } +// TestMisc_Where tests the where_strippath with multiple positive and negative results. func TestMisc_Where(t *testing.T) { type WhereTest struct { path string @@ -53,6 +56,7 @@ func TestMisc_Where(t *testing.T) { } } +// test_outesc tests the OutEsc() function. func test_outesc(t *testing.T, str string, exp string) { enc := OutEsc(str) @@ -63,6 +67,7 @@ func test_outesc(t *testing.T, str string, exp string) { return } +// TestMisc_OutEsc() tests the OutEsc function. func TestMisc_OutEsc(t *testing.T) { a := []string{ "test", "test", @@ -79,6 +84,7 @@ func TestMisc_OutEsc(t *testing.T) { } } +// test_url_ensureslash tests the URL_EnsureSlash() function. func test_url_ensureslash(t *testing.T, url string, expected string) { nurl := URL_EnsureSlash(url) @@ -89,6 +95,7 @@ func test_url_ensureslash(t *testing.T, url string, expected string) { } } +// TestMisc_URL_EnsureSlash tests the URL_EnsureSlash with multiple inputs/outputs. func TestMisc_URL_EnsureSlash(t *testing.T) { type urlensure struct { url string @@ -107,6 +114,7 @@ func TestMisc_URL_EnsureSlash(t *testing.T) { } } +// test_url_append tests the URL_Append() function. func test_url_append(t *testing.T, url1 string, url2 string, expected string) { url := URL_Append(url1, url2) @@ -117,6 +125,7 @@ func test_url_append(t *testing.T, url1 string, url2 string, expected string) { } } +// TestMisc_URL_Append tests the URL_Append() function against multiple inputs/outputs. func TestMisc_URL_Append(t *testing.T) { type urlappend struct { url1 string @@ -137,3 +146,56 @@ func TestMisc_URL_Append(t *testing.T) { test_url_append(t, tst.url1, tst.url2, tst.expected) } } + +func ExampleSplitArgs() { + // Produces "FirstArgument" "Second Argument" "Third Argument" "Fourth Argument" + SplitArgs("FirstArgument \"Second Argument\" \"Third Argument\" \"Fourth Argument\"") + + // Produces "FirstArgument" "Second Argument" "FourthArgument" + SplitArgs("FirstArgument 'Second Argument' FourthArgument") + + // Produces "First" "There is a ' in the middle" + SplitArgs("First \"There is a ' in the middle\"") +} + +/* ExampleTrackTime provides an example of TrackTime and TrackStart usage */ +func ExampleTrackTime() { + fmt.Printf("Example - start\n") + + t1 := TrackStart() + for i := 0; i < 10; i++ { + fmt.Printf("Example - loop %d", i) + } + + te := TrackTime(t1, "Example") + fmt.Printf("Example - took: %s", te) +} + +func ExampleTrackTimeDeferred() { + fmt.Printf("Example - start\n") + + defer TrackTime(TrackStart(), ThisFunc()+":Time Check") + + for i := 0; i < 10; i++ { + fmt.Printf("Example - loop %d", i) + } + + /* + * Now the function ends, the defered functions are called + * which causes the TrackTime function to report the result. + */ +} +func ExampleSortKeys() { + tbl := map[string]string{ + "two": "Two", + "three": "Three", + "one": "One", + "four": "Four", + } + + keys := SortKeys(tbl) + + for _, key := range keys { + fmt.Printf("%q = %q", key, tbl[key]) + } +} diff --git a/lib/ml.go b/lib/ml.go index db9396e..50d80d1 100755 --- a/lib/ml.go +++ b/lib/ml.go @@ -1,3 +1,4 @@ +// Pitchfork Mailinglist (ML) functions package pitchfork import ( @@ -7,6 +8,7 @@ import ( pfpgp "trident.li/pitchfork/lib/pgp" ) +// PfML describes a pitchfork Mailinglist type PfML struct { ListName string `label:"List Name" pfset:"nobody" pfget:"user" pfcol:"lhs" hint:"Name of this Mailing List"` GroupName string `label:"Group Name" pfset:"nobody" pfget:"user" pfcol:"trustgroup" hint:"Group this list belongs to"` @@ -23,6 +25,7 @@ type PfML struct { Subscribed bool /* Not retrieved with Fetch() */ } +// PfMLUser describes a Pitchfork Mailinglist User type PfMLUser struct { UserName string Uuid string @@ -32,22 +35,27 @@ type PfMLUser struct { LoginAttempts int } +// GetUserName returns the username. func (mlu *PfMLUser) GetUserName() string { return mlu.UserName } +// GetFullName returns the Fullname. func (mlu *PfMLUser) GetFullName() string { return mlu.FullName } +// GetAffiliation returns the affiliation. func (mlu *PfMLUser) GetAffiliation() string { return mlu.Affiliation } +// NewPfML creates a new Pitchfork Mailinglist. func NewPfML() *PfML { return &PfML{} } +// fetch retrieves the given mailinglist. func (ml *PfML) fetch(gr_name string, ml_name string) (err error) { /* Make sure the name is mostly sane */ gr_name, err = Chk_ident("Group Name", gr_name) @@ -72,6 +80,7 @@ func (ml *PfML) fetch(gr_name string, ml_name string) (err error) { return } +// Select selects the given mailinglist, given the user has access/permissions. func (ml *PfML) Select(ctx PfCtx, grp PfGroup, ml_name string, perms Perm) (err error) { ctx.Dbg("SelectML(" + ml_name + ")") @@ -86,12 +95,12 @@ func (ml *PfML) Select(ctx PfCtx, grp PfGroup, ml_name string, perms Perm) (err } /* Required to be group admin? */ - if ctx.IsPermSet(perms, PERM_GROUP_ADMIN) { + if perms.IsSet(PERM_GROUP_ADMIN) { if ctx.IAmGroupAdmin() { /* Yep */ return } - } else if ctx.IsPermSet(perms, PERM_GROUP_MEMBER) && ctx.IsGroupMember() { + } else if perms.IsSet(PERM_GROUP_MEMBER) && ctx.IsGroupMember() { /* All okay */ return } else { @@ -104,11 +113,13 @@ func (ml *PfML) Select(ctx PfCtx, grp PfGroup, ml_name string, perms Perm) (err return } +// Refresh reloads a PfML from the database. func (ml *PfML) Refresh() (err error) { err = ml.fetch(ml.GroupName, ml.ListName) return } +// ListGroupMembersMax returns the number of maximum results. func (ml *PfML) ListGroupMembersMax(search string) (total int, err error) { q := "SELECT COUNT(*) " + "FROM member_mailinglist mlm " + @@ -129,6 +140,7 @@ func (ml *PfML) ListGroupMembersMax(search string) (total int, err error) { return } +// ListGroupMembers returns the list of members for the given query. func (ml *PfML) ListGroupMembers(search string, offset int, max int) (members []PfMLUser, err error) { var rows *Rows @@ -179,6 +191,7 @@ func (ml *PfML) ListGroupMembers(search string, offset int, max int) (members [] return } +// IsMember returns if the user is a member. func (ml *PfML) IsMember(user PfUser) (ok bool, err error) { cnt := 0 ok = false @@ -202,6 +215,7 @@ func (ml *PfML) IsMember(user PfUser) (ok bool, err error) { return } +// List returns the list of mailinglists for a group. func (ml *PfML) List(ctx PfCtx, grp PfGroup) (mls []PfML, err error) { mls = nil @@ -241,6 +255,7 @@ func (ml *PfML) List(ctx PfCtx, grp PfGroup) (mls []PfML, err error) { return } +// ListWithUser returns the lists for a group for a given user. func (ml *PfML) ListWithUser(ctx PfCtx, grp PfGroup, user PfUser) (mls []PfML, err error) { mls = nil @@ -291,6 +306,7 @@ func (ml *PfML) ListWithUser(ctx PfCtx, grp PfGroup, user PfUser) (mls []PfML, e return } +// GetKeys returns the keys for a given mailinglist. func (ml *PfML) GetKey(ctx PfCtx, keyset map[[16]byte][]byte) (err error) { var key string @@ -318,6 +334,7 @@ func (ml *PfML) GetKey(ctx PfCtx, keyset map[[16]byte][]byte) (err error) { return } +// ml_list returns the list of mailinglists (CLI). func ml_list(ctx PfCtx, args []string) (err error) { gr_name := args[0] @@ -342,6 +359,7 @@ func ml_list(ctx PfCtx, args []string) (err error) { return } +// ml_member_list returns the members of a mailinglist (CLI). func ml_member_list(ctx PfCtx, args []string) (err error) { gr_name := args[0] ml_name := args[1] @@ -383,6 +401,7 @@ func ml_member_list(ctx PfCtx, args []string) (err error) { return } +// ListKeys returns the keys for a mailinglist. func ListKeys(ctx PfCtx, keyset map[[16]byte][]byte, gr_name string, ml_name string) (err error) { q := "SELECT me.keyring " + "FROM member_email me, " + @@ -415,6 +434,7 @@ func ListKeys(ctx PfCtx, keyset map[[16]byte][]byte, gr_name string, ml_name str return } +// ml_member_mod modifies details about a member. func ml_member_mod(ctx PfCtx, args []string, add bool) (err error) { var ok bool @@ -509,16 +529,19 @@ func ml_member_mod(ctx PfCtx, args []string, add bool) (err error) { return } +// ml_member_add adds a member to a mailinglist (CLI). func ml_member_add(ctx PfCtx, args []string) (err error) { err = ml_member_mod(ctx, args, true) return } +// ml_member_remove removes a member to a mailinglist (CLI). func ml_member_remove(ctx PfCtx, args []string) (err error) { err = ml_member_mod(ctx, args, false) return } +// ml_member is the CLI mailinglist member menu (CLI). func ml_member(ctx PfCtx, args []string) (err error) { var menu = NewPfMenu([]PfMEntry{ {"list", ml_member_list, 2, 2, []string{"group", "ml"}, PERM_GROUP_MEMBER, "List members of this Mailing List"}, @@ -552,6 +575,7 @@ func ml_member(ctx PfCtx, args []string) (err error) { return } +// ml_remove removes a mailinglist (CLI). func ml_remove(ctx PfCtx, args []string) (err error) { gr_name := args[0] ml_name := args[1] @@ -579,6 +603,7 @@ func ml_remove(ctx PfCtx, args []string) (err error) { return } +// ml_pgp_create creates PGP keys for a list. func ml_pgp_create(ctx PfCtx, ml PfML) (err error) { seckey, pubkey, err := pfpgp.CreateKey(ml.Address, ml.GroupName+" "+ml.ListName, ml.Descr) @@ -595,6 +620,7 @@ func ml_pgp_create(ctx PfCtx, ml PfML) (err error) { return } +// ml_getit retrieves the given group from the database. func ml_getit(ctx PfCtx, args []string) (ml PfML, err error) { gr_name := args[0] ml_name := args[1] @@ -613,6 +639,7 @@ func ml_getit(ctx PfCtx, args []string) (ml PfML, err error) { return } +// ml_newkeys generates new mailinglist PGP keys (CLI). */ func ml_newkeys(ctx PfCtx, args []string) (err error) { ml, err := ml_getit(ctx, args) if err != nil { @@ -622,6 +649,7 @@ func ml_newkeys(ctx PfCtx, args []string) (err error) { return ml_pgp_create(ctx, ml) } +// ml_pubkey returns the public key of a mailinglist (CLI). */ func ml_pubkey(ctx PfCtx, args []string) (err error) { ml, err := ml_getit(ctx, args) if err != nil { @@ -633,6 +661,7 @@ func ml_pubkey(ctx PfCtx, args []string) (err error) { return } +// ml_seckey returns the secret key of a mailinglist (CLI). */ func ml_seckey(ctx PfCtx, args []string) (err error) { ml, err := ml_getit(ctx, args) if err != nil { @@ -644,7 +673,9 @@ func ml_seckey(ctx PfCtx, args []string) (err error) { return } -/* Group must be selected with PERM_GROUP_ADMIN */ +// Ml_addv adds a mailinglist to a group. +// +// Group must be selected with PERM_GROUP_ADMIN. func Ml_addv(ctx PfCtx, grp PfGroup, ml_name string, descr string, member_only bool, can_add_self bool, automatic bool) (err error) { q := "INSERT INTO mailinglist " + "(lhs, descr, members_only, " + @@ -662,6 +693,7 @@ func Ml_addv(ctx PfCtx, grp PfGroup, ml_name string, descr string, member_only b return } +// ml_new creates a new mailinglist (CLI). func ml_new(ctx PfCtx, args []string) (err error) { gr_name := args[0] ml_name := args[1] @@ -703,6 +735,7 @@ func ml_new(ctx PfCtx, args []string) (err error) { return } +// ml_set_xxx sets the properties of a mailinglist (called by ml_set/ml_get) func ml_set_xxx(ctx PfCtx, args []string) (err error) { /* * args[.] == what, dropped by ctx.Menu() @@ -723,6 +756,7 @@ func ml_set_xxx(ctx PfCtx, args []string) (err error) { return } +// ml_sget sets or gets the properties of a mailinglist) func ml_sget(ctx PfCtx, args []string, fun PfFunc) (err error) { /* * args[0] == what @@ -775,14 +809,17 @@ func ml_sget(ctx PfCtx, args []string, fun PfFunc) (err error) { return } +// ml_set sets the property of a mailinglist (CLI). func ml_set(ctx PfCtx, args []string) (err error) { return ml_sget(ctx, args, ml_set_xxx) } +// ml_get gets the property from a mailinglist (CLI). func ml_get(ctx PfCtx, args []string) (err error) { return ml_sget(ctx, args, nil) } +// ml_menu is the CLI Mailinglist menu (CLI). func ml_menu(ctx PfCtx, args []string) (err error) { menu := NewPfMenu([]PfMEntry{ {"new", ml_new, 2, 2, []string{"group", "ml"}, PERM_GROUP_ADMIN, "Add a new Mailinglist"}, diff --git a/lib/oauth2.go b/lib/oauth2.go index ba395e9..1cc11eb 100644 --- a/lib/oauth2.go +++ b/lib/oauth2.go @@ -1,3 +1,4 @@ +// Pitchfork OAuth 2.0 (RFC7523) implementation package pitchfork /* TODO: verify against https://tools.ietf.org/html/rfc7523 */ @@ -6,6 +7,7 @@ import ( "errors" ) +// OAuth_Auth describes an OAuth Authentication type OAuth_Auth struct { ClientID string `label:"Client ID" pfset:"nobody" pfget:"none"` Scope string `label:"Scope" pfset:"nobody" pfget:"none"` @@ -15,6 +17,7 @@ type OAuth_Auth struct { Deny string `label:"Deny" pftype:"submit" htmlclass:"deny"` } +// OAuth2Claims describes OAuth2 Claims type OAuth2Claims struct { JWTClaims ClientID string `json:"oa_client_id"` @@ -23,6 +26,7 @@ type OAuth2Claims struct { Redirect string `json:"oa_redirect,omitempty"` } +// OAuth2_AuthToken_New generates a new AuthToken func OAuth2_AuthToken_New(ctx PfCtx, o OAuth_Auth) (tok string, err error) { if !ctx.IsLoggedIn() { tok = "" @@ -44,11 +48,13 @@ func OAuth2_AuthToken_New(ctx PfCtx, o OAuth_Auth) (tok string, err error) { return } +// OAuth2_AuthToken_Check checks if a AuthToken is valid and returns it's claims func OAuth2_AuthToken_Check(tok string) (claims *OAuth2Claims, err error) { _, err = Token_Parse(tok, "oauth_auth", claims) return } +// OAuth2_AccessToken_New creates a new AccessToken func OAuth2_AccessToken_New(ctx PfCtx, client_id string, scope string) (tok string, err error) { if !ctx.IsLoggedIn() { tok = "" diff --git a/lib/pgp/pgp.go b/lib/pgp/pgp.go index 5bf8af0..891e69b 100644 --- a/lib/pgp/pgp.go +++ b/lib/pgp/pgp.go @@ -1,3 +1,4 @@ +// Pitchfork's PGP functions package pfpgp import ( @@ -13,6 +14,7 @@ import ( "time" ) +// PubKey returns the public key from a entity. func PubKey(ent *openpgp.Entity) (pubkey string, err error) { buf := new(bytes.Buffer) armor, err := armor.Encode(buf, openpgp.PublicKeyType, nil) @@ -28,6 +30,7 @@ func PubKey(ent *openpgp.Entity) (pubkey string, err error) { return } +// PubKey returns the private key from a entity. func SecKey(ent *openpgp.Entity, cfg *packet.Config) (seckey string, err error) { buf := new(bytes.Buffer) armor, err := armor.Encode(buf, openpgp.PrivateKeyType, nil) @@ -43,6 +46,7 @@ func SecKey(ent *openpgp.Entity, cfg *packet.Config) (seckey string, err error) return } +// CreateKey creates a new PGP key. func CreateKey(email string, name string, descr string) (seckey string, pubkey string, err error) { var cfg *packet.Config = nil @@ -116,6 +120,7 @@ func CreateKey(email string, name string, descr string) (seckey string, pubkey s return } +// GetKeyInfo retrieves information about a key. func GetKeyInfo(keyring string, email string) (key_id string, key_exp time.Time, err error) { /* Parse the Keyring */ entities, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(keyring)) diff --git a/lib/pw.go b/lib/pw.go index 12e7dca..39ace29 100644 --- a/lib/pw.go +++ b/lib/pw.go @@ -1,3 +1,4 @@ +// Pitchfork Password functions. package pitchfork import ( @@ -17,9 +18,11 @@ import ( cc "trident.li/go/osutil-crypt/common" ) +// PfPass represents Password functions. type PfPass struct { } +// PfPWRules details the password rules. type PfPWRules struct { Min_length int Max_length int @@ -30,6 +33,7 @@ type PfPWRules struct { Min_specials int } +// GenRand generates a random set of bytes. func (pw *PfPass) GenRand(length int) (bytes []byte, err error) { bytes = make([]byte, length) _, err = rand.Read(bytes) @@ -41,6 +45,7 @@ func (pw *PfPass) GenRand(length int) (bytes []byte, err error) { return } +// GenRandHex returns a random hex-encoded string of a certain length. func (pw *PfPass) GenRandHex(length int) (hex string, err error) { bytes, err := pw.GenRand(length) if err != nil { @@ -57,6 +62,7 @@ func (pw *PfPass) GenRandHex(length int) (hex string, err error) { return } +// GenPass generates a random password in base64 of given length. func (pw *PfPass) GenPass(length int) (pass string, err error) { bytes, err := pw.GenRand(length) if err != nil { @@ -68,9 +74,7 @@ func (pw *PfPass) GenPass(length int) (pass string, err error) { return } -/* - * For the time being, all passwords are SHA512 hashed - */ +// Make creates a SHA512 encoded password from a given plaintext string. func (pw *PfPass) Make(password string) (hash string, err error) { c, err := crypt.NewFromHash("$6$") if err != nil { @@ -79,12 +83,16 @@ func (pw *PfPass) Make(password string) (hash string, err error) { return c.Generate([]byte(password), nil) } -/* - * hashedPassword is in the semi-standardized /etc/shadow passwd format - * the format can be: - * $$$ - * $$rounds=$$ - */ +// Verify verifies if a given plaintext password matches the given hashedPassword. +// +// hashedPassword is in the semi-standardized /etc/shadow passwd format. +// +// The format can be either: +// $$$ +// or: +// $$rounds=$$ +// +// An error is returned if the verification failed, nil if all is okay. func (pw *PfPass) Verify(password string, hashedPassword string) (err error) { c, err := crypt.NewFromHash(hashedPassword) if err != nil { @@ -98,6 +106,7 @@ func (pw *PfPass) Verify(password string, hashedPassword string) (err error) { return } +// calc_otp calculates the OTP code given the key and value. func calc_otp(key string, value int64) int { hash := hmac.New(sha1.New, []byte(key)) err := binary.Write(hash, binary.BigEndian, value) @@ -116,6 +125,7 @@ func calc_otp(key string, value int64) int { return int(code) } +// VerifyHOTP verifies a HOTP key based on the counter and twofactor string provided. func (pw *PfPass) VerifyHOTP(key string, counter int64, twofactor string) bool { var i int64 @@ -141,6 +151,7 @@ func (pw *PfPass) VerifyHOTP(key string, counter int64, twofactor string) bool { return false } +// VerifyTOTP verifies a key and twofactor. func (pw *PfPass) VerifyTOTP(key string, twofactor string) bool { tf, err := strconv.Atoi(twofactor) if err != nil { @@ -160,12 +171,14 @@ func (pw *PfPass) VerifyTOTP(key string, twofactor string) bool { return false } +// SOTPHash creates a Single-use OTP hash from a secret. func (pw *PfPass) SOTPHash(secret string) (out string) { h := sha256.New() h.Write([]byte(secret)) return Hex(h.Sum(nil)) } +// VerifySOTP verifies if a SOTP key is valid. func (pw *PfPass) VerifySOTP(key string, twofactor string) bool { enc := pw.SOTPHash(twofactor) @@ -176,6 +189,7 @@ func (pw *PfPass) VerifySOTP(key string, twofactor string) bool { return false } +// VerifyPWRules verifies if a password matches the given Password Rules. func (pw *PfPass) VerifyPWRules(password string, r PfPWRules) (probs []string) { var letters = 0 var uppers = 0 diff --git a/lib/pw_dict.go b/lib/pw_dict.go index bcebaf9..0bb7264 100644 --- a/lib/pw_dict.go +++ b/lib/pw_dict.go @@ -1,14 +1,19 @@ +// Pitchfork Password Dictionary checking package pitchfork import ( "bufio" "errors" + "fmt" "os" "strings" ) +// pw_dict is the map where we keep our weak passwords var pw_dict map[string]bool +var pw_dicts int +// Pw_checkweak_load loads the dictionaries from disk into pw_dict func Pw_checkweak_load() (err error) { /* Put something in there */ pw_dict = make(map[string]bool) @@ -19,10 +24,10 @@ func Pw_checkweak_load() (err error) { } Dbg("Loading Weak Password Dictionaries...") - cnt := 0 + pw_dicts = 0 for _, f := range Config.PW_WeakDicts { - cnt++ + pw_dicts++ cntpw := 0 fn := System_findfile("pwdicts/", f) @@ -62,12 +67,19 @@ func Pw_checkweak_load() (err error) { Dbgf("Loading Weak Password Dictionary %q... done (%d passwords)", f, cntpw) } - Logf("Loaded %d Weak Password Dictionaries with %d unique passwords", cnt, len(pw_dict)) + Logf("Loaded %d Weak Password Dictionaries with %d unique passwords", pw_dicts, len(pw_dict)) return } +// Pw_checkweak checks if a password is in our weak password list func Pw_checkweak(password string) (isweak bool) { _, isweak = pw_dict[strings.ToLower(password)] return } + +// Pw_details provides details about te password dictionary checker (used by system_report) +func Pw_details() (msg string) { + fmt.Sprintf("Password Dictionary Checker: Loaded %d Weak Password Dictionaries with %d unique passwords", pw_dicts, len(pw_dict)) + return +} diff --git a/lib/pw_dict_test.go b/lib/pw_dict_test.go index ea4a7c8..289b765 100644 --- a/lib/pw_dict_test.go +++ b/lib/pw_dict_test.go @@ -1,3 +1,4 @@ +/* Pitchfork Password Dictionary Check Tester */ package pitchfork /* @@ -9,6 +10,7 @@ import ( "testing" ) +// test_pwweak tests for a weak password. func test_pwweak(t *testing.T, pw string, isok bool) { ok := Pw_checkweak(pw) if isok != ok { @@ -22,6 +24,7 @@ func test_pwweak(t *testing.T, pw string, isok bool) { return } +// TestPW_Dict tests for a variety of weak and notweak passwords. func TestPW_Dict(t *testing.T) { weaks := []string{ "password", diff --git a/lib/search.go b/lib/search.go index 677c36e..a6ec915 100644 --- a/lib/search.go +++ b/lib/search.go @@ -1,3 +1,34 @@ +// Pitchfork Search interface +// +// The search interface is rendered in the UI at the top of the screen +// in the menu bar. It allows searching through a variety of components +// of pitchfork and other parts that register to the system. +// +// Typically an init function of a piece of code will register +// a search interface with the searcher code. +// This happens using the SearcherRegister function. +// From then on, when something is entered in the search box +// the searcher code will call Search which then asks each +// searcher if they have something that matches the given string. +// +// The Searcher then performs a search and when it finds a matching +// item it calls SearchResult() to add that search result to the +// found items list, which gets returned to the client, either +// in the form of a type-ahead result (JSON). +// +// To allow type-ahead, the abort channel, which is given down +// from the HTTP handler, is used to cancel the request when a new +// request comes in with a different queries. +// +// Each handler will return as many results as it can, though +// one will typically limit it to 20 per module. +// +// Each result consists of a source (typically the module returning the search item), the title of the item, a link to the item and a summary of the item. +// +// In the rendered results we hilight the parts of the summary that matches the searched query. +// +// This search code primarily facilitates the searching. +// The actual searching happens in the module providing the search. package pitchfork import ( @@ -6,25 +37,35 @@ import ( "time" ) +// PfSearcherI is a prototype of a function providing a Searcher type PfSearcherI func(ctx PfCtx, c chan PfSearchResult, search string, abort <-chan bool) (err error) +// searchers describes the list of registered searcher functions, see SearcherRegister. var searchers []PfSearcherI +// PfSearchResult is a single result of a search type PfSearchResult struct { - Source string `json:"source"` - Title string `json:"title"` - Link string `json:"link"` - Summary string `json:"summary"` + Source string `json:"source"` // Which module provided this result + Title string `json:"title"` // Title of the result + Link string `json:"link"` // Link to the result + Summary string `json:"summary"` // Summary of the result } +// SearcherRegister allows an application to register a searcher function. func SearcherRegister(f PfSearcherI) { searchers = append(searchers, f) } +// SearchResult is called by a Searcher when a result has been found. func SearchResult(c chan PfSearchResult, source string, title string, link string, summary string) { c <- PfSearchResult{source, title, link, summary} } +// Search is the start of a search - it calls the searchers. +// +// This gets called from the UI when somebody types a new search query. +// It serves both that autocomplete and the full result that can come +// out of the searches as an answer. func Search(ctx PfCtx, async bool, search string) (results []PfSearchResult, te time.Duration, err error) { if len(searchers) == 0 { err = errors.New("No Searchers available") diff --git a/lib/setup.go b/lib/setup.go index cfb49ab..5b99584 100755 --- a/lib/setup.go +++ b/lib/setup.go @@ -1,15 +1,13 @@ +// Pitchfork Lib Setup functions. +// +// Split out so that we can call it for Tests cases too next to normal server behaviour. package pitchfork import ( "time" ) -/* - * Trident Pitchfork Lib Setup - * - * Split out so that we can call it for Tests cases too next to normal server behaviour - */ - +// Setup sets-up a pitchfork tool given the parameters. func Setup(toolname string, confroot string, verbosedb bool, app_schema_version int) (err error) { /* Load configuration */ err = Config.Load(toolname, confroot) @@ -48,7 +46,7 @@ func Setup(toolname string, confroot string, verbosedb bool, app_schema_version return } -/* Start background services */ +// Starts starts background services. func Starts() { /* Start IP Tracker -- against brute force login attempts */ Iptrk_start(5, 10*time.Hour, "1 hour") @@ -57,7 +55,7 @@ func Starts() { JwtInv_start(30 * time.Minute) } -/* Should be deferred Starts() call */ +// Stops stops background services, should be matching and thus deferred after a Starts() call. func Stops() { Iptrk_stop() JwtInv_stop() diff --git a/lib/struct.go b/lib/struct.go index 3383802..d696b78 100755 --- a/lib/struct.go +++ b/lib/struct.go @@ -1,3 +1,4 @@ +// Pitchfork struct provides access functions to various ways to inspect or modify contents of structs. package pitchfork import ( @@ -9,14 +10,17 @@ import ( "time" ) +// PTypeField wraps the relect StructField for ease of use in various Struct related functions. type PTypeField struct { reflect.StructField } +// PTypeWrap simply wraps the relect.StructField into our own PTypField. func PTypeWrap(f reflect.StructField) PTypeField { return PTypeField{f} } +// PType is used to define the CRUD option. type PType int /* CRUD */ @@ -27,12 +31,20 @@ const ( PTYPE_DELETE /* Delete */ ) -/* - * Get the datatype from either the forced version - * or the actual type of the field using reflection - * - * If 'doignore' is set return 'ignore' - */ +// PfType is used to get the datatype from either the pftype tag +// or by basing it on the actual type of the field using reflection. +// +// PfType determines if a datatype needs to be recursed and if it +// is a compound structure. +// +// If 'doignore' is set, for some types the return type is 'ignore' to +// indicate that the field does not need to be processd. +// This option is for instance used for set/get purposes where +// 'note' and 'header' cannot be changed and thus can be ignored +// for that purpose. +// +// This is primarily a helper function for other functions that +// parse structs and thus want to adhere to the types and tags. func PfType(f reflect.StructField, v reflect.Value, doignore bool) (ttype string, dorecurse bool, compound bool) { /* Forced type */ ttype = f.Tag.Get("pftype") @@ -148,11 +160,15 @@ func PfType(f reflect.StructField, v reflect.Value, doignore bool) (ttype string return } -/* - * Check CanAddr() so that we do a recurse while - * we can with ability to set, but recurse otherwise - * in readonly version - */ +// StructRecurse is used to recurse through a structure +// this in the case it is wrapped or embedded. +// +// Check CanAddr() so that we do a recurse while +// we can with ability to set, but recurse otherwise +// in readonly version. +// +// This function is primarily used by other struct related functions +// and should rarely be called otherwise. func StructRecurse(v reflect.Value) interface{} { if v.Kind() != reflect.Interface && v.CanAddr() { return v.Addr().Interface() @@ -161,6 +177,9 @@ func StructRecurse(v reflect.Value) interface{} { return v.Interface() } +// StructNameT returns the name of a structure from a type (T). +// +// This function is primarily used by other struct related functions. func StructNameT(t reflect.Type) string { if t.Kind() == reflect.Ptr { t = t.Elem() @@ -174,6 +193,9 @@ func StructNameT(t reflect.Type) string { return n } +// StructNameObj returns the name of the object (Obj). +// +// This function is primarily used by other struct related functions. func StructNameObj(obj interface{}) string { s, _ := StructReflect(obj) n := s.PkgPath() + "." + s.Name() @@ -184,6 +206,9 @@ func StructNameObj(obj interface{}) string { return n } +// StructNameObjTrail returns the full trail of objects as a name. +// +// This function is primarily used by other struct related functions. func StructNameObjTrail(objtrail []interface{}) (oname string) { for _, obj := range objtrail { if oname != "" { @@ -195,6 +220,10 @@ func StructNameObjTrail(objtrail []interface{}) (oname string) { return } +// StructReflect performs reflection, getting out the type +// and the value, dereferencing the pointer where needed. +// +// This function is primarily used by other struct related functions. func StructReflect(obj interface{}) (s reflect.Type, va reflect.Value) { s = reflect.TypeOf(obj) @@ -209,6 +238,28 @@ func StructReflect(obj interface{}) (s reflect.Type, va reflect.Value) { return s, va } +// StructFetchFields builds a SELECT SQL query to retrieve +// all the fields in a structure from a database +// +// The object passed is in parsed, each structure field in-order. +// The table is used in the FROM query. +// The q parameter is where the composed SQL query is returned. +// The ifs parameter is where space for the storage of the to-be-retrieved +// data is stored. +// +// A caller thus calls this with a pointer to an empty query string +// and an empty ifs array and StructFetchFields then builds the query +// and returns that in the query argument and in the ifs array. +// +// The intermediary 'ifs' is thus used for temporary storage in a way +// that the SQL library wants to receive it. +// It also allows for multiple objects to be queried and later stored. +// +// This function cooperates with the StructFetchStore function which, +// after executing the query, can then store the result in the actual structure. +// +// This function is typically called from StructFetch or StructFetchMulti +// which calls StructFetchFields and StructFetchStore. func StructFetchFields(obj interface{}, table string, q *string, ifs *[]interface{}) (err error) { fun := "StructFetchFields() " @@ -350,6 +401,15 @@ func StructFetchFields(obj interface{}, table string, q *string, ifs *[]interfac return nil } +// StructFetchStore stores the result of a StructFetchFields build query into +// the object that is passed in. +// +// The obj argument represents the object we want the results to be stored in. +// The ifs argument is the result returned from StructFetchFields, and where +// the SQL QueryRow/QueryRows call has stored it's result. +// +// This function is typically called from StructFetch or StructFetchMulti +// which calls StructFetchFields and StructFetchStore. func StructFetchStore(obj interface{}, ifs []interface{}, ifs_n *int) (err error) { fun := "StructFetch() " @@ -448,6 +508,31 @@ func StructFetchStore(obj interface{}, ifs []interface{}, ifs_n *int) (err error return nil } +// StructFetchWhere allows filtering the fields returned with StructFetchFields +// +// StructFetch/StructFetchMulti use this to append the 'where' clause portion +// of the SQL query. +// +// The starting query (qi), containing the "SELECT , " is passed in. +// StructFetchWhere adds the FROM portion and any SQL table joins, from the join argument, +// and then, based on the params and the andor setting creates a WHERE query. +// +// andor defines if the where query should be composed of AND or OR statements +// params defines the parameters that should be verified +// matchopts defines the way the match in the WHERE should work (LIKE, =, etc) +// matches defines what should be matched against. +// +// The order argument then is additionally used for determining the order of the output. +// +// To illustrate the arguments: +// +// {qi} FROM {table} {join} +// +// and then followed per pair of params/matchopts/matches: +// {andor} {params} {matchopt} {matches} +// eg: AND param LIKE '%match%' +// +// appended by the {order}. func StructFetchWhere(qi string, table string, join string, andor DB_AndOr, params []string, matchopts []DB_Op, matches []interface{}, order string) (q string, vals []interface{}) { q = qi @@ -527,6 +612,15 @@ func StructFetchWhere(qi string, table string, join string, andor DB_AndOr, para return } +// StructFetchMulti returns multiple objects using StructFetchFields. +// +// The newobject function is called to create a new object to be filled in. +// Any type of object can be returned. These objects are returned in the objs parameter. +// +// See StructFetchWhere for the table/jon/andor/params/matchops/matches/order arguments. +// +// The limit argument can be used to add a LIMIT to the SQL query. +// The offset argument can be used to add a OFFSET to the SQL query. func StructFetchMulti(newobject func() interface{}, table string, join string, andor DB_AndOr, params []string, matchopts []DB_Op, matches []interface{}, order string, offset int, limit int) (objs []interface{}, err error) { var ifs []interface{} = nil @@ -582,6 +676,17 @@ func StructFetchMulti(newobject func() interface{}, table string, join string, a return objs, nil } +// StructFetchA exposes extra options than the simpler StructFetch +// it is used to fetch data from a database directly into a structure +// based on the fields in that structure and the parameters given. +// +// See StructFetchWhere for the table/jon/andor/params/matchops/matches/order arguments. +// +// The notfoundok boolean is used to return ErrNoRows when set to true. +// Otherwise it returns a string 'not found' error. +// This toggle primarily exists to ease the difference between programmatically +// calling this function, and wanting to process the ErrNoRows further or +// wanting to return the result to the CLI or other human readable error construct. func StructFetchA(obj interface{}, table string, join string, params []string, matches []string, order string, notfoundok bool) (err error) { q := "" @@ -642,10 +747,15 @@ func StructFetchA(obj interface{}, table string, join string, params []string, m return } +// StructFetch calls StructFetchA() but avoids the need to specify a few +// parameters that are not always needed (join and ordering). +// +// See StructFetchA for the details to the arguments. func StructFetch(obj interface{}, table string, params []string, matches []string) (err error) { return StructFetchA(obj, table, "", params, matches, "", false) } +// StructOp defines what operation to perform on a structure type StructOp uint const ( @@ -654,6 +764,11 @@ const ( STRUCTOP_REMOVE /* Remove the item */ ) +// StructFieldMod allows changing a field identified by fname to a new value. +// +// Set/add/remove are possible depending on datatype: set for non-slice, add/remove are slice only. +// +// This function is typically called through StructMod(). func StructFieldMod(op StructOp, fname string, f reflect.StructField, v reflect.Value, value interface{}) (err error) { fun := "StructFieldMod() " @@ -806,7 +921,7 @@ func StructFieldMod(op StructOp, fname string, f reflect.StructField, v reflect. break case reflect.Int, reflect.Int64: - no := NI64(value.(int64)) + no := ToNullInt64(value.(int64)) v.Set(reflect.ValueOf(no)) break @@ -1014,6 +1129,9 @@ func StructFieldMod(op StructOp, fname string, f reflect.StructField, v reflect. } } +// structModA modifies a single field. +// +// This is called by StructMod, recursively to be able to support nested structs. func StructModA(op StructOp, obj interface{}, field string, value interface{}) (done bool, err error) { fun := "StructMod() " @@ -1072,6 +1190,13 @@ func StructModA(op StructOp, obj interface{}, field string, value interface{}) ( return } +// StructMod modifies a single field of a object. +// +// Given the object obj, it finds the 'field' in the structure and sets it to the given value. +// +// ErrNoRows is returned when no such field was found. +// Other errors, eg permission errors or inability to set a field can also be returned. +// An error of nil is returned if the change of the value succeeded. func StructMod(op StructOp, obj interface{}, field string, value interface{}) (err error) { done, err := StructModA(op, obj, field, value) if err == nil && !done { @@ -1082,9 +1207,7 @@ func StructMod(op StructOp, obj interface{}, field string, value interface{}) (e return } -/* - * Return all fields of a struct that can be retrieved or modified - */ +// structVarsA is the recursive portion of StructVars and thus only called by that function func StructVars(ctx PfCtx, obj interface{}, ptype PType, doignore bool) (vars map[string]string, err error) { objtrail := []interface{}{} vars = make(map[string]string) @@ -1092,6 +1215,13 @@ func StructVars(ctx PfCtx, obj interface{}, ptype PType, doignore bool) (vars ma return vars, err } +// StructVars returns all fields in a struct that can be retrieved or modified, returning a map of strings. +// +// StructVars takes an object to inspect and a ptype indicating what permissions the field should satisfy for them to be included. +// +// The map consists out of the key being the fieldname and the value being the ToString()'d value of the field. +// +// Permissions are tested against the provided ptype and the context. func StructVarsA(ctx PfCtx, objtrail []interface{}, obj interface{}, ptype PType, doignore bool, vars map[string]string) (err error) { s, va := StructReflect(obj) @@ -1155,16 +1285,18 @@ func StructVarsA(ctx PfCtx, objtrail []interface{}, obj interface{}, ptype PType return } +// StructDetails_Options defines options to apply when checking StructDetails type StructDetails_Options int const ( - SD_None = 0 - SD_Perms_Check StructDetails_Options = 0 << iota - SD_Perms_Ignore - SD_Tags_Require - SD_Tags_Ignore + SD_None = 0 /* No Options */ + SD_Perms_Check StructDetails_Options = 0 << iota /* Check Permissions */ + SD_Perms_Ignore /* Ignore Permissions */ + SD_Tags_Require /* Require Tags */ + SD_Tags_Ignore /* Ignore Tags */ ) +// structDetailsA is the recursive portion of StructDetails, see that function for more details func StructDetailsA(ctx PfCtx, obj interface{}, field string, opts StructDetails_Options) (ftype string, fname string, fvalue string, err error) { checkperms := false if opts&SD_Perms_Check > 0 { @@ -1233,6 +1365,12 @@ func StructDetailsA(ctx PfCtx, obj interface{}, field string, opts StructDetails return "", "", "", nil } +// StructDetails returns the details of a structure's field. +// +// It determines the type of the field and the string value of the field. +// +// The opts can be used to influence if permission checking needs to be done +// and if tags are required to be present for the field to be checked. func StructDetails(ctx PfCtx, obj interface{}, field string, opts StructDetails_Options) (ftype string, fname string, fvalue string, err error) { field = strings.ToLower(field) @@ -1244,6 +1382,7 @@ func StructDetails(ctx PfCtx, obj interface{}, field string, opts StructDetails_ return } +// structTagA is the recursive portion of StructTag, see that function for details func StructTagA(obj interface{}, field string, tag string) (val string, err error) { s, va := StructReflect(obj) @@ -1289,6 +1428,14 @@ func StructTagA(obj interface{}, field string, tag string) (val string, err erro return "", nil } +// StructTag retrieves the requested tag from a field in a structure. +// +// Any type of object can be provided, it will be recursed incase of embedded structs. +// +// The field indicates the name of the structure's field. +// The tag indicates which tag to get for that field. +// +// The value returned is the content of the tag for the requested field. func StructTag(obj interface{}, field string, tag string) (val string, err error) { field = strings.ToLower(field) @@ -1300,31 +1447,9 @@ func StructTag(obj interface{}, field string, tag string) (val string, err error return } -/* Create a "get" or "set" menu from a struct */ -func StructMenu(ctx PfCtx, subjects []string, obj interface{}, onlyslices bool, fun PfFunc) (menu PfMenu, err error) { - var isedit bool - - /* Select the Object */ - ctx.SelectObject(&obj) - - /* Number of subjects */ - nargs := len(subjects) - - /* Edit or not? */ - if fun != nil { - isedit = true - - /* Edit's require one more argument */ - nargs++ - } else { - fun = structGet - } - - /* Recursive call */ - objtrail := []interface{}{} - return StructMenuA(ctx, subjects, objtrail, obj, onlyslices, fun, isedit, nargs) -} - +// structMenuA is the recursive portion of StructMenu +// +// See StructMenu for more details. func StructMenuA(ctx PfCtx, subjects []string, objtrail []interface{}, obj interface{}, onlyslices bool, fun PfFunc, isedit bool, nargs int) (menu PfMenu, err error) { /* Prepend this object to the trail */ objtrail = append([]interface{}{obj}, objtrail...) @@ -1408,7 +1533,7 @@ func StructMenuA(ctx PfCtx, subjects []string, objtrail []interface{}, obj inter } set := f.Tag.Get(tag) - perms, err = ctx.ConvertPerms(set) + err = perms.FromString(set) if err != nil { return } @@ -1491,6 +1616,52 @@ func StructMenuA(ctx PfCtx, subjects []string, objtrail []interface{}, obj inter return menu, nil } +// StructMenu can create "get", "set", "add" and "remove" CLI menus +// from a given structure. +// +// The subjects parameter indicates the field(s) that indicates what +// should be provided as an argument to select that specific object. +// +// The object is an example object (just the structure, no values needed) +// that has a set of fields with tags. The tags are used to retrieve the +// labels and descriptions for the field, but also the permissions needed +// to configure that field. +// +// onlyslices is used to switch between a 'get'/'set' menu and a 'add'/'remove' menu. +// When onlyslices is true only fields that are slices are listed, these will +// require an 'add'/'remove' construct instead of being able to be directly 'set' or 'get'. +// +// When a function is provided, the menu becomes a 'set' or add/remove menu (for slices). +// When no function is provided the resulting menu is a 'get' menu. +// The permissions for set/get are adhered to. +// +// See the example for a minimal example. The User portion of Pitchfork makes a good +// full example on how this code is used, see lib/user.go user_get/user_set etc. +func StructMenu(ctx PfCtx, subjects []string, obj interface{}, onlyslices bool, fun PfFunc) (menu PfMenu, err error) { + var isedit bool + + /* Select the Object */ + ctx.SelectObject(&obj) + + /* Number of subjects */ + nargs := len(subjects) + + /* Edit or not? */ + if fun != nil { + isedit = true + + /* Edit's require one more argument */ + nargs++ + } else { + fun = structGet + } + + /* Recursive call */ + objtrail := []interface{}{} + return StructMenuA(ctx, subjects, objtrail, obj, onlyslices, fun, isedit, nargs) +} + +// structGetA is the recursive part of StructGet. func structGetA(ctx PfCtx, obj interface{}, field string) (done bool, err error) { s, va := StructReflect(obj) @@ -1563,7 +1734,7 @@ func structGetA(ctx PfCtx, obj interface{}, field string) (done bool, err error) return } -/* Create a "get" or "set" menu from a struct -- called from StructMenuA() */ +// Create a "get" menu from a struct -- helper function of StructMenu. func structGet(ctx PfCtx, args []string) (err error) { obj := ctx.SelectedObject() @@ -1581,6 +1752,7 @@ func structGet(ctx PfCtx, args []string) (err error) { return } +// ToString converts any type of object to a string representation. func ToString(v interface{}) (str string) { s, _ := StructReflect(v) @@ -1662,10 +1834,19 @@ func ToString(v interface{}) (str string) { panic("ToString() Unhandled Type: " + s.String()) } +// ObjFuncI retains a object. type ObjFuncI struct { obj interface{} } +// ObjHasFunc is used to determine of an object has the given function, +// returning the (embedded) object that has the function +// +// An objecttrail consisting of one or more objects is passed in, thus +// allowing a function to be found in a nested set of objects. +// +// This call should be used before ObjFunc to ensure the function +// has the given object, and to select the correct object. func ObjHasFunc(objtrail []interface{}, fun string) (ok bool, obj ObjFuncI) { ok = false @@ -1687,6 +1868,14 @@ func ObjHasFunc(objtrail []interface{}, fun string) (ok bool, obj ObjFuncI) { return } +// ObjFunc calls, when available, a function in an object and returns it's result +// +// The 'fun' is retrieved from the given object, as typically found with ObjHasFunc. +// The function is then verified to be able to accept the parameters specified by params. +// And the function is then called dynamically. +// +// One major use-case is the Translate function of objects, where we typically do +// not know what object we will be calling that function on. func ObjFunc(obj ObjFuncI, fun string, params ...interface{}) (result []reflect.Value, err error) { result = nil err = nil @@ -1728,6 +1917,9 @@ func ObjFunc(obj ObjFuncI, fun string, params ...interface{}) (result []reflect. return } +// ObjFuncIface calls a function of an object and returns the result of an interface. +// +// See ObjFunc for more details on the parameters. func ObjFuncIface(obj ObjFuncI, fun string, params ...interface{}) (iface interface{}, err error) { res, err := ObjFunc(obj, fun, params...) @@ -1740,6 +1932,14 @@ func ObjFuncIface(obj ObjFuncI, fun string, params ...interface{}) (iface interf return } +// ObjFuncStr calls a function of an object and returns a string. +// +// When the returned type of the called function is not a string, +// this code will return a string indicating that in the string. +// Similary the string will be filled with a notion that the call failed. +// Next to having a non-nil error return. +// +// See ObjFunc for more details on the parameters. func ObjFuncStr(obj ObjFuncI, fun string, params ...interface{}) (str string, err error) { res, err := ObjFunc(obj, fun, params...) @@ -1756,6 +1956,17 @@ func ObjFuncStr(obj ObjFuncI, fun string, params ...interface{}) (str string, er return } +// objPermCheck calls custom PermCheck function on an object and determines +// if one has access and is allowed to edit or not. +// +// The ptype is used for the permission check to determine if read or write access is needed. +// Per-application permissions could be more strict and be full CRUD-style. +// +// The return of 'ok' indicates that it is allowed to access the field. +// The allowedit indicates if the field is allowed to be edited/modified. +// The error indicates whether anything failed, nil indicates success. +// +// This function is used by StructPermCheck. func ObjPermCheck(ctx PfCtx, obj ObjFuncI, ptype PType, f PTypeField) (ok bool, allowedit bool, err error) { res, err := ObjFunc(obj, "PermCheck", ctx, ptype, f) @@ -1776,6 +1987,19 @@ func ObjPermCheck(ctx PfCtx, obj ObjFuncI, ptype PType, f PTypeField) (ok bool, return } +// StructPermCheck checks the permissions of a struct, +// and determines if one has acccess and is allowed to edit. +// +// The ptype allows specifying of CRUD-style (Create/Read/Update/Delete) permissions to check for. +// The objtrail is passed in, to allow a surrounding object to be used for Permission checking. +// The PermCheck function of the first object in the trail is used for permission checks next to +// the pitchfork pfget/pfset permissions. +// +// The f parameter is the field we are checking permissions for. +// +// The return of 'ok' indicates that it is allowed to access the field. +// The allowedit indicates if the field is allowed to be edited/modified. +// The error indicates whether anything failed, nil indicates success. func StructPermCheck(ctx PfCtx, ptype PType, objtrail []interface{}, f PTypeField) (ok bool, allowedit bool, err error) { switch ptype { case PTYPE_CREATE, PTYPE_UPDATE: diff --git a/lib/struct_test.go b/lib/struct_test.go new file mode 100644 index 0000000..95bd0a3 --- /dev/null +++ b/lib/struct_test.go @@ -0,0 +1,61 @@ +package pitchfork + +import ( + "fmt" +) + +func structmenu_set_dummy(ctx PfCtx, args []string) (err error) { + return +} + +func ExampleStructMenu() { + /* Create a fake context, this is an example */ + ctx := testingctx() + + /* The structure we want to convert into a 'set' and 'add' menu */ + type Example struct { + ID int `label:"ID" pfset:"nobody" pfget:"user" hint:"The identify of this field"` + AField string `label:"Field" pfset:"user" pfget:"user" hint:"A Field a user can modify"` + AnotherField string `label:"Another pfset:"sysadmin" pfget:"user" hint:"Another Field that only a sysadmin could modify, but any user can read"` + Foods []string `label:"Foods" pfset:"user" pfget:"user"` + } + + /* Make an, empty, instance of the object */ + example := &Example{} + + getmenu, err := StructMenu(ctx, []string{"id"}, example, false, structmenu_set_dummy) + if err != nil { + fmt.Printf("Problem generating menu: %s", err.Error()) + } + fmt.Printf("%#v", getmenu) + /* + * When linked under 'example get' results in a menu that can be used with the CLI commands: + * example get id + * example get afield + * example get anotherfield + */ + + setmenu, err := StructMenu(ctx, []string{"id"}, example, false, structmenu_set_dummy) + if err != nil { + fmt.Printf("Problem generating menu: %s", err.Error()) + } + fmt.Printf("%#v", setmenu) + /* + * When linked under 'example set' results in a menu that can be used with the CLI commands: + * example set id + * example set afield + * example set anotherfield -- sysadmin required + */ + + addmenu, err := StructMenu(ctx, []string{"id"}, example, true, structmenu_set_dummy) + if err != nil { + fmt.Printf("Problem generating menu: %s", err.Error()) + } + fmt.Printf("%#v", addmenu) + /* + * When linked under 'example add' results in a menu that can be used with the CLI commands: + * example add foods + * + * The 'remove' menu is identical in output, but given another function will remove items. + */ +} diff --git a/lib/system.go b/lib/system.go index 2dd12d3..edcdc74 100755 --- a/lib/system.go +++ b/lib/system.go @@ -1,3 +1,4 @@ +// Pitchfork system functions. package pitchfork import ( @@ -12,6 +13,7 @@ import ( "time" ) +// PfSys contains all the global configuration details of Pitchfork type PfSys struct { Name string `label:"System Name" pfset:"sysadmin" hint:"Name of the System"` Welcome string `label:"Welcome Text" pftype:"text" pfset:"sysadmin" pfcol:"welcome_text" hint:"Welcome message shown on login page"` @@ -46,6 +48,7 @@ type PfSys struct { sar_cache []net.IPNet /* Cache for parsed version of SARestrict, populated by fetch() */ } +// PfAudit is used to store and list audit records about actions type PfAudit struct { Member string What string @@ -55,11 +58,14 @@ type PfAudit struct { Entered time.Time } +// Started indicates when pitchfork started running; used for 'uptime'. var Started = time.Now().UTC() +// Cached edition of the system object, along with mutex to protect it. var system_cached PfSys var system_cachedm sync.Mutex +// System_Get retrieves the PfSys object, from cache or fresh */ func System_Get() (system *PfSys) { system_cachedm.Lock() defer system_cachedm.Unlock() @@ -72,6 +78,7 @@ func System_Get() (system *PfSys) { return &system_cached } +// System_AuditMax returns the maximum number of entries for the audit list with the given parameters. func System_AuditMax(search string, user_name string, gr_name string) (total int, err error) { var args []interface{} @@ -99,6 +106,7 @@ func System_AuditMax(search string, user_name string, gr_name string) (total int return total, err } +// System_AuditList returns the entries for the audit list with the given parameters. func System_AuditList(search string, user_name string, gr_name string, offset int, max int) (audits []PfAudit, err error) { var args []interface{} var rows *Rows @@ -160,6 +168,7 @@ func System_AuditList(search string, user_name string, gr_name string, offset in return } +// fetch is used to fetch the configuration details from the database. func (system *PfSys) fetch() (err error) { q := "SELECT key, value " + "FROM config" @@ -234,12 +243,13 @@ func (system *PfSys) fetch() (err error) { return nil } +// Refresh refreshes a system object. func (system *PfSys) Refresh() (err error) { err = system.fetch() return } -/* Create a PfPWRules object */ +// PWRules returns a initialized PfPWRules object. func (system *PfSys) PWRules() (rules PfPWRules) { rules.Min_length = system.PW_Length rules.Max_length = system.PW_LengthMax @@ -251,6 +261,7 @@ func (system *PfSys) PWRules() (rules PfPWRules) { return } +// CheckPWRules checks if a password matches the configured password rules. func CheckPWRules(ctx PfCtx, password string) (err error) { var pw PfPass @@ -273,16 +284,25 @@ func CheckPWRules(ctx PfCtx, password string) (err error) { return } +// system_report returns a system report with misc important details about the system (CLI). func system_report(ctx PfCtx, args []string) (err error) { var msg string maxdb := 10 + // The application version. ctx.OutLn(VersionText()) + // When the daemon was started. ctx.OutLn("Daemon started at %s", Started.String()) ctx.OutLn("Daemon running for %s", time.Now().UTC().Sub(Started).String()) ctx.OutLn("") + // Password dictionary details. + msg = Pw_details() + ctx.OutLn(msg) + ctx.OutLn("") + + // Database details. msg, err = DB.Check() if err != nil { if msg != "" { @@ -332,6 +352,7 @@ func system_report(ctx PfCtx, args []string) (err error) { return } +// System_db_setup is used for initial setup of the database. func System_db_setup() (err error) { err = DB.Setup_psql() if err != nil { @@ -347,6 +368,7 @@ func System_db_setup() (err error) { return } +// System_findfile is used for find a file in the multiple shared file roots. func System_findfile(subdir string, name string) (fn string) { /* Try all the roots to find the file */ for _, root := range Config.File_roots { @@ -363,6 +385,7 @@ func System_findfile(subdir string, name string) (fn string) { return } +// System_SharedFile returns the best shared file. func System_SharedFile(thefile string) (fn string, err error) { err = nil @@ -382,6 +405,7 @@ func System_SharedFile(thefile string) (fn string, err error) { return } +// System_db_test_setup configures a test setup (Developer Use only). func System_db_test_setup() (err error) { err = DB.executeFile("test_data.psql") @@ -395,6 +419,7 @@ func System_db_test_setup() (err error) { return } +// System_db_upgrade upgrades the system database. func System_db_upgrade() (err error) { err = DB.Upgrade() if err != nil { @@ -405,6 +430,7 @@ func System_db_upgrade() (err error) { return } +// App_db_upgrade upgrades the application database. func App_db_upgrade() (err error) { err = DB.AppUpgrade() if err != nil { @@ -415,12 +441,13 @@ func App_db_upgrade() (err error) { return } +// System_db_cleanup cleans up (destroys&deletes) the full database. func System_db_cleanup() (err error) { err = DB.Cleanup_psql() return } -/* setup only */ +// System_adduser used at setup only time can be used to directly add a user to the database, the user will be a sysadmin with full powers. func System_adduser(username string, password string) (err error) { /* Hash the password */ var pw PfPass @@ -458,7 +485,7 @@ func System_adduser(username string, password string) (err error) { return } -/* setup only */ +// System_setpassword can be used through the setup command to forcefully change a password bypassing all checks. func System_setpassword(username string, password string) (err error) { /* Hash the password */ var pw PfPass @@ -487,7 +514,9 @@ func System_setpassword(username string, password string) (err error) { return } -/* args: [twofactor] */ +// system_login can be used to login to the system (CLI). +// +// args: [twofactor] func system_login(ctx PfCtx, args []string) (err error) { tf := "" if len(args) == 3 { @@ -503,11 +532,13 @@ func system_login(ctx PfCtx, args []string) (err error) { return } +// system_logout logs one out of the system (CLI). func system_logout(ctx PfCtx, args []string) (err error) { ctx.Logout() return nil } +// system_whoami can be used to check who one is logged in as (CLI). func system_whoami(ctx PfCtx, args []string) (err error) { if ctx.IsLoggedIn() { theuser := ctx.TheUser() @@ -519,6 +550,7 @@ func system_whoami(ctx PfCtx, args []string) (err error) { return nil } +// system_swapadmin is to swap between being a normal user and one with actual sysadmin privileges (CLI). func system_swapadmin(ctx PfCtx, args []string) (err error) { if !ctx.SwapSysAdmin() { err = errors.New("Swapping failed") @@ -534,6 +566,7 @@ func system_swapadmin(ctx PfCtx, args []string) (err error) { return nil } +// system_set_xxx is used to configure properties of the system (system_set/system_get). func system_set_xxx(ctx PfCtx, args []string) (err error) { var fname string var fval string @@ -582,6 +615,7 @@ func system_set_xxx(ctx PfCtx, args []string) (err error) { return } +// system_sget is used to set or get system properties. func system_sget(ctx PfCtx, args []string, fun PfFunc) (err error) { subjects := []string{} @@ -597,6 +631,7 @@ func system_sget(ctx PfCtx, args []string, fun PfFunc) (err error) { return } +// system_set is used to set system properties (CLI). func system_set(ctx PfCtx, args []string) (err error) { err = system_sget(ctx, args, system_set_xxx) @@ -606,10 +641,12 @@ func system_set(ctx PfCtx, args []string) (err error) { return } +// system_get is used to get system properties (CLI). func system_get(ctx PfCtx, args []string) (err error) { return system_sget(ctx, args, nil) } +// system_batch is used to run a batch file with CLI commands (CLI). func system_batch(ctx PfCtx, args []string) (err error) { na := len(args) @@ -642,6 +679,7 @@ func system_batch(ctx PfCtx, args []string) (err error) { return ctx.Batch(args[0]) } +// system_auditlog can be used to inspect the audit log of the system (CLI). func system_auditlog(ctx PfCtx, args []string) (err error) { offset := 0 max := 0 @@ -687,7 +725,7 @@ func system_auditlog(ctx PfCtx, args []string) (err error) { } for _, a := range audits { - ctx.Outf("Entered : %s\n", tmp_fmt_time(a.Entered)) + ctx.Outf("Entered : %s\n", Fmt_Time(a.Entered)) ctx.Outf(" Member : %s\n", a.Member) ctx.Outf(" What : %s\n", a.What) ctx.Outf(" Username: %s\n", a.UserName) @@ -699,6 +737,7 @@ func system_auditlog(ctx PfCtx, args []string) (err error) { return } +// system_menu is the CLI system menu (CLI). func system_menu(ctx PfCtx, args []string) (err error) { menu := NewPfMenu([]PfMEntry{ {"report", system_report, 0, 0, nil, PERM_SYS_ADMIN, "Report system statistics"}, diff --git a/lib/template.go b/lib/template.go index 91fdb06..aa7436f 100644 --- a/lib/template.go +++ b/lib/template.go @@ -1,3 +1,6 @@ +// Pitchfork Template functions - used from inside the templates +// +// These functions are for the templates, not to be otherwise called. package pitchfork import ( @@ -11,78 +14,111 @@ import ( "time" ) -/* Constants */ +// Number of items per page, for pagers const PAGER_PERPAGE = 10 -/* Templates */ +// The Templates, stored here cached and loaded along with functions. +// +// Can be retrieved with Template_Get so that one can then operate +// on these templates. +// +// Template_Load does the setup of this variable. +// There is no mutex protecting this variable as no modifications +// after the Template_Load are done on it. var g_tmp *template.Template +// Template Functions to make a variety of tasks +// in templates easier or more standardized. +// +// See the specific functions for more details about each of them. +// +// Extra, typically application specific, functions can be +// added using the Template_FuncAdd call. var template_funcs = template.FuncMap{ - "pager_less_ok": tmp_pager_less_ok, - "pager_less": tmp_pager_less, - "pager_more_ok": tmp_pager_more_ok, - "pager_more": tmp_pager_more, - "var_pager_less_ok": tmp_var_pager_less_ok, - "var_pager_less": tmp_var_pager_less, - "var_pager_more_ok": tmp_var_pager_more_ok, - "var_pager_more": tmp_var_pager_more, - "group_home_link": tmp_group_home_link, - "user_home_link": tmp_user_home_link, - "user_image_link": tmp_user_image_link, - "fmt_date": tmp_fmt_date, - "fmt_datemin": tmp_fmt_datemin, - "fmt_time": tmp_fmt_time, - "fmt_string": tmp_fmt_string, - "str_tolower": tmp_str_tolower, - "str_emboss": tmp_str_emboss, - "inc_file": tmp_inc_file, - "dumpvar": tmp_dumpvar, - "dict": tmp_dict, + "pager_less_ok": tmpPagerLessOk, + "pager_less": tmpPagerLess, + "pager_more_ok": tmpPagerMoreOk, + "pager_more": tmpPagerMore, + "var_pager_less_ok": tmpVarPagerLessOk, + "var_pager_less": tmpVarPagerLess, + "var_pager_more_ok": tmpVarPagerMoreOk, + "var_pager_more": tmpVarPagerMore, + "group_home_link": tmpGroupHomeLink, + "user_home_link": tmpUserHomeLink, + "user_image_link": tmpUserImageLink, + "fmt_date": tmpFmtDate, + "fmt_datemin": tmpFmtDateMin, + "fmt_time": tmpFmtTime, + "fmt_string": tmpFmtString, + "str_tolower": tmpStrToLower, + "str_emboss": tmpStrEmboss, + "inc_file": tmpIncFile, + "dumpvar": tmpDumpVar, + "dict": tmpDict, } +// Template_FuncAdd allows adding a application specific template function +// +// After adding, the function is available to all templates in the system. +// +// Noting that a template function has to be available before we load the +// template, thus typically Template_FuncAdd is called from a init() function. func Template_FuncAdd(name string, f interface{}) { template_funcs[name] = f } +// Template_Get retrieves the custom template cache along with configured custom template functions. +// +// After which ExecuteTemplate can be called passing the template name and the data to be rendered. +// +// Primarily used by UI's page_render. func Template_Get() *template.Template { return g_tmp } -/* Template Functions - used from inside the templates */ -func tmp_pager_less_ok(cur int) bool { +// tmpPagerLessOk returns true if there can be a 'previous' page. +func tmpPagerLessOk(cur int) bool { return cur >= PAGER_PERPAGE } -func tmp_pager_less(cur int) int { +// tmpPagerLess returns the offset of the 'previous' page. +func tmpPagerLess(cur int) int { return cur - PAGER_PERPAGE } -func tmp_pager_more_ok(cur int, max int) bool { +// tmpPagerMoreOk returns true if there can be a 'next' page. +func tmpPagerMoreOk(cur int, max int) bool { return cur < (max - PAGER_PERPAGE) } -func tmp_pager_more(cur int, max int) int { +// tmpPagerMore returns the offset of the 'next' page. +func tmpPagerMore(cur int, max int) int { return cur + PAGER_PERPAGE } -/* Variable size pager function. */ -func tmp_var_pager_less_ok(page int, cur int) bool { +// tmpVarPagerLessOk returns true if there can be a 'previous' page. +func tmpVarPagerLessOk(page int, cur int) bool { return cur >= page } -func tmp_var_pager_less(page int, cur int) int { +// tmpVarPagerLess returns the offset of the 'previous' page. +func tmpVarPagerLess(page int, cur int) int { return cur - page } -func tmp_var_pager_more_ok(page int, cur int, max int) bool { +// tmpVarPagerMoreOk returns true if there can be a 'next' page. +func tmpVarPagerMoreOk(page int, cur int, max int) bool { return cur < (max - page) } -func tmp_var_pager_more(page int, cur int, max int) int { +// tmpVarPagerMore returns the offset of the 'next' page. +func tmpVarPagerMore(page int, cur int, max int) int { return cur + page } -func tmp_group_home_link(ctx PfCtx, groupname string, username string, fullname string) template.HTML { +// tmpGroupHomeLink returns the HTML formatted link to the group's home +// though only returns the fullname when the configuration to link to them is disabled. +func tmpGroupHomeLink(ctx PfCtx, groupname string, username string, fullname string) template.HTML { html := "" /* In case the user has no full name use the username */ @@ -99,7 +135,9 @@ func tmp_group_home_link(ctx PfCtx, groupname string, username string, fullname return HEB(html) } -func tmp_user_home_link(ctx PfCtx, username string, fullname string) template.HTML { +// tmpUserHomeLink returns the HTML formatted link to the user's home +// though only returns the fullname when the configuration to link to them is disabled. +func tmpUserHomeLink(ctx PfCtx, username string, fullname string) template.HTML { html := "" /* In case the user has no full name use the username */ @@ -116,7 +154,11 @@ func tmp_user_home_link(ctx PfCtx, username string, fullname string) template.HT return HEB(html) } -func tmp_user_image_link(ctx PfCtx, username string, fullname string, extraclass string) template.HTML { +// tmpUserImageLink returns the HTML formatted link to the user's image. +// +// username and fullname provide the details about the user. +// extraclass can optionally be used to specify an extra HTML class to include. +func tmpUserImageLink(ctx PfCtx, username string, fullname string, extraclass string) template.HTML { link := false if Config.UserHomeLinks || ctx.IsSysAdmin() || username == ctx.TheUser().GetUserName() { link = true @@ -146,12 +188,15 @@ func tmp_user_image_link(ctx PfCtx, username string, fullname string, extraclass return HEB(html) } -func tmp_fmt_date(t time.Time) string { +// tmpFmtDate returns a formatted date stamp. +// +// The format depends on the system configuration. +func tmpFmtDate(t time.Time) string { return t.Format(Config.DateFormat) } -/* Minimum time display */ -func tmp_fmt_datemin(a time.Time, b time.Time, skipstamp string) (s string) { +// tmpFmtDateMin displays a time in a minimum way. +func tmpFmtDateMin(a time.Time, b time.Time, skipstamp string) (s string) { if skipstamp == "now" { /* This causes the year to be skipped if it is the same */ skipstamp = time.Now().Format("2006") @@ -200,19 +245,23 @@ func tmp_fmt_datemin(a time.Time, b time.Time, skipstamp string) (s string) { return } -func tmp_fmt_time(t time.Time) string { +/* tmp_fmt_time returns a standardized time format */ +func tmpFmtTime(t time.Time) string { return Fmt_Time(t) } -func tmp_fmt_string(obj interface{}) string { +/* tmpFmtString returns a object's String rendering */ +func tmpFmtString(obj interface{}) string { return ToString(obj) } -func tmp_str_tolower(str string) string { +/* tmp_str_tolower returns a lowercase version of the string */ +func tmpStrToLower(str string) string { return strings.ToLower(str) } -func tmp_str_emboss(in string, em string) (o template.HTML) { +/* tmp_str_emboss embosses part of a string */ +func tmpStrEmboss(in string, em string) (o template.HTML) { inlen := len(in) inlow := strings.ToLower(in) @@ -234,7 +283,8 @@ func tmp_str_emboss(in string, em string) (o template.HTML) { return } -func tmp_inc_file(fn string) template.HTML { +/* tmp_inc_file includes a file */ +func tmpIncFile(fn string) template.HTML { b, err := ioutil.ReadFile(fn) if err != nil { return HEB("Invalid File") @@ -244,20 +294,22 @@ func tmp_inc_file(fn string) template.HTML { } /* - * Dump a variable from a template + * tmpDumpVar dump a variable from a template. * * Useful for debugging so that one can check * the entire structure of a variable that is - * passed in to a template + * passed in to a template. */ -func tmp_dumpvar(v interface{}) template.HTML { +func tmpDumpVar(v interface{}) template.HTML { str := fmt.Sprintf("%#v", v) str = template.HTMLEscapeString(str) str = strings.Replace(str, "\n", "
", -1) return template.HTML("
" + str + "
") } -func tmp_dict(values ...interface{}) (map[string]interface{}, error) { +// tmpDict is a golang template trick to pass multiple values +// along to another template that one is including. +func tmpDict(values ...interface{}) (map[string]interface{}, error) { if len(values)%2 != 0 { return nil, errors.New("invalid dict call") } @@ -274,6 +326,7 @@ func tmp_dict(values ...interface{}) (map[string]interface{}, error) { return dict, nil } +// template_loader loads a template from a file. func template_loader(root string, path string) error { /* Name is the 'short' name without the root of the template dir */ if !strings.HasSuffix(path, ".tmpl") { @@ -308,6 +361,7 @@ func template_loader(root string, path string) error { return err } +// Template_Load loads our templates from the multiple configured roots, overriding where needed. func Template_Load() (err error) { g_tmp = template.New("Pitchfork Templates") @@ -342,10 +396,12 @@ func Template_Load() (err error) { return err } +// HE escapes a string as HTML. func HE(str string) string { return template.HTMLEscapeString(str) } +// HEB escapes a string as HTML and Blesses it as proper HTML (use only for strings that one controls). func HEB(str string) template.HTML { return template.HTML(str) } diff --git a/lib/template_test.go b/lib/template_test.go new file mode 100644 index 0000000..225f83a --- /dev/null +++ b/lib/template_test.go @@ -0,0 +1,54 @@ +package pitchfork + +import ( + "fmt" + "time" +) + +func ExampletmpFmtDateMin_daychange() { + time_layout := "2006-01-02" + + a, _ := time.Parse(time_layout, "2017-01-05") + b, _ := time.Parse(time_layout, "2017-01-07") + + out := tmpFmtDateMin(a, b, "") + + fmt.Println(out) + // Output: 2017-01-05...07 +} + +func ExampletmpFmtDateMin_monthchange() { + time_layout := "2006-01-02" + + a, _ := time.Parse(time_layout, "2017-01-31") + b, _ := time.Parse(time_layout, "2017-02-03") + + out := tmpFmtDateMin(a, b, "") + + fmt.Println(out) + // Output: 2017-01-31 - 02-03 +} + +func ExampletmpFmtDateMin_yearchange() { + time_layout := "2006-01-02" + + a, _ := time.Parse(time_layout, "2017-01-31") + b, _ := time.Parse(time_layout, "2018-02-01") + + out := tmpFmtDateMin(a, b, "") + + fmt.Println(out) + // Output: 2017-01-31 - 2018-02-01 +} + +func ExampletmpFmtDateMin_sameyear() { + time_layout := "2006-01-02" + + a, _ := time.Parse(time_layout, "2017-02-01") + b, _ := time.Parse(time_layout, "2017-02-05") + + out := tmpFmtDateMin(a, b, "2017") + + fmt.Println(out) + // Output: 02-01...05 +} diff --git a/lib/translate.go b/lib/translate.go index e8e0dd2..0a27476 100644 --- a/lib/translate.go +++ b/lib/translate.go @@ -1,5 +1,7 @@ package pitchfork +/* BUG(https://github.com/tridentli/pitchfork/issues/77) -- needs to be restored */ + import ( "errors" diff --git a/lib/user.go b/lib/user.go index 9aa72b2..3bc83d7 100755 --- a/lib/user.go +++ b/lib/user.go @@ -1,3 +1,4 @@ +// Pitchfork User management package pitchfork import ( @@ -10,13 +11,30 @@ import ( "github.com/pborman/uuid" ) +// Standardized error messages var ( Err_NoPassword = errors.New("Please actually provide a password") ) +// PfPostCreateI is a prototype to allow PostCreate to be overridden type PfPostCreateI func(ctx PfCtx, user PfUser) (err error) + +// PfPostFetchI is a prototype to allow PostFetch to be overridden +// +// PostFetch allows overriding what happens after fetching a user from +// the database (whether that succeeded or not) +// +// This allows extra details to be retrieved/derived. +// +// Or in the case of a failure for the application to autocreate +// an account in the database and allow access nevertheless. +// +// in_err passes in the error from the original User.Fetch() call. +// the returned error can pass this on, or return an alternative +// version when reality has changed. type PfPostFetchI func(ctx PfCtx, user PfUser, username string, in_err error) (err error) +// PfUser is the interface towards the User object type PfUser interface { SetUserName(name string) GetUserName() string @@ -53,24 +71,20 @@ type PfUser interface { Verify_Password(ctx PfCtx, password string) (err error) GetSF() (sf string, err error) GetPriEmail(ctx PfCtx, recovery bool) (tue PfUserEmail, err error) - GetPriEmailString(ctx PfCtx, recovery bool) (email string) Fetch2FA() (tokens []PfUser2FA, err error) Verify_TwoFactor(ctx PfCtx, twofactor string, id int) (err error) GetLastActivity(ctx PfCtx) (entered time.Time, ip string) - Create(ctx PfCtx, username string, email string, bio_info string, affiliation string, descr string) (err error) - PostCreate(ctx PfCtx) (err error) - PostFetch(ctx PfCtx, username string, in_err error) (err error) } -/* - * All values have to be exportable, otherwise our StructFetch() etc tricks do not work - * - * But that is also the extent that these should be used, they should always be accessed - * using the interface, not directly (direct access only internally). - * - * Even templates need to use Get*() variants as they receive interfaces. - */ +// PfUserS implements a standard Pitchfork PfUser +// +// All values have to be exportable, otherwise our StructFetch() etc tricks do not work +// +// But that is also the extent that these should be used, they should always be accessed +// using the interface, not directly (direct access only internally). +// +// Even templates need to use Get*() variants as they receive interfaces. type PfUserS struct { Uuid string `label:"UUID" coalesce:"00000000-0000-0000-0000-000000000000" pfset:"nobody" pfget:"sysadmin" pfskipfailperm:"yes"` Image string `label:"Image" pfset:"self" pfget:"user_view" pftype:"file" pfb64:"yes" hint:"Upload an image of yourself, the system will scale it" pfmaximagesize:"250x250"` @@ -102,29 +116,22 @@ type PfUserS struct { f_postfetch PfPostFetchI /* Function set at NewPfUser() */ } -/* Should not be directly called, use ctx or cui.NewUser() instead */ +// NewPfUser can be used to create a new user +// but normally it should not be directly called: +// use ctx/cui.NewUser() instead as that can be overridden. +// +// One typically calls this function from the constructor +// of the application NewUser function. +// func NewPfUser(postcreate PfPostCreateI, postfetch PfPostFetchI) PfUser { return &PfUserS{f_postcreate: postcreate, f_postfetch: postfetch} } +// NewPfUserA creates a new empty Pfuser object func NewPfUserA() PfUser { return NewPfUser(nil, nil) } -func (user *PfUserS) PostCreate(ctx PfCtx) (err error) { - if user.f_postcreate != nil { - err = user.f_postcreate(ctx, user) - } - return -} - -func (user *PfUserS) PostFetch(ctx PfCtx, username string, in_err error) (err error) { - if user.f_postfetch != nil { - err = user.f_postfetch(ctx, user, username, in_err) - } - return -} - func (user *PfUserS) SetUserName(name string) { user.UserName = strings.ToLower(name) } @@ -284,7 +291,9 @@ func (user *PfUserS) fetch(ctx PfCtx, username string) (err error) { } /* Call our PostFetch hook? */ - err = user.PostFetch(ctx, username, err) + if user.f_postfetch != nil { + err = user.f_postfetch(ctx, user, username, err) + } /* Do not retain the bit when the fetch failed */ if err == nil { @@ -325,7 +334,7 @@ func (user *PfUserS) Select(ctx PfCtx, username string, perms Perm) (err error) * No permissions needed? * This is used by password recovery */ - if ctx.IsPermSet(perms, PERM_NONE) { + if perms.IsSet(PERM_NONE) { return nil } @@ -335,19 +344,19 @@ func (user *PfUserS) Select(ctx PfCtx, username string, perms Perm) (err error) } /* Can select self */ - if ctx.IsPermSet(perms, PERM_USER_SELF) && + if perms.IsSet(PERM_USER_SELF) && ctx.IsLoggedIn() && user.UserName == ctx.TheUser().GetUserName() { return nil } /* Can always nominate people */ - if ctx.IsPermSet(perms, PERM_USER_NOMINATE) { + if perms.IsSet(PERM_USER_NOMINATE) { return nil } /* Can we view people? */ - if ctx.IsPermSet(perms, PERM_USER_VIEW) && ctx.IsLoggedIn() { + if perms.IsSet(PERM_USER_VIEW) && ctx.IsLoggedIn() { /* Only when they share a group */ var ok bool ok, err = user.SharedGroups(ctx, ctx.TheUser()) @@ -363,7 +372,7 @@ func (user *PfUserS) Select(ctx PfCtx, username string, perms Perm) (err error) /* Group admins can select users too */ /* XXX: Need to restrict this as a group admin is not all powerful or all-seeing */ - if ctx.IsPermSet(perms, PERM_GROUP_ADMIN) && ctx.IAmGroupAdmin() { + if perms.IsSet(PERM_GROUP_ADMIN) && ctx.IAmGroupAdmin() { return nil } @@ -977,11 +986,21 @@ func (user *PfUserS) Create(ctx PfCtx, username string, email string, bio_info s } /* Call our PostCreate hook? */ - err = user.PostCreate(ctx) + if user.f_postcreate != nil { + err = user.f_postcreate(ctx, user) + } return } +// User_new creates a new user. +// +// This function can be called to create a new user with the given properties. +// Further properties can be configured using the 'set' commands as exposed +// through both the CLI and UI. +// +// This function should be called only when the logged in user is allowed +// to perform such a function. func User_new(ctx PfCtx, username string, email string, bio_info string, affiliation string, descr string) (err error) { user := ctx.NewUser() @@ -1044,6 +1063,11 @@ func user_view(ctx PfCtx, args []string) (err error) { return } +// user_new creates a new user (CLI) +// +// This CLI command creates a new user in the system with +// mostly blank properties, which can be set with separate 'set' +// commands if wanted. func user_new(ctx PfCtx, args []string) (err error) { username := args[0] email := args[1] @@ -1054,11 +1078,10 @@ func user_new(ctx PfCtx, args []string) (err error) { return User_new(ctx, username, email, bio_info, affiliation, descr) } -/* - * curpass is only required for non-admin users - * curpass is the portal password irrespective of pwtype - */ - +// user_pw_set sets the password of a user (CLI). +// +// curpass is only required for non-admin users. +// curpass is the portal password irrespective of pwtype. func user_pw_set(ctx PfCtx, args []string) (err error) { pwtype := args[0] username := args[1] @@ -1102,6 +1125,16 @@ func user_pw_set(ctx PfCtx, args []string) (err error) { return } +// user_pw_recover finished the password recovery process +// if the provided token matches (CLI). +// +// Given the username, token and a new password the user +// +// The time at which the recovery password token was set is +// checked, the token cannot be more than a week old. +// +// If the token is valid and not expired, then the new password +// is made effective. func user_pw_recover(ctx PfCtx, args []string) (err error) { var recpw string var rectime time.Time @@ -1173,6 +1206,15 @@ func user_pw_recover(ctx PfCtx, args []string) (err error) { return } +// user_pw_resetcount can be used to reset the login attempt counter for a given user (CLI). +// +// Each failed login attempt for a user causes the login_attempts counter for that user to increase. +// This call causes the counter to be reset. +// +// Note that the user can also be locked out on the IPtrk level. +// The combo of login_attempts and IPtrk though ensure that even +// if an adversary uses a diverse set of IP addresses, they +// only have a few attempts to try a specific account. func user_pw_resetcount(ctx PfCtx, args []string) (err error) { username := args[1] @@ -1186,6 +1228,7 @@ func user_pw_resetcount(ctx PfCtx, args []string) (err error) { return } +// user_pw is the CLI menu for User Password actions (CLI). func user_pw(ctx PfCtx, args []string) (err error) { menu := NewPfMenu([]PfMEntry{ {"set", user_pw_set, 3, 4, []string{"pwtype", "username", "newpassword#password", "curpassword#password"}, PERM_USER_SELF, "Set password of type (portal|chat|jabber), requires providing current portal password"}, @@ -1197,6 +1240,7 @@ func user_pw(ctx PfCtx, args []string) (err error) { return } +// user_menu is the CLIE menu for User actions (CLI). func user_menu(ctx PfCtx, args []string) (err error) { menu := NewPfMenu([]PfMEntry{ {"new", user_new, 2, 2, []string{"username", "email"}, PERM_SYS_ADMIN, "Create a new user"}, diff --git a/lib/user_2fa.go b/lib/user_2fa.go index 71795a1..b192e4a 100644 --- a/lib/user_2fa.go +++ b/lib/user_2fa.go @@ -1,3 +1,4 @@ +// Pitchfork User 2FA (Two Factor Authentication) package pitchfork import ( @@ -12,9 +13,10 @@ import ( "trident.li/keyval" ) -/* Whether we verify 2FA tokens or not (--disabletwofactor) */ +// CheckTwoFactor is a setting that controls whether we verify 2FA tokens or not (--disabletwofactor). var CheckTwoFactor = true +// PfUser2FA describes the variables for a User's 2FA configuration. type PfUser2FA struct { Id int `label:"ID" pfset:"none" pfcol:"id"` UserName string `label:"UserName" pfset:"self" pfcol:"member" pftype:"ident" hint:"Owner of the Token"` @@ -26,10 +28,16 @@ type PfUser2FA struct { Counter int `trilanel:"Count" pfset:"self" pfcol:"counter" hint:"HOTP counter"` } +// NewPfUser2FA creates a new PfUser2FA object. func NewPfUser2FA() *PfUser2FA { return &PfUser2FA{} } +// TwoFactorTypes lists the types of +// Two Factor Authentications that Pitchfork supports. +// +// These types are stored in the database thus allowing +// the descriptions of the various types to be updated. func TwoFactorTypes() (types keyval.KeyVals) { q := "SELECT type, descr " + "FROM second_factor_types " + @@ -58,6 +66,7 @@ func TwoFactorTypes() (types keyval.KeyVals) { return } +// fetch fetches a PfUser2FA by id. func (tfa *PfUser2FA) fetch(id int) (err error) { p := []string{"id"} v := []string{strconv.Itoa(id)} @@ -70,11 +79,12 @@ func (tfa *PfUser2FA) fetch(id int) (err error) { return } +// Refresh refreshes a PfUser2FA from the database. func (tfa *PfUser2FA) Refresh() (err error) { return tfa.fetch(tfa.Id) } -/* Extends PfUserS object */ +// Fetch2FA retrieves a 2FA for a user (Extends PfUserS object). func (user *PfUserS) Fetch2FA() (tokens []PfUser2FA, err error) { q := "SELECT id, member, descr, type, entered, active, key, counter " + "FROM second_factors " + @@ -102,6 +112,10 @@ func (user *PfUserS) Fetch2FA() (tokens []PfUser2FA, err error) { return } +// Select selects a PfUser2FA +// +// System administrators can select any kind of 2FA object. +// All other users can only select 2fa tokens that they themself own. func (tfa *PfUser2FA) Select(ctx PfCtx, id int, perms Perm) (err error) { /* Fetch it if it exists */ err = tfa.fetch(id) @@ -116,7 +130,7 @@ func (tfa *PfUser2FA) Select(ctx PfCtx, id int, perms Perm) (err error) { } /* Can select self */ - if ctx.IsPermSet(perms, PERM_USER_SELF) && + if perms.IsSet(PERM_USER_SELF) && ctx.IsLoggedIn() && tfa.UserName == ctx.TheUser().GetUserName() { return nil @@ -125,10 +139,16 @@ func (tfa *PfUser2FA) Select(ctx PfCtx, id int, perms Perm) (err error) { return errors.New("Could not select 2FA Token") } -/* - * If id is set to a value other than zero this function will - * compare with that one and only one token, even if it disabled. - */ +// Verify_TwoFactor verifies a given twofactor code +// +// If id is set to a value other than zero this function will +// compare with that one and only one token, even if it disabled. +// +// The function checks the 2fa tokens for the user, optionally +// restricting to checking just the given id. +// +// Based on the type of the token it verifies that type's codes +// and allows further access if the code is correct. func (user *PfUserS) Verify_TwoFactor(ctx PfCtx, twofactor string, id int) (err error) { var pw PfPass var rows *Rows @@ -237,6 +257,9 @@ func (user *PfUserS) Verify_TwoFactor(ctx PfCtx, twofactor string, id int) (err return nil } +// String converts a PfUser2FA into a human readable string. +// +// Useful for displaying an overview of the token. func (tfa *PfUser2FA) String() (out string) { out = tfa.UserName + " " + strconv.Itoa(tfa.Id) out += " " + tfa.Name + " " + tfa.Type + "\n" @@ -244,7 +267,11 @@ func (tfa *PfUser2FA) String() (out string) { return } -func CreateKey(length int) (out string, err error) { +// tfa_createKey creates a random key to be used by 2FA. +// +// It requests 10 random hex numbers and decodes that +// into hexadecimal representation. +func tfaCreateKey(length int) (out string, err error) { /* Generate Key */ var pw PfPass pwd, err := pw.GenRandHex(10) @@ -261,11 +288,13 @@ func CreateKey(length int) (out string, err error) { return } -func EncodeKey(secret string) (out string) { +// tfaEncodeKey base32 encodes a given secret key. +func tfaEncodeKey(secret string) (out string) { out = base32.StdEncoding.EncodeToString([]byte(secret)) return } +// user_2fa_list lists the 2FA tokens for a given user (CLI). func user_2fa_list(ctx PfCtx, args []string) (err error) { var tfa PfUser2FA var tokens []PfUser2FA @@ -281,6 +310,19 @@ func user_2fa_list(ctx PfCtx, args []string) (err error) { return } +// user_2fa_add adds a 2fa token to a user (CLI) +// +// Is intended to be used for adding a new 2fa token. +// The first argument is the username, the second +// the password, which is used to verify that the +// person changing these security properties is +// at minimum controlling the password. +// +// The token_type indicates what type of token +// one wants to add. +// +// The descr is a short description field primarily +// to be able to distinguish the different tokens. func user_2fa_add(ctx PfCtx, args []string) (err error) { var id int var secret string @@ -306,12 +348,12 @@ func user_2fa_add(ctx PfCtx, args []string) (err error) { case "TOTP", "HOTP": counter := 0 - secret, err = CreateKey(10) + secret, err = tfaCreateKey(10) if err != nil { return } - key := EncodeKey(secret) + key := tfaEncodeKey(secret) /* * Create otpauth:// URL @@ -369,7 +411,7 @@ func user_2fa_add(ctx PfCtx, args []string) (err error) { var pw PfPass count := 5 for count > 0 { - secret, err = CreateKey(6) + secret, err = tfaCreateKey(6) if err != nil { return } @@ -410,6 +452,22 @@ func user_2fa_add(ctx PfCtx, args []string) (err error) { return } +// user_2fa_active_mod modifies a user's 2fa token. +// +// Given the user's password (to verify they still have +// it when changing this security related property) and +// the ID of the 2FA token. +// A sysadmin does not have to provide a valid password +// and bypasses the password check. +// +// The code is required when activating a token. +// As it proves that one can generate a proper code related +// to this token, and thus enabling it can be performed +// without a direct fear of locking the user out, in case +// they would not be able to generate the code for the token. +// +// Disabling a token can be done with solely the current +// password of the user. func user_2fa_active_mod(ctx PfCtx, id string, curpassword string, active bool, code string) (err error) { var member string var curact bool @@ -483,6 +541,12 @@ func user_2fa_active_mod(ctx PfCtx, id string, curpassword string, active bool, return } +// user_2fa_enable enables a user's 2fa token (CLI). +// +// Given a TokenID, a valid user password and a code from running the token's function +// we can enable that token. +// +// See user_2fa_active_mod for a few more details. func user_2fa_enable(ctx PfCtx, args []string) (err error) { /* username := args[0] */ id := args[1] @@ -493,6 +557,10 @@ func user_2fa_enable(ctx PfCtx, args []string) (err error) { return } +// user_2fa_disable disables a user's 2fa token (CLI). +// +// Given a TokenID and the user's current password we +// can disable the token with this command. func user_2fa_disable(ctx PfCtx, args []string) (err error) { /* username := args[0] */ id := args[1] @@ -502,6 +570,11 @@ func user_2fa_disable(ctx PfCtx, args []string) (err error) { return } +// user_2fa_remove removes a user's 2fa token (CLI) +// +// A sysadmin can remove a 2fa token without password details +// but for any other user the current password is needed +// to remove the token. func user_2fa_remove(ctx PfCtx, args []string) (err error) { user := ctx.SelectedUser() username := user.GetUserName() @@ -534,6 +607,7 @@ func user_2fa_remove(ctx PfCtx, args []string) (err error) { return } +// user_2fa_types lists the types of 2fa tokens (CLI). func user_2fa_types(ctx PfCtx, args []string) (err error) { types := TwoFactorTypes() for _, kv := range types { @@ -544,6 +618,7 @@ func user_2fa_types(ctx PfCtx, args []string) (err error) { return } +// user_2fa_menu is the CLI menu for User 2FA details (CLI). func user_2fa_menu(ctx PfCtx, args []string) (err error) { perms := PERM_USER_SELF diff --git a/lib/user_detail.go b/lib/user_detail.go index aac2c68..43ab6d8 100644 --- a/lib/user_detail.go +++ b/lib/user_detail.go @@ -4,18 +4,21 @@ import ( "time" ) +// PfUserDetail describes a user detail type PfUserDetail struct { - Detail PfDetail - Value string - Entered time.Time + Detail PfDetail // The detail + Value string // What it is set to + Entered time.Time // When it was entered } -func (ud *PfUserDetail) toString() (out string) { +// ToString Converts a detail into a textual string. +func (ud *PfUserDetail) ToString() (out string) { out = ud.Detail.ToString() out += " " + ud.Value + " Entered: " + ud.Entered.Format(time.UnixDate) return } +// GetDetails returns the details of a user detail. func (user *PfUserS) GetDetails() (details []PfUserDetail, err error) { q := "SELECT " + "md.type, " + @@ -50,6 +53,7 @@ func (user *PfUserS) GetDetails() (details []PfUserDetail, err error) { return } +// user_detail_list returns the list of details (CLI). func user_detail_list(ctx PfCtx, args []string) (err error) { username := args[0] @@ -64,12 +68,13 @@ func user_detail_list(ctx PfCtx, args []string) (err error) { var detail PfUserDetail for _, detail = range details { - ctx.OutLn(detail.toString()) + ctx.OutLn(detail.ToString()) } return } +// user_detail_set configures a detail (CLI). func user_detail_set(ctx PfCtx, args []string) (err error) { username := args[0] @@ -103,6 +108,7 @@ func user_detail_set(ctx PfCtx, args []string) (err error) { return } +// user_detail_new_type adds a new detail type (CLI). func user_detail_new_type(ctx PfCtx, args []string) (err error) { type_name := args[0] type_descr := args[1] @@ -122,6 +128,7 @@ func user_detail_new_type(ctx PfCtx, args []string) (err error) { return } +// user_detail_delete removes a detail (CLI). func user_detail_delete(ctx PfCtx, args []string) (err error) { username := args[0] @@ -153,6 +160,7 @@ func user_detail_delete(ctx PfCtx, args []string) (err error) { return } +// user_detail is the user's detail menu (CLI). func user_detail(ctx PfCtx, args []string) (err error) { menu := NewPfMenu([]PfMEntry{ {"list", user_detail_list, 1, 1, []string{"username"}, PERM_USER, "List user details"}, diff --git a/lib/user_email.go b/lib/user_email.go index 8056bfe..cd0da7c 100644 --- a/lib/user_email.go +++ b/lib/user_email.go @@ -1,3 +1,4 @@ +// Pitchfork User Email Management package pitchfork import ( @@ -9,6 +10,7 @@ import ( pfpgp "trident.li/pitchfork/lib/pgp" ) +// PfUserEmail describes the variables for the settings of a user's email type PfUserEmail struct { Member string `label:"Member" pfset:"self" pfcol:"member" pftype:"ident" hint:"Owner of the Verification Code"` FullName string `label:"Full Name" pfset:"none" pftable:"member" pfcol:"descr"` @@ -22,14 +24,17 @@ type PfUserEmail struct { Groups []PfGroupMember /* Used by List() */ } +// NewPfUserEmail creates a new object. func NewPfUserEmail() *PfUserEmail { return &PfUserEmail{} } +// Fetch retrieves a user's email details from the database. func NewPfUserEmailI() interface{} { return &PfUserEmail{} } +// Fetch retrieves a user's email details from the database. func (uem *PfUserEmail) Fetch(email string) (err error) { if email == "" { err = errors.New("No email address provided") @@ -53,10 +58,12 @@ func (uem *PfUserEmail) Fetch(email string) (err error) { return } +// FetchGroups populates the Groups attribute of a UserEmail object. func (uem *PfUserEmail) FetchGroups(ctx PfCtx) (err error) { - // Populate the Groups attribute of a UserEmail object. var groups []PfGroupMember + grp := ctx.NewGroup() + /* Get the groups this user is a member of */ groups, err = grp.GetGroups(ctx, uem.Member) if err != nil { @@ -71,6 +78,10 @@ func (uem *PfUserEmail) FetchGroups(ctx PfCtx) (err error) { return } +// List lists the user's email details. +// +// Given a user, this returns all the email addresses, along with properties +// in the form of an array of PfUserEmail objects. func (uem *PfUserEmail) List(ctx PfCtx, user PfUser) (emails []PfUserEmail, err error) { q := "SELECT member, email, descr, pgpkey_id, pgpkey_expire, keyring, " + "keyring_update_at, verify_token, verified " + @@ -130,7 +141,15 @@ func (uem *PfUserEmail) List(ctx PfCtx, user PfUser) (emails []PfUserEmail, err return } -/* Extends PfUserS */ +// GetPriEmail return's the user's primary email address. +// +// It can be used for sending email to the user. +// +// The first email address that is verified is considered +// the primary email address. +// +// If a recovery address is given and it is verified that +// takes precedence. func (user *PfUserS) GetPriEmail(ctx PfCtx, recovery bool) (tue PfUserEmail, err error) { var emails []PfUserEmail var recemail string @@ -167,15 +186,10 @@ func (user *PfUserS) GetPriEmail(ctx PfCtx, recovery bool) (tue PfUserEmail, err return } -func (user *PfUserS) GetPriEmailString(ctx PfCtx, recovery bool) (email string) { - em, err := user.GetPriEmail(ctx, recovery) - if err != nil { - return "[Email unavailable]" - } - - return em.Email -} - +// user_email_add allows adding an email address to a user (CLI). +// +// Given a username and email address, the email address is added +// to the list of email addresses for the given user. func user_email_add(ctx PfCtx, args []string) (err error) { username := args[0] address := args[1] @@ -218,6 +232,9 @@ func user_email_add(ctx PfCtx, args []string) (err error) { return } +// user_email_remove allows removing of an email address for a user (CLI). +// +// As an argument the email address is needed. func user_email_remove(ctx PfCtx, args []string) (err error) { err = ctx.SelectEmail(args[0]) if err != nil { @@ -243,6 +260,9 @@ func user_email_remove(ctx PfCtx, args []string) (err error) { return } +// user_email_list lists the email addresses for a given user (CLI). +// +// As an argument only a username is expected. func user_email_list(ctx PfCtx, args []string) (err error) { var tue PfUserEmail var emails []PfUserEmail @@ -263,6 +283,10 @@ func user_email_list(ctx PfCtx, args []string) (err error) { return } +// user_group_list lists the groups a user is in along with the email +// addresses selected for those groups (CLI). +// +// As an argument the username is expected. func user_group_list(ctx PfCtx, args []string) (err error) { grp := ctx.NewGroup() @@ -296,6 +320,11 @@ func user_group_list(ctx PfCtx, args []string) (err error) { return } +// group_email_set configures the email address for a group (CLI). +// +// Given a username, groupname and email address, this call +// configures the system so that for email for the given +// group, the selected email address is used. func group_email_set(ctx PfCtx, args []string) (err error) { username := args[0] gr_name := args[1] @@ -345,6 +374,15 @@ func group_email_set(ctx PfCtx, args []string) (err error) { return } +// user_email_pgp_add adds a PGP key to an email address (CLI) +// +// It takes an email address and the keyring. +// +// It extracts the keys from the given keyring. +// And fetches the details about the PGP key. +// +// Given a valid and matching key is provided, +// it replaces the keyring for that user's mail address. func user_email_pgp_add(ctx PfCtx, args []string) (err error) { err = ctx.SelectEmail(args[0]) if err != nil { @@ -387,6 +425,7 @@ func user_email_pgp_add(ctx PfCtx, args []string) (err error) { return } +// user_email_pgp_get retrieves the PGP key related to the email address. func user_email_pgp_get(ctx PfCtx, args []string) (err error) { err = ctx.SelectEmail(args[0]) if err != nil { @@ -413,6 +452,7 @@ func user_email_pgp_get(ctx PfCtx, args []string) (err error) { return } +// user_email_pgp_check verifies that the email address matches PGP key (CLI). func user_email_pgp_check(ctx PfCtx, args []string) (err error) { now := time.Now() toexp := now.Add(time.Duration(30*24) * time.Hour) @@ -463,6 +503,8 @@ func user_email_pgp_check(ctx PfCtx, args []string) (err error) { return } +// user_email_confirm starts the email confirmation process by +// sending the user a new verification token. func user_email_confirm_start(ctx PfCtx, args []string) (err error) { err = ctx.SelectEmail(args[0]) if err != nil { @@ -498,6 +540,14 @@ func user_email_confirm_start(ctx PfCtx, args []string) (err error) { return } +// user_email_confirm allows a user to confirm an email +// address as theirs by providing the code they where +// sent with the verification token (CLI). +// +// This solely needs a verification code. +// The database is checked if such a verification code +// is currently attached to an email address and if it is +// it confirms that email address as verified. func user_email_confirm(ctx PfCtx, args []string) (err error) { verifycode := args[0] @@ -521,6 +571,12 @@ func user_email_confirm(ctx PfCtx, args []string) (err error) { return } +// User_Email_Verify allows marking a email address as verified. +// +// It receives the username and emailaddress and updates the database +// marking the email address as verified. +// +// An error is returned if the process did not succeed. func User_Email_Verify(ctx PfCtx, username string, emailaddr string) (err error) { /* Invalid token and set to verified when found */ q := "UPDATE member_email " + @@ -537,20 +593,26 @@ func User_Email_Verify(ctx PfCtx, username string, emailaddr string) (err error) return } -func user_email_confirm_force(ctx PfCtx, args []string) (err error) { +// user_email_confirm_admin marks an email address as verified without +// the verification code (CLI). +// +// It takes the username and email address to confirm as verified +// and verifies it. +func user_email_confirm_admin(ctx PfCtx, args []string) (err error) { username := args[0] emailaddr := args[1] return User_Email_Verify(ctx, username, emailaddr) } +// user_email_menu is the CLI menu for User Email actions (CLI). func user_email_menu(ctx PfCtx, args []string) (err error) { menu := NewPfMenu([]PfMEntry{ {"add", user_email_add, 2, 2, []string{"username", "email"}, PERM_USER, "Add email address"}, {"remove", user_email_remove, 1, 1, []string{"email"}, PERM_USER, "Remove email address"}, {"confirm_begin", user_email_confirm_start, 1, 1, []string{"email"}, PERM_USER, "Send an e-mail confirmation token."}, {"confirm", user_email_confirm, 1, 1, []string{"verifycode"}, PERM_USER, "Confirm email address"}, - {"confirm_force", user_email_confirm_force, 2, 2, []string{"username", "email"}, PERM_SYS_ADMIN, "force and email verification"}, + {"confirm_admin", user_email_confirm_admin, 2, 2, []string{"username", "email"}, PERM_SYS_ADMIN, "force an email verification"}, {"list", user_email_list, 1, 1, []string{"username"}, PERM_USER, "List email addresses"}, {"pgp_add", user_email_pgp_add, 2, 2, []string{"email", "keyring#file"}, PERM_USER, "Add PGP Key"}, {"pgp_get", user_email_pgp_get, 1, 1, []string{"email"}, PERM_USER, "Get PGP Key"}, @@ -562,6 +624,7 @@ func user_email_menu(ctx PfCtx, args []string) (err error) { return } +// user_email_group_menu is the CLI menu for User Group Email actions (CLI). func user_email_group_menu(ctx PfCtx, args []string) (err error) { menu := NewPfMenu([]PfMEntry{ {"list", user_group_list, 1, 1, []string{"username"}, PERM_USER, "List trust group associated email addresses"}, diff --git a/lib/user_events.go b/lib/user_events.go index df6e659..178db0f 100644 --- a/lib/user_events.go +++ b/lib/user_events.go @@ -1,9 +1,18 @@ +// Pitchfork User events Module package pitchfork import ( "time" ) +// userevent logs an event for a user. +// +// Any arbitrary string can be used as an event. +// +// In addition to the event string, the username, +// IP address, full remote address (including XFF), +// the browser string, the Operating system and +// the full User-Agent are logged. func userevent(ctx PfCtx, event string) { ident := ctx.TheUser().GetUserName() ip := ctx.GetClientIP() @@ -24,6 +33,20 @@ func userevent(ctx PfCtx, event string) { return } +// GetLastActivity retrieves the last activity of a user. +// +// To be able to notify the user of their last activity +// this function allows fetching this detail. +// +// The activity includes a timestamp and the IP address +// that the user came from. +// +// It is based on the events table, as this is where +// at minimum login events are logged and logins are +// noted as activity. +// +// We return only the direct remote address, not the +// complete XFF which we also have recorded in the database. func (user *PfUserS) GetLastActivity(ctx PfCtx) (entered time.Time, ip string) { ident := user.GetUserName() @@ -43,6 +66,11 @@ func (user *PfUserS) GetLastActivity(ctx PfCtx) (entered time.Time, ip string) { return } +// user_events_list lists the events for a user (CLI). +// +// This allows a user to list the events about the given user. +// Users can list these events of themselves only unless the +// user is a sysadmin. func user_events_list(ctx PfCtx, args []string) (err error) { username := args[0] @@ -71,6 +99,7 @@ func user_events_list(ctx PfCtx, args []string) (err error) { return } +// user_events is the CLI menu for User Events (CLI). func user_events(ctx PfCtx, args []string) (err error) { menu := NewPfMenu([]PfMEntry{ {"list", user_events_list, 1, 1, []string{"username"}, PERM_USER, "List all events related to a user"}, diff --git a/lib/user_language.go b/lib/user_language.go index 25d53ef..a0a795e 100755 --- a/lib/user_language.go +++ b/lib/user_language.go @@ -1,22 +1,26 @@ +// Pitchfork Language Module package pitchfork import ( "time" ) +// PfUserLanguage describes a user language setting type PfUserLanguage struct { - Language PfLanguage - Skill string - Entered time.Time + Language PfLanguage // The language + Skill string // The attained skill + Entered time.Time // When it was added } -func (ul *PfUserLanguage) toString() (out string) { +// ToString returns a textual rendering of the language detail +func (ul *PfUserLanguage) ToString() (out string) { out = ul.Language.ToString() + " " + ul.Skill + " " + "Entered:" + ul.Entered.Format(time.UnixDate) return } +// GetLanguages gets the possible languages func (user *PfUserS) GetLanguages() (output []PfUserLanguage, err error) { q := "SELECT " + "mls.language, " + @@ -50,6 +54,7 @@ func (user *PfUserS) GetLanguages() (output []PfUserLanguage, err error) { return } +// LanguageSkillList lists the skills for a language. func LanguageSkillList() (languageskill []string) { q := "SELECT skill " + "FROM language_skill " + @@ -77,6 +82,7 @@ func LanguageSkillList() (languageskill []string) { return } +// user_lang_list lists the languages for a user (CLI). func user_lang_list(ctx PfCtx, args []string) (err error) { username := args[0] @@ -91,12 +97,13 @@ func user_lang_list(ctx PfCtx, args []string) (err error) { var ls PfUserLanguage for _, ls = range languages { - ctx.OutLn(ls.toString()) + ctx.OutLn(ls.ToString()) } return } +// user_lang_skill shows the language skill levels (CLI). func user_lang_skill(ctx PfCtx, args []string) (err error) { types := LanguageSkillList() @@ -109,6 +116,7 @@ func user_lang_skill(ctx PfCtx, args []string) (err error) { return } +// user_lang_set sets a language skill level (CLI). func user_lang_set(ctx PfCtx, args []string) (err error) { username := args[0] @@ -135,6 +143,7 @@ func user_lang_set(ctx PfCtx, args []string) (err error) { return } +// user_lang_delete removes a language (CLI). func user_lang_delete(ctx PfCtx, args []string) (err error) { username := args[0] @@ -160,6 +169,7 @@ func user_lang_delete(ctx PfCtx, args []string) (err error) { return } +// user_language is the user's language menu (CLI). func user_language(ctx PfCtx, args []string) (err error) { menu := NewPfMenu([]PfMEntry{ {"list", user_lang_list, 1, 1, []string{"username"}, PERM_USER, "List user language skills"}, diff --git a/lib/wiki.go b/lib/wiki.go index fdd8897..2528acf 100755 --- a/lib/wiki.go +++ b/lib/wiki.go @@ -1,3 +1,4 @@ +// Pitchfork Wiki Module. package pitchfork import ( @@ -10,10 +11,12 @@ import ( "time" ) +// PfWikiOpts provides the options the wiki module needs. type PfWikiOpts struct { PfModOptsS } +// Wiki_GetModOpts retrieves the wiki modopts. func Wiki_GetModOpts(ctx PfCtx) PfWikiOpts { mopts := ctx.GetModOpts() if mopts == nil { @@ -23,15 +26,18 @@ func Wiki_GetModOpts(ctx PfCtx) PfWikiOpts { return mopts.(PfWikiOpts) } +// Wiki_ModOpts constructs the options for the Wiki. func Wiki_ModOpts(ctx PfCtx, cmdpfx string, path_root string, web_root string) { ctx.SetModOpts(PfWikiOpts{PfModOpts(ctx, cmdpfx, path_root, web_root)}) } +// wiki_ApplyModOpts fixes up paths based on ModOpts. func wiki_ApplyModOpts(ctx PfCtx, path string) string { mopts := Wiki_GetModOpts(ctx) return URL_Append(mopts.Pathroot, path) } +// PfWikiHTML contains the details for a wiki page. type PfWikiHTML struct { HTML_TOC template.HTML `pfcol:"html_toc"` HTML_Body template.HTML `pfcol:"html_body"` @@ -40,10 +46,12 @@ type PfWikiHTML struct { FullName string `pfcol:"descr" pftable:"member"` } +// PfWikiMarkdown represents just the markdown of a wiki page. type PfWikiMarkdown struct { Markdown string `pfcol:"markdown"` } +// PfWikiRev depicts the revision of a wiki page. type PfWikiRev struct { Revision int RevisionB int @@ -53,6 +61,7 @@ type PfWikiRev struct { ChangeMsg string } +// PfWikiPage depicts a Wiki Page. type PfWikiPage struct { Path string Entered time.Time @@ -60,6 +69,7 @@ type PfWikiPage struct { FullPath string /* Not in the DB, see ApplyModOpts() */ } +// ApplyModOpts applies the modopts to the page. func (wiki *PfWikiPage) ApplyModOpts(ctx PfCtx) { mopts := Wiki_GetModOpts(ctx) root := mopts.Pathroot @@ -71,12 +81,14 @@ func (wiki *PfWikiPage) ApplyModOpts(ctx PfCtx) { wiki.FullPath = URL_Append(root, wiki.Path) } +// PfWikiResult depicts a search result in the wiki. type PfWikiResult struct { Path string Title string Snippet string } +// Wiki_TitleComponent generates a Title from a Wiki Page name. func Wiki_TitleComponent(title string) string { if title == "" { title = "Index" @@ -87,12 +99,14 @@ func Wiki_TitleComponent(title string) string { return title } +// Wiki_Title generates a title from just the top part of the path of the page. func Wiki_Title(path string) (title string) { t := strings.Split(path, "/") title = Wiki_TitleComponent(t[len(t)-1]) return } +// Wiki_RevisionMax returns the maximum revision of a page. func Wiki_RevisionMax(ctx PfCtx, path string) (total int, err error) { path = wiki_ApplyModOpts(ctx, path) @@ -106,6 +120,7 @@ func Wiki_RevisionMax(ctx PfCtx, path string) (total int, err error) { return total, err } +// Wiki_RevisionList returns the list of revisions for a page. func Wiki_RevisionList(ctx PfCtx, path string, offset int, max int) (revs []PfWikiRev, err error) { revs = nil var rows *Rows @@ -152,6 +167,7 @@ func Wiki_RevisionList(ctx PfCtx, path string, offset int, max int) (revs []PfWi return } +// Wiki_SearchMax returns the maximum number of results for a search. func Wiki_SearchMax(ctx PfCtx, search string) (total int, err error) { /* Restrict the path */ path := wiki_ApplyModOpts(ctx, "") + "%" @@ -172,6 +188,7 @@ func Wiki_SearchMax(ctx PfCtx, search string) (total int, err error) { return total, err } +// Wiki_SearchList returns the search results (used in combo with Wiki_SearchMax for results). func Wiki_SearchList(ctx PfCtx, search string, offset int, max int) (results []PfWikiResult, err error) { results = nil var rows *Rows @@ -239,6 +256,7 @@ func Wiki_SearchList(ctx PfCtx, search string, offset int, max int) (results []P return } +// Wiki_ChildPagesMax returns the number of child pages. func Wiki_ChildPagesMax(ctx PfCtx, path string) (total int, err error) { path = wiki_ApplyModOpts(ctx, path) @@ -259,6 +277,7 @@ func Wiki_ChildPagesMax(ctx PfCtx, path string) (total int, err error) { return total, err } +// Wiki_ChildPagesList lists the children of a page. func Wiki_ChildPagesList(ctx PfCtx, path string, offset int, max int) (paths []PfWikiPage, err error) { paths = nil @@ -320,6 +339,7 @@ func Wiki_ChildPagesList(ctx PfCtx, path string, offset int, max int) (paths []P return } +// Fetch fetches the Markdown for a given path. func (wiki *PfWikiMarkdown) Fetch(ctx PfCtx, path string, rev string) (err error) { path = wiki_ApplyModOpts(ctx, path) @@ -342,6 +362,7 @@ func (wiki *PfWikiMarkdown) Fetch(ctx PfCtx, path string, rev string) (err error return } +// Fetch fetches the HTML for a given path. func (wiki *PfWikiHTML) Fetch(ctx PfCtx, path string, rev string) (err error) { path = wiki_ApplyModOpts(ctx, path) @@ -366,6 +387,7 @@ func (wiki *PfWikiHTML) Fetch(ctx PfCtx, path string, rev string) (err error) { return } +// wiki_updateA updates a page given the new content, title and a message describing the change. func wiki_updateA(ctx PfCtx, path string, message string, title string, markdown string) (err error) { user := ctx.SelectedUser().GetUserName() mopts := Wiki_GetModOpts(ctx) @@ -464,6 +486,7 @@ func wiki_updateA(ctx PfCtx, path string, message string, title string, markdown return } +// wiki_update is the CLI interface for changing a page (CLI). func wiki_update(ctx PfCtx, args []string) (err error) { path := args[0] message := args[1] @@ -473,6 +496,7 @@ func wiki_update(ctx PfCtx, args []string) (err error) { return wiki_updateA(ctx, path, message, title, markdown) } +// wiki_updatef allows updating a page from a file (CLI). func wiki_updatef(ctx PfCtx, args []string) (err error) { path := args[0] message := args[1] @@ -489,6 +513,7 @@ func wiki_updatef(ctx PfCtx, args []string) (err error) { return wiki_updateA(ctx, path, message, title, markdown) } +// wiki_get retrieves the markdown or html for a given path (CLI). func wiki_get(ctx PfCtx, args []string) (err error) { path := args[0] fmt := args[1] @@ -529,6 +554,7 @@ func wiki_get(ctx PfCtx, args []string) (err error) { return } +// wiki_list retrieves the list of children related to a path (CLI). func wiki_list(ctx PfCtx, args []string) (err error) { path := args[0] @@ -546,6 +572,7 @@ func wiki_list(ctx PfCtx, args []string) (err error) { return } +// wiki_getrevs returns the revisions for a given path and versions. func wiki_getrevs(ctx PfCtx, path string, revA string, revB string) (a string, b string, err error) { var mA PfWikiMarkdown err = mA.Fetch(ctx, path, revA) @@ -568,6 +595,7 @@ func wiki_getrevs(ctx PfCtx, path string, revA string, revB string) (a string, b return mA.Markdown, mB.Markdown, err } +// Wiki_Diff returns the differences between two revisions of a page. func Wiki_Diff(ctx PfCtx, path string, revA string, revB string) (diff []PfDiff, err error) { var a string var b string @@ -580,6 +608,7 @@ func Wiki_Diff(ctx PfCtx, path string, revA string, revB string) (diff []PfDiff, return DoDiff(a, b), nil } +// wiki_diff returns the diff between two revisions of a page (CLI). func wiki_diff(ctx PfCtx, args []string) (err error) { path := args[0] revA := args[1] @@ -598,6 +627,7 @@ func wiki_diff(ctx PfCtx, args []string) (err error) { return nil } +// wiki_move moves a page from one location to another (CLI). func wiki_move(ctx PfCtx, args []string) (err error) { path := wiki_ApplyModOpts(ctx, args[0]) newpath := wiki_ApplyModOpts(ctx, args[1]) @@ -660,6 +690,7 @@ func wiki_move(ctx PfCtx, args []string) (err error) { return nil } +// wiki_delete a page from the system, including all revisions (CLI). func wiki_delete(ctx PfCtx, args []string) (err error) { path := wiki_ApplyModOpts(ctx, args[0]) children := args[1] @@ -711,6 +742,7 @@ func wiki_delete(ctx PfCtx, args []string) (err error) { return nil } +// wiki_copy copies a page from one to another (CLI). func wiki_copy(ctx PfCtx, args []string) (err error) { path := wiki_ApplyModOpts(ctx, args[0]) newpath := wiki_ApplyModOpts(ctx, args[1]) @@ -775,6 +807,7 @@ func wiki_copy(ctx PfCtx, args []string) (err error) { return nil } +// Wiki_menu is the CLI menu for the Wiki module. func Wiki_menu(ctx PfCtx, args []string) (err error) { menu := NewPfMenu([]PfMEntry{ {"update", wiki_update, 4, 4, []string{"wikipath", "message", "title", "markdown"}, PERM_USER, "Update a Wiki page"}, diff --git a/lib/wiki_export.go b/lib/wiki_export.go index e0b6fcb..de35325 100644 --- a/lib/wiki_export.go +++ b/lib/wiki_export.go @@ -1,3 +1,4 @@ +// Pitchfork Wiki Exporter as static/rendered HTML package pitchfork import ( @@ -9,9 +10,19 @@ import ( "strings" ) +// The permissions to give to rendered wiki files const Wiki_Perms_Export os.FileMode = 0755 -// "wikipath", "dir" +// wiki_export statically renders a wikipath into a directory. +// +// It takes the wikipath and uses it as the root of the to-be-exported directory. +// It then renders all the pages below it and exports it to the given directory. +// +// The special inc/render.tmpl template (from the share/templates directory) is used +// to render the pages. +// +// The share/rendered directory is copied into the directory, providing a source +// for static files that are resources needed to visualize the page properly. func wiki_export(ctx PfCtx, args []string) (err error) { path := strings.TrimSpace(args[0]) dname := strings.TrimSpace(args[1]) diff --git a/lib/wiki_import.go b/lib/wiki_import.go index aa56774..9507f49 100644 --- a/lib/wiki_import.go +++ b/lib/wiki_import.go @@ -1,3 +1,4 @@ +// Pitchfork Wiki Importer from FosWiki. package pitchfork import ( @@ -12,7 +13,19 @@ import ( "strings" ) -// "group", "format", "file", "wikipath" +// wiki_import is a CLI command for importing Wiki's into Pitchfork +// +// arguments: "group", "format", "file", "wikipath" +// +// group is the group for which the import should happen. (XXX change to modopts). +// format is currently only foswiki for FosWiki. +// file is the filename where the archive should be read from. +// wikipath describes the path where the import would happen. +// +// wiki_import loads, uncompresses and parses the file and then +// stores each file in either the File or Wiki module depending on type. +// +// Markdown files are reformatted to standard markdown. func wiki_import(ctx PfCtx, args []string) (err error) { var df *os.File var gr *gzip.Reader @@ -154,7 +167,9 @@ func wiki_import(ctx PfCtx, args []string) (err error) { return } -/* https://foswiki.org/System/TopicMarkupLanguage */ +// wiki_TML2Markdown converts TopicMarkupLanguage (TML) used by FosWiki to standard Markdown +// +// Details about TML: http://foswiki.org/System/TopicMarkupLanguage func wiki_TML2Markdown(tml string, gr_name string, fname string) (md string) { /* It will be markdown soon */ md = tml diff --git a/lib/wiki_rnd.go b/lib/wiki_rnd.go index 8be8895..ea7a74a 100755 --- a/lib/wiki_rnd.go +++ b/lib/wiki_rnd.go @@ -1,3 +1,14 @@ +// Pitchfork Wiki Renderer +// +// The Pitchfork Renderer provides a standard way for rendering Markdown, +// as used in the wiki, into HTML. +// +// This so that it can be used for a variety of parts of the Wiki code. +// +// The markdown renderer uses: +// - blackfriday to render Markdown into HTML. +// - bluemonday to sanitize the HTML. +// - highlight_go and syntaxhighlight to do syntaxhighlighting of code examples. package pitchfork import ( @@ -10,14 +21,22 @@ import ( "strings" ) -/* Wrap blackfriday */ +// PfRenderer wraps blackfriday so that we can extend it with extra functionality. type PfRenderer struct { *blackfriday.Html } -/* Override blockcode */ +// BlockCode overrides blockcode rendering allowing specification +// of the programming language and proper hilighting +// +// This is a plugin to BlackFriday, and thus takes an output buffer, +// the text included in the markdown and the language this code is +// written in, thus allowing highlighting in the style of that language. func (rnd *PfRenderer) BlockCode(out *bytes.Buffer, text []byte, lang string) { - doubleSpace(out) + // Add a newline if we are not at the front. + if out.Len() > 0 { + out.WriteByte('\n') + } /* Which language? */ count := 0 @@ -56,6 +75,10 @@ func (rnd *PfRenderer) BlockCode(out *bytes.Buffer, text []byte, lang string) { out.WriteString("\n") } +// highlightCode highlights code based on the given language. +// +// It takes the source text and the language as an input +// and outputs the highlighted code or an error, in case one occurs. func highlightCode(src []byte, lang string) (highlightedCode []byte, err error) { var buf bytes.Buffer @@ -85,12 +108,7 @@ func highlightCode(src []byte, lang string) (highlightedCode []byte, err error) return buf.Bytes(), err } -func doubleSpace(out *bytes.Buffer) { - if out.Len() > 0 { - out.WriteByte('\n') - } -} - +// escapeSingleChar HTML escapes a single character func escapeSingleChar(char byte) (string, bool) { switch char { case '"': @@ -105,6 +123,7 @@ func escapeSingleChar(char byte) (string, bool) { return "", false } +// attrEscape HTML escapes an attribute. func attrEscape(out *bytes.Buffer, src []byte) { org := 0 @@ -126,6 +145,8 @@ func attrEscape(out *bytes.Buffer, src []byte) { } } +// PfRender renders a markdown text into HTML in standard Pitchfork way. +// Optionally Table of Contents (TOC) only is rendered. func PfRender(markdown string, toconly bool) (html string) { /* Configure Black Friday */ extensions := 0 | diff --git a/lib/wiki_search.go b/lib/wiki_search.go index 4cf52b3..8ffa233 100644 --- a/lib/wiki_search.go +++ b/lib/wiki_search.go @@ -1,5 +1,10 @@ +// Pitchfork Wiki Search Module package pitchfork +// Wiki_search provides a search interface for the Pitchfork Wiki Module +// +// It searches for the requested string in the markdown of the wiki pages +// and returns the wiki pages that have a match. func Wiki_search(ctx PfCtx, pathroot string, c chan PfSearchResult, search string, abort <-chan bool) (err error) { /* XXX: Namespace limiter (Groups) */ diff --git a/ui/cmd.go b/ui/cmd.go index 8b53bc6..37c5388 100644 --- a/ui/cmd.go +++ b/ui/cmd.go @@ -4,7 +4,7 @@ import ( pf "trident.li/pitchfork/lib" ) -/* /api/// */ +// h_api is the handler for the /api/ URL: /api/// func h_api(cui PfUI) { var err error @@ -21,7 +21,7 @@ func h_api(cui PfUI) { } } -/* Simple CLI interface */ +// h_cli is the handler providing a simple CLI interface func h_cli(cui PfUI) { var err error out := "" diff --git a/ui/doc.go b/ui/doc.go new file mode 100644 index 0000000..ede9266 --- /dev/null +++ b/ui/doc.go @@ -0,0 +1,8 @@ +/* +Pitchfork is a Golang framework for secure communication platforms. + +The pitchfork 'ui' is the UI core of Pitchfork. + +Website: https://trident.li/ +*/ +package pitchforkui diff --git a/ui/error.go b/ui/error.go index fa71412..ae40eb4 100644 --- a/ui/error.go +++ b/ui/error.go @@ -6,7 +6,7 @@ import ( pf "trident.li/pitchfork/lib" ) -/* Aliases */ +// HTTP Status Aliases for easy use const ( StatusMovedPermanently = http.StatusMovedPermanently /* 301 */ StatusFound = http.StatusFound /* 302 */ @@ -20,11 +20,9 @@ const ( StatusServiceUnavailable = http.StatusServiceUnavailable /* 503 */ ) -/* - * Error messages - * - * Human readable, not computer thus with 200 OK - */ +// H_errmsgs renders Error messages. +// +// Human readable, not computer thus with 200 OK. func H_errmsgs(cui PfUI, msg []string) { type Page struct { *PfPage @@ -38,18 +36,19 @@ func H_errmsgs(cui PfUI, msg []string) { cui.Page_show("misc/error.tmpl", p) } +// H_errmsg renders an err as an error message func H_errmsg(cui PfUI, err error) { H_errmsgs(cui, []string{err.Error()}) } +// H_errmsg renders a textual string as an error message func H_errtxt(cui PfUI, txt string) { H_errmsgs(cui, []string{txt}) } -/* - * Error page generator - * nginx should catch these though - */ +// H_error generates a error page with a non-200 HTTP response. +// +// nginx should catch these and replace them with a hardcoded page. func H_error(cui PfUI, status int) { /* HTTP Error */ cui.SetStatus(status) @@ -90,6 +89,7 @@ func H_error(cui PfUI, status int) { cui.Errf("HTTP Error %s: %s for %s", status_str, msg, cui.GetFullPath()) } +// H_NoAccess renders a 403 Access Forbidden or 404 Not Found when the users is logged in func H_NoAccess(cui PfUI) { if cui.IsLoggedIn() { cui.Errf("NoAccess: Logged In %#v", cui.TheUser()) diff --git a/ui/file.go b/ui/file.go index e6e127a..384d312 100755 --- a/ui/file.go +++ b/ui/file.go @@ -6,6 +6,7 @@ import ( pf "trident.li/pitchfork/lib" ) +// h_file_history is the handler for displaying the history of a file func h_file_history(cui PfUI) { var err error var revs []pf.PfFile @@ -45,6 +46,7 @@ func h_file_history(cui PfUI) { cui.Page_show("file/history.tmpl", p) } +// FileUIFixup fixes up file details to include the ModOpts func FileUIApplyModOpts(cui PfUI, file *pf.PfFile) { opts := pf.File_GetModOpts(cui) op := file.FullPath @@ -53,12 +55,14 @@ func FileUIApplyModOpts(cui PfUI, file *pf.PfFile) { file.FullPath = np } +// FileUIFixupM calls FileUIFixup on multiple files func FileUIApplyModOptsM(cui PfUI, files []pf.PfFile) { for i := range files { FileUIApplyModOpts(cui, &files[i]) } } +// h_file_list_dir handles listing a directory func h_file_list_dir(cui PfUI) { path := cui.GetSubPath() @@ -97,6 +101,7 @@ func h_file_list_dir(cui PfUI) { cui.Page_show("file/list.tmpl", p) } +// H_file_list_file handles listing a file func H_file_list_file(cui PfUI) { var m pf.PfFile var err error @@ -134,6 +139,7 @@ func H_file_list_file(cui PfUI) { cui.Page_show("file/view.tmpl", p) } +// h_file_list handles listing a directory or file func h_file_list(cui PfUI) { path := cui.GetSubPath() @@ -145,6 +151,7 @@ func h_file_list(cui PfUI) { H_file_list_file(cui) } +// h_file_details handles the details and operations for a file func h_file_details(cui PfUI) { var f pf.PfFile var err error @@ -301,6 +308,7 @@ func h_file_details(cui PfUI) { cui.Page_show("file/details.tmpl", p) } +// h_file_add_dir handles the adding of a directory func h_file_add_dir(cui PfUI) { path := cui.GetSubPath() @@ -356,6 +364,7 @@ func h_file_add_dir(cui PfUI) { cui.Page_show("file/add_dir.tmpl", p) } +// h_file_add_file handles adding of a file func h_file_add_file(cui PfUI) { path := cui.GetSubPath() @@ -415,6 +424,7 @@ func h_file_add_file(cui PfUI) { cui.Page_show("file/add_file.tmpl", p) } +// file_edit_form handles editing of a file using a form func file_edit_form(cui PfUI, path string) (err error) { mopts := pf.File_GetModOpts(cui) cmd := mopts.Cmdpfx + " update" @@ -424,6 +434,7 @@ func file_edit_form(cui PfUI, path string) (err error) { return } +// H_file is the main entry point, called after having set the ModOpts func H_file(cui PfUI) { /* URL of the page */ cui.SetSubPath("/" + cui.GetPathString()) diff --git a/ui/form.go b/ui/form.go index c966014..0ebc067 100644 --- a/ui/form.go +++ b/ui/form.go @@ -1,3 +1,87 @@ +// Pitchfork's pfform function +// +// pfform is a powerful template function that can render HTML5 forms from +// a golang structure using the tags in the structure to influence how the +// form gets rendered and the options the input elements receive. +// +// It avoids the need for maintaining HTML and adheres to the +// security/permission model that is embedded in Pitchfork. +// +// A struct's fields are rendered in order. +// +// Translation of all fields is attempted; a custom translation function +// can be specified by having the object have a Translate() function that +// accepts two arguments typed string, the first being the label to be +// translated, the second the requested target language. +// +// Tag pfcol describes the column/fieldname in a SQL table, used also as +// part of the HTML 'id' field. +// The default, when not specified, is the lowercase version of the golang +// field name. +// +// Tag isvisible, when defined provides the name of a custom function for +// determining if a field should be visible or not. +// It is called with the first parameter being the string describing the +// fieldname (see: pfcol) +// +// Tag label indicates the human readable label shown before the input field. +// When not specified the field is ignored from rendering. +// +// Tag options indicates a function to be called to retrieve a keyval to be +// used for generating a select style input. +// The ObjectContext function in addition allows specifiying a context +// for that call. +// +// Tag hint indicates the HTML hint to be included for the given input. +// +// Tag placeholder indicates the HTML placeholder to be included for the +// given input. +// +// Tag min can be used to specify a minimum string length or a minimum number. +// +// Tag max can be used to specify a maximum string length or a maximum number. +// +// Tag htmlclass can be used to specify a custom HTML class (CSS) for the +// input. +// +// Tag pfreq indicates, when set, that the field is required and thus that the +// input should be marked and decorated as such. +// +// Tag pfsection indicates a section. These sections are grouped together and +// allows a header to surround one or more inputs. +// +// Tag pfomitempty indicates that the input field should be omitted when the +// value is empty. +// +// Tag mask indicates that we wrap a HTML mask around the field, thus requiring +// a user to first click on the expand button to reveal the field. +// +// Tag content is used for headers and notes, it allows the content/value of +// the item to be included in the tag instead of having to be separately set +// in the struct. +// +// Strings are rendered as strings, strings with keyvals are rendered as +// select boxes, with the default being the value of the string. +// +// Numbers are rendered as integers that can be increased/decreased. +// +// Dates and Time objects are rendered as dates allowing a HTML5 calendar +// control for selecting the details. +// +// SQL Nullstrings are treated like their native types. +// +// Maps are rendered as multi select options. +// +// Slices are rendered as multiple options with an add/remove button when +// editing is allowed. +// +// pftype = string (default) | text | tel | email | bool (stored as string) | +// note | header +// +// 'note' and 'header' are not a real string, just renders as non-input text +// 'note' renders inline with the input boxes, while 'header' uses the full +// width that also uses the space for the labels. + package pitchforkui import ( @@ -11,23 +95,21 @@ import ( pf "trident.li/pitchfork/lib" ) +// init registers the pfform function as a template function func init() { pf.Template_FuncAdd("pfform", pfform) } +// PFFORM_READONLY is the marker used to indicate a readonly HTML input const PFFORM_READONLY = "readonly" -/* - * pftype = string (default) | text | tel | email | bool (stored as string) | note - * - * 'note' = not a real string, just renders as non-input text - */ - +// TFErr renders a Template error and reports it in the error log func TFErr(cui PfUI, str string) (o string, buttons string, neditable int, subs []string, multipart bool) { cui.Errf("pfform: %s", str) return pf.HE("Problem encountered while rendering template"), "", 0, []string{}, false } +// pfform_keyval returns the value associated with the given key func pfform_keyval(kvs keyval.KeyVals, val string) string { if kvs == nil { return val @@ -45,6 +127,7 @@ func pfform_keyval(kvs keyval.KeyVals, val string) string { return val } +// pfform_select renders a HTML select form from a keyval func pfform_select(kvs keyval.KeyVals, def, idpfx, fname, opts string) (t string) { t += "