summaryrefslogtreecommitdiff
path: root/pkg/app
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/app')
-rw-r--r--pkg/app/handler/about/index.go28
-rw-r--r--pkg/app/handler/about/utils.go58
-rw-r--r--pkg/app/handler/account/password.go109
-rw-r--r--pkg/app/handler/account/twofactor.go200
-rw-r--r--pkg/app/handler/admin/edit.go293
-rw-r--r--pkg/app/handler/admin/index.go27
-rw-r--r--pkg/app/handler/admin/passwordreset.go73
-rw-r--r--pkg/app/handler/admin/utils.go112
-rw-r--r--pkg/app/handler/all/index.go40
-rw-r--r--pkg/app/handler/all/utils.go36
-rw-r--r--pkg/app/handler/archive/index.go44
-rw-r--r--pkg/app/handler/archive/utils.go36
-rw-r--r--pkg/app/handler/authentication/accessDenied.go10
-rw-r--r--pkg/app/handler/authentication/auth_session/authsession.go177
-rw-r--r--pkg/app/handler/authentication/login.go100
-rw-r--r--pkg/app/handler/authentication/logout.go29
-rw-r--r--pkg/app/handler/authentication/templates/admin.go36
-rw-r--r--pkg/app/handler/authentication/templates/login.go32
-rw-r--r--pkg/app/handler/authentication/templates/totp.go23
-rw-r--r--pkg/app/handler/authentication/templates/webauthn.go23
-rw-r--r--pkg/app/handler/authentication/totp/totp.go60
-rw-r--r--pkg/app/handler/authentication/utils/utils.go81
-rw-r--r--pkg/app/handler/authentication/webauthn/login.go118
-rw-r--r--pkg/app/handler/authentication/webauthn/register.go111
-rw-r--r--pkg/app/handler/cvetool/bug.go85
-rw-r--r--pkg/app/handler/cvetool/comments.go74
-rw-r--r--pkg/app/handler/cvetool/index.go169
-rw-r--r--pkg/app/handler/cvetool/state.go75
-rw-r--r--pkg/app/handler/cvetool/update.go23
-rw-r--r--pkg/app/handler/cvetool/utils.go47
-rw-r--r--pkg/app/handler/dashboard/index.go58
-rw-r--r--pkg/app/handler/dashboard/utils.go44
-rw-r--r--pkg/app/handler/drafts/index.go44
-rw-r--r--pkg/app/handler/drafts/utils.go37
-rw-r--r--pkg/app/handler/glsa/bugs.go63
-rw-r--r--pkg/app/handler/glsa/comments.go110
-rw-r--r--pkg/app/handler/glsa/delete.go42
-rw-r--r--pkg/app/handler/glsa/edit.go185
-rw-r--r--pkg/app/handler/glsa/release.go70
-rw-r--r--pkg/app/handler/glsa/utils.go79
-rw-r--r--pkg/app/handler/glsa/view.go48
-rw-r--r--pkg/app/handler/home/index.go16
-rw-r--r--pkg/app/handler/home/utils.go34
-rw-r--r--pkg/app/handler/newRequest/index.go186
-rw-r--r--pkg/app/handler/newRequest/utils.go36
-rw-r--r--pkg/app/handler/requests/index.go44
-rw-r--r--pkg/app/handler/requests/utils.go36
-rw-r--r--pkg/app/handler/search/index.go126
-rw-r--r--pkg/app/handler/search/utils.go38
-rw-r--r--pkg/app/handler/statistics/index.go42
-rw-r--r--pkg/app/handler/statistics/utils.go48
-rw-r--r--pkg/app/serve.go214
-rw-r--r--pkg/app/utils.go89
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)
+ }
+}