From ba8ceb81daa18d99f18a8baedcfd2e7f64a93a80 Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Tue, 19 Feb 2019 20:33:50 -0600 Subject: [PATCH] Initial commit of RBAC support. (#1366) * Initial commit of RBAC support. Closes #1333 --- controllers/api.go | 2 +- controllers/route.go | 25 +++-- .../migrations/20190105192341_0.8.0_rbac.sql | 85 ++++++++++++++ .../migrations/20190105192341_0.8.0_rbac.sql | 77 +++++++++++++ middleware/middleware.go | 47 +++++++- middleware/middleware_test.go | 104 ++++++++++++++++++ models/maillog_test.go | 5 +- models/models.go | 7 ++ models/rbac.go | 88 +++++++++++++++ models/rbac_test.go | 57 ++++++++++ models/user.go | 8 +- static/js/dist/app/gophish.min.js | 2 +- static/js/src/app/gophish.js | 9 ++ templates/base.html | 7 +- templates/campaign_results.html | 38 ------- templates/campaigns.html | 38 ------- templates/dashboard.html | 38 ------- templates/landing_pages.html | 30 ----- templates/nav.html | 39 +++++++ templates/register.html | 12 +- templates/sending_profiles.html | 30 ----- templates/settings.html | 32 +----- templates/templates.html | 30 ----- templates/users.html | 38 ------- 24 files changed, 549 insertions(+), 299 deletions(-) create mode 100644 db/db_mysql/migrations/20190105192341_0.8.0_rbac.sql create mode 100644 db/db_sqlite3/migrations/20190105192341_0.8.0_rbac.sql create mode 100644 middleware/middleware_test.go create mode 100644 models/rbac.go create mode 100644 models/rbac_test.go create mode 100644 templates/nav.html diff --git a/controllers/api.go b/controllers/api.go index 153c514f..a4341355 100644 --- a/controllers/api.go +++ b/controllers/api.go @@ -23,7 +23,7 @@ import ( "github.com/sirupsen/logrus" ) -// APIReset (/api/reset) resets a user's API key +// APIReset (/api/reset) resets the currently authenticated user's API key func (as *AdminServer) APIReset(w http.ResponseWriter, r *http.Request) { switch { case r.Method == "POST": diff --git a/controllers/route.go b/controllers/route.go index b71f4e0d..9adc3e4f 100644 --- a/controllers/route.go +++ b/controllers/route.go @@ -103,12 +103,13 @@ func (as *AdminServer) registerRoutes() { router.HandleFunc("/users", Use(as.Users, mid.RequireLogin)) router.HandleFunc("/landing_pages", Use(as.LandingPages, mid.RequireLogin)) router.HandleFunc("/sending_profiles", Use(as.SendingProfiles, mid.RequireLogin)) - router.HandleFunc("/register", Use(as.Register, mid.RequireLogin)) router.HandleFunc("/settings", Use(as.Settings, mid.RequireLogin)) + router.HandleFunc("/register", Use(as.Register, mid.RequireLogin, mid.RequirePermission(models.PermissionModifySystem))) // Create the API routes api := router.PathPrefix("/api").Subrouter() api = api.StrictSlash(true) api.Use(mid.RequireAPIKey) + api.Use(mid.EnforceViewOnly) api.HandleFunc("/reset", as.APIReset) api.HandleFunc("/campaigns/", as.APICampaigns) api.HandleFunc("/campaigns/summary", as.APICampaignsSummary) @@ -160,20 +161,24 @@ func Use(handler http.HandlerFunc, mid ...func(http.Handler) http.HandlerFunc) h } type templateParams struct { - Title string - Flashes []interface{} - User models.User - Token string - Version string + Title string + Flashes []interface{} + User models.User + Token string + Version string + ModifySystem bool } // newTemplateParams returns the default template parameters for a user and // the CSRF token. func newTemplateParams(r *http.Request) templateParams { + user := ctx.Get(r, "user").(models.User) + modifySystem, _ := user.HasPermission(models.PermissionModifySystem) return templateParams{ - Token: csrf.Token(r), - User: ctx.Get(r, "user").(models.User), - Version: config.Version, + Token: csrf.Token(r), + User: user, + ModifySystem: modifySystem, + Version: config.Version, } } @@ -354,7 +359,7 @@ func (as *AdminServer) Logout(w http.ResponseWriter, r *http.Request) { func getTemplate(w http.ResponseWriter, tmpl string) *template.Template { templates := template.New("template") - _, err := templates.ParseFiles("templates/base.html", "templates/"+tmpl+".html", "templates/flashes.html") + _, err := templates.ParseFiles("templates/base.html", "templates/nav.html", "templates/"+tmpl+".html", "templates/flashes.html") if err != nil { log.Error(err) } diff --git a/db/db_mysql/migrations/20190105192341_0.8.0_rbac.sql b/db/db_mysql/migrations/20190105192341_0.8.0_rbac.sql new file mode 100644 index 00000000..54d0536c --- /dev/null +++ b/db/db_mysql/migrations/20190105192341_0.8.0_rbac.sql @@ -0,0 +1,85 @@ + +-- +goose Up +-- SQL in section 'Up' is executed when this migration is applied +CREATE TABLE IF NOT EXISTS `roles` ( + `id` INTEGER PRIMARY KEY AUTO_INCREMENT, + `slug` VARCHAR(255) NOT NULL UNIQUE, + `name` VARCHAR(255) NOT NULL UNIQUE, + `description` VARCHAR(255) +); + +ALTER TABLE `users` ADD COLUMN `role_id` INTEGER; + +CREATE TABLE IF NOT EXISTS `permissions` ( + `id` INTEGER PRIMARY KEY AUTO_INCREMENT, + `slug` VARCHAR(255) NOT NULL UNIQUE, + `name` VARCHAR(255) NOT NULL UNIQUE, + `description` VARCHAR(255) +); + + +CREATE TABLE IF NOT EXISTS `role_permissions` ( + `role_id` INTEGER NOT NULL, + `permission_id` INTEGER NOT NULL +); + +INSERT INTO `roles` (`slug`, `name`, `description`) +VALUES + ("admin", "Admin", "System administrator with full permissions"), + ("user", "User", "User role with edit access to objects and campaigns"); + +INSERT INTO `permissions` (`slug`, `name`, `description`) +VALUES + ("view_objects", "View Objects", "View objects in Gophish"), + ("modify_objects", "Modify Objects", "Create and edit objects in Gophish"), + ("modify_system", "Modify System", "Manage system-wide configuration"); + +-- Our rules for generating the admin user are: +-- * The user with the name `admin` +-- * OR the first user, if no `admin` user exists +-- MySQL apparently makes these queries gross. Thanks MySQL. +UPDATE `users` SET `role_id`=( + SELECT `id` FROM `roles` WHERE `slug`="admin") +WHERE `id`=( + SELECT `id` FROM ( + SELECT * FROM `users` + ) as u WHERE `username`="admin" + OR `id`=( + SELECT MIN(`id`) FROM ( + SELECT * FROM `users` + ) as u + ) LIMIT 1); + +-- Every other user will be considered a standard user account. The admin user +-- will be able to change the role of any other user at any time. +UPDATE `users` SET `role_id`=( + SELECT `id` FROM `roles` AS role_id WHERE `slug`="user") +WHERE role_id IS NULL; + +-- Our default permission set will: +-- * Allow admins the ability to do anything +-- * Allow users to modify objects + +-- Allow any user to view objects +INSERT INTO `role_permissions` (`role_id`, `permission_id`) +SELECT r.id, p.id FROM roles AS r, `permissions` AS p +WHERE r.id IN (SELECT `id` FROM roles WHERE `slug`="admin" OR `slug`="user") +AND p.id=(SELECT `id` FROM `permissions` WHERE `slug`="view_objects"); + +-- Allow admins and users to modify objects +INSERT INTO `role_permissions` (`role_id`, `permission_id`) +SELECT r.id, p.id FROM roles AS r, `permissions` AS p +WHERE r.id IN (SELECT `id` FROM roles WHERE `slug`="admin" OR `slug`="user") +AND p.id=(SELECT `id` FROM `permissions` WHERE `slug`="modify_objects"); + +-- Allow admins to modify system level configuration +INSERT INTO `role_permissions` (`role_id`, `permission_id`) +SELECT r.id, p.id FROM roles AS r, `permissions` AS p +WHERE r.id IN (SELECT `id` FROM roles WHERE `slug`="admin") +AND p.id=(SELECT `id` FROM `permissions` WHERE `slug`="modify_system"); + +-- +goose Down +-- SQL section 'Down' is executed when this migration is rolled back +DROP TABLE `roles` +DROP TABLE `user_roles` +DROP TABLE `permissions` diff --git a/db/db_sqlite3/migrations/20190105192341_0.8.0_rbac.sql b/db/db_sqlite3/migrations/20190105192341_0.8.0_rbac.sql new file mode 100644 index 00000000..f926e438 --- /dev/null +++ b/db/db_sqlite3/migrations/20190105192341_0.8.0_rbac.sql @@ -0,0 +1,77 @@ + +-- +goose Up +-- SQL in section 'Up' is executed when this migration is applied +CREATE TABLE "roles" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "slug" VARCHAR(255) NOT NULL UNIQUE, + "name" VARCHAR(255) NOT NULL UNIQUE, + "description" VARCHAR(255) +); + +ALTER TABLE "users" ADD COLUMN "role_id" INTEGER; + +CREATE TABLE "permissions" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "slug" VARCHAR(255) NOT NULL UNIQUE, + "name" VARCHAR(255) NOT NULL UNIQUE, + "description" VARCHAR(255) +); + + +CREATE TABLE "role_permissions" ( + "role_id" INTEGER NOT NULL, + "permission_id" INTEGER NOT NULL +); + +INSERT INTO "roles" ("slug", "name", "description") +VALUES + ("admin", "Admin", "System administrator with full permissions"), + ("user", "User", "User role with edit access to objects and campaigns"); + +INSERT INTO "permissions" ("slug", "name", "description") +VALUES + ("view_objects", "View Objects", "View objects in Gophish"), + ("modify_objects", "Modify Objects", "Create and edit objects in Gophish"), + ("modify_system", "Modify System", "Manage system-wide configuration"); + +-- Our rules for generating the admin user are: +-- * The user with the name "admin" +-- * OR the first user, if no "admin" user exists +UPDATE "users" SET "role_id"=( + SELECT "id" FROM "roles" WHERE "slug"="admin") +WHERE "id"=( + SELECT "id" FROM "users" WHERE "username"="admin" OR "id"=(SELECT MIN("id") FROM "users") LIMIT 1); + +-- Every other user will be considered a standard user account. The admin user +-- will be able to change the role of any other user at any time. +UPDATE "users" SET "role_id"=( + SELECT "id" FROM "roles" WHERE "slug"="user") +WHERE role_id IS NULL; + +-- Our default permission set will: +-- * Allow admins the ability to do anything +-- * Allow users to modify objects + +-- Allow any user to view objects +INSERT INTO "role_permissions" ("role_id", "permission_id") +SELECT r.id, p.id FROM roles AS r, "permissions" AS p +WHERE r.id IN (SELECT "id" FROM roles WHERE "slug"="admin" OR "slug"="user") +AND p.id=(SELECT "id" FROM "permissions" WHERE "slug"="view_objects"); + +-- Allow admins and users to modify objects +INSERT INTO "role_permissions" ("role_id", "permission_id") +SELECT r.id, p.id FROM roles AS r, "permissions" AS p +WHERE r.id IN (SELECT "id" FROM roles WHERE "slug"="admin" OR "slug"="user") +AND p.id=(SELECT "id" FROM "permissions" WHERE "slug"="modify_objects"); + +-- Allow admins to modify system level configuration +INSERT INTO "role_permissions" ("role_id", "permission_id") +SELECT r.id, p.id FROM roles AS r, "permissions" AS p +WHERE r.id IN (SELECT "id" FROM roles WHERE "slug"="admin") +AND p.id=(SELECT "id" FROM "permissions" WHERE "slug"="modify_system"); + +-- +goose Down +-- SQL section 'Down' is executed when this migration is rolled back +DROP TABLE "roles" +DROP TABLE "user_roles" +DROP TABLE "permissions" diff --git a/middleware/middleware.go b/middleware/middleware.go index 1e5455b1..281aa6be 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -94,13 +94,14 @@ func RequireAPIKey(handler http.Handler) http.Handler { JSONError(w, http.StatusUnauthorized, "Invalid API Key") return } + r = ctx.Set(r, "user", u) r = ctx.Set(r, "user_id", u.Id) r = ctx.Set(r, "api_key", ak) handler.ServeHTTP(w, r) }) } -// RequireLogin is a simple middleware which checks to see if the user is currently logged in. +// RequireLogin checks to see if the user is currently logged in. // If not, the function returns a 302 redirect to the login page. func RequireLogin(handler http.Handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -114,6 +115,50 @@ func RequireLogin(handler http.Handler) http.HandlerFunc { } } +// EnforceViewOnly is a global middleware that limits the ability to edit +// objects to accounts with the PermissionModifyObjects permission. +func EnforceViewOnly(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // If the request is for any non-GET HTTP method, e.g. POST, PUT, + // or DELETE, we need to ensure the user has the appropriate + // permission. + if r.Method != http.MethodGet && r.Method != http.MethodHead && r.Method != http.MethodOptions { + user := ctx.Get(r, "user").(models.User) + access, err := user.HasPermission(models.PermissionModifyObjects) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + if !access { + http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) + return + } + } + next.ServeHTTP(w, r) + }) +} + +// RequirePermission checks to see if the user has the requested permission +// before executing the handler. If the request is unauthorized, a JSONError +// is returned. +func RequirePermission(perm string) func(http.Handler) http.HandlerFunc { + return func(next http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + user := ctx.Get(r, "user").(models.User) + access, err := user.HasPermission(perm) + if err != nil { + JSONError(w, http.StatusInternalServerError, err.Error()) + return + } + if !access { + JSONError(w, http.StatusForbidden, http.StatusText(http.StatusForbidden)) + return + } + next.ServeHTTP(w, r) + } + } +} + // JSONError returns an error in JSON format with the given // status code and message func JSONError(w http.ResponseWriter, c int, m string) { diff --git a/middleware/middleware_test.go b/middleware/middleware_test.go new file mode 100644 index 00000000..b17b6351 --- /dev/null +++ b/middleware/middleware_test.go @@ -0,0 +1,104 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gophish/gophish/config" + ctx "github.com/gophish/gophish/context" + "github.com/gophish/gophish/models" + "github.com/stretchr/testify/suite" +) + +var successHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("success")) +}) + +type MiddlewareSuite struct { + suite.Suite +} + +func (s *MiddlewareSuite) SetupSuite() { + conf := &config.Config{ + DBName: "sqlite3", + DBPath: ":memory:", + MigrationsPath: "../db/db_sqlite3/migrations/", + } + err := models.Setup(conf) + if err != nil { + s.T().Fatalf("Failed creating database: %v", err) + } +} + +// MiddlewarePermissionTest maps an expected HTTP Method to an expected HTTP +// status code +type MiddlewarePermissionTest map[string]int + +// TestEnforceViewOnly ensures that only users with the ModifyObjects +// permission have the ability to send non-GET requests. +func (s *MiddlewareSuite) TestEnforceViewOnly() { + permissionTests := map[string]MiddlewarePermissionTest{ + models.RoleAdmin: MiddlewarePermissionTest{ + http.MethodGet: http.StatusOK, + http.MethodHead: http.StatusOK, + http.MethodOptions: http.StatusOK, + http.MethodPost: http.StatusOK, + http.MethodPut: http.StatusOK, + http.MethodDelete: http.StatusOK, + }, + models.RoleUser: MiddlewarePermissionTest{ + http.MethodGet: http.StatusOK, + http.MethodHead: http.StatusOK, + http.MethodOptions: http.StatusOK, + http.MethodPost: http.StatusOK, + http.MethodPut: http.StatusOK, + http.MethodDelete: http.StatusOK, + }, + } + for r, checks := range permissionTests { + role, err := models.GetRoleBySlug(r) + s.Nil(err) + + for method, expected := range checks { + req := httptest.NewRequest(method, "/", nil) + response := httptest.NewRecorder() + + req = ctx.Set(req, "user", models.User{ + Role: role, + RoleID: role.ID, + }) + + EnforceViewOnly(successHandler).ServeHTTP(response, req) + s.Equal(response.Code, expected) + } + } +} + +func (s *MiddlewareSuite) TestRequirePermission() { + middleware := RequirePermission(models.PermissionModifySystem) + handler := middleware(successHandler) + + permissionTests := map[string]int{ + models.RoleUser: http.StatusForbidden, + models.RoleAdmin: http.StatusOK, + } + + for role, expected := range permissionTests { + req := httptest.NewRequest(http.MethodGet, "/", nil) + response := httptest.NewRecorder() + // Test that with the requested permission, the request succeeds + role, err := models.GetRoleBySlug(role) + s.Nil(err) + req = ctx.Set(req, "user", models.User{ + Role: role, + RoleID: role.ID, + }) + handler.ServeHTTP(response, req) + s.Equal(response.Code, expected) + } +} + +func TestMiddlewareSuite(t *testing.T) { + suite.Run(t, new(MiddlewareSuite)) +} diff --git a/models/maillog_test.go b/models/maillog_test.go index 5bce9ce0..cab6a126 100644 --- a/models/maillog_test.go +++ b/models/maillog_test.go @@ -192,9 +192,8 @@ func (s *ModelsSuite) TestMailLogSuccess(ch *check.C) { func (s *ModelsSuite) TestGenerateMailLog(ch *check.C) { campaign := Campaign{ - Id: 1, - UserId: 1, - LaunchDate: time.Now().UTC(), + Id: 1, + UserId: 1, } result := Result{ RId: "abc1234", diff --git a/models/models.go b/models/models.go index d3b7fbfc..3b1c82f9 100644 --- a/models/models.go +++ b/models/models.go @@ -111,10 +111,17 @@ func Setup(c *config.Config) error { // Create the admin user if it doesn't exist var userCount int64 db.Model(&User{}).Count(&userCount) + adminRole, err := GetRoleBySlug(RoleAdmin) + if err != nil { + log.Error(err) + return err + } if userCount == 0 { initUser := User{ Username: "admin", Hash: "$2a$10$IYkPp0.QsM81lYYPrQx6W.U6oQGw7wMpozrKhKAHUBVL4mkm/EvAS", //gophish + Role: adminRole, + RoleID: adminRole.ID, } initUser.ApiKey = generateSecureKey() err = db.Save(&initUser).Error diff --git a/models/rbac.go b/models/rbac.go new file mode 100644 index 00000000..fc1139fd --- /dev/null +++ b/models/rbac.go @@ -0,0 +1,88 @@ +package models + +/* +Design: + +Gophish implements simple Role-Based-Access-Control (RBAC) to control access to +certain resources. + +By default, Gophish has two separate roles, with each user being assigned to +a single role: + +* Admin - Can modify all objects as well as system-level configuration +* User - Can modify all objects + +It's important to note that these are global roles. In the future, we'll likely +add the concept of teams, which will include their own roles and permission +system similar to these global permissions. + +Each role maps to one or more permissions, making it easy to add more granular +permissions over time. + +This is supported through a simple API on a user object, +`HasPermission(Permission)`, which returns a boolean and an error. +This API checks the role associated with the user to see if that role has the +requested permission. +*/ + +const ( + // RoleAdmin is used for Gophish system administrators. Users with this + // role have the ability to manage all objects within Gophish, as well as + // system-level configuration, such as users and URLs. + RoleAdmin = "admin" + // RoleUser is used for standard Gophish users. Users with this role can + // create, manage, and view Gophish objects and campaigns. + RoleUser = "user" + + // PermissionViewObjects determines if a role can view standard Gophish + // objects such as campaigns, groups, landing pages, etc. + PermissionViewObjects = "view_objects" + // PermissionModifyObjects determines if a role can create and modify + // standard Gophish objects. + PermissionModifyObjects = "modify_objects" + // PermissionModifySystem determines if a role can manage system-level + // configuration. + PermissionModifySystem = "modify_system" +) + +// Role represents a user role within Gophish. Each user has a single role +// which maps to a set of permissions. +type Role struct { + ID int64 `json:"id"` + Slug string `json:"slug"` + Name string `json:"name"` + Description string `json:"description"` + Permissions []Permission `json:"-" gorm:"many2many:role_permissions;"` +} + +// Permission determines what a particular role can do. Each role may have one +// or more permissions. +type Permission struct { + ID int64 `json:"id"` + Slug string `json:"slug"` + Name string `json:"name"` + Description string `json:"description"` +} + +// GetRoleBySlug returns a role that can be assigned to a user. +func GetRoleBySlug(slug string) (Role, error) { + role := Role{} + err := db.Where("slug=?", slug).First(&role).Error + return role, err +} + +// HasPermission checks to see if the user has a role with the requested +// permission. +func (u *User) HasPermission(slug string) (bool, error) { + perm := []Permission{} + err := db.Model(Role{ID: u.RoleID}).Where("slug=?", slug).Association("Permissions").Find(&perm).Error + if err != nil { + return false, err + } + // Gorm doesn't return an ErrRecordNotFound whe scanning into a slice, so + // we need to check the length (ref jinzhu/gorm#228) + if len(perm) == 0 { + return false, nil + } + return true, nil +} diff --git a/models/rbac_test.go b/models/rbac_test.go new file mode 100644 index 00000000..0ec2b243 --- /dev/null +++ b/models/rbac_test.go @@ -0,0 +1,57 @@ +package models + +import ( + "fmt" + + check "gopkg.in/check.v1" +) + +type PermissionCheck map[string]bool + +func (s *ModelsSuite) TestHasPermission(c *check.C) { + + permissionTests := map[string]PermissionCheck{ + RoleAdmin: PermissionCheck{ + PermissionModifySystem: true, + PermissionModifyObjects: true, + PermissionViewObjects: true, + }, + RoleUser: PermissionCheck{ + PermissionModifySystem: false, + PermissionModifyObjects: true, + PermissionViewObjects: true, + }, + } + + for r, checks := range permissionTests { + // Create the user with the provided role + role, err := GetRoleBySlug(r) + c.Assert(err, check.Equals, nil) + user := User{ + Username: fmt.Sprintf("test-%s", r), + Hash: "12345", + ApiKey: fmt.Sprintf("%s-key", r), + RoleID: role.ID, + } + PutUser(&user) + + // Perform the permission checks + for permission, expected := range checks { + access, err := user.HasPermission(permission) + fmt.Printf("Checking %s -> %s\n", r, permission) + c.Assert(err, check.Equals, nil) + c.Assert(access, check.Equals, expected) + } + } +} + +func (s *ModelsSuite) TestGetRoleBySlug(c *check.C) { + roles := []string{RoleAdmin, RoleUser} + for _, role := range roles { + got, err := GetRoleBySlug(role) + c.Assert(err, check.Equals, nil) + c.Assert(got.Slug, check.Equals, role) + } + _, err := GetRoleBySlug("bogus") + c.Assert(err, check.NotNil) +} diff --git a/models/user.go b/models/user.go index fe8761de..9ce92bbc 100644 --- a/models/user.go +++ b/models/user.go @@ -6,13 +6,15 @@ type User struct { Username string `json:"username" sql:"not null;unique"` Hash string `json:"-"` ApiKey string `json:"api_key" sql:"not null;unique"` + Role Role `json:"role" gorm:"association_autoupdate:false;association_autocreate:false"` + RoleID int64 `json:"-"` } // GetUser returns the user that the given id corresponds to. If no user is found, an // error is thrown. func GetUser(id int64) (User, error) { u := User{} - err := db.Where("id=?", id).First(&u).Error + err := db.Preload("Role").Where("id=?", id).First(&u).Error return u, err } @@ -20,7 +22,7 @@ func GetUser(id int64) (User, error) { // error is thrown. func GetUserByAPIKey(key string) (User, error) { u := User{} - err := db.Where("api_key = ?", key).First(&u).Error + err := db.Preload("Role").Where("api_key = ?", key).First(&u).Error return u, err } @@ -28,7 +30,7 @@ func GetUserByAPIKey(key string) (User, error) { // error is thrown. func GetUserByUsername(username string) (User, error) { u := User{} - err := db.Where("username = ?", username).First(&u).Error + err := db.Preload("Role").Where("username = ?", username).First(&u).Error return u, err } diff --git a/static/js/dist/app/gophish.min.js b/static/js/dist/app/gophish.min.js index be8ce70d..d8b9cb99 100644 --- a/static/js/dist/app/gophish.min.js +++ b/static/js/dist/app/gophish.min.js @@ -1 +1 @@ -function errorFlash(e){$("#flashes").empty(),$("#flashes").append('
'+e+"
")}function successFlash(e){$("#flashes").empty(),$("#flashes").append('
'+e+"
")}function modalError(e){$("#modal\\.flashes").empty().append('
'+e+"
")}function query(e,t,n,r){return $.ajax({url:"/api"+e+"?api_key="+user.api_key,async:r,method:t,data:JSON.stringify(n),dataType:"json",contentType:"application/json"})}function escapeHtml(e){return $("
").text(e).html()}function unescapeHtml(e){return $("
").html(e).text()}var capitalize=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},api={campaigns:{get:function(){return query("/campaigns/","GET",{},!1)},post:function(e){return query("/campaigns/","POST",e,!1)},summary:function(){return query("/campaigns/summary","GET",{},!1)}},campaignId:{get:function(e){return query("/campaigns/"+e,"GET",{},!0)},delete:function(e){return query("/campaigns/"+e,"DELETE",{},!1)},results:function(e){return query("/campaigns/"+e+"/results","GET",{},!0)},complete:function(e){return query("/campaigns/"+e+"/complete","GET",{},!0)},summary:function(e){return query("/campaigns/"+e+"/summary","GET",{},!0)}},groups:{get:function(){return query("/groups/","GET",{},!1)},post:function(e){return query("/groups/","POST",e,!1)},summary:function(){return query("/groups/summary","GET",{},!0)}},groupId:{get:function(e){return query("/groups/"+e,"GET",{},!1)},put:function(e){return query("/groups/"+e.id,"PUT",e,!1)},delete:function(e){return query("/groups/"+e,"DELETE",{},!1)}},templates:{get:function(){return query("/templates/","GET",{},!1)},post:function(e){return query("/templates/","POST",e,!1)}},templateId:{get:function(e){return query("/templates/"+e,"GET",{},!1)},put:function(e){return query("/templates/"+e.id,"PUT",e,!1)},delete:function(e){return query("/templates/"+e,"DELETE",{},!1)}},pages:{get:function(){return query("/pages/","GET",{},!1)},post:function(e){return query("/pages/","POST",e,!1)}},pageId:{get:function(e){return query("/pages/"+e,"GET",{},!1)},put:function(e){return query("/pages/"+e.id,"PUT",e,!1)},delete:function(e){return query("/pages/"+e,"DELETE",{},!1)}},SMTP:{get:function(){return query("/smtp/","GET",{},!1)},post:function(e){return query("/smtp/","POST",e,!1)}},SMTPId:{get:function(e){return query("/smtp/"+e,"GET",{},!1)},put:function(e){return query("/smtp/"+e.id,"PUT",e,!1)},delete:function(e){return query("/smtp/"+e,"DELETE",{},!1)}},import_email:function(e){return query("/import/email","POST",e,!1)},clone_site:function(e){return query("/import/site","POST",e,!1)},send_test_email:function(e){return query("/util/send_test_email","POST",e,!0)},reset:function(){return query("/reset","POST",{},!0)}};$(document).ready(function(){$.fn.dataTable.moment("MMMM Do YYYY, h:mm:ss a"),$('[data-toggle="tooltip"]').tooltip()}); \ No newline at end of file +function errorFlash(e){$("#flashes").empty(),$("#flashes").append('
'+e+"
")}function successFlash(e){$("#flashes").empty(),$("#flashes").append('
'+e+"
")}function modalError(e){$("#modal\\.flashes").empty().append('
'+e+"
")}function query(e,t,n,r){return $.ajax({url:"/api"+e+"?api_key="+user.api_key,async:r,method:t,data:JSON.stringify(n),dataType:"json",contentType:"application/json"})}function escapeHtml(e){return $("
").text(e).html()}function unescapeHtml(e){return $("
").html(e).text()}var capitalize=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},api={campaigns:{get:function(){return query("/campaigns/","GET",{},!1)},post:function(e){return query("/campaigns/","POST",e,!1)},summary:function(){return query("/campaigns/summary","GET",{},!1)}},campaignId:{get:function(e){return query("/campaigns/"+e,"GET",{},!0)},delete:function(e){return query("/campaigns/"+e,"DELETE",{},!1)},results:function(e){return query("/campaigns/"+e+"/results","GET",{},!0)},complete:function(e){return query("/campaigns/"+e+"/complete","GET",{},!0)},summary:function(e){return query("/campaigns/"+e+"/summary","GET",{},!0)}},groups:{get:function(){return query("/groups/","GET",{},!1)},post:function(e){return query("/groups/","POST",e,!1)},summary:function(){return query("/groups/summary","GET",{},!0)}},groupId:{get:function(e){return query("/groups/"+e,"GET",{},!1)},put:function(e){return query("/groups/"+e.id,"PUT",e,!1)},delete:function(e){return query("/groups/"+e,"DELETE",{},!1)}},templates:{get:function(){return query("/templates/","GET",{},!1)},post:function(e){return query("/templates/","POST",e,!1)}},templateId:{get:function(e){return query("/templates/"+e,"GET",{},!1)},put:function(e){return query("/templates/"+e.id,"PUT",e,!1)},delete:function(e){return query("/templates/"+e,"DELETE",{},!1)}},pages:{get:function(){return query("/pages/","GET",{},!1)},post:function(e){return query("/pages/","POST",e,!1)}},pageId:{get:function(e){return query("/pages/"+e,"GET",{},!1)},put:function(e){return query("/pages/"+e.id,"PUT",e,!1)},delete:function(e){return query("/pages/"+e,"DELETE",{},!1)}},SMTP:{get:function(){return query("/smtp/","GET",{},!1)},post:function(e){return query("/smtp/","POST",e,!1)}},SMTPId:{get:function(e){return query("/smtp/"+e,"GET",{},!1)},put:function(e){return query("/smtp/"+e.id,"PUT",e,!1)},delete:function(e){return query("/smtp/"+e,"DELETE",{},!1)}},import_email:function(e){return query("/import/email","POST",e,!1)},clone_site:function(e){return query("/import/site","POST",e,!1)},send_test_email:function(e){return query("/util/send_test_email","POST",e,!0)},reset:function(){return query("/reset","POST",{},!0)}};$(document).ready(function(){var e=location.pathname;$(".nav-sidebar li").each(function(){var t=$(this);t.find("a").attr("href")===e&&t.addClass("active")}),$.fn.dataTable.moment("MMMM Do YYYY, h:mm:ss a"),$('[data-toggle="tooltip"]').tooltip()}); \ No newline at end of file diff --git a/static/js/src/app/gophish.js b/static/js/src/app/gophish.js index 212cfdba..31c90d92 100644 --- a/static/js/src/app/gophish.js +++ b/static/js/src/app/gophish.js @@ -212,6 +212,15 @@ var api = { // Register our moment.js datatables listeners $(document).ready(function () { + // Setup nav highlighting + var path = location.pathname; + $('.nav-sidebar li').each(function () { + var $this = $(this); + // if the current path is like this link, make it active + if ($this.find("a").attr('href') === path) { + $this.addClass('active'); + } + }) $.fn.dataTable.moment('MMMM Do YYYY, h:mm:ss a'); // Setup tooltips $('[data-toggle="tooltip"]').tooltip() diff --git a/templates/base.html b/templates/base.html index 130f7946..7956e6e5 100644 --- a/templates/base.html +++ b/templates/base.html @@ -16,7 +16,7 @@ @@ -83,4 +84,4 @@ -{{ end }} +{{ end }} \ No newline at end of file diff --git a/templates/campaign_results.html b/templates/campaign_results.html index 9c24bbe5..fa3106c2 100644 --- a/templates/campaign_results.html +++ b/templates/campaign_results.html @@ -1,42 +1,4 @@ {{define "body"}} -
-
- -
-
diff --git a/templates/campaigns.html b/templates/campaigns.html index 3b641e42..a059498a 100644 --- a/templates/campaigns.html +++ b/templates/campaigns.html @@ -1,42 +1,4 @@ {{define "body"}} -
-
- -
-

diff --git a/templates/dashboard.html b/templates/dashboard.html index 7d4d5ddb..76770242 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -1,42 +1,4 @@ {{define "body"}} -
-
- -
-

Dashboard diff --git a/templates/landing_pages.html b/templates/landing_pages.html index df944dae..2c809db2 100644 --- a/templates/landing_pages.html +++ b/templates/landing_pages.html @@ -1,34 +1,4 @@ {{define "body"}} -

Landing Pages diff --git a/templates/nav.html b/templates/nav.html new file mode 100644 index 00000000..09af8982 --- /dev/null +++ b/templates/nav.html @@ -0,0 +1,39 @@ +{{define "nav"}} +
+
+ +
+
+{{end}} \ No newline at end of file diff --git a/templates/register.html b/templates/register.html index fae07375..518aec09 100644 --- a/templates/register.html +++ b/templates/register.html @@ -45,10 +45,12 @@

{{template "flashes" .Flashes}} - - - - + + + +
@@ -57,4 +59,4 @@ -{{ end }} +{{ end }} \ No newline at end of file diff --git a/templates/sending_profiles.html b/templates/sending_profiles.html index f0fff3c8..97f9f37f 100644 --- a/templates/sending_profiles.html +++ b/templates/sending_profiles.html @@ -1,34 +1,4 @@ {{define "body"}} -

Sending Profiles diff --git a/templates/settings.html b/templates/settings.html index ece7f518..b08842f4 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -1,34 +1,4 @@ {{define "body"}} -

Settings

@@ -44,6 +14,7 @@

+ {{if .ModifySystem }}
@@ -58,6 +29,7 @@

+ {{end}}
diff --git a/templates/templates.html b/templates/templates.html index 86df4e75..6ef478b5 100644 --- a/templates/templates.html +++ b/templates/templates.html @@ -1,34 +1,4 @@ {{define "body"}} -

diff --git a/templates/users.html b/templates/users.html index a5a50435..adbcad01 100644 --- a/templates/users.html +++ b/templates/users.html @@ -1,42 +1,4 @@ {{define "body"}} -
-
- -
-