Initial commit of RBAC support. (#1366)

* Initial commit of RBAC support. Closes #1333
pull/1367/head
Jordan Wright 2019-02-19 20:33:50 -06:00 committed by GitHub
parent 4ec9f07859
commit ba8ceb81da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 549 additions and 299 deletions

View File

@ -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":

View File

@ -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)
}

View File

@ -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`

View File

@ -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"

View File

@ -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) {

View File

@ -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))
}

View File

@ -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",

View File

@ -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

88
models/rbac.go Normal file
View File

@ -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
}

57
models/rbac_test.go Normal file
View File

@ -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)
}

View File

@ -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
}

View File

@ -1 +1 @@
function errorFlash(e){$("#flashes").empty(),$("#flashes").append('<div style="text-align:center" class="alert alert-danger"> <i class="fa fa-exclamation-circle"></i> '+e+"</div>")}function successFlash(e){$("#flashes").empty(),$("#flashes").append('<div style="text-align:center" class="alert alert-success"> <i class="fa fa-check-circle"></i> '+e+"</div>")}function modalError(e){$("#modal\\.flashes").empty().append('<div style="text-align:center" class="alert alert-danger"> <i class="fa fa-exclamation-circle"></i> '+e+"</div>")}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 $("<div/>").text(e).html()}function unescapeHtml(e){return $("<div/>").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()});
function errorFlash(e){$("#flashes").empty(),$("#flashes").append('<div style="text-align:center" class="alert alert-danger"> <i class="fa fa-exclamation-circle"></i> '+e+"</div>")}function successFlash(e){$("#flashes").empty(),$("#flashes").append('<div style="text-align:center" class="alert alert-success"> <i class="fa fa-check-circle"></i> '+e+"</div>")}function modalError(e){$("#modal\\.flashes").empty().append('<div style="text-align:center" class="alert alert-danger"> <i class="fa fa-exclamation-circle"></i> '+e+"</div>")}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 $("<div/>").text(e).html()}function unescapeHtml(e){return $("<div/>").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()});

View File

@ -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()

View File

@ -16,7 +16,7 @@
<link href='https://fonts.googleapis.com/css?family=Roboto:700,500' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,300,600,700' rel='stylesheet' type='text/css'>
<script>
{{if .User}}
{{if .User}}
var user = {
api_key : {{ .User.ApiKey }},
username : {{ .User.Username }}
@ -62,7 +62,7 @@
<div class="btn-group" id="navbar-dropdown">
<a class="btn btn-primary" href="/settings"><i class="fa fa-user"></i> {{.User.Username}}</a>
<a class="btn btn-primary dropdown-toggle" href="/logout">
<i class="fa fa-sign-out"></i>
<i class="fa fa-sign-out"></i>
</a>
</div>
{{else}}
@ -75,6 +75,7 @@
</div>
</div>
</div>
{{template "nav" .}}
{{template "body" .}}
<!-- Placed at the end of the document so the pages load faster -->
<script src="/js/dist/vendor.min.js"></script>
@ -83,4 +84,4 @@
</body>
</html>
{{ end }}
{{ end }}

View File

@ -1,42 +1,4 @@
{{define "body"}}
<div class="container-fluid">
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<li>
<a href="/">Dashboard</a>
</li>
<li class="active">
<a href="/campaigns">Campaigns</a>
</li>
<li>
<a href="/users">Users &amp; Groups</a>
</li>
<li>
<a href="/templates">Email Templates</a>
</li>
<li>
<a href="/landing_pages">Landing Pages</a>
</li>
<li>
<a href="/sending_profiles">Sending Profiles</a>
</li>
<li>
<a href="/settings">Settings</a>
</li>
<li>
<hr>
</li>
<li>
<a href="https://docs.getgophish.com/user-guide/">User Guide</a>
</li>
<li>
<a href="https://docs.getgophish.com/api-documentation/">API Documentation</a>
</li>
</ul>
</div>
</div>
</div>
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<div id="loading">
<i class="fa fa-spinner fa-spin fa-4x"></i>

View File

@ -1,42 +1,4 @@
{{define "body"}}
<div class="container-fluid">
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<li>
<a href="/">Dashboard</a>
</li>
<li class="active">
<a href="/campaigns">Campaigns</a>
</li>
<li>
<a href="/users">Users &amp; Groups</a>
</li>
<li>
<a href="/templates">Email Templates</a>
</li>
<li>
<a href="/landing_pages">Landing Pages</a>
</li>
<li>
<a href="/sending_profiles">Sending Profiles</a>
</li>
<li>
<a href="/settings">Settings</a>
</li>
<li>
<hr>
</li>
<li>
<a href="https://docs.getgophish.com/user-guide/">User Guide</a>
</li>
<li>
<a href="https://docs.getgophish.com/api-documentation/">API Documentation</a>
</li>
</ul>
</div>
</div>
</div>
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<div class="row">
<h1 class="page-header">

View File

@ -1,42 +1,4 @@
{{define "body"}}
<div class="container-fluid">
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<li class="active">
<a href="/">Dashboard</a>
</li>
<li>
<a href="/campaigns">Campaigns</a>
</li>
<li>
<a href="/users">Users &amp; Groups</a>
</li>
<li>
<a href="/templates">Email Templates</a>
</li>
<li>
<a href="/landing_pages">Landing Pages</a>
</li>
<li>
<a href="/sending_profiles">Sending Profiles</a>
</li>
<li>
<a href="/settings">Settings</a>
</li>
<li>
<hr>
</li>
<li>
<a href="https://docs.getgophish.com/user-guide/">User Guide</a>
</li>
<li>
<a href="https://docs.getgophish.com/api-documentation/">API Documentation</a>
</li>
</ul>
</div>
</div>
</div>
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h1 class="page-header">
Dashboard

View File

@ -1,34 +1,4 @@
{{define "body"}}
<div class="container-fluid">
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<li><a href="/">Dashboard</a>
</li>
<li><a href="/campaigns">Campaigns</a>
</li>
<li><a href="/users">Users &amp; Groups</a>
</li>
<li><a href="/templates">Email Templates</a>
</li>
<li class="active"><a href="/landing_pages">Landing Pages</a>
</li>
<li><a href="/sending_profiles">Sending Profiles</a>
</li>
<li><a href="/settings">Settings</a>
</li>
<li>
<hr>
</li>
<li>
<a href="https://docs.getgophish.com/user-guide/">User Guide</a>
</li>
<li><a href="https://docs.getgophish.com/api-documentation/">API Documentation</a>
</li>
</ul>
</div>
</div>
</div>
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h1 class="page-header">
Landing Pages

39
templates/nav.html Normal file
View File

@ -0,0 +1,39 @@
{{define "nav"}}
<div class="container-fluid">
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<li>
<a href="/">Dashboard</a>
</li>
<li>
<a href="/campaigns">Campaigns</a>
</li>
<li>
<a href="/users">Users &amp; Groups</a>
</li>
<li> <a href="/templates">Email Templates</a>
</li>
<li>
<a href="/landing_pages">Landing Pages</a>
</li>
<li>
<a href="/sending_profiles">Sending Profiles</a>
</li>
<li>
<a href="/settings">Settings <span class="badge pull-right">Admin</span></a>
</li>
<li>
<hr>
</li>
<li>
<a href="https://docs.getgophish.com/user-guide/">User Guide</a>
</li>
<li>
<a href="https://docs.getgophish.com/api-documentation/">API Documentation</a>
</li>
</ul>
</div>
</div>
</div>
{{end}}

View File

@ -45,10 +45,12 @@
<img id="logo" src="/images/logo_purple.png" />
<h2 class="form-signin-heading">Please register below</h2>
{{template "flashes" .Flashes}}
<input type="text" name="username" class="form-control top-input" placeholder="Username" required autofocus/>
<input type="password" name="password" class="form-control middle-input" placeholder="Password" autocomplete="off" required/>
<input type="password" name="confirm_password" class="form-control bottom-input" placeholder="Confirm Password" autocomplete="off" required/>
<input type="hidden" name="csrf_token" value="{{.Token}}"/>
<input type="text" name="username" class="form-control top-input" placeholder="Username" required autofocus />
<input type="password" name="password" class="form-control middle-input" placeholder="Password"
autocomplete="off" required />
<input type="password" name="confirm_password" class="form-control bottom-input" placeholder="Confirm Password"
autocomplete="off" required />
<input type="hidden" name="csrf_token" value="{{.Token}}" />
<button class="btn btn-lg btn-primary btn-block" type="submit">Register</button>
</form>
</div>
@ -57,4 +59,4 @@
</body>
</html>
{{ end }}
{{ end }}

View File

@ -1,34 +1,4 @@
{{define "body"}}
<div class="container-fluid">
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<li><a href="/">Dashboard</a>
</li>
<li><a href="/campaigns">Campaigns</a>
</li>
<li><a href="/users">Users &amp; Groups</a>
</li>
<li><a href="/templates">Email Templates</a>
</li>
<li><a href="/landing_pages">Landing Pages</a>
</li>
<li class="active"><a href="/sending_profiles">Sending Profiles</a>
</li>
<li><a href="/settings">Settings</a>
</li>
<li>
<hr>
</li>
<li>
<a href="https://docs.getgophish.com/user-guide/">User Guide</a>
</li>
<li><a href="https://docs.getgophish.com/api-documentation/">API Documentation</a>
</li>
</ul>
</div>
</div>
</div>
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h1 class="page-header">
Sending Profiles

View File

@ -1,34 +1,4 @@
{{define "body"}}
<div class="container-fluid">
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<li><a href="/">Dashboard</a>
</li>
<li><a href="/campaigns">Campaigns</a>
</li>
<li><a href="/users">Users &amp; Groups</a>
</li>
<li><a href="/templates">Email Templates</a>
</li>
<li><a href="/landing_pages">Landing Pages</a>
</li>
<li><a href="/sending_profiles">Sending Profiles</a>
</li>
<li class="active"><a href="/settings">Settings</a>
</li>
<li>
<hr>
</li>
<li>
<a href="https://docs.getgophish.com/user-guide/">User Guide</a>
</li>
<li><a href="https://docs.getgophish.com/api-documentation/">API Documentation</a>
</li>
</ul>
</div>
</div>
</div>
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<div class="row">
<h1 class="page-header">Settings</h1>
@ -44,6 +14,7 @@
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="mainSettings">
<br />
{{if .ModifySystem }}
<div class="row">
<label class="col-sm-2 control-label form-label">Gophish version</label>
<div class="col-md-6">
@ -58,6 +29,7 @@
</div>
</div>
<br />
{{end}}
<div class="row">
<label for="api_key" class="col-sm-2 control-label form-label">API Key:</label>
<div class="col-md-6">

View File

@ -1,34 +1,4 @@
{{define "body"}}
<div class="container-fluid">
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<li><a href="/">Dashboard</a>
</li>
<li><a href="/campaigns">Campaigns</a>
</li>
<li><a href="/users">Users &amp; Groups</a>
</li>
<li class="active"><a href="/templates">Email Templates</a>
</li>
<li><a href="/landing_pages">Landing Pages</a>
</li>
<li><a href="/sending_profiles">Sending Profiles</a>
</li>
<li><a href="/settings">Settings</a>
</li>
<li>
<hr>
</li>
<li>
<a href="https://docs.getgophish.com/user-guide/">User Guide</a>
</li>
<li><a href="https://docs.getgophish.com/api-documentation/">API Documentation</a>
</li>
</ul>
</div>
</div>
</div>
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<div class="row">
<h1 class="page-header">

View File

@ -1,42 +1,4 @@
{{define "body"}}
<div class="container-fluid">
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<li>
<a href="/">Dashboard</a>
</li>
<li>
<a href="/campaigns">Campaigns</a>
</li>
<li class="active">
<a href="/users">Users &amp; Groups</a>
</li>
<li>
<a href="/templates">Email Templates</a>
</li>
<li>
<a href="/landing_pages">Landing Pages</a>
</li>
<li>
<a href="/sending_profiles">Sending Profiles</a>
</li>
<li>
<a href="/settings">Settings</a>
</li>
<li>
<hr>
</li>
<li>
<a href="https://docs.getgophish.com/user-guide/">User Guide</a>
</li>
<li>
<a href="https://docs.getgophish.com/api-documentation/">API Documentation</a>
</li>
</ul>
</div>
</div>
</div>
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<div class="row">
<h1 class="page-header">