diff options
Diffstat (limited to 'pkg/app')
53 files changed, 4018 insertions, 0 deletions
diff --git a/pkg/app/handler/about/index.go b/pkg/app/handler/about/index.go new file mode 100644 index 0000000..edeb45b --- /dev/null +++ b/pkg/app/handler/about/index.go @@ -0,0 +1,28 @@ +// Used to show the about pages of the application + +package about + +import ( + "glsamaker/pkg/app/handler/authentication/utils" + "net/http" +) + +// Show renders a template to show the main about page of the application +func Show(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + renderAboutTemplate(w, user) +} + +// ShowSearch renders a template to show the about +// page about the search functionality +func ShowSearch(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + renderAboutSearchTemplate(w, user) +} + +// ShowCLI renders a template to show the about +// page about the command line tool +func ShowCLI(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + renderAboutCLITemplate(w, user) +} diff --git a/pkg/app/handler/about/utils.go b/pkg/app/handler/about/utils.go new file mode 100644 index 0000000..77f1383 --- /dev/null +++ b/pkg/app/handler/about/utils.go @@ -0,0 +1,58 @@ +// miscellaneous utility functions used for the about pages of the application + +package about + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderAboutTemplate renders all templates used for the main about page +func renderAboutTemplate(w http.ResponseWriter, user *users.User) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/about/*.tmpl")) + + templates.ExecuteTemplate(w, "about.tmpl", createPageData("about", user)) +} + +// renderAboutSearchTemplate renders all templates used for +// the about page about the search functionality +func renderAboutSearchTemplate(w http.ResponseWriter, user *users.User) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/about/*.tmpl")) + + templates.ExecuteTemplate(w, "aboutSearch.tmpl", createPageData("about", user)) +} + +// renderAboutCLITemplate renders all templates used for +// the about page about the command line tool +func renderAboutCLITemplate(w http.ResponseWriter, user *users.User) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/about/*.tmpl")) + + templates.ExecuteTemplate(w, "aboutCLI.tmpl", createPageData("about", user)) +} + +// createPageData creates the data used in the templates of the about pages +func createPageData(page string, user *users.User) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + } +} diff --git a/pkg/app/handler/account/password.go b/pkg/app/handler/account/password.go new file mode 100644 index 0000000..5cdb9e5 --- /dev/null +++ b/pkg/app/handler/account/password.go @@ -0,0 +1,109 @@ +// Used to show the change password page + +package account + +import ( + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// ChangePassword changes the password of a user in case of a valid POST request. +// In case of a GET request the dialog for the password change is displayed +func ChangePassword(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if r.Method == "POST" { + + r.ParseForm() + + oldPassword := getStringParam("oldPassword", r) + newPassword := getStringParam("newPassword", r) + confirmedNewPassword := getStringParam("confirmedNewPassword", r) + + if newPassword != confirmedNewPassword { + renderPasswordChangeTemplate(w, r, user, false, "The passwords you have entered do not match") + return + } + + if !user.CheckPassword(oldPassword) { + renderPasswordChangeTemplate(w, r, user, false, "The old password you have entered is not correct") + return + } + + err := user.UpdatePassword(newPassword) + if err != nil { + renderPasswordChangeTemplate(w, r, user, false, "Internal error during hash calculation.") + return + } + + wasForcedToChange := user.ForcePasswordRotation + user.ForcePasswordRotation = false + + _, err = connection.DB.Model(user).Column("password").WherePK().Update() + _, err = connection.DB.Model(user).Column("force_password_rotation").WherePK().Update() + + if err != nil { + logger.Info.Println("error during password update") + logger.Info.Println(err) + renderPasswordChangeTemplate(w, r, user, false, "Internal error during password update.") + return + } + + if wasForcedToChange { + http.Redirect(w, r, "/", 301) + return + } + + updatedUser := utils.GetAuthenticatedUser(r) + + renderPasswordChangeTemplate(w, r, updatedUser, true, "Your password has been changed successfully.") + return + } + + renderPasswordChangeTemplate(w, r, user, false, "") +} + +// renderPasswordChangeTemplate renders all templates used for the login page +func renderPasswordChangeTemplate(w http.ResponseWriter, r *http.Request, user *users.User, success bool, message string) { + + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/account/password/*.tmpl")) + + templates.ExecuteTemplate(w, "password.tmpl", createPasswordChangeData("account", user, success, message)) +} + +// createPasswordChangeData creates the data used in the template of the password change page +func createPasswordChangeData(page string, user *users.User, success bool, message string) interface{} { + + return struct { + Page string + Application *models.GlobalSettings + User *users.User + Success bool + Message string + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + Success: success, + Message: message, + } +} + +// returns the value of a parameter with the given key of a POST request +func getStringParam(key string, r *http.Request) string { + if len(r.Form[key]) > 0 { + return r.Form[key][0] + } + + return "" +} diff --git a/pkg/app/handler/account/twofactor.go b/pkg/app/handler/account/twofactor.go new file mode 100644 index 0000000..4d87426 --- /dev/null +++ b/pkg/app/handler/account/twofactor.go @@ -0,0 +1,200 @@ +package account + +import ( + "glsamaker/pkg/app/handler/authentication/totp" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "bytes" + "github.com/duo-labs/webauthn/webauthn" + "html/template" + "net/http" +) + +// landing page + +func TwoFactorAuth(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + render2FATemplate(w, r, user) +} + +// webauthn + +func ActivateWebAuthn(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + + if user.WebauthnCredentials != nil && len(user.WebauthnCredentials) >= 0 { + updatedUser := &users.User{ + Id: user.Id, + IsUsingTOTP: false, + IsUsingWebAuthn: true, + Show2FANotice: false, + } + + _, err := connection.DB.Model(updatedUser).Column("is_using_totp").WherePK().Update() + _, err = connection.DB.Model(updatedUser).Column("is_using_web_authn").WherePK().Update() + _, err = connection.DB.Model(updatedUser).Column("show2fa_notice").WherePK().Update() + + if err != nil { + logger.Error.Println("Error activating webauthn") + logger.Error.Println(err) + } + + } + + http.Redirect(w, r, "/account/2fa", 301) +} + +func DisableWebAuthn(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + updatedUser := &users.User{ + Id: user.Id, + IsUsingWebAuthn: false, + } + + _, err := connection.DB.Model(updatedUser).Column("is_using_web_authn").WherePK().Update() + + if err != nil { + logger.Error.Println("Error disabling webauthn") + logger.Error.Println(err) + } + + http.Redirect(w, r, "/account/2fa", 301) +} + +// totp + +func ActivateTOTP(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + + updatedUser := &users.User{ + Id: user.Id, + IsUsingTOTP: true, + IsUsingWebAuthn: false, + Show2FANotice: false, + } + + _, err := connection.DB.Model(updatedUser).Column("is_using_totp").WherePK().Update() + _, err = connection.DB.Model(updatedUser).Column("is_using_web_authn").WherePK().Update() + _, err = connection.DB.Model(updatedUser).Column("show2fa_notice").WherePK().Update() + + if err != nil { + logger.Error.Println("Error activating totp") + logger.Error.Println(err) + } + + http.Redirect(w, r, "/account/2fa", 301) +} + +func DisableTOTP(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + + updatedUser := &users.User{ + Id: user.Id, + IsUsingTOTP: false, + } + + _, err := connection.DB.Model(updatedUser).Column("is_using_totp").WherePK().Update() + + if err != nil { + logger.Error.Println("Error updating 2fa") + logger.Error.Println(err) + } + + http.Redirect(w, r, "/account/2fa", 301) +} + +func VerifyTOTP(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + token := getToken(r) + + validToken := "false" + + if totp.IsValidTOTPToken(user, token) { + validToken = "true" + } + + w.Write([]byte(validToken)) +} + +func Disable2FANotice(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + updatedUser := &users.User{ + Id: user.Id, + Show2FANotice: false, + } + + _, err := connection.DB.Model(updatedUser).Column("show2fa_notice").WherePK().Update() + + if err != nil { + logger.Error.Println("Error disabling 2fa notice") + logger.Error.Println(err) + } + + w.Write([]byte("ok")) +} + +// utility functions + +func getToken(r *http.Request) string { + err := r.ParseForm() + if err != nil { + return "" + } + return r.Form.Get("token") +} + +// renderIndexTemplate renders all templates used for the login page +func render2FATemplate(w http.ResponseWriter, r *http.Request, user *users.User) { + + funcMap := template.FuncMap{ + "WebAuthnID": WebAuthnCredentialID, + "CredentialName": GetCredentialName, + } + + templates := template.Must( + template.Must( + template.New("Show"). + Funcs(funcMap). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/account/*.tmpl")) + + templates.ExecuteTemplate(w, "2fa.tmpl", createPageData("account", user)) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User) interface{} { + return struct { + Page string + Application *models.GlobalSettings + QRcode string + User *users.User + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + QRcode: user.TOTPQRCode, + User: user, + } +} + +// WebAuthnCredentials returns credentials owned by the user +func WebAuthnCredentialID(cred webauthn.Credential) []byte { + return cred.ID[:5] +} + +func GetCredentialName(user *users.User, cred webauthn.Credential) string { + + for _, WebauthnCredentialName := range user.WebauthnCredentialNames { + if bytes.Compare(WebauthnCredentialName.Id, cred.ID) == 0 { + return WebauthnCredentialName.Name + } + } + + return "Unnamed Authenticator" +} diff --git a/pkg/app/handler/admin/edit.go b/pkg/app/handler/admin/edit.go new file mode 100644 index 0000000..8cf9291 --- /dev/null +++ b/pkg/app/handler/admin/edit.go @@ -0,0 +1,293 @@ +package admin + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/totp" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models/users" + "math/rand" + "net/http" + "strconv" + "strings" + "time" +) + +// Show renders a template to show the landing page of the application +func EditUsers(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Admin.ManageUsers { + authentication.AccessDenied(w, r) + return + } + + var allUsers []*users.User + connection.DB.Model(&allUsers).Order("email ASC").Select() + + if r.Method == "POST" { + + r.ParseForm() + + if !(getStringParam("edit", r) == "1") { + http.Redirect(w, r, "/admin", 301) + } + + userIds := getArrayParam("userId", r) + userNicks := getArrayParam("userNick", r) + userNames := getArrayParam("userName", r) + userEmails := getArrayParam("userEmail", r) + userPasswordRotations := getArrayParam("userPasswordRotation", r) + userForce2FA := getArrayParam("userForce2FA", r) + userActive := getArrayParam("userActive", r) + + newUserIndex := -1 + + for index, userId := range userIds { + + parsedUserId, err := strconv.ParseInt(userId, 10, 64) + + if err != nil { + continue + } + + count, _ := connection.DB.Model((*users.User)(nil)).Where("id = ?", parsedUserId).Count() + + // user is present + if count == 1 { + + updatedUser := users.User{ + Id: parsedUserId, + Email: userEmails[index], + Nick: userNicks[index], + Name: userNames[index], + //Badge: users.Badge{}, + ForcePasswordRotation: containsStr(userPasswordRotations, userId), + Force2FA: containsStr(userForce2FA, userId), + Disabled: !containsStr(userActive, userId), + } + + connection.DB.Model(&updatedUser). + Column("email"). + Column("nick"). + Column("name"). + Column("force_password_rotation"). + Column("force2fa"). + Column("disabled"). + WherePK().Update() + + } else { + + newUserIndex = index + + } + + } + + if newUserIndex != -1 { + + newPassword := generateNewPassword(14) + + createNewUser( + userNicks[newUserIndex], + userNames[newUserIndex], + userEmails[newUserIndex], + newPassword, + containsStr(userForce2FA, "-1"), + !containsStr(userActive, "-1")) + + var updatedUsers []*users.User + connection.DB.Model(&updatedUsers).Order("email ASC").Select() + + renderAdminNewUserTemplate(w, user, updatedUsers, userNicks[newUserIndex], newPassword) + return + + } else { + + http.Redirect(w, r, "/admin", 301) + return + + } + + } + + renderEditUsersTemplate(w, user, allUsers) +} + +// Show renders a template to show the landing page of the application +func EditPermissions(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Admin.ManageUsers { + authentication.AccessDenied(w, r) + return + } + + var allUsers []*users.User + connection.DB.Model(&allUsers).Order("email ASC").Select() + + if r.Method == "POST" { + + r.ParseForm() + + if !(getStringParam("edit", r) == "1") { + http.Redirect(w, r, "/admin", 301) + } + + glsaView := getArrayParam("glsa-view", r) + glsaUpdateBugs := getArrayParam("glsa-updateBugs", r) + glsaComment := getArrayParam("glsa-comment", r) + glsaCreate := getArrayParam("glsa-create", r) + glsaEdit := getArrayParam("glsa-edit", r) + glsaDelete := getArrayParam("glsa-delete", r) + glsaApprove := getArrayParam("glsa-approve", r) + glsaApproveOwnGlsa := getArrayParam("glsa-approveOwnGlsa", r) + glsaDecline := getArrayParam("glsa-decline", r) + glsaRelease := getArrayParam("glsa-release", r) + glsaConfidential := getArrayParam("glsa-confidential", r) + + cveView := getArrayParam("cve-view", r) + cveUpdateCVEs := getArrayParam("cve-updateCVEs", r) + cveComment := getArrayParam("cve-comment", r) + cveAddPackage := getArrayParam("cve-addPackage", r) + cveChangeState := getArrayParam("cve-changeState", r) + cveAssignBug := getArrayParam("cve-assignBug", r) + + adminView := getArrayParam("admin-view", r) + adminCreateTemplates := getArrayParam("admin-createTemplates", r) + adminGlobalSettings := getArrayParam("admin-globalSettings", r) + adminManageUsers := getArrayParam("admin-manageUsers", r) + + for _, changedUser := range allUsers { + + updatedUserPermissions := users.Permissions{ + Glsa: users.GlsaPermissions{ + View: containsInt(glsaView, changedUser.Id), + UpdateBugs: containsInt(glsaUpdateBugs, changedUser.Id), + Comment: containsInt(glsaComment, changedUser.Id), + Create: containsInt(glsaCreate, changedUser.Id), + Edit: containsInt(glsaEdit, changedUser.Id), + Approve: containsInt(glsaApprove, changedUser.Id), + ApproveOwnGlsa: containsInt(glsaApproveOwnGlsa, changedUser.Id), + Decline: containsInt(glsaDecline, changedUser.Id), + Delete: containsInt(glsaDelete, changedUser.Id), + Release: containsInt(glsaRelease, changedUser.Id), + Confidential: containsInt(glsaConfidential, changedUser.Id), + }, + CVETool: users.CVEToolPermissions{ + View: containsInt(cveView, changedUser.Id), + UpdateCVEs: containsInt(cveUpdateCVEs, changedUser.Id), + Comment: containsInt(cveComment, changedUser.Id), + AddPackage: containsInt(cveAddPackage, changedUser.Id), + ChangeState: containsInt(cveChangeState, changedUser.Id), + AssignBug: containsInt(cveAssignBug, changedUser.Id), + }, + Admin: users.AdminPermissions{ + View: containsInt(adminView, changedUser.Id), + CreateTemplates: containsInt(adminCreateTemplates, changedUser.Id), + ManageUsers: containsInt(adminManageUsers, changedUser.Id), + GlobalSettings: containsInt(adminGlobalSettings, changedUser.Id), + }, + } + + updatedUser := users.User{ + Id: changedUser.Id, + Permissions: updatedUserPermissions, + } + + connection.DB.Model(&updatedUser).Column("permissions").WherePK().Update() + } + + http.Redirect(w, r, "/admin", 301) + return + } + + renderEditPermissionsTemplate(w, user, allUsers) +} + +func containsInt(arr []string, element int64) bool { + return containsStr(arr, strconv.FormatInt(element, 10)) +} + +func containsStr(arr []string, element string) bool { + for _, a := range arr { + if a == element { + return true + } + } + return false +} + +func getStringParam(key string, r *http.Request) string { + if len(r.Form[key]) > 0 { + return r.Form[key][0] + } + + return "" +} + +func getArrayParam(key string, r *http.Request) []string { + return r.Form[key] +} + +func createNewUser(nick, name, email, password string, force2FA, disabled bool) { + + token, qrcode := totp.Generate("user@gentoo.org") + + badge := users.Badge{ + Name: "user", + Description: "Normal user", + Color: "#54487A", + } + + passwordParameters := users.Argon2Parameters{ + Type: "argon2id", + Time: 1, + Memory: 64 * 1024, + Threads: 4, + KeyLen: 32, + } + passwordParameters.GenerateSalt(32) + passwordParameters.GeneratePassword(password) + + defaultUser := &users.User{ + Email: email, + Nick: nick, + Name: name, + Password: passwordParameters, + Role: "user", + ForcePasswordChange: false, + TOTPSecret: token, + TOTPQRCode: qrcode, + IsUsingTOTP: false, + WebauthnCredentials: nil, + IsUsingWebAuthn: false, + Show2FANotice: true, + Badge: badge, + Disabled: disabled, + ForcePasswordRotation: true, + Force2FA: force2FA, + } + + _, err := connection.DB.Model(defaultUser).OnConflict("(id) DO Nothing").Insert() + if err != nil { + logger.Error.Println("Err during creating default admin user") + logger.Error.Println(err) + } +} + +func generateNewPassword(length int) string { + rand.Seed(time.Now().UnixNano()) + chars := []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZÅÄÖ" + + "abcdefghijklmnopqrstuvwxyz" + + "0123456789" + + "!&!$%&/()=?") + var b strings.Builder + for i := 0; i < length; i++ { + b.WriteRune(chars[rand.Intn(len(chars))]) + } + return b.String() +} diff --git a/pkg/app/handler/admin/index.go b/pkg/app/handler/admin/index.go new file mode 100644 index 0000000..5f1f579 --- /dev/null +++ b/pkg/app/handler/admin/index.go @@ -0,0 +1,27 @@ +// Used to show the landing page of the application + +package admin + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/models/users" + "net/http" +) + +// Show renders a template to show the landing page of the application +func Show(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Admin.View { + authentication.AccessDenied(w, r) + return + } + + var users []*users.User + connection.DB.Model(&users).Order("email ASC").Select() + + renderAdminTemplate(w, user, users) +} diff --git a/pkg/app/handler/admin/passwordreset.go b/pkg/app/handler/admin/passwordreset.go new file mode 100644 index 0000000..e4d36b9 --- /dev/null +++ b/pkg/app/handler/admin/passwordreset.go @@ -0,0 +1,73 @@ +package admin + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/models/users" + "net/http" + "strconv" +) + +// Show renders a template to show the landing page of the application +func ResetPassword(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Admin.ManageUsers { + authentication.AccessDenied(w, r) + return + } + + userPasswordResetId := r.URL.Path[len("/admin/edit/password/reset/"):] + + parsedUserPasswordResetId, err := strconv.ParseInt(userPasswordResetId, 10, 64) + + if err != nil { + http.NotFound(w, r) + return + } + + selectedUser := &users.User{Id: parsedUserPasswordResetId} + err = connection.DB.Model(selectedUser).WherePK().Select() + + if err != nil || selectedUser == nil { + http.NotFound(w, r) + return + } + + if r.Method == "POST" { + + newPassword := generateNewPassword(14) + passwordParameters := users.Argon2Parameters{ + Type: "argon2id", + Time: 1, + Memory: 64 * 1024, + Threads: 4, + KeyLen: 32, + } + passwordParameters.GenerateSalt(32) + passwordParameters.GeneratePassword(newPassword) + + updatedUser := &users.User{ + Id: parsedUserPasswordResetId, + Password: passwordParameters, + ForcePasswordRotation: true, + } + + _, err = connection.DB.Model(updatedUser).Column("password").WherePK().Update() + _, err = connection.DB.Model(updatedUser).Column("force_password_rotation").WherePK().Update() + if err != nil { + http.NotFound(w, r) + return + } + + var updatedUsers []*users.User + connection.DB.Model(&updatedUsers).Order("email ASC").Select() + + renderAdminNewUserTemplate(w, user, updatedUsers, selectedUser.Nick, newPassword) + return + } + + renderPasswordResetTemplate(w, user, selectedUser.Id, selectedUser.Nick) +} diff --git a/pkg/app/handler/admin/utils.go b/pkg/app/handler/admin/utils.go new file mode 100644 index 0000000..85a25ec --- /dev/null +++ b/pkg/app/handler/admin/utils.go @@ -0,0 +1,112 @@ +// miscellaneous utility functions used for the landing page of the application + +package admin + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderAdminTemplate(w http.ResponseWriter, user *users.User, allUsers []*users.User) { + templates := template.Must( + template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/admin/components/*.tmpl")). + ParseGlob("web/templates/admin/*.tmpl")) + + templates.ExecuteTemplate(w, "view.tmpl", createPageData("admin", user, allUsers, "", "")) +} + +// renderIndexTemplate renders all templates used for the landing page +func renderAdminNewUserTemplate(w http.ResponseWriter, user *users.User, allUsers []*users.User, newUserNick, newUserPass string) { + templates := template.Must( + template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/admin/components/*.tmpl")). + ParseGlob("web/templates/admin/*.tmpl")) + + templates.ExecuteTemplate(w, "view.tmpl", createPageData("admin", user, allUsers, newUserNick, newUserPass)) +} + +// renderIndexTemplate renders all templates used for the landing page +func renderEditUsersTemplate(w http.ResponseWriter, user *users.User, allUsers []*users.User) { + templates := template.Must( + template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/admin/components/*.tmpl")). + ParseGlob("web/templates/admin/edit/*.tmpl")) + + templates.ExecuteTemplate(w, "users.tmpl", createPageData("admin", user, allUsers, "", "")) +} + +// renderIndexTemplate renders all templates used for the landing page +func renderPasswordResetTemplate(w http.ResponseWriter, user *users.User, userId int64, userNick string) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/admin/passwordreset.tmpl")) + + templates.ExecuteTemplate(w, "passwordreset.tmpl", createPasswordResetData("admin", user, userId, userNick)) +} + +// renderIndexTemplate renders all templates used for the landing page +func renderEditPermissionsTemplate(w http.ResponseWriter, user *users.User, allUsers []*users.User) { + templates := template.Must( + template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/admin/components/*.tmpl")). + ParseGlob("web/templates/admin/edit/*.tmpl")) + + templates.ExecuteTemplate(w, "permissions.tmpl", createPageData("admin", user, allUsers, "", "")) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User, allUsers []*users.User, newUserNick, newUserPassword string) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + Users []*users.User + NewUserNick string + NewUserPassword string + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + Users: allUsers, + NewUserNick: newUserNick, + NewUserPassword: newUserPassword, + } +} + +// createPageData creates the data used in the template of the landing page +func createPasswordResetData(page string, user *users.User, userId int64, userNick string) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + Users []*users.User + NewUserNick string + NewUserPassword string + UserId int64 + UserNick string + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + UserId: userId, + UserNick: userNick, + } +} diff --git a/pkg/app/handler/all/index.go b/pkg/app/handler/all/index.go new file mode 100644 index 0000000..ef6104a --- /dev/null +++ b/pkg/app/handler/all/index.go @@ -0,0 +1,40 @@ +// Used to show the landing page of the application + +package all + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/models" + "net/http" +) + +// Show renders a template to show the landing page of the application +func Show(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Glsa.View { + authentication.AccessDenied(w, r) + return + } + + var all []*models.Glsa + err := user.CanAccess(connection.DB.Model(&all). + Relation("Bugs"). + Relation("Creator"). + Relation("Comments")). + Select() + + if err != nil { + http.NotFound(w, r) + return + } + + for _, glsa := range all { + glsa.ComputeStatus(user) + } + + renderAllTemplate(w, user, all) +} diff --git a/pkg/app/handler/all/utils.go b/pkg/app/handler/all/utils.go new file mode 100644 index 0000000..a0cfc65 --- /dev/null +++ b/pkg/app/handler/all/utils.go @@ -0,0 +1,36 @@ +// miscellaneous utility functions used for the landing page of the application + +package all + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderAllTemplate(w http.ResponseWriter, user *users.User, all []*models.Glsa) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/all/*.tmpl")) + + templates.ExecuteTemplate(w, "all.tmpl", createPageData("all", user, all)) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User, all []*models.Glsa) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + All []*models.Glsa + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + All: all, + } +} diff --git a/pkg/app/handler/archive/index.go b/pkg/app/handler/archive/index.go new file mode 100644 index 0000000..61d77dd --- /dev/null +++ b/pkg/app/handler/archive/index.go @@ -0,0 +1,44 @@ +// Used to show the landing page of the application + +package archive + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "net/http" +) + +// Show renders a template to show the landing page of the application +func Show(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Glsa.View { + authentication.AccessDenied(w, r) + return + } + + var glsas []*models.Glsa + err := user.CanAccess(connection.DB.Model(&glsas). + Where("type = ?", "glsa"). + Relation("Bugs"). + Relation("Creator"). + Relation("Comments")). + Select() + + if err != nil { + logger.Info.Println("Error during glsa selection") + logger.Info.Println(err) + http.NotFound(w, r) + return + } + + for _, glsa := range glsas { + glsa.ComputeStatus(user) + } + + renderArchiveTemplate(w, user, glsas) +} diff --git a/pkg/app/handler/archive/utils.go b/pkg/app/handler/archive/utils.go new file mode 100644 index 0000000..6653691 --- /dev/null +++ b/pkg/app/handler/archive/utils.go @@ -0,0 +1,36 @@ +// miscellaneous utility functions used for the landing page of the application + +package archive + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderArchiveTemplate(w http.ResponseWriter, user *users.User, glsas []*models.Glsa) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/archive/*.tmpl")) + + templates.ExecuteTemplate(w, "archive.tmpl", createPageData("archive", user, glsas)) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User, glsas []*models.Glsa) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + GLSAs []*models.Glsa + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + GLSAs: glsas, + } +} diff --git a/pkg/app/handler/authentication/accessDenied.go b/pkg/app/handler/authentication/accessDenied.go new file mode 100644 index 0000000..de06ab2 --- /dev/null +++ b/pkg/app/handler/authentication/accessDenied.go @@ -0,0 +1,10 @@ +package authentication + +import ( + "glsamaker/pkg/app/handler/authentication/templates" + "net/http" +) + +func AccessDenied(w http.ResponseWriter, r *http.Request) { + templates.RenderAccessDeniedTemplate(w, r) +} diff --git a/pkg/app/handler/authentication/auth_session/authsession.go b/pkg/app/handler/authentication/auth_session/authsession.go new file mode 100644 index 0000000..c86ca99 --- /dev/null +++ b/pkg/app/handler/authentication/auth_session/authsession.go @@ -0,0 +1,177 @@ +package auth_session + +import ( + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "github.com/google/uuid" + "net/http" + "strings" + "time" +) + +func Create(w http.ResponseWriter, r *http.Request, user *users.User, bindSessionToIP bool, secondFactorMissing bool) { + sessionID := createSessionID() + sessionIP := "*" + expires := time.Now().AddDate(0, 1, 0) + + if bindSessionToIP { + sessionIP = getIP(r) + } + + if secondFactorMissing { + expires = time.Now().Add(10 * time.Minute) + } + + session := &models.Session{ + Id: sessionID, + UserId: user.Id, + IP: sessionIP, + SecondFactorMissing: secondFactorMissing, + Expires: expires, + } + + _, err := connection.DB.Model(session).OnConflict("(id) DO UPDATE").Insert() + if err != nil { + logger.Error.Println("Err during creating session") + logger.Error.Println(err) + } + + createSessionCookie(w, sessionID) +} + +func createSessionID() string { + id, _ := uuid.NewUUID() + return id.String() +} + +func createSessionCookie(w http.ResponseWriter, sessionID string) { + + expires := time.Now().AddDate(0, 1, 0) + + ck := http.Cookie{ + Name: "session", + Domain: "localhost", + Path: "/", + Expires: expires, + } + + ck.Value = sessionID + + http.SetCookie(w, &ck) + +} + +func GetUserId(sessionId, userIP string) int64 { + session := &models.Session{Id: sessionId} + err := connection.DB.Model(session).Relation("User").WherePK().Select() + + if err != nil || session.User.Disabled { + return -1 + } + + if session != nil && + session.Expires.After(time.Now()) && + isValidIP(session.IP, userIP) { + return session.UserId + } else { + return -1 + } +} + +func Only2FAMissing(sessionId, userIP string) bool { + session := &models.Session{Id: sessionId} + err := connection.DB.Model(session).Relation("User").WherePK().Select() + + if err != nil { + return false + } + + invalidateExpiredSession(session) + + return session != nil && + session.Expires.After(time.Now()) && + !session.User.Disabled && + session.SecondFactorMissing && + isValidIP(session.IP, userIP) +} + +func IsLoggedIn(sessionId, userIP string) bool { + + session := &models.Session{Id: sessionId} + err := connection.DB.Model(session).Relation("User").WherePK().Select() + + if err != nil { + return false + } + + invalidateExpiredSession(session) + + return session != nil && + !session.SecondFactorMissing && + !session.User.Disabled && + session.Expires.After(time.Now()) && + isValidIP(session.IP, userIP) +} + +func IsLoggedInAndNeedsNewPassword(sessionId, userIP string) bool { + + session := &models.Session{Id: sessionId} + err := connection.DB.Model(session).Relation("User").WherePK().Select() + + if err != nil { + return false + } + + invalidateExpiredSession(session) + + return session != nil && + !session.SecondFactorMissing && + !session.User.Disabled && + session.User.ForcePasswordRotation && + session.Expires.After(time.Now()) && + isValidIP(session.IP, userIP) +} + +func IsLoggedInAndNeeds2FA(sessionId, userIP string) bool { + + session := &models.Session{Id: sessionId} + err := connection.DB.Model(session).Relation("User").WherePK().Select() + + if err != nil { + return false + } + + invalidateExpiredSession(session) + + return session != nil && + !session.SecondFactorMissing && + !session.User.Disabled && + session.User.Force2FA && + !session.User.IsUsing2FA() && + session.Expires.After(time.Now()) && + isValidIP(session.IP, userIP) +} + +func invalidateExpiredSession(session *models.Session) { + if session.Expires.Before(time.Now()) { + _, err := connection.DB.Model(session).WherePK().Delete() + if err != nil { + logger.Error.Println("Error deleting expired session.") + logger.Error.Println(err) + } + } +} + +func isValidIP(sessionIP, userIP string) bool { + return sessionIP == "*" || userIP == sessionIP +} + +func getIP(r *http.Request) string { + forwarded := r.Header.Get("X-FORWARDED-FOR") + if forwarded != "" { + return strings.Split(forwarded, ":")[0] + } + return strings.Split(r.RemoteAddr, ":")[0] +} diff --git a/pkg/app/handler/authentication/login.go b/pkg/app/handler/authentication/login.go new file mode 100644 index 0000000..7cd5c87 --- /dev/null +++ b/pkg/app/handler/authentication/login.go @@ -0,0 +1,100 @@ +package authentication + +import ( + "glsamaker/pkg/app/handler/authentication/auth_session" + "glsamaker/pkg/app/handler/authentication/templates" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/models/users" + "golang.org/x/crypto/argon2" + "net/http" +) + +func Login(w http.ResponseWriter, r *http.Request) { + + // in case '/login' is request but the user is + // already authenticated we will redirect to '/' + if utils.IsAuthenticated(w, r) { + http.Redirect(w, r, "/", 301) + } + + username, pass, cameFrom, bindLoginToIP, _ := getParams(r) + + if IsValidPassword(username, pass) { + user, _ := getLoginUser(username) + auth_session.Create(w, r, user, bindLoginToIP, user.IsUsing2FA()) + if user.IsUsing2FA() { + http.Redirect(w, r, "/login/2fa", 301) + } else { + http.Redirect(w, r, cameFrom, 301) + } + } else { + templates.RenderLoginTemplate(w, r) + } + +} + +func SecondFactorLogin(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + + if user == nil || !user.IsUsing2FA() { + // this should not occur + http.NotFound(w, r) + return + } + + if user.IsUsingTOTP { + templates.RenderTOTPTemplate(w, r) + } else if user.IsUsingWebAuthn { + templates.RenderWebAuthnTemplate(w, r) + } else { + // this should not occur + http.NotFound(w, r) + } +} + +// utility functions + +func getLoginUser(username string) (*users.User, bool) { + var potenialUsers []*users.User + err := connection.DB.Model(&potenialUsers).Where("nick = ?", username).Select() + isValidUser := err == nil + + if len(potenialUsers) < 1 { + return &users.User{}, false + } + + return potenialUsers[0], isValidUser +} + +func getParams(r *http.Request) (string, string, string, bool, error) { + err := r.ParseForm() + if err != nil { + return "", "", "", false, err + } + username := r.Form.Get("username") + password := r.Form.Get("password") + cameFrom := r.Form.Get("cameFrom") + restrictLogin := r.Form.Get("restrictlogin") + return username, password, cameFrom, restrictLogin == "on", err +} + +func IsValidPassword(username string, password string) bool { + user, isValidUser := getLoginUser(username) + if !isValidUser { + return false + } + + hashedPassword := argon2.IDKey( + []byte(password), + user.Password.Salt, + user.Password.Time, + user.Password.Memory, + user.Password.Threads, + user.Password.KeyLen) + + if user != nil && !user.Disabled && string(user.Password.Hash) == string(hashedPassword) { + return true + } + return false +} diff --git a/pkg/app/handler/authentication/logout.go b/pkg/app/handler/authentication/logout.go new file mode 100644 index 0000000..87d17f4 --- /dev/null +++ b/pkg/app/handler/authentication/logout.go @@ -0,0 +1,29 @@ +package authentication + +import ( + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "net/http" +) + +func Logout(w http.ResponseWriter, r *http.Request) { + + sessionID, err := r.Cookie("session") + + if err != nil || sessionID == nil { + // TODO Error + } + + session := &models.Session{Id: sessionID.Value} + _, err = connection.DB.Model(session).WherePK().Delete() + + if err != nil { + logger.Info.Println("Error deleting session") + logger.Error.Println("Error deleting session") + logger.Error.Println(err) + } + + http.Redirect(w, r, "/", 301) + +} diff --git a/pkg/app/handler/authentication/templates/admin.go b/pkg/app/handler/authentication/templates/admin.go new file mode 100644 index 0000000..12d039e --- /dev/null +++ b/pkg/app/handler/authentication/templates/admin.go @@ -0,0 +1,36 @@ +package templates + +import ( + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the login page +func RenderAccessDeniedTemplate(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/authentication/accessDenied.tmpl")) + + templates.ExecuteTemplate(w, "accessDenied.tmpl", createAccessDeniedData(user)) +} + +// createPageData creates the data used in the template of the landing page +func createAccessDeniedData(user *users.User) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + }{ + Page: "", + Application: models.GetDefaultGlobalSettings(), + User: user, + } +} diff --git a/pkg/app/handler/authentication/templates/login.go b/pkg/app/handler/authentication/templates/login.go new file mode 100644 index 0000000..2e3b241 --- /dev/null +++ b/pkg/app/handler/authentication/templates/login.go @@ -0,0 +1,32 @@ +package templates + +import ( + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the login page +func RenderLoginTemplate(w http.ResponseWriter, r *http.Request) { + + data := struct { + CameFrom string + }{ + CameFrom: getPath(r), + } + + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/authentication/login.tmpl")) + + templates.ExecuteTemplate(w, "login.tmpl", data) +} + +func getPath(r *http.Request) string { + if r.URL.RawQuery == "" { + return r.URL.Path + } else { + return r.URL.Path + "?" + r.URL.RawQuery + } +} diff --git a/pkg/app/handler/authentication/templates/totp.go b/pkg/app/handler/authentication/templates/totp.go new file mode 100644 index 0000000..acb34e5 --- /dev/null +++ b/pkg/app/handler/authentication/templates/totp.go @@ -0,0 +1,23 @@ +package templates + +import ( + "html/template" + "net/http" +) + +func RenderTOTPTemplate(w http.ResponseWriter, r *http.Request) { + + data := struct { + CameFrom string + }{ + CameFrom: getPath(r), + } + + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/authentication/totp.tmpl")) + + templates.ExecuteTemplate(w, "totp.tmpl", data) +} diff --git a/pkg/app/handler/authentication/templates/webauthn.go b/pkg/app/handler/authentication/templates/webauthn.go new file mode 100644 index 0000000..148f475 --- /dev/null +++ b/pkg/app/handler/authentication/templates/webauthn.go @@ -0,0 +1,23 @@ +package templates + +import ( + "html/template" + "net/http" +) + +func RenderWebAuthnTemplate(w http.ResponseWriter, r *http.Request) { + + data := struct { + CameFrom string + }{ + CameFrom: getPath(r), + } + + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/authentication/webauthn.tmpl")) + + templates.ExecuteTemplate(w, "webauthn.tmpl", data) +} diff --git a/pkg/app/handler/authentication/totp/totp.go b/pkg/app/handler/authentication/totp/totp.go new file mode 100644 index 0000000..00e6b83 --- /dev/null +++ b/pkg/app/handler/authentication/totp/totp.go @@ -0,0 +1,60 @@ +package totp + +import ( + "glsamaker/pkg/app/handler/authentication/auth_session" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/models/users" + "bytes" + "encoding/base64" + "github.com/pquerna/otp/totp" + "image/png" + "net/http" + "time" +) + +func Login(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + token, err := getParam(r) + + if user == nil || err != nil || !IsValidTOTPToken(user, token) { + http.Redirect(w, r, "/login/2fa", 301) + } else { + auth_session.Create(w, r, user, true, false) + http.Redirect(w, r, "/", 301) + } + +} + +func IsValidTOTPToken(user *users.User, token string) bool { + return totp.Validate(token, user.TOTPSecret) +} + +func GetToken(user *users.User) string { + token, _ := totp.GenerateCode(user.TOTPSecret, time.Now()) + return token +} + +func Generate(email string) (string, string) { + + key, _ := totp.Generate(totp.GenerateOpts{ + Issuer: "glsamakertest.gentoo.org", + AccountName: email, + }) + + var buf bytes.Buffer + img, _ := key.Image(250, 250) + + png.Encode(&buf, img) + + return key.Secret(), base64.StdEncoding.EncodeToString(buf.Bytes()) +} + +func getParam(r *http.Request) (string, error) { + err := r.ParseForm() + if err != nil { + return "", err + } + token := r.Form.Get("token") + return token, err +} diff --git a/pkg/app/handler/authentication/utils/utils.go b/pkg/app/handler/authentication/utils/utils.go new file mode 100644 index 0000000..d06a2d7 --- /dev/null +++ b/pkg/app/handler/authentication/utils/utils.go @@ -0,0 +1,81 @@ +package utils + +import ( + "glsamaker/pkg/app/handler/authentication/auth_session" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/models/users" + "net/http" + "strings" +) + +// utility methods to check whether a user is authenticated + +func Only2FAMissing(w http.ResponseWriter, r *http.Request) bool { + sessionID, err := r.Cookie("session") + userIP := getIP(r) + + return err == nil && sessionID != nil && auth_session.Only2FAMissing(sessionID.Value, userIP) +} + +func IsAuthenticated(w http.ResponseWriter, r *http.Request) bool { + sessionID, err := r.Cookie("session") + userIP := getIP(r) + + return err == nil && sessionID != nil && auth_session.IsLoggedIn(sessionID.Value, userIP) +} + +func IsAuthenticatedAndNeedsNewPassword(w http.ResponseWriter, r *http.Request) bool { + sessionID, err := r.Cookie("session") + userIP := getIP(r) + + return err == nil && sessionID != nil && auth_session.IsLoggedInAndNeedsNewPassword(sessionID.Value, userIP) +} + +func IsAuthenticatedAndNeeds2FA(w http.ResponseWriter, r *http.Request) bool { + sessionID, err := r.Cookie("session") + userIP := getIP(r) + + return err == nil && sessionID != nil && auth_session.IsLoggedInAndNeeds2FA(sessionID.Value, userIP) +} + +func IsAuthenticatedAsAdmin(w http.ResponseWriter, r *http.Request) bool { + sessionID, err := r.Cookie("session") + userIP := getIP(r) + + if err != nil || sessionID == nil || !auth_session.IsLoggedIn(sessionID.Value, userIP) { + return false + } + + user := GetAuthenticatedUser(r) + + return user != nil && user.Permissions.Admin.View + +} + +func GetAuthenticatedUser(r *http.Request) *users.User { + sessionID, err := r.Cookie("session") + userIP := getIP(r) + + if err != nil || sessionID == nil || !(auth_session.IsLoggedIn(sessionID.Value, userIP) || auth_session.Only2FAMissing(sessionID.Value, userIP)) { + return nil + } + + userId := auth_session.GetUserId(sessionID.Value, userIP) + + user := &users.User{Id: userId} + err = connection.DB.Select(user) + + if err != nil { + return nil + } + + return user +} + +func getIP(r *http.Request) string { + forwarded := r.Header.Get("X-FORWARDED-FOR") + if forwarded != "" { + return strings.Split(forwarded, ":")[0] + } + return strings.Split(r.RemoteAddr, ":")[0] +} diff --git a/pkg/app/handler/authentication/webauthn/login.go b/pkg/app/handler/authentication/webauthn/login.go new file mode 100644 index 0000000..7bf9c1d --- /dev/null +++ b/pkg/app/handler/authentication/webauthn/login.go @@ -0,0 +1,118 @@ +package webauthn + +import ( + "glsamaker/pkg/app/handler/authentication/auth_session" + "glsamaker/pkg/app/handler/authentication/utils" + "encoding/json" + "fmt" + "github.com/duo-labs/webauthn.io/session" + webauthn_lib "github.com/duo-labs/webauthn/webauthn" + "log" + "net/http" +) + +var ( + WebAuthn *webauthn_lib.WebAuthn + SessionStore *session.Store +) + + +func BeginLogin(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + CreateWebAuthn() + CreateSessionStore() + + // user doesn't exist + if user == nil { + log.Println("Error fetching the user.") + JsonResponse(w, "Error fetching the user.", http.StatusBadRequest) + return + } + + // generate PublicKeyCredentialRequestOptions, session data + options, sessionData, err := WebAuthn.BeginLogin(user) + if err != nil { + log.Println(err) + JsonResponse(w, err.Error(), http.StatusInternalServerError) + return + } + + // store session data as marshaled JSON + err = SessionStore.SaveWebauthnSession("authentication", sessionData, r, w) + if err != nil { + log.Println(err) + JsonResponse(w, err.Error(), http.StatusInternalServerError) + return + } + + JsonResponse(w, options, http.StatusOK) +} + +func FinishLogin(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + // user doesn't exist + if user == nil { + log.Println("Error fetching the user.") + JsonResponse(w, "Error fetching the user.", http.StatusBadRequest) + return + } + + // load the session data + sessionData, err := SessionStore.GetWebauthnSession("authentication", r) + if err != nil { + log.Println(err) + JsonResponse(w, err.Error(), http.StatusBadRequest) + return + } + + // in an actual implementation, we should perform additional checks on + // the returned 'credential', i.e. check 'credential.Authenticator.CloneWarning' + // and then increment the credentials counter + _, err = WebAuthn.FinishLogin(user, sessionData, r) + if err != nil { + log.Println(err) + JsonResponse(w, err.Error(), http.StatusBadRequest) + return + } + + // handle successful login + // TODO handle bindLoginToIP correctly + auth_session.Create(w, r, user, true, false) + JsonResponse(w, "Login Success", http.StatusOK) +} + +// from: https://github.com/duo-labs/webauthn.io/blob/3f03b482d21476f6b9fb82b2bf1458ff61a61d41/server/response.go#L15 +func JsonResponse(w http.ResponseWriter, d interface{}, c int) { + dj, err := json.Marshal(d) + if err != nil { + http.Error(w, "Error creating JSON response", http.StatusInternalServerError) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(c) + fmt.Fprintf(w, "%s", dj) +} + +func CreateWebAuthn() { + + if WebAuthn == nil { + authn, _ := webauthn_lib.New(&webauthn_lib.Config{ + RPDisplayName: "Gentoo GLSAMaker", // Display Name for your site + RPID: "glsamakertest.gentoo.org", // Generally the domain name for your site + RPOrigin: "https://glsamakertest.gentoo.org", // The origin URL for WebAuthn requests + RPIcon: "https://assets.gentoo.org/tyrian/site-logo.png", // Optional icon URL for your site + }) + + WebAuthn = authn + } + +} + +func CreateSessionStore() { + if SessionStore == nil { + SessionStore, _ = session.NewStore() + } +} diff --git a/pkg/app/handler/authentication/webauthn/register.go b/pkg/app/handler/authentication/webauthn/register.go new file mode 100644 index 0000000..4e299b3 --- /dev/null +++ b/pkg/app/handler/authentication/webauthn/register.go @@ -0,0 +1,111 @@ +package webauthn + +import ( + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "fmt" + "github.com/duo-labs/webauthn/protocol" + "log" + "net/http" +) + +func BeginRegistration(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + + CreateWebAuthn() + CreateSessionStore() + + if user == nil { + JsonResponse(w, fmt.Errorf("must supply a valid username i.e. foo@bar.com"), http.StatusBadRequest) + return + } + + registerOptions := func(credCreationOpts *protocol.PublicKeyCredentialCreationOptions) { + credCreationOpts.CredentialExcludeList = user.CredentialExcludeList() + } + + // generate PublicKeyCredentialCreationOptions, session data + //var options *protocol.CredentialCreation + //var err error + options, sessionData, err := WebAuthn.BeginRegistration( + user, + registerOptions, + ) + + if err != nil { + log.Println("Error begin register") + log.Println(err) + JsonResponse(w, err.Error(), http.StatusInternalServerError) + return + } + + // store session data as marshaled JSON + err = SessionStore.SaveWebauthnSession("registration", sessionData, r, w) + if err != nil { + log.Println("Error store session") + log.Println(err) + JsonResponse(w, err.Error(), http.StatusInternalServerError) + return + } + + JsonResponse(w, options, http.StatusOK) +} + +func FinishRegistration(w http.ResponseWriter, r *http.Request) { + + authname := getParams(r) + user := utils.GetAuthenticatedUser(r) + + if user == nil { + JsonResponse(w, "Cannot find User", http.StatusBadRequest) + return + } + + // load the session data + sessionData, err := SessionStore.GetWebauthnSession("registration", r) + if err != nil { + log.Println("Error loading session") + log.Println(err) + JsonResponse(w, err.Error(), http.StatusBadRequest) + return + } + + credential, err := WebAuthn.FinishRegistration(user, sessionData, r) + if err != nil { + log.Println("Error finish session") + log.Println(err) + JsonResponse(w, err.Error(), http.StatusBadRequest) + return + } + + user.AddCredential(*credential, authname) + + _, err = connection.DB.Model(user).Column("webauthn_credentials").WherePK().Update() + _, err = connection.DB.Model(user).Column("webauthn_credential_names").WherePK().Update() + if err != nil { + logger.Error.Println("Error adding WebAuthn credentials.") + logger.Error.Println(err) + } + + JsonResponse(w, "Registration Success", http.StatusOK) +} + +func getParams(r *http.Request) string { + + keys, ok := r.URL.Query()["name"] + + if !ok || len(keys[0]) < 1 { + logger.Info.Println("Url Param 'name' is missing") + return "Unnamed Authenticator" + } + + // we only want the single item. + key := keys[0] + + if len(key) > 20 { + key = key[0:20] + } + + return key +} diff --git a/pkg/app/handler/cvetool/bug.go b/pkg/app/handler/cvetool/bug.go new file mode 100644 index 0000000..7725c88 --- /dev/null +++ b/pkg/app/handler/cvetool/bug.go @@ -0,0 +1,85 @@ +package cvetool + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models/bugzilla" + "glsamaker/pkg/models/cve" + "net/http" +) + +// Show renders a template to show the landing page of the application +func AssignBug(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.CVETool.AssignBug { + authentication.AccessDenied(w, r) + return + } + + cveId, bugId, err := getBugAssignParams(r) + + // TODO validate bug using bugzilla api before continue + + cveItem := &cve.DefCveItem{Id: cveId} + err = connection.DB.Select(cveItem) + + if err != nil { + w.Write([]byte("err")) + return + } + + cveItem.State = "Assigned" + + logger.Info.Println("bugId") + logger.Info.Println(bugId) + + //assign bug + newBugs := bugzilla.GetBugsByIds([]string{bugId}) + + for _, newBug := range newBugs { + _, err = connection.DB.Model(&newBug).OnConflict("(id) DO UPDATE").Insert() + + if err != nil { + logger.Info.Println("Error creating bug") + logger.Info.Println(err) + } + + cveToBug := &cve.DefCveItemToBug{ + DefCveItemId: cveId, + BugId: newBug.Id, + } + + connection.DB.Model(cveToBug).Insert() + + } + + // TODO MIGRATION + //cveItem.Bugs = append(cveItem.Bugs, bugId) + + _, err = connection.DB.Model(cveItem).Column("bugs").WherePK().Update() + _, err = connection.DB.Model(cveItem).Column("state").WherePK().Update() + + if err != nil { + logger.Info.Println("Err") + logger.Info.Println(err) + w.Write([]byte("err")) + return + } + + w.Write([]byte("ok")) + +} + +func getBugAssignParams(r *http.Request) (string, string, error) { + err := r.ParseForm() + if err != nil { + return "", "", err + } + cveid := r.Form.Get("cveid") + bugid := r.Form.Get("bugid") + return cveid, bugid, err +} diff --git a/pkg/app/handler/cvetool/comments.go b/pkg/app/handler/cvetool/comments.go new file mode 100644 index 0000000..d36122a --- /dev/null +++ b/pkg/app/handler/cvetool/comments.go @@ -0,0 +1,74 @@ +package cvetool + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models/cve" + "encoding/json" + "net/http" + "time" +) + +// Show renders a template to show the landing page of the application +func AddComment(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.CVETool.Comment { + authentication.AccessDenied(w, r) + return + } + + id, comment, err := getParams(r) + + newComment, err := addNewCommment(id, user.Id, comment) + + if err != nil { + logger.Info.Println("Err") + logger.Info.Println(err) + w.Write([]byte("err")) + return + } + + newCommentString, _ := json.Marshal(newComment) + + w.Write(newCommentString) + +} + +func addNewCommment(id string, userID int64, comment string) (cve.Comment, error) { + + cveItem := &cve.DefCveItem{Id: id} + err := connection.DB.Select(cveItem) + + if err != nil { + return cve.Comment{}, err + } + + newComment := cve.Comment{ + CVEId: id, + User: userID, + Message: comment, + Date: time.Now(), + } + + //cveItem.Comments = append(cveItem.Comments, newComment) + + //_, err = connection.DB.Model(cveItem).Column("comments").WherePK().Update() + _, err = connection.DB.Model(&newComment).Insert() + + return newComment, err + +} + +func getParams(r *http.Request) (string, string, error) { + err := r.ParseForm() + if err != nil { + return "", "", err + } + id := r.Form.Get("cveid") + comment := r.Form.Get("comment") + return id, comment, err +} diff --git a/pkg/app/handler/cvetool/index.go b/pkg/app/handler/cvetool/index.go new file mode 100644 index 0000000..9c54a01 --- /dev/null +++ b/pkg/app/handler/cvetool/index.go @@ -0,0 +1,169 @@ +// Used to show the landing page of the application + +package cvetool + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models/cve" + "encoding/json" + "fmt" + "github.com/go-pg/pg/v9/orm" + "net/http" + "strconv" + "strings" +) + +// Show renders a template to show the landing page of the application +func Show(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.CVETool.View { + authentication.AccessDenied(w, r) + return + } + + renderIndexTemplate(w, user) +} + +// Show renders a template to show the landing page of the application +func ShowFullscreen(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.CVETool.View { + authentication.AccessDenied(w, r) + return + } + + renderIndexFullscreenTemplate(w, user) +} + +// Show renders a template to show the landing page of the application +func Add(w http.ResponseWriter, r *http.Request) { + //renderIndexTemplate(w) +} + +// Show renders a template to show the landing page of the application +func CveData(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.CVETool.View { + authentication.AccessDenied(w, r) + return + } + + type DataTableData struct { + Draw int `json:"draw"` + RecordsTotal int `json:"recordsTotal"` + RecordsFiltered int `json:"recordsFiltered"` + Data [][]string `json:"data"` + } + + draw, _ := strconv.Atoi(getParam(r, "draw")) + start, _ := strconv.Atoi(getParam(r, "start")) + length, _ := strconv.Atoi(getParam(r, "length")) + order_column := getParam(r, "order[0][column]") + order_dir := strings.ToUpper(getParam(r, "order[0][dir]")) + search_value := strings.ToUpper(getParam(r, "search[value]")) + + state_value := getParam(r, "columns[10][search][value]") + logger.Info.Println("state_value") + logger.Info.Println(state_value) + + count_overall, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Count() + count, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Where("state LIKE " + "'%" + state_value + "%'").WhereGroup(func(q *orm.Query) (*orm.Query, error) { + q = q.WhereOr("description LIKE " + "'%" + search_value + "%'"). + WhereOr("id LIKE " + "'%" + search_value + "%'") + return q, nil + }).Count() + + order := "id" + if order_column == "0" { + order = "id" + } else if order_column == "8" { + order = "last_modified_date" + } else if order_column == "9" { + order = "published_date" + } else if order_column == "10" { + order = "state" + } + + var dataTableEntries [][]string + var cves []*cve.DefCveItem + err := connection.DB.Model(&cves).Order(order + " " + order_dir).Offset(start).Limit(length).Where("state LIKE " + "'%" + state_value + "%'").WhereGroup(func(q *orm.Query) (*orm.Query, error) { + q = q.WhereOr("description LIKE " + "'%" + search_value + "%'"). + WhereOr("id LIKE " + "'%" + search_value + "%'") + return q, nil + }).Relation("Bugs").Relation("Comments").Select() + + if err != nil || len(cves) == 0 { + logger.Info.Println("Error finding cves:") + logger.Info.Println(err) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"draw":` + strconv.Itoa(draw) + `,"recordsTotal":` + strconv.Itoa(count_overall) + `,"recordsFiltered":0,"data":[]}`)) + return + } else { + for _, cve := range cves { + + // TODO handle empty + + baseScore := "" + impact := "" + if cve.Impact != nil { + baseScore = fmt.Sprintf("%.2f", cve.Impact.BaseMetricV3.CvssV3.BaseScore) + impact = cve.Impact.BaseMetricV3.CvssV3.VectorString + } + + var referenceList []string + for _, reference := range cve.Cve.References.ReferenceData { + referenceList = append(referenceList, "<a href=\""+reference.Url+"\">source</a>") + //referenceList = append(referenceList, "<a href=\"" + reference.Url + "\">" + strings.ToLower(reference.Refsource) + "</a>") + } + references := strings.Join(referenceList, ", ") + + comments, _ := json.Marshal(cve.Comments) + + packages, _ := json.Marshal(cve.Packages) + bugs, _ := json.Marshal(cve.Bugs) + + dataTableEntries = append(dataTableEntries, []string{ + cve.Id, + cve.Description, + string(packages), // TODO MIGRATION strings.Join(cve.Packages, ","), + string(bugs), // TODO MIGRATION strings.Join(cve.Bugs, ","), + baseScore, + impact, + references, + string(comments), + cve.LastModifiedDate, + cve.PublishedDate, + cve.State, + "changelog"}) + } + } + + dataTableData := DataTableData{ + Draw: draw, + RecordsTotal: count_overall, + RecordsFiltered: count, + Data: dataTableEntries, + } + + res, _ := json.Marshal(dataTableData) + + w.Header().Set("Content-Type", "application/json") + w.Write(res) +} + +func getParam(r *http.Request, keyname string) string { + keys, ok := r.URL.Query()[keyname] + if !ok || len(keys[0]) < 1 { + return "" + } + result := keys[0] + return result +} diff --git a/pkg/app/handler/cvetool/state.go b/pkg/app/handler/cvetool/state.go new file mode 100644 index 0000000..608691c --- /dev/null +++ b/pkg/app/handler/cvetool/state.go @@ -0,0 +1,75 @@ +package cvetool + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models/cve" + "encoding/json" + "net/http" +) + +// Show renders a template to show the landing page of the application +func ChangeState(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.CVETool.ChangeState { + authentication.AccessDenied(w, r) + return + } + + if !user.CanEditCVEs() { + logger.Error.Println("Err, user can not edit.") + w.Write([]byte("err")) + return + } + + id, newState, reason, err := getStateParams(r) + + cveItem := &cve.DefCveItem{Id: id} + err = connection.DB.Select(cveItem) + + if err != nil || reason == "" || cveItem.State == "Assigned" || !(newState == "NFU" || newState == "Later" || newState == "Invalid") { + logger.Error.Println("Err, invalid data") + logger.Error.Println(err) + w.Write([]byte("err")) + return + } + + cveItem.State = newState + _, err = connection.DB.Model(cveItem).Column("state").WherePK().Update() + + if err != nil { + logger.Error.Println("Err") + logger.Error.Println(err) + w.Write([]byte("err")) + return + } + + newComment, err := addNewCommment(id, user.Id, "Changed status to "+newState+": "+reason) + + if err != nil { + logger.Error.Println("Err") + logger.Error.Println(err) + w.Write([]byte("err")) + return + } + + newCommentString, _ := json.Marshal(newComment) + + w.Write(newCommentString) + +} + +func getStateParams(r *http.Request) (string, string, string, error) { + err := r.ParseForm() + if err != nil { + return "", "", "", err + } + id := r.Form.Get("cveid") + newstate := r.Form.Get("newstate") + reason := r.Form.Get("reason") + return id, newstate, reason, err +} diff --git a/pkg/app/handler/cvetool/update.go b/pkg/app/handler/cvetool/update.go new file mode 100644 index 0000000..8ce12f5 --- /dev/null +++ b/pkg/app/handler/cvetool/update.go @@ -0,0 +1,23 @@ +package cvetool + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/cveimport" + "net/http" +) + +// Show renders a template to show the landing page of the application +func Update(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.CVETool.UpdateCVEs { + authentication.AccessDenied(w, r) + return + } + + go cveimport.IncrementalCVEImport() + + http.Redirect(w, r, "/", 301) +} diff --git a/pkg/app/handler/cvetool/utils.go b/pkg/app/handler/cvetool/utils.go new file mode 100644 index 0000000..7e78660 --- /dev/null +++ b/pkg/app/handler/cvetool/utils.go @@ -0,0 +1,47 @@ +// miscellaneous utility functions used for the landing page of the application + +package cvetool + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderIndexTemplate(w http.ResponseWriter, user *users.User) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/index/*.tmpl")) + + templates.ExecuteTemplate(w, "show.tmpl", createPageData("cvetool", user)) +} + +// renderIndexTemplate renders all templates used for the landing page +func renderIndexFullscreenTemplate(w http.ResponseWriter, user *users.User) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/index/*.tmpl")) + + templates.ExecuteTemplate(w, "showFullscreen.tmpl", createPageData("cvetool", user)) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + CanEdit bool + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + CanEdit: user.CanEditCVEs(), + } +} diff --git a/pkg/app/handler/dashboard/index.go b/pkg/app/handler/dashboard/index.go new file mode 100644 index 0000000..82fd858 --- /dev/null +++ b/pkg/app/handler/dashboard/index.go @@ -0,0 +1,58 @@ +// Used to show the landing page of the application + +package dashboard + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/app/handler/statistics" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/models" + "glsamaker/pkg/models/cve" + "net/http" +) + +// Show renders a template to show the landing page of the application +func Show(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !(user.Permissions.Glsa.View && user.Permissions.CVETool.View) { + authentication.AccessDenied(w, r) + return + } + + var glsas []*models.Glsa + user.CanAccess(connection.DB.Model(&glsas).Relation("Creator").Order("updated DESC").Limit(5)).Select() + + var cves []*cve.DefCveItem + connection.DB.Model(&cves).Order("last_modified_date DESC").Limit(5).Select() + + var comments []*cve.Comment + connection.DB.Model(&comments).Order("date DESC").Limit(5).Select() + + requests, _ := connection.DB.Model((*models.Glsa)(nil)).Where("type = ?", "request").Count() + drafts, _ := connection.DB.Model((*models.Glsa)(nil)).Where("type = ?", "draft").Count() + glsasCount, _ := connection.DB.Model((*models.Glsa)(nil)).Where("type = ?", "glsa").Count() + allGlsas, _ := connection.DB.Model((*models.Glsa)(nil)).Count() + + new, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Where("state = ?", "New").Count() + assigned, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Where("state = ?", "Assigned").Count() + nfu, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Where("state = ?", "NFU").Count() + later, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Where("state = ?", "Later").Count() + invalid, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Where("state = ?", "Invalid").Count() + allCVEs, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Count() + + statisticsData := statistics.StatisticsData{ + Requests: float64(requests) / float64(allGlsas), + Drafts: float64(drafts) / float64(allGlsas), + Glsas: float64(glsasCount) / float64(allGlsas), + New: float64(new) / float64(allCVEs), + Assigned: float64(assigned) / float64(allCVEs), + NFU: float64(nfu) / float64(allCVEs), + Later: float64(later) / float64(allCVEs), + Invalid: float64(invalid) / float64(allCVEs), + } + + renderDashboardTemplate(w, user, glsas, cves, comments, &statisticsData) +} diff --git a/pkg/app/handler/dashboard/utils.go b/pkg/app/handler/dashboard/utils.go new file mode 100644 index 0000000..ed0bc91 --- /dev/null +++ b/pkg/app/handler/dashboard/utils.go @@ -0,0 +1,44 @@ +// miscellaneous utility functions used for the landing page of the application + +package dashboard + +import ( + "glsamaker/pkg/app/handler/statistics" + "glsamaker/pkg/models" + "glsamaker/pkg/models/cve" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderDashboardTemplate(w http.ResponseWriter, user *users.User, glsas []*models.Glsa, cves []*cve.DefCveItem, comments []*cve.Comment, statisticsData *statistics.StatisticsData) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/dashboard/*.tmpl")) + + templates.ExecuteTemplate(w, "dashboard.tmpl", createPageData("dashboard", user, glsas, cves, comments, statisticsData)) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User, glsas []*models.Glsa, cves []*cve.DefCveItem, comments []*cve.Comment, statisticsData *statistics.StatisticsData) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + GLSAs []*models.Glsa + CVEs []*cve.DefCveItem + Comments []*cve.Comment + StatisticsData *statistics.StatisticsData + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + GLSAs: glsas, + CVEs: cves, + Comments: comments, + StatisticsData: statisticsData, + } +} diff --git a/pkg/app/handler/drafts/index.go b/pkg/app/handler/drafts/index.go new file mode 100644 index 0000000..cfc699f --- /dev/null +++ b/pkg/app/handler/drafts/index.go @@ -0,0 +1,44 @@ +// Used to show the landing page of the application + +package drafts + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "net/http" +) + +// Show renders a template to show the landing page of the application +func Show(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Glsa.View { + authentication.AccessDenied(w, r) + return + } + + var drafts []*models.Glsa + err := user.CanAccess(connection.DB.Model(&drafts). + Where("type = ?", "draft"). + Relation("Bugs"). + Relation("Creator"). + Relation("Comments")). + Select() + + if err != nil { + logger.Info.Println("Error during draft selection") + logger.Info.Println(err) + http.NotFound(w, r) + return + } + + for _, draft := range drafts { + draft.ComputeStatus(user) + } + + renderDraftsTemplate(w, user, drafts) +} diff --git a/pkg/app/handler/drafts/utils.go b/pkg/app/handler/drafts/utils.go new file mode 100644 index 0000000..f7f4f57 --- /dev/null +++ b/pkg/app/handler/drafts/utils.go @@ -0,0 +1,37 @@ +// miscellaneous utility functions used for the landing page of the application + +package drafts + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderDraftsTemplate(w http.ResponseWriter, user *users.User, drafts []*models.Glsa) { + + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/drafts/*.tmpl")) + + templates.ExecuteTemplate(w, "drafts.tmpl", createPageData("drafts", user, drafts)) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User, drafts []*models.Glsa) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + Drafts []*models.Glsa + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + Drafts: drafts, + } +} diff --git a/pkg/app/handler/glsa/bugs.go b/pkg/app/handler/glsa/bugs.go new file mode 100644 index 0000000..9b3d32e --- /dev/null +++ b/pkg/app/handler/glsa/bugs.go @@ -0,0 +1,63 @@ +package glsa + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "glsamaker/pkg/models/bugzilla" + "net/http" + "strconv" +) + +// Show renders a template to show the landing page of the application +func UpdateBugs(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Glsa.UpdateBugs { + authentication.AccessDenied(w, r) + return + } + + go bugUpdate() + + http.Redirect(w, r, "/", 301) +} + +func bugUpdate() { + + var allBugs []*bugzilla.Bug + connection.DB.Model(&allBugs).Select() + + var bugIdsLists [][]string + bugIdsLists = append(bugIdsLists, []string{}) + for _, bug := range allBugs { + lastElem := bugIdsLists[len(bugIdsLists)-1] + + if len(lastElem) < 100 { + bugIdsLists[len(bugIdsLists)-1] = append(lastElem, strconv.FormatInt(bug.Id, 10)) + } else { + bugIdsLists = append(bugIdsLists, []string{strconv.FormatInt(bug.Id, 10)}) + } + } + + for _, bugIdsList := range bugIdsLists { + updatedBugs := bugzilla.GetBugsByIds(bugIdsList) + + for _, updatedBug := range updatedBugs { + _, err := connection.DB.Model(&updatedBug).WherePK().Update() + if err != nil { + logger.Error.Println("Error during bug data update") + logger.Error.Println(err) + } + } + } + + // Possibly delete deleted bugs + // Do we even delete bugs? + + // update the time of the last bug update + models.SetApplicationValue("LastBugUpdate", "") +} diff --git a/pkg/app/handler/glsa/comments.go b/pkg/app/handler/glsa/comments.go new file mode 100644 index 0000000..9412b62 --- /dev/null +++ b/pkg/app/handler/glsa/comments.go @@ -0,0 +1,110 @@ +package glsa + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "glsamaker/pkg/models/cve" + "glsamaker/pkg/models/users" + "encoding/json" + "errors" + "net/http" + "strconv" + "time" +) + +// Show renders a template to show the landing page of the application +func AddComment(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Glsa.Comment { + authentication.AccessDenied(w, r) + return + } + + if !user.CanEditCVEs() { + w.Write([]byte("err")) + return + } + + id, comment, commentType, err := getParams(r) + + newComment, err := AddNewCommment(id, user, comment, commentType) + + if err != nil { + logger.Info.Println("Err") + logger.Info.Println(err) + w.Write([]byte("err")) + return + } + + newCommentString, _ := json.Marshal(newComment) + + w.Write(newCommentString) + +} + +func AddNewCommment(id string, user *users.User, comment string, commentType string) (cve.Comment, error) { + + glsaID, err := strconv.ParseInt(id, 10, 64) + + if err != nil { + return cve.Comment{}, err + } + + glsa := &models.Glsa{Id: glsaID} + err = user.CanAccess(connection.DB.Model(glsa).WherePK()).Select() + + if err != nil { + return cve.Comment{}, err + } + + // TODO: VALIDATE !! + + if commentType == "approve" && !user.Permissions.Glsa.Approve { + return cve.Comment{}, errors.New("ACCESS DENIED") + } else if commentType == "approve" && glsa.CreatorId == user.Id && !user.Permissions.Glsa.ApproveOwnGlsa { + return cve.Comment{}, errors.New("ACCESS DENIED") + } else if commentType == "decline" && !user.Permissions.Glsa.Decline { + return cve.Comment{}, errors.New("ACCESS DENIED") + } + + if commentType == "approve" { + glsa.ApprovedBy = append(glsa.ApprovedBy, user.Id) + _, err = connection.DB.Model(glsa).Column("approved_by").WherePK().Update() + } else if commentType == "decline" { + glsa.DeclinedBy = append(glsa.DeclinedBy, user.Id) + _, err = connection.DB.Model(glsa).Column("declined_by").WherePK().Update() + } + + newComment := cve.Comment{ + GlsaId: glsaID, + User: user.Id, + UserBadge: user.Badge, + Type: commentType, + Message: comment, + Date: time.Now(), + } + + glsa.Comments = append(glsa.Comments, newComment) + + //_, err = connection.DB.Model(glsa).Column("comments").WherePK().Update() + _, err = connection.DB.Model(&newComment).Insert() + + return newComment, err + +} + +func getParams(r *http.Request) (string, string, string, error) { + err := r.ParseForm() + if err != nil { + return "", "", "", err + } + id := r.Form.Get("glsaid") + comment := r.Form.Get("comment") + commentType := r.Form.Get("commentType") + return id, comment, commentType, err +} diff --git a/pkg/app/handler/glsa/delete.go b/pkg/app/handler/glsa/delete.go new file mode 100644 index 0000000..b036319 --- /dev/null +++ b/pkg/app/handler/glsa/delete.go @@ -0,0 +1,42 @@ +// Used to show the landing page of the application + +package glsa + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/models" + "glsamaker/pkg/models/cve" + "net/http" + "strconv" +) + +// Show renders a template to show the landing page of the application +func Delete(w http.ResponseWriter, r *http.Request) { + + // TODO delete confidential bugs? + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Glsa.Delete { + authentication.AccessDenied(w, r) + return + } + + glsaID := r.URL.Path[len("/glsa/delete/"):] + + if _, err := strconv.Atoi(glsaID); err != nil { + http.Redirect(w, r, "/", 301) + w.Write([]byte("err")) + } + + var glsa *models.Glsa + var glsaToBug *models.GlsaToBug + var comment *cve.Comment + connection.DB.Model(glsa).Where("id = ?", glsaID).Delete() + connection.DB.Model(glsaToBug).Where("glsa_id = ?", glsaID).Delete() + connection.DB.Model(comment).Where("glsa_id = ?", glsaID).Delete() + + w.Write([]byte("ok")) +} diff --git a/pkg/app/handler/glsa/edit.go b/pkg/app/handler/glsa/edit.go new file mode 100644 index 0000000..04e67b3 --- /dev/null +++ b/pkg/app/handler/glsa/edit.go @@ -0,0 +1,185 @@ +package glsa + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "glsamaker/pkg/models/bugzilla" + "glsamaker/pkg/models/gpackage" + "net/http" + "strconv" + "time" +) + +func getStringParam(key string, r *http.Request) string { + if len(r.Form[key]) > 0 { + return r.Form[key][0] + } + + return "" +} + +func getArrayParam(key string, r *http.Request) []string { + return r.Form[key] +} + +// Show renders a template to show the landing page of the application +func Edit(w http.ResponseWriter, r *http.Request) { + + // TODO edit confidential bugs? + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Glsa.Edit { + authentication.AccessDenied(w, r) + return + } + + glsaID := r.URL.Path[len("/glsa/edit/"):] + + parsedGlsaId, _ := strconv.ParseInt(glsaID, 10, 64) + currentGlsa := &models.Glsa{Id: parsedGlsaId} + err := user.CanAccess(connection.DB.Model(currentGlsa). + Relation("Bugs"). + Relation("Creator"). + Relation("Comments"). + WherePK()). + Select() + + if r.Method == "POST" { + + r.ParseForm() + + id, err := strconv.ParseInt(glsaID, 10, 64) + + if err != nil { + http.NotFound(w, r) + return + } + + // if + var packages []gpackage.Package + for k, _ := range getArrayParam("package_atom", r) { + newPackage := gpackage.Package{ + Affected: r.Form["package_vulnerable"][k] == "true", + Atom: r.Form["package_atom"][k], + Identifier: r.Form["package_identifier"][k], + Version: r.Form["package_version"][k], + Slot: r.Form["package_slot"][k], + Arch: r.Form["package_arch"][k], + Auto: r.Form["package_auto"][k] == "true", + } + packages = append(packages, newPackage) + } + + var references []models.Reference + for k, _ := range getArrayParam("reference_title", r) { + newReference := models.Reference{ + Title: r.Form["reference_title"][k], + URL: r.Form["reference_url"][k], + } + references = append(references, newReference) + } + + // Update Bugs: delete old mapping first + _, err = connection.DB.Model(&[]models.GlsaToBug{}).Where("glsa_id = ?", glsaID).Delete() + if err != nil { + logger.Error.Println("ERR during delete") + logger.Error.Println(err) + } + + newBugs := bugzilla.GetBugsByIds(getArrayParam("bugs", r)) + + for _, newBug := range newBugs { + _, err = connection.DB.Model(&newBug).OnConflict("(id) DO UPDATE").Insert() + + if err != nil { + logger.Error.Println("Error creating bug") + logger.Error.Println(err) + } + + parsedGlsaID, _ := strconv.ParseInt(glsaID, 10, 64) + + glsaToBug := &models.GlsaToBug{ + GlsaId: parsedGlsaID, + BugId: newBug.Id, + } + + connection.DB.Model(glsaToBug).Insert() + + } + + glsa := &models.Glsa{ + Id: id, + // Alias: getStringParam("alias", r), + // Type: getStringParam("status", r), + Title: getStringParam("title", r), + Synopsis: getStringParam("synopsis", r), + Packages: packages, + Description: getStringParam("description", r), + Impact: getStringParam("impact", r), + Workaround: getStringParam("workaround", r), + Resolution: getStringParam("resolution", r), + References: references, + Permission: getStringParam("permission", r), + Access: getStringParam("access", r), + Severity: getStringParam("severity", r), + Keyword: getStringParam("keyword", r), + Background: getStringParam("background", r), + //TODO + //Bugs: , + //Comments: nil, + Revision: "r9999", + // Created: time.Time{}, + Updated: time.Time{}, + } + + if currentGlsa.Type == "request" && glsa.Description != "" { + glsa.Type = "draft" + } else { + glsa.Type = currentGlsa.Type + } + + _, err = connection.DB.Model(glsa).Column( + "type", + "title", + "synopsis", + "packages", + "description", + "impact", + "workaround", + "resolution", + "references", + "permission", + "access", + "severity", + "keyword", + "background", + "updated", + "revision").WherePK().Update() + + if err != nil { + http.NotFound(w, r) + logger.Error.Println("ERR NOT FOUND") + logger.Error.Println(err) + return + } + + http.Redirect(w, r, "/glsa/"+glsaID, 301) + return + } + + if err != nil { + http.NotFound(w, r) + return + } + + currentGlsa.ComputeStatus(user) + currentGlsa.ComputeCommentBadges() + + glsaCount, err := connection.DB.Model((*models.Glsa)(nil)).Count() + + renderEditTemplate(w, user, currentGlsa, int64(glsaCount)) +} diff --git a/pkg/app/handler/glsa/release.go b/pkg/app/handler/glsa/release.go new file mode 100644 index 0000000..c24e5b0 --- /dev/null +++ b/pkg/app/handler/glsa/release.go @@ -0,0 +1,70 @@ +package glsa + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "net/http" + "strconv" + "strings" + "time" +) + +// Show renders a template to show the landing page of the application +func Release(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Glsa.Release { + authentication.AccessDenied(w, r) + return + } + + glsaID := r.URL.Path[len("/glsa/release/"):] + + currentGlsa := new(models.Glsa) + err := user.CanAccess(connection.DB.Model(currentGlsa). + Where("id = ?", glsaID)). + Select() + + if err != nil { + http.NotFound(w, r) + return + } + + currentGlsa.Type = "glsa" + currentGlsa.Alias = computeNextGLSAId() + + _, err = connection.DB.Model(currentGlsa).Column("type").WherePK().Update() + _, err = connection.DB.Model(currentGlsa).Column("alias").WherePK().Update() + + http.Redirect(w, r, "/archive", 301) +} + +func computeNextGLSAId() string { + + logger.Info.Println("compute Next GLSA") + + newGLSAID := "" + var glsas []*models.Glsa + err := connection.DB.Model(&glsas).Where("type = ?", "glsa").Order("alias DESC").Limit(1).Select() + + if err != nil || len(glsas) == 0 { + newGLSAID = time.Now().Format("200601") + "-" + "01" + } else if !strings.HasPrefix(glsas[0].Alias, time.Now().Format("200601")+"-") { + newGLSAID = time.Now().Format("200601") + "-" + "01" + } else { + oldId := strings.Replace(glsas[0].Alias, time.Now().Format("200601")+"-", "", 1) + parsedOldId, _ := strconv.Atoi(oldId) + parsedOldId = parsedOldId + 1 + newID := strconv.Itoa(parsedOldId) + if len(newID) < 2 { + newID = "0" + newID + } + newGLSAID = time.Now().Format("200601") + "-" + newID + } + + return newGLSAID +} diff --git a/pkg/app/handler/glsa/utils.go b/pkg/app/handler/glsa/utils.go new file mode 100644 index 0000000..b417a80 --- /dev/null +++ b/pkg/app/handler/glsa/utils.go @@ -0,0 +1,79 @@ +package glsa + +import ( + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "glsamaker/pkg/models/bugzilla" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderViewTemplate(w http.ResponseWriter, user *users.User, glsa *models.Glsa, glsaCount int64) { + + templates := template.Must( + template.Must( + template.New("Show"). + Funcs(getFuncMap()). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/glsa/show.tmpl")) + + templates.ExecuteTemplate(w, "show.tmpl", createPageData("show", user, glsa, glsaCount)) +} + +// renderIndexTemplate renders all templates used for the landing page +func renderEditTemplate(w http.ResponseWriter, user *users.User, glsa *models.Glsa, glsaCount int64) { + templates := template.Must( + template.Must( + template.New("Show"). + Funcs(getFuncMap()). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/glsa/edit.tmpl")) + + templates.ExecuteTemplate(w, "edit.tmpl", createPageData("edit", user, glsa, glsaCount)) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User, glsa *models.Glsa, glsaCount int64) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + Glsa *models.Glsa + GlsaCount int64 + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + Glsa: glsa, + GlsaCount: glsaCount, + } +} + +func getFuncMap() template.FuncMap { + return template.FuncMap{ + "bugIsReady": BugIsReady, + "prevGLSA": PrevGLSA, + "nextGLSA": NextGLSA, + } +} + +func BugIsReady(bug bugzilla.Bug) bool { + return bug.IsReady() +} + +func PrevGLSA(id int64, min int64) int64 { + logger.Info.Println("prev glsa") + if id == min { + return id + } + return id - 1 +} + +func NextGLSA(id int64, max int64) int64 { + if id == max { + return id + } + return id + 1 +} diff --git a/pkg/app/handler/glsa/view.go b/pkg/app/handler/glsa/view.go new file mode 100644 index 0000000..d84273c --- /dev/null +++ b/pkg/app/handler/glsa/view.go @@ -0,0 +1,48 @@ +package glsa + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/models" + "net/http" + "strconv" +) + +// Show renders a template to show the landing page of the application +func View(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Glsa.View { + authentication.AccessDenied(w, r) + return + } + + glsaID := r.URL.Path[len("/glsa/"):] + + parsedGlsaId, _ := strconv.ParseInt(glsaID, 10, 64) + glsa := &models.Glsa{Id: parsedGlsaId} + err := user.CanAccess(connection.DB.Model(glsa). + Relation("Bugs"). + Relation("Creator"). + Relation("Comments").WherePK()). + Select() + + if err != nil { + http.NotFound(w, r) + return + } + + if glsa.Permission == "confidential" && user.Confidential() != "confidential" { + authentication.AccessDenied(w, r) + return + } + + glsa.ComputeStatus(user) + glsa.ComputeCommentBadges() + + glsaCount, err := connection.DB.Model((*models.Glsa)(nil)).Count() + + renderViewTemplate(w, user, glsa, int64(glsaCount)) +} diff --git a/pkg/app/handler/home/index.go b/pkg/app/handler/home/index.go new file mode 100644 index 0000000..8f514f7 --- /dev/null +++ b/pkg/app/handler/home/index.go @@ -0,0 +1,16 @@ +// Used to show the landing page of the application + +package home + +import ( + "glsamaker/pkg/app/handler/authentication/utils" + "net/http" +) + +// Show renders a template to show the landing page of the application +func Show(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + renderHomeTemplate(w, user) +} diff --git a/pkg/app/handler/home/utils.go b/pkg/app/handler/home/utils.go new file mode 100644 index 0000000..5f6e0b5 --- /dev/null +++ b/pkg/app/handler/home/utils.go @@ -0,0 +1,34 @@ +// miscellaneous utility functions used for the landing page of the application + +package home + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderHomeTemplate(w http.ResponseWriter, user *users.User) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/home/*.tmpl")) + + templates.ExecuteTemplate(w, "home.tmpl", createPageData("home", user)) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + } +} diff --git a/pkg/app/handler/newRequest/index.go b/pkg/app/handler/newRequest/index.go new file mode 100644 index 0000000..929ac5b --- /dev/null +++ b/pkg/app/handler/newRequest/index.go @@ -0,0 +1,186 @@ +// Used to show the landing page of the application + +package newRequest + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/app/handler/glsa" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "glsamaker/pkg/models/bugzilla" + "glsamaker/pkg/models/cve" + "crypto/sha256" + "fmt" + "github.com/go-pg/pg/v9" + "net/http" + "strconv" + "strings" + "time" +) + +// Show renders a template to show the landing page of the application +func Show(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Glsa.Create { + authentication.AccessDenied(w, r) + return + } + + bugs, title, synopsis, description, workaround, impact, background, resolution, importReferences, permissions, access, severity, keyword, comment, err := getParams(r) + newID := getNextGLSAId() + + if err != nil || bugs == "" { + // render without message + renderNewTemplate(w, user, strconv.FormatInt(newID, 10)) + return + } + + // create bugs + newBugs := bugzilla.GetBugsByIds(strings.Split(bugs, ",")) + + for _, newBug := range newBugs { + _, err = connection.DB.Model(&newBug).OnConflict("(id) DO UPDATE").Insert() + + if err != nil { + logger.Error.Println("Error creating bug") + logger.Error.Println(err) + } + + glsaToBug := &models.GlsaToBug{ + GlsaId: newID, + BugId: newBug.Id, + } + + connection.DB.Model(glsaToBug).Insert() + + } + + var references []models.Reference + + // TODO if title is empty try to import from bug + // TODO validate permissions + if importReferences { + // TODO import references + + // import from CVE + for _, bug := range strings.Split(bugs, ",") { + var cves []cve.DefCveItem + connection.DB.Model(&cves).Where("bugs::jsonb @> ?", "\""+bug+"\"").Select() + + for _, cve := range cves { + references = append(references, models.Reference{ + Title: cve.Id, + URL: "https://nvd.nist.gov/vuln/detail/" + cve.Id, + }) + } + + } + + // import from BUG + for _, bug := range newBugs { + for _, alias := range bug.Alias { + if strings.HasPrefix(alias, "CVE-") { + alreadyPresent := false + for _, reference := range references { + if reference.Title == alias { + alreadyPresent = true + } + } + if !alreadyPresent { + references = append(references, models.Reference{ + Title: alias, + URL: "https://nvd.nist.gov/vuln/detail/" + alias, + }) + } + } + } + } + + } + + id := title + bugs + time.Now().String() + id = fmt.Sprintf("%x", sha256.Sum256([]byte(id))) + + glsaType := "request" + if description != "" { + glsaType = "draft" + } + + newGlsa := &models.Glsa{ + //Id: id, + Type: glsaType, + Title: title, + Synopsis: synopsis, + Description: description, + Workaround: workaround, + Impact: impact, + Background: background, + Resolution: resolution, + References: references, + Permission: permissions, + Access: access, + Severity: severity, + Keyword: keyword, + Revision: "r0", + CreatorId: user.Id, + Created: time.Now(), + Updated: time.Now(), + } + + _, err = connection.DB.Model(newGlsa).OnConflict("(id) DO Nothing").Insert() + if err != nil { + logger.Error.Println("Err during creating new GLSA") + logger.Error.Println(err) + } + + if comment != "" { + glsa.AddNewCommment(strconv.FormatInt(newID, 10), user, comment, "comment") + } + + if glsaType == "draft" { + http.Redirect(w, r, "/drafts", 301) + } else { + http.Redirect(w, r, "/requests", 301) + } +} + +func getParams(r *http.Request) (string, string, string, string, string, string, string, string, bool, string, string, string, string, string, error) { + err := r.ParseForm() + if err != nil { + return "", "", "", "", "", "", "", "", false, "", "", "", "", "", err + } + bugs := r.Form.Get("bugs") + title := r.Form.Get("title") + synopsis := r.Form.Get("synopsis") + description := r.Form.Get("description") + workaround := r.Form.Get("workaround") + impact := r.Form.Get("impact") + background := r.Form.Get("background") + resolution := r.Form.Get("resolution") + importReferences := r.Form.Get("importReferences") + permissions := r.Form.Get("permissions") + access := r.Form.Get("access") + severity := r.Form.Get("severity") + keyword := r.Form.Get("keyword") + comment := r.Form.Get("comment") + return bugs, title, synopsis, description, workaround, impact, background, resolution, importReferences == "on", permissions, access, severity, keyword, comment, err +} + +func getNextGLSAId() int64 { + var newID int64 + newID = 1 + var glsas []*models.Glsa + err := connection.DB.Model(&glsas).Order("id DESC").Limit(1).Select() + + if err != nil && err != pg.ErrNoRows { + newID = -1 + } else if glsas != nil && len(glsas) == 1 { + newID = glsas[0].Id + 1 + } + + return newID +} diff --git a/pkg/app/handler/newRequest/utils.go b/pkg/app/handler/newRequest/utils.go new file mode 100644 index 0000000..7192939 --- /dev/null +++ b/pkg/app/handler/newRequest/utils.go @@ -0,0 +1,36 @@ +// miscellaneous utility functions used for the landing page of the application + +package newRequest + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderNewTemplate(w http.ResponseWriter, user *users.User, newID string) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/new/*.tmpl")) + + templates.ExecuteTemplate(w, "new.tmpl", createPageData("new", user, newID)) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User, newID string) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + NewID string + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + NewID: newID, + } +} diff --git a/pkg/app/handler/requests/index.go b/pkg/app/handler/requests/index.go new file mode 100644 index 0000000..fddbd86 --- /dev/null +++ b/pkg/app/handler/requests/index.go @@ -0,0 +1,44 @@ +// Used to show the landing page of the application + +package requests + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "net/http" +) + +// Show renders a template to show the landing page of the application +func Show(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Glsa.View { + authentication.AccessDenied(w, r) + return + } + + var requests []*models.Glsa + err := user.CanAccess(connection.DB.Model(&requests). + Where("type = ?", "request"). + Relation("Bugs"). + Relation("Creator"). + Relation("Comments")). + Select() + + if err != nil { + logger.Info.Println("Error during request selection") + logger.Info.Println(err) + http.NotFound(w, r) + return + } + + for _, request := range requests { + request.ComputeStatus(user) + } + + renderRequestsTemplate(w, user, requests) +} diff --git a/pkg/app/handler/requests/utils.go b/pkg/app/handler/requests/utils.go new file mode 100644 index 0000000..85bc472 --- /dev/null +++ b/pkg/app/handler/requests/utils.go @@ -0,0 +1,36 @@ +// miscellaneous utility functions used for the landing page of the application + +package requests + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderRequestsTemplate(w http.ResponseWriter, user *users.User, requests []*models.Glsa) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/requests/*.tmpl")) + + templates.ExecuteTemplate(w, "requests.tmpl", createPageData("requests", user, requests)) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User, requests []*models.Glsa) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + Requests []*models.Glsa + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + Requests: requests, + } +} diff --git a/pkg/app/handler/search/index.go b/pkg/app/handler/search/index.go new file mode 100644 index 0000000..1503d9c --- /dev/null +++ b/pkg/app/handler/search/index.go @@ -0,0 +1,126 @@ +// Used to show the landing page of the application + +package search + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "github.com/go-pg/pg/v9/orm" + "net/http" + "strconv" +) + +// Show renders a template to show the landing page of the application +func Search(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + keys, ok := r.URL.Query()["q"] + + if !ok || len(keys[0]) < 1 { + http.NotFound(w, r) + return + } + + // Query()["key"] will return an array of items, + // we only want the single item. + key := keys[0] + + // redirect to glsa if isNumeric + if _, err := strconv.Atoi(key); err == nil { + http.Redirect(w, r, "/glsa/"+key, 301) + } + + if key == "#home" { + http.Redirect(w, r, "/", 301) + return + } else if key == "#dashboard" { + http.Redirect(w, r, "/dashboard", 301) + return + } else if key == "#new" { + http.Redirect(w, r, "/new", 301) + return + } else if key == "#cvetool" { + http.Redirect(w, r, "/cve/tool", 301) + return + } else if key == "#requests" { + http.Redirect(w, r, "/requests", 301) + return + } else if key == "#drafts" { + http.Redirect(w, r, "/drafts", 301) + return + } else if key == "#all" { + http.Redirect(w, r, "/all", 301) + return + } else if key == "#archive" { + http.Redirect(w, r, "/archive", 301) + return + } else if key == "#about" { + http.Redirect(w, r, "/about", 301) + return + } else if key == "#bugzilla" { + http.Redirect(w, r, "https://bugs.gentoo.org/", 301) + return + } else if key == "#admin" { + http.Redirect(w, r, "/admin", 301) + return + } else if key == "#password" { + http.Redirect(w, r, "/account/password", 301) + return + } else if key == "#2fa" { + http.Redirect(w, r, "/account/2fa", 301) + return + } else if key == "#statistics" { + http.Redirect(w, r, "/statistics", 301) + return + } + + if key == "#logout" { + http.Redirect(w, r, "/logout", 301) + return + } + + if !user.Permissions.Glsa.View { + authentication.AccessDenied(w, r) + return + } + + var glsas []*models.Glsa + err := user.CanAccess(connection.DB.Model(&glsas). + Relation("Bugs"). + Relation("Comments"). + Relation("Creator"). + WhereGroup(func(q *orm.Query) (*orm.Query, error) { + q = q.WhereOr("title LIKE " + "'%" + key + "%'"). + WhereOr("type LIKE " + "'%" + key + "%'"). + WhereOr("synopsis LIKE " + "'%" + key + "%'"). + WhereOr("description LIKE " + "'%" + key + "%'"). + WhereOr("workaround LIKE " + "'%" + key + "%'"). + WhereOr("resolution LIKE " + "'%" + key + "%'"). + WhereOr("keyword LIKE " + "'%" + key + "%'"). + WhereOr("background LIKE " + "'%" + key + "%'") + //WhereOr("creator LIKE " + "'%" + key + "%'") + return q, nil + })). + Select() + + // TODO search in comments + // TODO search in bugs + + if err != nil { + logger.Info.Println("Error during searching") + logger.Info.Println(err) + http.NotFound(w, r) + return + } + + for _, glsa := range glsas { + glsa.ComputeStatus(user) + } + + renderSearchTemplate(w, user, key, glsas) + +} diff --git a/pkg/app/handler/search/utils.go b/pkg/app/handler/search/utils.go new file mode 100644 index 0000000..81e4e88 --- /dev/null +++ b/pkg/app/handler/search/utils.go @@ -0,0 +1,38 @@ +// miscellaneous utility functions used for the landing page of the application + +package search + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderSearchTemplate(w http.ResponseWriter, user *users.User, searchQuery string, searchResults []*models.Glsa) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/search/*.tmpl")) + + templates.ExecuteTemplate(w, "search.tmpl", createPageData("search", user, searchQuery, searchResults)) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User, searchQuery string, searchResults []*models.Glsa) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + GLSAs []*models.Glsa + SearchQuery string + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + GLSAs: searchResults, + SearchQuery: searchQuery, + } +} diff --git a/pkg/app/handler/statistics/index.go b/pkg/app/handler/statistics/index.go new file mode 100644 index 0000000..d4b3a4d --- /dev/null +++ b/pkg/app/handler/statistics/index.go @@ -0,0 +1,42 @@ +// Used to show the landing page of the application + +package statistics + +import ( + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/models" + "glsamaker/pkg/models/cve" + "net/http" +) + +// Show renders a template to show the landing page of the application +func Show(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + requests, _ := connection.DB.Model((*models.Glsa)(nil)).Where("type = ?", "request").Count() + drafts, _ := connection.DB.Model((*models.Glsa)(nil)).Where("type = ?", "draft").Count() + glsas, _ := connection.DB.Model((*models.Glsa)(nil)).Where("type = ?", "glsa").Count() + allGlsas, _ := connection.DB.Model((*models.Glsa)(nil)).Count() + + new, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Where("state = ?", "New").Count() + assigned, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Where("state = ?", "Assigned").Count() + nfu, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Where("state = ?", "NFU").Count() + later, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Where("state = ?", "Later").Count() + invalid, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Where("state = ?", "Invalid").Count() + allCVEs, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Count() + + statisticsData := StatisticsData{ + Requests: float64(requests) / float64(allGlsas), + Drafts: float64(drafts) / float64(allGlsas), + Glsas: float64(glsas) / float64(allGlsas), + New: float64(new) / float64(allCVEs), + Assigned: float64(assigned) / float64(allCVEs), + NFU: float64(nfu) / float64(allCVEs), + Later: float64(later) / float64(allCVEs), + Invalid: float64(invalid) / float64(allCVEs), + } + + renderStatisticsTemplate(w, user, &statisticsData) +} diff --git a/pkg/app/handler/statistics/utils.go b/pkg/app/handler/statistics/utils.go new file mode 100644 index 0000000..3e64ddf --- /dev/null +++ b/pkg/app/handler/statistics/utils.go @@ -0,0 +1,48 @@ +// miscellaneous utility functions used for the landing page of the application + +package statistics + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderStatisticsTemplate(w http.ResponseWriter, user *users.User, statisticsData *StatisticsData) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/statistics/*.tmpl")) + + templates.ExecuteTemplate(w, "statistics.tmpl", createPageData("statistics", user, statisticsData)) +} + +type StatisticsData struct { + Requests float64 + Drafts float64 + Glsas float64 + // CVEs + New float64 + Assigned float64 + NFU float64 + Later float64 + Invalid float64 +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User, statisticsData *StatisticsData) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + Data *StatisticsData + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + Data: statisticsData, + } +} diff --git a/pkg/app/serve.go b/pkg/app/serve.go new file mode 100644 index 0000000..1f16d9a --- /dev/null +++ b/pkg/app/serve.go @@ -0,0 +1,214 @@ +// Entrypoint for the web application + +package app + +import ( + "glsamaker/pkg/app/handler/about" + "glsamaker/pkg/app/handler/account" + "glsamaker/pkg/app/handler/admin" + "glsamaker/pkg/app/handler/all" + "glsamaker/pkg/app/handler/archive" + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/totp" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/app/handler/authentication/webauthn" + "glsamaker/pkg/app/handler/cvetool" + "glsamaker/pkg/app/handler/dashboard" + "glsamaker/pkg/app/handler/drafts" + "glsamaker/pkg/app/handler/glsa" + "glsamaker/pkg/app/handler/home" + "glsamaker/pkg/app/handler/newRequest" + "glsamaker/pkg/app/handler/requests" + "glsamaker/pkg/app/handler/search" + "glsamaker/pkg/app/handler/statistics" + "glsamaker/pkg/config" + "glsamaker/pkg/database" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "log" + "net/http" + "strings" +) + +// Serve is used to serve the web application +func Serve() { + + database.Connect() + defer connection.DB.Close() + + CreateDefaultAdmin() + models.SeedInitialApplicationData() + + // public login page + loginPage("/login", authentication.Login) + + // second factor login page + // (either totp or webauthn, depending on the user settings) + twoFactorLogin("/login/2fa", authentication.SecondFactorLogin) + + // webauthn login endpoints + twoFactorLogin("/login/2fa/totp", totp.Login) + + // webauthn login endpoints + twoFactorLogin("/login/2fa/webauthn/begin", webauthn.BeginLogin) + twoFactorLogin("/login/2fa/webauthn/finish", webauthn.FinishLogin) + + requireLogin("/", home.Show) + + requireLogin("/dashboard", dashboard.Show) + + requireLogin("/statistics", statistics.Show) + + requireLogin("/search", search.Search) + + requireLogin("/about", about.Show) + requireLogin("/about/search", about.ShowSearch) + requireLogin("/about/cli", about.ShowCLI) + + requireLogin("/archive", archive.Show) + + requireLogin("/drafts", drafts.Show) + + requireLogin("/requests", requests.Show) + + requireLogin("/all", all.Show) + + requireLogin("/new", newRequest.Show) + + requireLogin("/cve/update", cvetool.Update) + requireLogin("/cve/tool", cvetool.Show) + requireLogin("/cve/tool/fullscreen", cvetool.ShowFullscreen) + requireLogin("/cve/data", cvetool.CveData) + requireLogin("/cve/add", cvetool.Add) + requireLogin("/cve/comment/add", cvetool.AddComment) + requireLogin("/cve/bug/assign", cvetool.AssignBug) + requireLogin("/cve/state/change", cvetool.ChangeState) + + requireLogin("/logout", authentication.Logout) + + requireLogin("/account/password", account.ChangePassword) + requireLogin("/account/2fa", account.TwoFactorAuth) + requireLogin("/account/2fa/notice/disable", account.Disable2FANotice) + requireLogin("/account/2fa/totp/activate", account.ActivateTOTP) + requireLogin("/account/2fa/totp/disable", account.DisableTOTP) + requireLogin("/account/2fa/totp/verify", account.VerifyTOTP) + requireLogin("/account/2fa/webauthn/activate", account.ActivateWebAuthn) + requireLogin("/account/2fa/webauthn/disable", account.DisableWebAuthn) + requireLogin("/account/2fa/webauthn/register/begin", webauthn.BeginRegistration) + requireLogin("/account/2fa/webauthn/register/finish", webauthn.FinishRegistration) + + requireLogin("/glsa/", glsa.View) + requireLogin("/glsa/edit/", glsa.Edit) + requireLogin("/glsa/comment/add", glsa.AddComment) + requireLogin("/glsa/delete/", glsa.Delete) + requireLogin("/glsa/release/", glsa.Release) + requireLogin("/glsa/bugs/update", glsa.UpdateBugs) + + requireAdmin("/admin", admin.Show) + requireAdmin("/admin/", admin.Show) + requireAdmin("/admin/edit/users", admin.EditUsers) + requireAdmin("/admin/edit/permissions", admin.EditPermissions) + requireAdmin("/admin/edit/password/reset/", admin.ResetPassword) + + fs := http.StripPrefix("/assets/", http.FileServer(http.Dir("/go/src/glsamaker/assets"))) + requireLogin("/assets/", fs.ServeHTTP) + + logger.Info.Println("Serving on port " + config.Port()) + log.Fatal(http.ListenAndServe(":"+config.Port(), nil)) +} + +func loginPage(path string, handler http.HandlerFunc) { + http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + setDefaultHeaders(w) + + if utils.IsAuthenticated(w, r) { + http.Redirect(w, r, "/", 301) + } else if utils.Only2FAMissing(w, r) { + http.Redirect(w, r, "/login/2fa", 301) + } else { + handler(w, r) + } + }) +} + +func twoFactorLogin(path string, handler http.HandlerFunc) { + http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + setDefaultHeaders(w) + + if utils.IsAuthenticated(w, r) { + http.Redirect(w, r, "/", 301) + } else if utils.Only2FAMissing(w, r) { + handler(w, r) + } else { + http.Redirect(w, r, "/login", 301) + } + }) +} + +// define a route using the default middleware and the given handler +func requireLogin(path string, handler http.HandlerFunc) { + http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + setDefaultHeaders(w) + + if utils.IsAuthenticatedAndNeedsNewPassword(w, r) { + if strings.HasPrefix(path, "/logout") || + strings.HasPrefix(path, "/assets/") || + strings.HasPrefix(path, "/account/password") { + handler(w, r) + } else { + http.Redirect(w, r, "/account/password", 301) + } + } else if utils.IsAuthenticatedAndNeeds2FA(w, r) { + if strings.HasPrefix(path, "/logout") || + strings.HasPrefix(path, "/assets/") || + strings.HasPrefix(path, "/account/2fa") { + handler(w, r) + } else { + http.Redirect(w, r, "/account/2fa", 301) + } + } else if utils.IsAuthenticated(w, r) { + handler(w, r) + } else if utils.Only2FAMissing(w, r) { + http.Redirect(w, r, "/login/2fa", 301) + } else { + http.Redirect(w, r, "/login", 301) + } + }) +} + +// define a route using the default middleware and the given handler +func requireAdmin(path string, handler http.HandlerFunc) { + http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + setDefaultHeaders(w) + + if utils.IsAuthenticatedAndNeedsNewPassword(w, r) { + if strings.HasPrefix(path, "/logout") || + strings.HasPrefix(path, "/assets/") || + strings.HasPrefix(path, "/account/password") { + handler(w, r) + } else { + http.Redirect(w, r, "/account/password", 301) + } + } else if utils.IsAuthenticatedAndNeeds2FA(w, r) { + if strings.HasPrefix(path, "/logout") || + strings.HasPrefix(path, "/assets/") || + strings.HasPrefix(path, "/account/2fa") { + handler(w, r) + } else { + http.Redirect(w, r, "/account/2fa", 301) + } + } else if utils.IsAuthenticatedAsAdmin(w, r) { + handler(w, r) + } else if utils.IsAuthenticated(w, r) { + authentication.AccessDenied(w, r) + } else { + http.Redirect(w, r, "/login", 301) + } + }) +} + +// setDefaultHeaders sets the default headers that apply for all pages +func setDefaultHeaders(w http.ResponseWriter) { + w.Header().Set("Cache-Control", "no-store") +} diff --git a/pkg/app/utils.go b/pkg/app/utils.go new file mode 100644 index 0000000..9d66c13 --- /dev/null +++ b/pkg/app/utils.go @@ -0,0 +1,89 @@ +package app + +import ( + "glsamaker/pkg/app/handler/authentication/totp" + "glsamaker/pkg/config" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models/users" +) + +func defaultAdminPermissions() users.Permissions { + return users.Permissions{ + Glsa: users.GlsaPermissions{ + View: true, + UpdateBugs: true, + Comment: true, + Create: true, + Edit: true, + Approve: true, + ApproveOwnGlsa: true, + Decline: true, + Delete: true, + Release: true, + Confidential: true, + }, + CVETool: users.CVEToolPermissions{ + View: true, + UpdateCVEs: true, + Comment: true, + AddPackage: true, + ChangeState: true, + AssignBug: true, + CreateBug: true, + }, + Admin: users.AdminPermissions{ + View: true, + CreateTemplates: true, + ManageUsers: true, + GlobalSettings: true, + }, + } +} + +func CreateDefaultAdmin() { + + token, qrcode := totp.Generate(config.AdminEmail()) + + badge := users.Badge{ + Name: "admin", + Description: "Admin Account", + Color: "orange", + } + + passwordParameters := users.Argon2Parameters{ + Type: "argon2id", + Time: 1, + Memory: 64 * 1024, + Threads: 4, + KeyLen: 32, + } + passwordParameters.GenerateSalt(32) + passwordParameters.GeneratePassword(config.AdminInitialPassword()) + + defaultUser := &users.User{ + Email: config.AdminEmail(), + Password: passwordParameters, + Nick: "admin", + Name: "Admin Account", + Role: "admin", + ForcePasswordChange: false, + TOTPSecret: token, + TOTPQRCode: qrcode, + IsUsingTOTP: false, + WebauthnCredentials: nil, + IsUsingWebAuthn: false, + Show2FANotice: true, + Badge: badge, + Disabled: false, + ForcePasswordRotation: false, + Force2FA: false, + Permissions: defaultAdminPermissions(), + } + + _, err := connection.DB.Model(defaultUser).OnConflict("(email) DO Nothing").Insert() + if err != nil { + logger.Error.Println("Err during creating default admin user") + logger.Error.Println(err) + } +} |