From c0904f1942071ce870ab9d87bd6c7f49f614ee82 Mon Sep 17 00:00:00 2001
From: Schwobaland <Schwobaland@users.noreply.github.com>
Date: Sat, 31 Dec 2016 03:33:30 +0100
Subject: [PATCH] Restrict creating organisations by user (#193)

* restrict creating organizations based on right on user

* revert bindata.go

* reverse vendor lib

* revert goimports change

* set AllowCreateOrganization default value to true

* revert locale

* added default value for AllowCreateOrganization

* fix typo in migration-comment

* fix comment

* add coments in migration
---
 models/error.go                 | 14 ++++++++++++++
 models/migrations/migrations.go |  4 +++-
 models/migrations/v15.go        | 30 ++++++++++++++++++++++++++++++
 models/org.go                   |  4 ++++
 models/user.go                  | 17 ++++++++++++-----
 modules/auth/admin.go           | 27 ++++++++++++++-------------
 options/locale/locale_en-US.ini |  2 ++
 routers/admin/users.go          |  1 +
 routers/org/org.go              |  8 ++++++++
 templates/admin/user/edit.tmpl  |  6 ++++++
 templates/base/head.tmpl        |  2 ++
 11 files changed, 96 insertions(+), 19 deletions(-)
 create mode 100644 models/migrations/v15.go

diff --git a/models/error.go b/models/error.go
index d11a9eeb1f..f0f3bd1f76 100644
--- a/models/error.go
+++ b/models/error.go
@@ -123,6 +123,20 @@ func (err ErrUserHasOrgs) Error() string {
 	return fmt.Sprintf("user still has membership of organizations [uid: %d]", err.UID)
 }
 
+// ErrUserNotAllowedCreateOrg represents a "UserNotAllowedCreateOrg" kind of error.
+type ErrUserNotAllowedCreateOrg struct {
+}
+
+// IsErrUserNotAllowedCreateOrg checks if an error is an ErrUserNotAllowedCreateOrg.
+func IsErrUserNotAllowedCreateOrg(err error) bool {
+	_, ok := err.(ErrUserNotAllowedCreateOrg)
+	return ok
+}
+
+func (err ErrUserNotAllowedCreateOrg) Error() string {
+	return fmt.Sprintf("user is not allowed to create organizations")
+}
+
 // ErrReachLimitOfRepo represents a "ReachLimitOfRepo" kind of error.
 type ErrReachLimitOfRepo struct {
 	Limit int
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 66f80dac30..69408a071d 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -76,8 +76,10 @@ var migrations = []Migration{
 
 	// v13 -> v14:v0.9.87
 	NewMigration("set comment updated with created", setCommentUpdatedWithCreated),
-
+	// v14
 	NewMigration("create user column diff view style", createUserColumnDiffViewStyle),
+	// v15
+	NewMigration("create user column allow create organization", createAllowCreateOrganizationColumn),
 }
 
 // Migrate database to current version
diff --git a/models/migrations/v15.go b/models/migrations/v15.go
new file mode 100644
index 0000000000..90fc22c6d3
--- /dev/null
+++ b/models/migrations/v15.go
@@ -0,0 +1,30 @@
+// Copyright 2016 Gitea. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package migrations
+
+import (
+	"fmt"
+
+	"github.com/go-xorm/xorm"
+)
+
+// UserV15 describes the added field for User
+type UserV15 struct {
+	AllowCreateOrganization bool
+}
+
+// TableName will be invoked by XORM to customrize the table name
+func (*UserV15) TableName() string {
+	return "user"
+}
+
+func createAllowCreateOrganizationColumn(x *xorm.Engine) error {
+	if err := x.Sync2(new(UserV15)); err != nil {
+		return fmt.Errorf("Sync2: %v", err)
+	} else if _, err = x.Where("type=0").Cols("allow_create_organization").Update(&UserV15{AllowCreateOrganization: true}); err != nil {
+		return fmt.Errorf("set allow_create_organization: %v", err)
+	}
+	return nil
+}
diff --git a/models/org.go b/models/org.go
index 172295e7ae..881b8bfc7e 100644
--- a/models/org.go
+++ b/models/org.go
@@ -97,6 +97,10 @@ func (org *User) RemoveOrgRepo(repoID int64) error {
 
 // CreateOrganization creates record of a new organization.
 func CreateOrganization(org, owner *User) (err error) {
+	if !owner.CanCreateOrganization() {
+		return ErrUserNotAllowedCreateOrg{}
+	}
+
 	if err = IsUsableUsername(org.Name); err != nil {
 		return err
 	}
diff --git a/models/user.go b/models/user.go
index d48397ef7e..9f19b1c84e 100644
--- a/models/user.go
+++ b/models/user.go
@@ -102,11 +102,12 @@ type User struct {
 	MaxRepoCreation int `xorm:"NOT NULL DEFAULT -1"`
 
 	// Permissions
-	IsActive         bool // Activate primary email
-	IsAdmin          bool
-	AllowGitHook     bool
-	AllowImportLocal bool // Allow migrate repository by local path
-	ProhibitLogin    bool
+	IsActive                bool // Activate primary email
+	IsAdmin                 bool
+	AllowGitHook            bool
+	AllowImportLocal        bool // Allow migrate repository by local path
+	AllowCreateOrganization bool `xorm:"DEFAULT true"`
+	ProhibitLogin           bool
 
 	// Avatar
 	Avatar          string `xorm:"VARCHAR(2048) NOT NULL"`
@@ -210,6 +211,11 @@ func (u *User) CanCreateRepo() bool {
 	return u.NumRepos < u.MaxRepoCreation
 }
 
+// CanCreateOrganization returns true if user can create organisation.
+func (u *User) CanCreateOrganization() bool {
+	return u.IsAdmin || u.AllowCreateOrganization
+}
+
 // CanEditGitHook returns true if user can edit Git hooks.
 func (u *User) CanEditGitHook() bool {
 	return u.IsAdmin || u.AllowGitHook
@@ -611,6 +617,7 @@ func CreateUser(u *User) (err error) {
 		return err
 	}
 	u.EncodePasswd()
+	u.AllowCreateOrganization = true
 	u.MaxRepoCreation = -1
 
 	sess := x.NewSession()
diff --git a/modules/auth/admin.go b/modules/auth/admin.go
index 033dfe9388..76b2bd0c89 100644
--- a/modules/auth/admin.go
+++ b/modules/auth/admin.go
@@ -27,19 +27,20 @@ func (f *AdminCreateUserForm) Validate(ctx *macaron.Context, errs binding.Errors
 
 // AdminEditUserForm form for admin to create user
 type AdminEditUserForm struct {
-	LoginType        string `binding:"Required"`
-	LoginName        string
-	FullName         string `binding:"MaxSize(100)"`
-	Email            string `binding:"Required;Email;MaxSize(254)"`
-	Password         string `binding:"MaxSize(255)"`
-	Website          string `binding:"MaxSize(50)"`
-	Location         string `binding:"MaxSize(50)"`
-	MaxRepoCreation  int
-	Active           bool
-	Admin            bool
-	AllowGitHook     bool
-	AllowImportLocal bool
-	ProhibitLogin    bool
+	LoginType               string `binding:"Required"`
+	LoginName               string
+	FullName                string `binding:"MaxSize(100)"`
+	Email                   string `binding:"Required;Email;MaxSize(254)"`
+	Password                string `binding:"MaxSize(255)"`
+	Website                 string `binding:"MaxSize(50)"`
+	Location                string `binding:"MaxSize(50)"`
+	MaxRepoCreation         int
+	Active                  bool
+	Admin                   bool
+	AllowGitHook            bool
+	AllowImportLocal        bool
+	AllowCreateOrganization bool
+	ProhibitLogin           bool
 }
 
 // Validate validates form fields
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index da2db622c5..6a8edad79e 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -844,6 +844,7 @@ team_permission_desc = What permission level should this team have?
 
 form.name_reserved = Organization name '%s' is reserved.
 form.name_pattern_not_allowed = Organization name pattern '%s' is not allowed.
+form.create_org_not_allowed = This user is not allowed to create an organization.
 
 settings = Settings
 settings.options = Options
@@ -994,6 +995,7 @@ users.prohibit_login = This account is prohibited to login
 users.is_admin = This account has administrator permissions
 users.allow_git_hook = This account has permissions to create Git hooks
 users.allow_import_local = This account has permissions to import local repositories
+users.allow_create_organization = This account has permissions to create Organizations
 users.update_profile = Update Account Profile
 users.delete_account = Delete This Account
 users.still_own_repo = This account still has ownership over at least one repository, you have to delete or transfer them first.
diff --git a/routers/admin/users.go b/routers/admin/users.go
index 782012d95d..c02f366f66 100644
--- a/routers/admin/users.go
+++ b/routers/admin/users.go
@@ -214,6 +214,7 @@ func EditUserPost(ctx *context.Context, form auth.AdminEditUserForm) {
 	u.IsAdmin = form.Admin
 	u.AllowGitHook = form.AllowGitHook
 	u.AllowImportLocal = form.AllowImportLocal
+	u.AllowCreateOrganization = form.AllowCreateOrganization
 	u.ProhibitLogin = form.ProhibitLogin
 
 	if err := models.UpdateUser(u); err != nil {
diff --git a/routers/org/org.go b/routers/org/org.go
index 579a2917b4..d0988bfcf5 100644
--- a/routers/org/org.go
+++ b/routers/org/org.go
@@ -5,6 +5,8 @@
 package org
 
 import (
+	"errors"
+
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/auth"
 	"code.gitea.io/gitea/modules/base"
@@ -21,6 +23,10 @@ const (
 // Create render the page for create organization
 func Create(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("new_org")
+	if !ctx.User.CanCreateOrganization() {
+		ctx.Handle(500, "Not allowed", errors.New(ctx.Tr("org.form.create_org_not_allowed")))
+		return
+	}
 	ctx.HTML(200, tplCreateOrg)
 }
 
@@ -48,6 +54,8 @@ func CreatePost(ctx *context.Context, form auth.CreateOrgForm) {
 			ctx.RenderWithErr(ctx.Tr("org.form.name_reserved", err.(models.ErrNameReserved).Name), tplCreateOrg, &form)
 		case models.IsErrNamePatternNotAllowed(err):
 			ctx.RenderWithErr(ctx.Tr("org.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplCreateOrg, &form)
+		case models.IsErrUserNotAllowedCreateOrg(err):
+			ctx.RenderWithErr(ctx.Tr("org.form.create_org_not_allowed"), tplCreateOrg, &form)
 		default:
 			ctx.Handle(500, "CreateOrganization", err)
 		}
diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl
index bed3097a50..91fbf781f3 100644
--- a/templates/admin/user/edit.tmpl
+++ b/templates/admin/user/edit.tmpl
@@ -97,6 +97,12 @@
 								<input name="allow_import_local" type="checkbox" {{if .User.CanImportLocal}}checked{{end}}>
 							</div>
 						</div>
+						<div class="inline field">
+							<div class="ui checkbox">
+								<label><strong>{{.i18n.Tr "admin.users.allow_create_organization"}}</strong></label>
+								<input name="allow_create_organization" type="checkbox" {{if .User.CanCreateOrganization}}checked{{end}}>
+							</div>
+						</div>
 
 						<div class="ui divider"></div>
 
diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl
index fe14742510..847503022e 100644
--- a/templates/base/head.tmpl
+++ b/templates/base/head.tmpl
@@ -94,9 +94,11 @@
 												<a class="item" href="{{AppSubUrl}}/repo/migrate">
 													<i class="octicon octicon-repo-clone"></i> {{.i18n.Tr "new_migrate"}}
 												</a>
+												{{if .SignedUser.CanCreateOrganization}}
 												<a class="item" href="{{AppSubUrl}}/org/create">
 													<i class="octicon octicon-organization"></i> {{.i18n.Tr "new_org"}}
 												</a>
+												{{end}}
 											</div><!-- end content create new menu -->
 										</div><!-- end dropdown menu create new -->