mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-02-12 16:12:44 +00:00
491 lines
13 KiB
Go
491 lines
13 KiB
Go
package controller
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/go-errors/errors"
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/phishingclub/phishingclub/cli"
|
|
"github.com/phishingclub/phishingclub/data"
|
|
"github.com/phishingclub/phishingclub/errs"
|
|
"github.com/phishingclub/phishingclub/model"
|
|
"github.com/phishingclub/phishingclub/password"
|
|
"github.com/phishingclub/phishingclub/repository"
|
|
"github.com/phishingclub/phishingclub/service"
|
|
"github.com/phishingclub/phishingclub/vo"
|
|
"golang.org/x/net/context"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// SetupAdminRequest is the request for the install action
|
|
type SetupAdminRequest struct {
|
|
Username string `json:"username" binding:"required"`
|
|
UserFullname string `json:"userFullname" binding:"required"`
|
|
NewPassword string `json:"newPassword" binding:"required"`
|
|
}
|
|
|
|
// InitialSetup is a controller used by the CLI in the
|
|
// initial setup process - it is not an API controller
|
|
type InitialSetup struct {
|
|
Common
|
|
CLIOutputter cli.Outputter
|
|
OptionRepository *repository.Option
|
|
InstallService *service.InstallSetup
|
|
OptionService *service.Option
|
|
}
|
|
|
|
// IsInstalled checks if the application is installed
|
|
// not as a
|
|
func (is *InitialSetup) IsInstalled(ctx context.Context) (bool, error) {
|
|
isInstalledOption, err := is.OptionRepository.GetByKey(
|
|
ctx,
|
|
data.OptionKeyIsInstalled,
|
|
)
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return false, nil
|
|
}
|
|
if err != nil {
|
|
return false, fmt.Errorf("could not get '%s' option: %w", data.OptionKeyIsInstalled, err)
|
|
}
|
|
return isInstalledOption.Value.String() == data.OptionValueIsInstalled, nil
|
|
}
|
|
|
|
// HandleInitialSetup handles the initial setup of the application
|
|
// this includes inserting the isInstalled option to not installed
|
|
// and making or updating the sacrificial admin account
|
|
func (is *InitialSetup) HandleInitialSetup(ctx context.Context) error {
|
|
// setup option for is installed
|
|
isInstalledOption, err := is.OptionRepository.GetByKey(
|
|
ctx,
|
|
data.OptionKeyIsInstalled,
|
|
)
|
|
// if the option does not exist, create it
|
|
if err != nil {
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return fmt.Errorf("%w: could not get '%s' option", err, data.OptionKeyIsInstalled)
|
|
}
|
|
key := vo.NewString64Must(data.OptionKeyIsInstalled)
|
|
value := vo.NewOptionalString1MBMust(data.OptionValueIsNotInstalled)
|
|
isInstalledOptionWithoutID := model.Option{
|
|
Key: *key,
|
|
Value: *value,
|
|
}
|
|
_, err = is.OptionRepository.Insert(
|
|
ctx,
|
|
&isInstalledOptionWithoutID,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: could not insert entity for option '%s'", err, data.OptionKeyIsInstalled)
|
|
}
|
|
isInstalledOption, err = is.OptionRepository.GetByKey(
|
|
ctx,
|
|
isInstalledOptionWithoutID.Key.String(),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: could not get created '%s' option", err, data.OptionKeyIsInstalled)
|
|
}
|
|
}
|
|
// if no instance ID exists, add it
|
|
instanceIDOption, err := is.OptionRepository.GetByKey(
|
|
ctx,
|
|
data.OptionKeyInstanceID,
|
|
)
|
|
// if the instance id option does not exist, create it
|
|
if err != nil {
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return fmt.Errorf("%w: could not get '%s' option", err, data.OptionKeyInstanceID)
|
|
}
|
|
key := vo.NewString64Must(data.OptionKeyInstanceID)
|
|
instanceID := uuid.New()
|
|
value := vo.NewOptionalString1MBMust(instanceID.String())
|
|
instanceIDOption = &model.Option{
|
|
Key: *key,
|
|
Value: *value,
|
|
}
|
|
_, err = is.OptionRepository.Insert(
|
|
ctx,
|
|
instanceIDOption,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("could not insert instance ID: %w", err)
|
|
}
|
|
}
|
|
|
|
// if installation is already complete, return error
|
|
if isInstalledOption.Value.String() == data.OptionValueIsInstalled {
|
|
return errs.ErrAlreadyInstalled
|
|
}
|
|
// setup accounts
|
|
admin, password, err := is.InstallService.SetupAccounts(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("could not setup initial admin account: %w", err)
|
|
}
|
|
is.CLIOutputter.PrintInitialAdminAccount(
|
|
admin.Username.MustGet().String(),
|
|
password.String(),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Install is the Install controller used by the API
|
|
type Install struct {
|
|
Common
|
|
UserRepository *repository.User
|
|
CompanyRepository *repository.Company
|
|
OptionRepository *repository.Option
|
|
DB *gorm.DB
|
|
PasswordHasher password.Argon2Hasher
|
|
ImportService *service.Import
|
|
}
|
|
|
|
// Install completes the installation by setting the initial administrators and options
|
|
func (in *Install) Install(g *gin.Context) {
|
|
tx := in.DB.Begin()
|
|
var committed bool
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
if !committed {
|
|
tx.Rollback()
|
|
}
|
|
}
|
|
}()
|
|
ok := in.install(g, tx)
|
|
if !ok {
|
|
if err := tx.Rollback().Error; err != nil {
|
|
in.Logger.Errorw("failed to install - could not rollback transaction",
|
|
"error", err,
|
|
)
|
|
}
|
|
return
|
|
}
|
|
result := tx.Commit()
|
|
if result.Error != nil {
|
|
in.Logger.Errorw("failed to install - could not commit transaction",
|
|
"error", result.Error,
|
|
)
|
|
in.Response.ServerError(g)
|
|
return
|
|
}
|
|
committed = true
|
|
// the admin user changed username and password
|
|
// however as the install process is a special case, we wont
|
|
// require re-authentication
|
|
in.Response.OK(g, gin.H{})
|
|
}
|
|
|
|
// Install completes the installation by setting the initial administrators
|
|
// username, password, email, name and company name
|
|
func (in *Install) install(g *gin.Context, tx *gorm.DB) bool {
|
|
// handle session
|
|
_, user, ok := in.handleSession(g)
|
|
if !ok {
|
|
return false
|
|
}
|
|
role := user.Role
|
|
if role == nil {
|
|
in.Logger.Error("failed to install - session contain no role")
|
|
in.Response.ServerError(g)
|
|
return false
|
|
}
|
|
if !role.IsSuperAdministrator() {
|
|
in.Logger.Info("failed to install - not super admin")
|
|
// TODO add audit log
|
|
in.Response.Forbidden(g)
|
|
return false
|
|
}
|
|
// defer rollback or commit tx
|
|
var request SetupAdminRequest
|
|
if err := g.ShouldBindJSON(&request); err != nil {
|
|
in.Logger.Debugw("failed to parse request",
|
|
"error", err,
|
|
)
|
|
in.Response.BadRequest(g)
|
|
return false
|
|
}
|
|
ctx := g.Request.Context()
|
|
// check if already installed
|
|
isInstalled, err := in.OptionRepository.GetByKey(ctx, data.OptionKeyIsInstalled)
|
|
if err != nil {
|
|
in.Logger.Errorw("failed to install - could not get option",
|
|
"optionKey", data.OptionKeyIsInstalled,
|
|
"error", err,
|
|
)
|
|
in.Response.ServerError(g)
|
|
return false
|
|
}
|
|
if isInstalled.Value.String() == data.OptionValueIsInstalled {
|
|
in.Logger.Info("failed to install - already installed")
|
|
in.Response.ServerErrorMessage(
|
|
g,
|
|
"Installation is already complete",
|
|
)
|
|
return false
|
|
}
|
|
// update the username
|
|
newUsername, err := vo.NewUsername(request.Username)
|
|
if err != nil {
|
|
in.Logger.Infow("failed to install - invalid username",
|
|
"username", request.Username,
|
|
"error", err,
|
|
)
|
|
in.Response.ValidationFailed(g, "Username", err)
|
|
return false
|
|
}
|
|
if newUsername.String() == user.Username.MustGet().String() {
|
|
in.Logger.Infow("failed to install - new username is the same as the current",
|
|
"username", newUsername.String(),
|
|
"error", err,
|
|
)
|
|
in.Response.BadRequestMessage(
|
|
g,
|
|
"Username may not be the same as the current",
|
|
)
|
|
return false
|
|
}
|
|
userID := user.ID.MustGet()
|
|
err = in.UserRepository.UpdateUsernameByIDWithTransaction(
|
|
ctx,
|
|
tx,
|
|
&userID,
|
|
newUsername,
|
|
)
|
|
if err != nil {
|
|
in.Logger.Infow("failed to install - could not update username",
|
|
"username", newUsername.String(),
|
|
"error", err,
|
|
)
|
|
in.Response.ServerError(g)
|
|
return false
|
|
}
|
|
// update the password
|
|
newPassword, err := vo.NewReasonableLengthPassword(request.NewPassword)
|
|
if err != nil {
|
|
in.Logger.Infow("failed to install - invalid password",
|
|
"error", err,
|
|
)
|
|
in.Response.ValidationFailed(g, "Password", err)
|
|
return false
|
|
}
|
|
hash, err := in.PasswordHasher.Hash(newPassword.String())
|
|
if err != nil {
|
|
in.Logger.Errorw("failed to install - could not hash password",
|
|
"error", err,
|
|
)
|
|
in.Response.ServerError(g)
|
|
return false
|
|
}
|
|
err = in.UserRepository.UpdatePasswordHashByIDWithTransaction(
|
|
ctx,
|
|
tx,
|
|
&userID,
|
|
hash,
|
|
)
|
|
if err != nil {
|
|
in.Logger.Errorw("failed to install - could not update password",
|
|
"error", err,
|
|
)
|
|
in.Response.ServerError(g)
|
|
return false
|
|
}
|
|
// update the name
|
|
newName, err := vo.NewUserFullname(request.UserFullname)
|
|
if err != nil {
|
|
in.Logger.Infow("failed to install - invalid name",
|
|
"error", err,
|
|
)
|
|
in.Response.ValidationFailed(g, "Name", err)
|
|
return false
|
|
}
|
|
err = in.UserRepository.UpdateFullNameByIDWithTransaction(
|
|
ctx,
|
|
tx,
|
|
&userID,
|
|
newName,
|
|
)
|
|
if err != nil {
|
|
in.Logger.Infow("failed to install - could not update name",
|
|
"error", err,
|
|
)
|
|
in.Response.ServerError(g)
|
|
return false
|
|
}
|
|
// update installed option to installed
|
|
option := model.Option{
|
|
Key: *vo.NewString64Must(data.OptionKeyIsInstalled),
|
|
Value: *vo.NewOptionalString1MBMust(data.OptionValueIsInstalled),
|
|
}
|
|
err = in.OptionRepository.UpdateByKeyWithTransaction(
|
|
ctx,
|
|
tx,
|
|
&option,
|
|
)
|
|
if err != nil {
|
|
in.Logger.Errorw("failed to install - could not create install option",
|
|
"error", err,
|
|
)
|
|
in.Response.ServerErrorMessage(g, "failed to create install option")
|
|
return false
|
|
|
|
}
|
|
return true
|
|
}
|
|
|
|
// InstallTemplates downloads and imports example templates from GitHub
|
|
func (in *Install) InstallTemplates(g *gin.Context) {
|
|
// handle session
|
|
session, user, ok := in.handleSession(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
role := user.Role
|
|
if role == nil {
|
|
in.Logger.Error("failed to install templates - session contain no role")
|
|
in.Response.ServerError(g)
|
|
return
|
|
}
|
|
if !role.IsSuperAdministrator() {
|
|
in.Logger.Info("failed to install templates - not super admin")
|
|
in.Response.Forbidden(g)
|
|
return
|
|
}
|
|
|
|
ctx := g.Request.Context()
|
|
|
|
// check if already installed
|
|
isInstalled, err := in.OptionRepository.GetByKey(ctx, data.OptionKeyIsInstalled)
|
|
if err != nil {
|
|
in.Logger.Errorw("failed to install templates - could not get option",
|
|
"optionKey", data.OptionKeyIsInstalled,
|
|
"error", err,
|
|
)
|
|
in.Response.ServerError(g)
|
|
return
|
|
}
|
|
if isInstalled.Value.String() != data.OptionValueIsInstalled {
|
|
in.Logger.Info("failed to install templates - installation not complete")
|
|
in.Response.BadRequestMessage(g, "Installation must be completed first")
|
|
return
|
|
}
|
|
|
|
// create http client with timeout
|
|
client := &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
}
|
|
|
|
// get latest release info from GitHub API
|
|
releaseURL := "https://api.github.com/repos/phishingclub/templates/releases/latest"
|
|
resp, err := client.Get(releaseURL)
|
|
if err != nil {
|
|
in.Logger.Errorw("failed to get latest release info",
|
|
"url", releaseURL,
|
|
"error", err,
|
|
)
|
|
in.Response.ServerErrorMessage(g, "Failed to get latest templates release info")
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
in.Logger.Errorw("failed to get release info - bad status",
|
|
"url", releaseURL,
|
|
"status", resp.StatusCode,
|
|
)
|
|
in.Response.ServerErrorMessage(g, fmt.Sprintf("Failed to get release info: HTTP %d", resp.StatusCode))
|
|
return
|
|
}
|
|
|
|
// parse release response
|
|
var release struct {
|
|
Assets []struct {
|
|
BrowserDownloadURL string `json:"browser_download_url"`
|
|
Name string `json:"name"`
|
|
} `json:"assets"`
|
|
}
|
|
|
|
releaseBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
in.Logger.Errorw("failed to read release response",
|
|
"error", err,
|
|
)
|
|
in.Response.ServerErrorMessage(g, "Failed to read release info")
|
|
return
|
|
}
|
|
|
|
if err := json.Unmarshal(releaseBody, &release); err != nil {
|
|
in.Logger.Errorw("failed to parse release response",
|
|
"error", err,
|
|
)
|
|
in.Response.ServerErrorMessage(g, "Failed to parse release info")
|
|
return
|
|
}
|
|
|
|
if len(release.Assets) == 0 {
|
|
in.Logger.Error("no assets found in latest release")
|
|
in.Response.ServerErrorMessage(g, "No template assets found in latest release")
|
|
return
|
|
}
|
|
|
|
// use the first asset (should be the templates zip)
|
|
templatesURL := release.Assets[0].BrowserDownloadURL
|
|
in.Logger.Infow("downloading templates from latest release",
|
|
"url", templatesURL,
|
|
"asset", release.Assets[0].Name,
|
|
)
|
|
|
|
// download the templates
|
|
resp2, err := client.Get(templatesURL)
|
|
if err != nil {
|
|
in.Logger.Errorw("failed to download templates",
|
|
"url", templatesURL,
|
|
"error", err,
|
|
)
|
|
in.Response.ServerErrorMessage(g, "Failed to download templates from GitHub")
|
|
return
|
|
}
|
|
defer resp2.Body.Close()
|
|
|
|
if resp2.StatusCode != http.StatusOK {
|
|
in.Logger.Errorw("failed to download templates - bad status",
|
|
"url", templatesURL,
|
|
"status", resp2.StatusCode,
|
|
)
|
|
in.Response.ServerErrorMessage(g, fmt.Sprintf("Failed to download templates: HTTP %d", resp2.StatusCode))
|
|
return
|
|
}
|
|
|
|
// read the response body
|
|
body, err := io.ReadAll(resp2.Body)
|
|
if err != nil {
|
|
in.Logger.Errorw("failed to read templates response",
|
|
"error", err,
|
|
)
|
|
in.Response.ServerErrorMessage(g, "Failed to read templates download")
|
|
return
|
|
}
|
|
|
|
// import the templates from raw bytes (for global use, not company-specific)
|
|
summary, err := in.ImportService.ImportFromBytes(g, session, body, false, nil)
|
|
if err != nil {
|
|
in.Logger.Errorw("failed to import templates",
|
|
"error", err,
|
|
)
|
|
in.Response.ServerErrorMessage(g, "Failed to import templates")
|
|
return
|
|
}
|
|
|
|
in.Logger.Infow("successfully installed templates",
|
|
"assetsCreated", summary.AssetsCreated,
|
|
"pagesCreated", summary.PagesCreated,
|
|
"emailsCreated", summary.EmailsCreated,
|
|
)
|
|
|
|
in.Response.OK(g, summary)
|
|
}
|