Repository: ovh/cds
Pull request #6293 (2022-09-20 12:41:03)

fix(vcs): missing data when getting hook by its ID. #6292
fix for this issue #6292 

ovh-cds: 
CDS Report build-all-cds#17942.0 ✘  
* Build
  * Build and Package All ✔ 
  * Build Debpacker ✔ 
  * Build OS-Ansible-Inventory ✔ 
  * Build SMTPMock ✔ 
  * Lint UI Translate ✔ 
  * Test CLI ✔ 
  * Test Engine ✘  
  * Test SDK ✔ 
  * Test UI ✔ 


Patch:
diff --git a/cli/cdsctl/admin.go b/cli/cdsctl/admin.go
index 3bfe300e4..3686006d9 100644
--- a/cli/cdsctl/admin.go
+++ b/cli/cdsctl/admin.go
@@ -17,6 +17,7 @@ func adminCommands() []*cobra.Command {
 		adminServices(),
 		adminCdn(),
 		adminHooks(),
+		adminOrganization(),
 		adminIntegrationModels(),
 		adminMaintenance(),
 		adminMetadata(),
diff --git a/cli/cdsctl/admin_organization.go b/cli/cdsctl/admin_organization.go
new file mode 100644
index 000000000..445ff5f16
--- /dev/null
+++ b/cli/cdsctl/admin_organization.go
@@ -0,0 +1,91 @@
+package main
+
+import (
+	"context"
+	"github.com/spf13/cobra"
+
+	"github.com/ovh/cds/cli"
+	"github.com/ovh/cds/sdk"
+)
+
+var adminOrganizationCmd = cli.Command{
+	Name:    "organization",
+	Aliases: []string{"org", "orga", "organizations"},
+	Short:   "Manage CDS Organization",
+}
+
+func adminOrganization() *cobra.Command {
+	return cli.NewCommand(adminOrganizationCmd, nil, []*cobra.Command{
+		cli.NewListCommand(adminOrganizationListCmd, adminOrganizationListRun, nil),
+		cli.NewCommand(adminOrganizationAddCmd, adminOrganizationAddRun, nil),
+		cli.NewDeleteCommand(adminOrganizationDeleteCmd, adminOrganizationDeleteRun, nil),
+		cli.NewCommand(adminOrganizationUserMigrateCmd, adminOrganizationUserMigrateRun, nil),
+	})
+}
+
+var adminOrganizationAddCmd = cli.Command{
+	Name:    "add",
+	Short:   "Add a new Organization on CDS",
+	Example: "cdsctl admin organization add <organization-name>",
+	Args: []cli.Arg{
+		{
+			Name: "organization-name",
+		},
+	},
+}
+
+func adminOrganizationAddRun(v cli.Values) error {
+	if err := client.AdminOrganizationCreate(context.Background(), sdk.Organization{Name: v.GetString("organization-name")}); err != nil {
+		return err
+	}
+	return nil
+}
+
+var adminOrganizationListCmd = cli.Command{
+	Name:    "list",
+	Aliases: []string{"ls"},
+	Short:   "List all organizations",
+	Example: "cdsctl admin organization list",
+}
+
+func adminOrganizationListRun(_ cli.Values) (cli.ListResult, error) {
+	orgs, err := client.AdminOrganizationList(context.Background())
+	return cli.AsListResult(orgs), err
+}
+
+var adminOrganizationDeleteCmd = cli.Command{
+	Name:    "delete",
+	Aliases: []string{"remove", "rm"},
+	Short:   "Delete an organization",
+	Example: "cdsctl admin organization delete <organization-name>",
+	Args: []cli.Arg{
+		{
+			Name: "organization-name",
+		},
+	},
+}
+
+func adminOrganizationDeleteRun(v cli.Values) error {
+	if err := client.AdminOrganizationDelete(context.Background(), v.GetString("organization-name")); err != nil {
+		return err
+	}
+	return nil
+}
+
+var adminOrganizationUserMigrateCmd = cli.Command{
+	Name:    "user-migrate",
+	Short:   "Associate an organization to all empty without it",
+	Example: "cdsctl admin organization user-migrate <organization-name>",
+	Args: []cli.Arg{
+		{
+			Name: "organization-name",
+		},
+	},
+}
+
+func adminOrganizationUserMigrateRun(v cli.Values) error {
+	if err := client.AdminOrganizationMigrateUser(context.Background(), v.GetString("organization-name")); err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/cli/cdsctl/experimental.go b/cli/cdsctl/experimental.go
index 072fdd37b..d8e59dc9d 100644
--- a/cli/cdsctl/experimental.go
+++ b/cli/cdsctl/experimental.go
@@ -15,8 +15,10 @@ var experimentalCmd = cli.Command{
 
 func experimentalCommands() []*cobra.Command {
 	return []*cobra.Command{
-		experimentalRbac(),
+		experimentalOrganization(),
+		experimentalRegion(),
 		experimentalProject(),
+		experimentalRbac(),
 		experimentalWorkerModel(),
 	}
 }
diff --git a/cli/cdsctl/experimental_organization.go b/cli/cdsctl/experimental_organization.go
new file mode 100644
index 000000000..358509ada
--- /dev/null
+++ b/cli/cdsctl/experimental_organization.go
@@ -0,0 +1,99 @@
+package main
+
+import (
+	"context"
+	"github.com/ovh/cds/sdk"
+	"github.com/spf13/cobra"
+
+	"github.com/ovh/cds/cli"
+)
+
+var experimentalOrganizationCmd = cli.Command{
+	Name:    "organization",
+	Aliases: []string{"org", "orga"},
+	Short:   "CDS Experimental organization commands",
+}
+
+func experimentalOrganization() *cobra.Command {
+	return cli.NewCommand(experimentalOrganizationCmd, nil, []*cobra.Command{
+		cli.NewCommand(organizationAddCmd, organizationAddFunc, nil, withAllCommandModifiers()...),
+		cli.NewGetCommand(organizationGetCmd, organizationGetFunc, nil, withAllCommandModifiers()...),
+		cli.NewListCommand(organizationListCmd, organizationListFunc, nil, withAllCommandModifiers()...),
+		cli.NewDeleteCommand(organizationDeleteCmd, organizationDeleteFunc, nil, withAllCommandModifiers()...),
+	})
+}
+
+var organizationAddCmd = cli.Command{
+	Name:    "add",
+	Aliases: []string{"create"},
+	Short:   "Create a new organization",
+	Example: "cdsctl organization add <organization_name>",
+	Ctx:     []cli.Arg{},
+	Args: []cli.Arg{
+		{Name: "organizationIdentifier"},
+	},
+}
+
+func organizationAddFunc(v cli.Values) error {
+	orga := sdk.Organization{Name: v.GetString("organizationIdentifier")}
+	if err := client.OrganizationAdd(context.Background(), orga); err != nil {
+		return err
+	}
+	return nil
+}
+
+var organizationGetCmd = cli.Command{
+	Name:    "show",
+	Aliases: []string{"get"},
+	Short:   "Get an organization by its identifier",
+	Example: "cdsctl organization show <organization_identifier>",
+	Ctx:     []cli.Arg{},
+	Args: []cli.Arg{
+		{Name: "organizationIdentifier"},
+	},
+}
+
+func organizationGetFunc(v cli.Values) (interface{}, error) {
+	orga, err := client.OrganizationGet(context.Background(), v.GetString("organizationIdentifier"))
+	if err != nil {
+		return orga, err
+	}
+	return orga, nil
+}
+
+var organizationListCmd = cli.Command{
+	Name:    "list",
+	Aliases: []string{"ls"},
+	Short:   "List all organizations",
+	Example: "cdsctl organization list",
+	Ctx:     []cli.Arg{},
+}
+
+func organizationListFunc(_ cli.Values) (cli.ListResult, error) {
+	orgas, err := client.OrganizationList(context.Background())
+	if err != nil {
+		return nil, err
+	}
+	return cli.AsListResult(orgas), nil
+}
+
+var organizationDeleteCmd = cli.Command{
+	Name:    "delete",
+	Aliases: []string{"remove", "rm"},
+	Short:   "Remove organization",
+	Example: "cdsctl organization delete",
+	Ctx:     []cli.Arg{},
+	Args: []cli.Arg{
+		{
+			Name: "organizationIdentifier",
+		},
+	},
+}
+
+func organizationDeleteFunc(v cli.Values) error {
+	err := client.OrganizationDelete(context.Background(), v.GetString("organizationIdentifier"))
+	if v.GetBool("force") && sdk.ErrorIs(err, sdk.ErrNotFound) {
+		return nil
+	}
+	return err
+}
diff --git a/cli/cdsctl/experimental_region.go b/cli/cdsctl/experimental_region.go
new file mode 100644
index 000000000..3ede10267
--- /dev/null
+++ b/cli/cdsctl/experimental_region.go
@@ -0,0 +1,99 @@
+package main
+
+import (
+	"context"
+	"github.com/ovh/cds/sdk"
+	"github.com/spf13/cobra"
+
+	"github.com/ovh/cds/cli"
+)
+
+var experimentalRegionCmd = cli.Command{
+	Name:    "region",
+	Aliases: []string{"org", "orga"},
+	Short:   "CDS Experimental region commands",
+}
+
+func experimentalRegion() *cobra.Command {
+	return cli.NewCommand(experimentalRegionCmd, nil, []*cobra.Command{
+		cli.NewCommand(regionAddCmd, regionAddFunc, nil, withAllCommandModifiers()...),
+		cli.NewGetCommand(regionGetCmd, regionGetFunc, nil, withAllCommandModifiers()...),
+		cli.NewListCommand(regionListCmd, regionListFunc, nil, withAllCommandModifiers()...),
+		cli.NewDeleteCommand(regionDeleteCmd, regionDeleteFunc, nil, withAllCommandModifiers()...),
+	})
+}
+
+var regionAddCmd = cli.Command{
+	Name:    "add",
+	Aliases: []string{"create"},
+	Short:   "Create a new region",
+	Example: "cdsctl region add <region_name>",
+	Ctx:     []cli.Arg{},
+	Args: []cli.Arg{
+		{Name: "regionIdentifier"},
+	},
+}
+
+func regionAddFunc(v cli.Values) error {
+	reg := sdk.Region{Name: v.GetString("regionIdentifier")}
+	if err := client.RegionAdd(context.Background(), reg); err != nil {
+		return err
+	}
+	return nil
+}
+
+var regionGetCmd = cli.Command{
+	Name:    "show",
+	Aliases: []string{"get"},
+	Short:   "Get an region by its identifier",
+	Example: "cdsctl region show <region_identifier>",
+	Ctx:     []cli.Arg{},
+	Args: []cli.Arg{
+		{Name: "regionIdentifier"},
+	},
+}
+
+func regionGetFunc(v cli.Values) (interface{}, error) {
+	reg, err := client.RegionGet(context.Background(), v.GetString("regionIdentifier"))
+	if err != nil {
+		return reg, err
+	}
+	return reg, nil
+}
+
+var regionListCmd = cli.Command{
+	Name:    "list",
+	Aliases: []string{"ls"},
+	Short:   "List all regions",
+	Example: "cdsctl region list",
+	Ctx:     []cli.Arg{},
+}
+
+func regionListFunc(_ cli.Values) (cli.ListResult, error) {
+	regions, err := client.RegionList(context.Background())
+	if err != nil {
+		return nil, err
+	}
+	return cli.AsListResult(regions), nil
+}
+
+var regionDeleteCmd = cli.Command{
+	Name:    "delete",
+	Aliases: []string{"remove", "rm"},
+	Short:   "Remove region",
+	Example: "cdsctl region delete",
+	Ctx:     []cli.Arg{},
+	Args: []cli.Arg{
+		{
+			Name: "regionIdentifier",
+		},
+	},
+}
+
+func regionDeleteFunc(v cli.Values) error {
+	err := client.RegionDelete(context.Background(), v.GetString("regionIdentifier"))
+	if v.GetBool("force") && sdk.ErrorIs(err, sdk.ErrNotFound) {
+		return nil
+	}
+	return err
+}
diff --git a/engine/api/action.go b/engine/api/action.go
index b0f630175..1d0945926 100644
--- a/engine/api/action.go
+++ b/engine/api/action.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"io"
 	"net/http"
+	"net/url"
 
 	"github.com/go-gorp/gorp"
 	"github.com/gorilla/mux"
@@ -186,7 +187,12 @@ func (api *API) getActionHandler() service.Handler {
 		vars := mux.Vars(r)
 
 		groupName := vars["permGroupName"]
-		actionName := vars["permActionName"]
+		actionNameEscaped := vars["permActionName"]
+
+		actionName, err := url.PathUnescape(actionNameEscaped)
+		if err != nil {
+			return sdk.NewErrorFrom(sdk.ErrWrongRequest, "%s", err)
+		}
 
 		g, err := group.LoadByName(ctx, api.mustDB(), groupName, group.LoadOptions.WithMembers)
 		if err != nil {
@@ -221,7 +227,13 @@ func (api *API) putActionHandler() service.Handler {
 		vars := mux.Vars(r)
 
 		groupName := vars["permGroupName"]
-		actionName := vars["permActionName"]
+
+		actionNameEscaped := vars["permActionName"]
+
+		actionName, err := url.PathUnescape(actionNameEscaped)
+		if err != nil {
+			return sdk.NewErrorFrom(sdk.ErrWrongRequest, "%s", err)
+		}
 
 		g, err := group.LoadByName(ctx, api.mustDB(), groupName)
 		if err != nil {
@@ -318,7 +330,13 @@ func (api *API) deleteActionHandler() service.Handler {
 		vars := mux.Vars(r)
 
 		groupName := vars["permGroupName"]
-		actionName := vars["permActionName"]
+
+		actionNameEscaped := vars["permActionName"]
+
+		actionName, err := url.PathUnescape(actionNameEscaped)
+		if err != nil {
+			return sdk.NewErrorFrom(sdk.ErrWrongRequest, "%s", err)
+		}
 
 		g, err := group.LoadByName(ctx, api.mustDB(), groupName)
 		if err != nil {
@@ -364,7 +382,13 @@ func (api *API) getActionAuditHandler() service.Handler {
 		vars := mux.Vars(r)
 
 		groupName := vars["permGroupName"]
-		actionName := vars["permActionName"]
+
+		actionNameEscaped := vars["permActionName"]
+
+		actionName, err := url.PathUnescape(actionNameEscaped)
+		if err != nil {
+			return sdk.NewErrorFrom(sdk.ErrWrongRequest, "%s", err)
+		}
 
 		g, err := group.LoadByName(ctx, api.mustDB(), groupName)
 		if err != nil {
@@ -440,7 +464,12 @@ func (api *API) postActionAuditRollbackHandler() service.Handler {
 		vars := mux.Vars(r)
 
 		groupName := vars["permGroupName"]
-		actionName := vars["permActionName"]
+		actionNameEscaped := vars["permActionName"]
+
+		actionName, err := url.PathUnescape(actionNameEscaped)
+		if err != nil {
+			return sdk.NewErrorFrom(sdk.ErrWrongRequest, "%s", err)
+		}
 
 		auditID, err := requestVarInt(r, "auditID")
 		if err != nil {
@@ -575,7 +604,12 @@ func (api *API) getActionUsageHandler() service.Handler {
 		vars := mux.Vars(r)
 
 		groupName := vars["permGroupName"]
-		actionName := vars["permActionName"]
+		actionNameEscaped := vars["permActionName"]
+
+		actionName, err := url.PathUnescape(actionNameEscaped)
+		if err != nil {
+			return sdk.NewErrorFrom(sdk.ErrWrongRequest, "%s", err)
+		}
 
 		g, err := group.LoadByName(ctx, api.mustDB(), groupName)
 		if err != nil {
@@ -604,7 +638,12 @@ func (api *API) getActionExportHandler() service.Handler {
 		vars := mux.Vars(r)
 
 		groupName := vars["permGroupName"]
-		actionName := vars["permActionName"]
+		actionNameEscaped := vars["permActionName"]
+
+		actionName, err := url.PathUnescape(actionNameEscaped)
+		if err != nil {
+			return sdk.NewErrorFrom(sdk.ErrWrongRequest, "%s", err)
+		}
 
 		format := FormString(r, "format")
 		if format == "" {
@@ -800,7 +839,12 @@ func (api *API) getActionBuiltinHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 		vars := mux.Vars(r)
 
-		actionName := vars["permActionBuiltinName"]
+		actionNameEscaped := vars["permActionBuiltinName"]
+
+		actionName, err := url.PathUnescape(actionNameEscaped)
+		if err != nil {
+			return sdk.NewErrorFrom(sdk.ErrWrongRequest, "%s", err)
+		}
 
 		a, err := action.LoadByTypesAndName(ctx, api.mustDB(), []string{sdk.BuiltinAction, sdk.PluginAction}, actionName,
 			action.LoadOptions.WithRequirements,
@@ -822,7 +866,12 @@ func (api *API) getActionBuiltinUsageHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 		vars := mux.Vars(r)
 
-		actionName := vars["permActionBuiltinName"]
+		actionNameEscaped := vars["permActionBuiltinName"]
+
+		actionName, err := url.PathUnescape(actionNameEscaped)
+		if err != nil {
+			return sdk.NewErrorFrom(sdk.ErrWrongRequest, "%s", err)
+		}
 
 		a, err := action.LoadByTypesAndName(ctx, api.mustDB(), []string{sdk.BuiltinAction, sdk.PluginAction}, actionName,
 			action.LoadOptions.WithRequirements,
diff --git a/engine/api/admin.go b/engine/api/admin.go
index df9570142..64a265cb7 100644
--- a/engine/api/admin.go
+++ b/engine/api/admin.go
@@ -12,8 +12,10 @@ import (
 	"github.com/rockbears/log"
 
 	"github.com/ovh/cds/engine/api/database/gorpmapping"
+	"github.com/ovh/cds/engine/api/organization"
 	"github.com/ovh/cds/engine/api/project"
 	"github.com/ovh/cds/engine/api/services"
+	"github.com/ovh/cds/engine/api/user"
 	"github.com/ovh/cds/engine/api/workflow"
 	"github.com/ovh/cds/engine/database"
 	"github.com/ovh/cds/engine/featureflipping"
@@ -21,6 +23,96 @@ import (
 	"github.com/ovh/cds/sdk"
 )
 
+func (api *API) postOrganizationMigrateUserHandler() service.Handler {
+	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+		vars := mux.Vars(r)
+		orgaIdentifier := vars["organizationIdentifier"]
+
+		orga, err := api.getOrganizationByIdentifier(ctx, orgaIdentifier)
+		if err != nil {
+			return err
+		}
+
+		users, err := user.LoadUsersWithoutOrganization(ctx, api.mustDB())
+		if err != nil {
+			return err
+		}
+
+		for i := range users {
+			tx, err := api.mustDB().Begin()
+			if err != nil {
+				return sdk.WithStack(err)
+			}
+			u := &users[i]
+
+			if err := api.userSetOrganization(ctx, tx, u, orga.Name); err != nil {
+				_ = tx.Rollback()
+				return err
+			}
+			if err := tx.Commit(); err != nil {
+				_ = tx.Rollback()
+				return sdk.WithStack(err)
+			}
+		}
+		return service.WriteJSON(w, nil, http.StatusOK)
+	}
+}
+
+func (api *API) postAdminOrganizationHandler() service.Handler {
+	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+		var org sdk.Organization
+		if err := service.UnmarshalBody(r, &org); err != nil {
+			return err
+		}
+		tx, err := api.mustDB().Begin()
+		if err != nil {
+			return sdk.WithStack(err)
+		}
+		defer tx.Rollback() // nolint
+
+		if err := organization.Insert(ctx, tx, &org); err != nil {
+			return err
+		}
+		if err := tx.Commit(); err != nil {
+			return sdk.WithStack(err)
+		}
+		return service.WriteMarshal(w, r, nil, http.StatusCreated)
+	}
+}
+
+func (api *API) getAdminOrganizationsHandler() service.Handler {
+	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+		orgas, err := organization.LoadAllOrganizations(ctx, api.mustDB())
+		if err != nil {
+			return err
+		}
+		return service.WriteMarshal(w, r, orgas, http.StatusOK)
+	}
+}
+
+func (api *API) deleteAdminOrganizationsHandler() service.Handler {
+	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+		vars := mux.Vars(r)
+		orgaIdentifier := vars["organizationIdentifier"]
+
+		orga, err := api.getOrganizationByIdentifier(ctx, orgaIdentifier)
+		if err != nil {
+			return err
+		}
+
+		tx, err := api.mustDB().Begin()
+		if err != nil {
+			return sdk.WithStack(err)
+		}
+		defer tx.Rollback() // nolint
+
+		if err := organization.Delete(tx, orga.ID); err != nil {
+			return err
+		}
+		return sdk.WithStack(tx.Commit())
+	}
+}
+
 func (api *API) postMaintenanceHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 		enable := service.FormBool(r, "enable")
diff --git a/engine/api/api.go b/engine/api/api.go
index 3653131ff..c11e2e357 100644
--- a/engine/api/api.go
+++ b/engine/api/api.go
@@ -102,7 +102,7 @@ type Configuration struct {
 		DisableAddUserInDefaultGroup bool                       `toml:"disableAddUserInDefaultGroup" default:"false" comment:"If false, user are automatically added in the default group" json:"disableAddUserInDefaultGroup"`
 		RSAPrivateKey                string                     `toml:"rsaPrivateKey" default:"" comment:"The RSA Private Key used to sign and verify the JWT Tokens issued by the API \nThis is mandatory." json:"-"`
 		RSAPrivateKeys               []authentication.KeyConfig `toml:"rsaPrivateKeys" default:"" comment:"RSA Private Keys used to sign and verify the JWT Tokens issued by the API \nThis is mandatory." json:"-" mapstructure:"rsaPrivateKeys"`
-		AllowedOrganizations         sdk.StringSlice            `toml:"allowedOrganizations" default:"" comment:"The list of allowed organizations for CDS users, let empty to authorize all organizations." json:"allowedOrganizations"`
+		AllowedOrganizations         sdk.StringSlice            `toml:"allowedOrganizations" default:"[default]" comment:"The list of allowed organizations for CDS users, let empty to authorize all organizations." json:"allowedOrganizations"`
 		LDAP                         struct {
 			Enabled         bool   `toml:"enabled" default:"false" json:"enabled"`
 			SignupDisabled  bool   `toml:"signupDisabled" default:"false" json:"signupDisabled"`
@@ -120,6 +120,7 @@ type Configuration struct {
 			Enabled              bool   `toml:"enabled" default:"true" json:"enabled"`
 			SignupDisabled       bool   `toml:"signupDisabled" default:"false" json:"signupDisabled"`
 			SignupAllowedDomains string `toml:"signupAllowedDomains" default:"" comment:"Allow signup from selected domains only - comma separated. Example: your-domain.com,another-domain.com" commented:"true" json:"signupAllowedDomains"`
+			Organization         string `toml:"organization" default:"default" comment:"Organization assigned to user created by local authentication" commented:"true" json:"organization"`
 		} `toml:"local" json:"local"`
 		CorporateSSO struct {
 			MFASupportEnabled bool   `json:"mfa_support_enabled" default:"false" toml:"mfaSupportEnabled"`
@@ -143,6 +144,7 @@ type Configuration struct {
 			APIURL         string `toml:"apiUrl" json:"apiUrl" default:"https://api.github.com" comment:"GitHub API URL"`
 			ClientID       string `toml:"clientId" json:"-" comment:"GitHub OAuth Client ID"`
 			ClientSecret   string `toml:"clientSecret" json:"-" comment:"GitHub OAuth Client Secret"`
+			Organization   string `toml:"organization" default:"default" comment:"Organization assigned to user created by github authentication" commented:"true" json:"organization"`
 		} `toml:"github" json:"github" comment:"#######\n CDS <-> GitHub Auth. Documentation on https://ovh.github.io/cds/docs/integrations/github/github_authentication/ \n######"`
 		Gitlab struct {
 			Enabled        bool   `toml:"enabled" default:"false" json:"enabled"`
@@ -150,6 +152,7 @@ type Configuration struct {
 			URL            string `toml:"url" json:"url" default:"https://gitlab.com" comment:"GitLab URL"`
 			ApplicationID  string `toml:"applicationID" json:"-" comment:"GitLab OAuth Application ID"`
 			Secret         string `toml:"secret" json:"-" comment:"GitLab OAuth Application Secret"`
+			Organization   string `toml:"organization" default:"default" comment:"Organization assigned to user created by gitlab authentication" commented:"true" json:"organization"`
 		} `toml:"gitlab" json:"gitlab" comment:"#######\n CDS <-> GitLab Auth. Documentation on https://ovh.github.io/cds/docs/integrations/gitlab/gitlab_authentication/ \n######"`
 		OIDC struct {
 			Enabled        bool   `toml:"enabled" default:"false" json:"enabled"`
@@ -157,6 +160,7 @@ type Configuration struct {
 			URL            string `toml:"url" json:"url" default:"" comment:"Open ID connect config URL"`
 			ClientID       string `toml:"clientId" json:"-" comment:"OIDC Client ID"`
 			ClientSecret   string `toml:"clientSecret" json:"-" comment:"OIDC Client Secret"`
+			Organization   string `toml:"organization" default:"default" comment:"Organization assigned to user created by openid authentication" commented:"true" json:"organization"`
 		} `toml:"oidc" json:"oidc" comment:"#######\n CDS <-> Open ID Connect Auth. Documentation on https://ovh.github.io/cds/docs/integrations/openid-connect/ \n######"`
 	} `toml:"auth" comment:"##############################\n CDS Authentication Settings# \n#############################" json:"auth"`
 	SMTP struct {
@@ -619,6 +623,7 @@ func (a *API) Serve(ctx context.Context) error {
 			a.Config.Auth.Local.SignupDisabled,
 			a.Config.URL.UI,
 			a.Config.Auth.Local.SignupAllowedDomains,
+			a.Config.Auth.Local.Organization,
 		)
 	}
 
@@ -650,6 +655,7 @@ func (a *API) Serve(ctx context.Context) error {
 			a.Config.Auth.Github.APIURL,
 			a.Config.Auth.Github.ClientID,
 			a.Config.Auth.Github.ClientSecret,
+			a.Config.Auth.Github.Organization,
 		)
 	}
 	if a.Config.Auth.Gitlab.Enabled {
@@ -659,6 +665,7 @@ func (a *API) Serve(ctx context.Context) error {
 			a.Config.Auth.Gitlab.URL,
 			a.Config.Auth.Gitlab.ApplicationID,
 			a.Config.Auth.Gitlab.Secret,
+			a.Config.Auth.Gitlab.Organization,
 		)
 	}
 	if a.Config.Auth.OIDC.Enabled {
@@ -668,6 +675,7 @@ func (a *API) Serve(ctx context.Context) error {
 			a.Config.Auth.OIDC.URL,
 			a.Config.Auth.OIDC.ClientID,
 			a.Config.Auth.OIDC.ClientSecret,
+			a.Config.Auth.OIDC.Organization,
 		)
 		if err != nil {
 			return err
diff --git a/engine/api/api_routes.go b/engine/api/api_routes.go
index d5172acff..9d148f5c3 100644
--- a/engine/api/api_routes.go
+++ b/engine/api/api_routes.go
@@ -103,6 +103,11 @@ func (api *API) InitRouter() {
 	r.Handle("/admin/database/encryption/{entity}", Scope(sdk.AuthConsumerScopeAdmin), r.GET(api.getAdminDatabaseEncryptedTuplesByEntity, service.OverrideAuth(api.authAdminMiddleware)))
 	r.Handle("/admin/database/encryption/{entity}/roll/{pk}", Scope(sdk.AuthConsumerScopeAdmin), r.POST(api.postAdminDatabaseRollEncryptedEntityByPrimaryKey, service.OverrideAuth(api.authAdminMiddleware)))
 
+	// Organization
+	r.Handle("/admin/organization", Scope(sdk.AuthConsumerScopeAdmin), r.GET(api.getAdminOrganizationsHandler, service.OverrideAuth(api.authAdminMiddleware)), r.POST(api.postAdminOrganizationHandler, service.OverrideAuth(api.authAdminMiddleware)))
+	r.Handle("/admin/organization/{organizationIdentifier}", Scope(sdk.AuthConsumerScopeAdmin), r.DELETE(api.deleteAdminOrganizationsHandler, service.OverrideAuth(api.authAdminMiddleware)))
+	r.Handle("/admin/organization/{organizationIdentifier}/migrate-user", Scope(sdk.AuthConsumerScopeAdmin), r.POST(api.postOrganizationMigrateUserHandler, service.OverrideAuth(api.authAdminMiddleware)))
+
 	// Feature flipping
 	r.Handle("/admin/features", Scope(sdk.AuthConsumerScopeAdmin), r.GET(api.getAdminFeatureFlipping, service.OverrideAuth(api.authAdminMiddleware)), r.POST(api.postAdminFeatureFlipping, service.OverrideAuth(api.authAdminMiddleware)))
 	r.Handle("/admin/features/{name}", Scope(sdk.AuthConsumerScopeAdmin), r.GET(api.getAdminFeatureFlippingByName, service.OverrideAuth(api.authAdminMiddleware)), r.PUT(api.putAdminFeatureFlipping, service.OverrideAuth(api.authAdminMiddleware)), r.DELETE(api.deleteAdminFeatureFlipping, service.OverrideAuth(api.authAdminMiddleware)))
@@ -433,8 +438,14 @@ func (api *API) InitRouter() {
 	r.Handle("/template/{groupName}/{templateSlug}/instance/{instanceID}", Scope(sdk.AuthConsumerScopeTemplate), r.DELETE(api.deleteTemplateInstanceHandler))
 	r.Handle("/template/{groupName}/{templateSlug}/usage", Scope(sdk.AuthConsumerScopeTemplate), r.GET(api.getTemplateUsageHandler))
 
+	r.Handle("/v2/organization", nil, r.POSTv2(api.postOrganizationHandler), r.GETv2(api.getOrganizationsHandler))
+	r.Handle("/v2/organization/{organizationIdentifier}", nil, r.GETv2(api.getOrganizationHandler), r.DELETEv2(api.deleteOrganizationHandler))
+
 	r.Handle("/v2/rbac/import", nil, r.POSTv2(api.postImportRbacHandler))
 
+	r.Handle("/v2/region", nil, r.POSTv2(api.postRegionHandler), r.GETv2(api.getRegionsHandler))
+	r.Handle("/v2/region/{regionIdentifier}", nil, r.GETv2(api.getRegionHandler), r.DELETEv2(api.deleteRegionHandler))
+
 	r.Handle("/v2/repository/analyze", Scope(sdk.AuthConsumerScopeHooks), r.POSTv2(api.postRepositoryAnalysisHandler))
 	r.Handle("/v2/project/repositories", Scope(sdk.AuthConsumerScopeHooks), r.GETv2(api.getAllRepositoriesHandler))
 	r.Handle("/v2/project/repositories/{repositoryIdentifier}/hook", Scope(sdk.AuthConsumerScopeHooks), r.GETv2(api.getRepositoryHookHandler))
diff --git a/engine/api/auth_local.go b/engine/api/auth_local.go
index 62baca4f3..e8bda21c7 100644
--- a/engine/api/auth_local.go
+++ b/engine/api/auth_local.go
@@ -148,8 +148,6 @@ func initBuiltinConsumersFromStartupConfig(ctx context.Context, tx gorpmapper.Sq
 			scopes = sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeService, sdk.AuthConsumerScopeRunExecution)
 		case StartupConfigConsumerTypeCDNStorageCDS:
 			scopes = sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeProject, sdk.AuthConsumerScopeRun)
-		case StartupConfigConsumerTypeVCS:
-			scopes = sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeService, sdk.AuthConsumerScopeProject, sdk.AuthConsumerScopeRun)
 		case StartupConfigConsumerTypeRepositories:
 			scopes = sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeService, sdk.AuthConsumerScopeProject, sdk.AuthConsumerScopeRun)
 		default:
@@ -213,6 +211,14 @@ func (api *API) postAuthLocalSigninHandler() service.Handler {
 			return sdk.NewErrorWithStack(err, sdk.ErrUnauthorized)
 		}
 
+		userInfo, err := driver.GetUserInfo(ctx, reqData)
+		if err != nil {
+			return err
+		}
+		if err := api.userSetOrganization(ctx, tx, usr, userInfo.Organization); err != nil {
+			return err
+		}
+
 		// Try to load a local consumer for user
 		consumer, err := authentication.LoadConsumerByTypeAndUserID(ctx, tx, sdk.ConsumerLocal, usr.ID, authentication.LoadConsumerOptions.WithAuthentifiedUser)
 		if err != nil {
@@ -354,6 +360,14 @@ func (api *API) postAuthLocalVerifyHandler() service.Handler {
 			}
 		}
 
+		userInfo, err := driver.GetUserInfo(ctx, reqData)
+		if err != nil {
+			return err
+		}
+		if err := api.userSetOrganization(ctx, tx, &newUser, userInfo.Organization); err != nil {
+			return err
+		}
+
 		// Create new local consumer for new user, set this consumer as pending validation
 		consumer, err := local.NewConsumerWithHash(ctx, tx, newUser.ID, reg.Hash)
 		if err != nil {
diff --git a/engine/api/authentication/github/github.go b/engine/api/authentication/github/github.go
index 587f65e85..745a9a588 100644
--- a/engine/api/authentication/github/github.go
+++ b/engine/api/authentication/github/github.go
@@ -17,7 +17,7 @@ var _ sdk.AuthDriverWithRedirect = new(authDriver)
 var _ sdk.AuthDriverWithSigninStateToken = new(authDriver)
 
 // NewDriver returns a new Github auth driver for given config.
-func NewDriver(signupDisabled bool, cdsURL, url, urlAPI, clientID, clientSecret string) sdk.AuthDriver {
+func NewDriver(signupDisabled bool, cdsURL, url, urlAPI, clientID, clientSecret, orga string) sdk.AuthDriver {
 	return &authDriver{
 		signupDisabled: signupDisabled,
 		cdsURL:         cdsURL,
@@ -25,6 +25,7 @@ func NewDriver(signupDisabled bool, cdsURL, url, urlAPI, clientID, clientSecret
 		urlAPI:         urlAPI,
 		clientID:       clientID,
 		clientSecret:   clientSecret,
+		organization:   orga,
 	}
 }
 
@@ -35,6 +36,7 @@ type authDriver struct {
 	urlAPI         string
 	clientID       string
 	clientSecret   string
+	organization   string
 }
 
 func (d authDriver) GetManifest() sdk.AuthDriverManifest {
@@ -136,6 +138,7 @@ func (d authDriver) GetUserInfo(ctx context.Context, req sdk.AuthConsumerSigninR
 	info.Username = githubUser.Login
 	info.Fullname = githubUser.Name
 	info.Email = githubUser.Email
+	info.Organization = d.organization
 
 	return info, nil
 }
diff --git a/engine/api/authentication/gitlab/gitlab.go b/engine/api/authentication/gitlab/gitlab.go
index a7ec3af9f..8179e3f34 100644
--- a/engine/api/authentication/gitlab/gitlab.go
+++ b/engine/api/authentication/gitlab/gitlab.go
@@ -18,13 +18,14 @@ var _ sdk.AuthDriverWithRedirect = new(authDriver)
 var _ sdk.AuthDriverWithSigninStateToken = new(authDriver)
 
 // NewDriver returns a new Gitlab auth driver for given config.
-func NewDriver(signupDisabled bool, cdsURL, url, applicationID, secret string) sdk.AuthDriver {
+func NewDriver(signupDisabled bool, cdsURL, url, applicationID, secret, orga string) sdk.AuthDriver {
 	return &authDriver{
 		signupDisabled: signupDisabled,
 		cdsURL:         cdsURL,
 		url:            url,
 		applicationID:  applicationID,
 		secret:         secret,
+		organization:   orga,
 	}
 }
 
@@ -34,6 +35,7 @@ type authDriver struct {
 	url            string
 	applicationID  string
 	secret         string
+	organization   string
 }
 
 func (d authDriver) GetManifest() sdk.AuthDriverManifest {
@@ -116,6 +118,7 @@ func (d authDriver) GetUserInfo(ctx context.Context, req sdk.AuthConsumerSigninR
 	info.Username = me.Username
 	info.Fullname = me.Name
 	info.Email = me.Email
+	info.Organization = d.organization
 
 	return info, nil
 }
diff --git a/engine/api/authentication/ldap/ldap.go b/engine/api/authentication/ldap/ldap.go
index 51e5c64c4..e8b845fa8 100644
--- a/engine/api/authentication/ldap/ldap.go
+++ b/engine/api/authentication/ldap/ldap.go
@@ -83,7 +83,7 @@ func (d AuthDriver) GetUserInfo(ctx context.Context, req sdk.AuthConsumerSigninR
 		return userInfo, sdk.NewError(sdk.ErrUnauthorized, err)
 	}
 
-	entry, err := d.search(ctx, bind, "uid", "dn", "cn", "ou", "givenName", "sn", "mail", "memberOf")
+	entry, err := d.search(ctx, bind, "uid", "dn", "cn", "ou", "givenName", "sn", "mail", "memberOf", "company")
 	if err != nil && err.Error() != errUserNotFound {
 		return userInfo, sdk.NewError(sdk.ErrUnauthorized, err)
 	}
@@ -113,6 +113,7 @@ func (d AuthDriver) GetUserInfo(ctx context.Context, req sdk.AuthConsumerSigninR
 	userInfo.Email = entry[0].Attributes["mail"]
 	userInfo.ExternalID = entry[0].Attributes["uid"]
 	userInfo.Username = req.String("bind")
+	userInfo.Organization = req.String("company")
 
 	return userInfo, nil
 }
diff --git a/engine/api/authentication/local/driver.go b/engine/api/authentication/local/driver.go
index 856083636..36c84810c 100644
--- a/engine/api/authentication/local/driver.go
+++ b/engine/api/authentication/local/driver.go
@@ -11,7 +11,7 @@ import (
 var _ sdk.AuthDriver = new(AuthDriver)
 
 // NewDriver returns a new initialized driver for local authentication.
-func NewDriver(ctx context.Context, signupDisabled bool, uiURL, allowedDomains string) sdk.AuthDriver {
+func NewDriver(ctx context.Context, signupDisabled bool, uiURL, allowedDomains string, orga string) sdk.AuthDriver {
 	var domains []string
 
 	if allowedDomains != "" {
@@ -21,6 +21,7 @@ func NewDriver(ctx context.Context, signupDisabled bool, uiURL, allowedDomains s
 	return &AuthDriver{
 		signupDisabled: signupDisabled,
 		allowedDomains: domains,
+		organization:   orga,
 	}
 }
 
@@ -28,6 +29,7 @@ func NewDriver(ctx context.Context, signupDisabled bool, uiURL, allowedDomains s
 type AuthDriver struct {
 	signupDisabled bool
 	allowedDomains []string
+	organization   string
 }
 
 // GetManifest .
@@ -117,6 +119,5 @@ func (d AuthDriver) isAllowedDomain(email string) bool {
 
 // GetUserInfo .
 func (d AuthDriver) GetUserInfo(ctx context.Context, req sdk.AuthConsumerSigninRequest) (sdk.AuthDriverUserInfo, error) {
-	// not used for local auth
-	return sdk.AuthDriverUserInfo{}, nil
+	return sdk.AuthDriverUserInfo{Organization: d.organization}, nil
 }
diff --git a/engine/api/authentication/oidc/openid.go b/engine/api/authentication/oidc/openid.go
index 943752dc0..069b5bf9c 100644
--- a/engine/api/authentication/oidc/openid.go
+++ b/engine/api/authentication/oidc/openid.go
@@ -18,7 +18,7 @@ var _ sdk.AuthDriverWithRedirect = (*authDriver)(nil)
 var _ sdk.AuthDriverWithSigninStateToken = (*authDriver)(nil)
 
 // NewDriver returns a new OIDC auth driver for given config.
-func NewDriver(signupDisabled bool, cdsURL, url, clientID, clientSecret string) (sdk.AuthDriver, error) {
+func NewDriver(signupDisabled bool, cdsURL, url, clientID, clientSecret, orga string) (sdk.AuthDriver, error) {
 	provider, err := oidc.NewProvider(context.Background(), url)
 	if err != nil {
 		return nil, sdk.WrapError(err, "failed to initialize OIDC driver")
@@ -43,6 +43,7 @@ func NewDriver(signupDisabled bool, cdsURL, url, clientID, clientSecret string)
 		cdsURL:         cdsURL,
 		OAuth2Config:   oauth2Config,
 		Verifier:       verifier,
+		organization:   orga,
 	}, nil
 }
 
@@ -51,6 +52,7 @@ type authDriver struct {
 	cdsURL         string
 	OAuth2Config   oauth2.Config
 	Verifier       *oidc.IDTokenVerifier
+	organization   string
 }
 
 func (d authDriver) GetManifest() sdk.AuthDriverManifest {
@@ -137,6 +139,7 @@ func (d authDriver) GetUserInfo(ctx context.Context, req sdk.AuthConsumerSigninR
 	if info.Email, ok = tokenClaim["email"].(string); !ok {
 		return info, sdk.WithStack(errors.New("missing user's email in OIDC token claim"))
 	}
+	info.Organization = d.organization
 
 	return info, nil
 }
diff --git a/engine/api/organization/dao_organization.go b/engine/api/organization/dao_organization.go
new file mode 100644
index 000000000..733555fe9
--- /dev/null
+++ b/engine/api/organization/dao_organization.go
@@ -0,0 +1,84 @@
+package organization
+
+import (
+	"context"
+
+	"github.com/go-gorp/gorp"
+	"github.com/rockbears/log"
+
+	"github.com/ovh/cds/engine/api/database/gorpmapping"
+	"github.com/ovh/cds/engine/gorpmapper"
+	"github.com/ovh/cds/sdk"
+)
+
+func Insert(ctx context.Context, db gorpmapper.SqlExecutorWithTx, orga *sdk.Organization) error {
+	orga.ID = sdk.UUID()
+	dbData := &dbOrganization{Organization: *orga}
+	if err := gorpmapping.InsertAndSign(ctx, db, dbData); err != nil {
+		return err
+	}
+	*orga = dbData.Organization
+	return nil
+}
+
+func Delete(db gorpmapper.SqlExecutorWithTx, organizationID string) error {
+	_, err := db.Exec("DELETE FROM organization WHERE id = $1", organizationID)
+	return sdk.WrapError(err, "cannot delete organization %s", organizationID)
+}
+
+func getOrganization(ctx context.Context, db gorp.SqlExecutor, query gorpmapping.Query) (*sdk.Organization, error) {
+	var dbOrg dbOrganization
+	found, err := gorpmapping.Get(ctx, db, query, &dbOrg)
+	if err != nil {
+		return nil, err
+	}
+	if !found {
+		return nil, sdk.WithStack(sdk.ErrNotFound)
+	}
+
+	isValid, err := gorpmapping.CheckSignature(dbOrg, dbOrg.Signature)
+	if err != nil {
+		return nil, err
+	}
+	if !isValid {
+		log.Error(ctx, "organization %s / %s data corrupted", dbOrg.ID, dbOrg.Name)
+		return nil, sdk.WithStack(sdk.ErrNotFound)
+	}
+	return &dbOrg.Organization, nil
+}
+
+func getAllOrganizations(ctx context.Context, db gorp.SqlExecutor, query gorpmapping.Query) ([]sdk.Organization, error) {
+	var res []dbOrganization
+	if err := gorpmapping.GetAll(ctx, db, query, &res); err != nil {
+		return nil, err
+	}
+
+	orgs := make([]sdk.Organization, 0, len(res))
+	for _, r := range res {
+		isValid, err := gorpmapping.CheckSignature(r, r.Signature)
+		if err != nil {
+			return nil, err
+		}
+		if !isValid {
+			log.Error(ctx, "organization %d / %s data corrupted", r.ID, r.Name)
+			continue
+		}
+		orgs = append(orgs, r.Organization)
+	}
+	return orgs, nil
+}
+
+func LoadAllOrganizations(ctx context.Context, db gorp.SqlExecutor) ([]sdk.Organization, error) {
+	query := gorpmapping.NewQuery(`SELECT organization.* FROM organization`)
+	return getAllOrganizations(ctx, db, query)
+}
+
+func LoadOrganizationByName(ctx context.Context, db gorp.SqlExecutor, name string) (*sdk.Organization, error) {
+	query := gorpmapping.NewQuery(`SELECT organization.* FROM organization WHERE organization.name = $1`).Args(name)
+	return getOrganization(ctx, db, query)
+}
+
+func LoadOrganizationByID(ctx context.Context, db gorp.SqlExecutor, ID string) (*sdk.Organization, error) {
+	query := gorpmapping.NewQuery(`SELECT organization.* FROM organization WHERE organization.id = $1`).Args(ID)
+	return getOrganization(ctx, db, query)
+}
diff --git a/engine/api/organization/gorp_model.go b/engine/api/organization/gorp_model.go
new file mode 100644
index 000000000..736320d59
--- /dev/null
+++ b/engine/api/organization/gorp_model.go
@@ -0,0 +1,23 @@
+package organization
+
+import (
+	"github.com/ovh/cds/engine/api/database/gorpmapping"
+	"github.com/ovh/cds/engine/gorpmapper"
+	"github.com/ovh/cds/sdk"
+)
+
+func init() {
+	gorpmapping.Register(gorpmapping.New(dbOrganization{}, "organization", false, "id"))
+}
+
+type dbOrganization struct {
+	sdk.Organization
+	gorpmapper.SignedEntity
+}
+
+func (o dbOrganization) Canonical() gorpmapper.CanonicalForms {
+	_ = []interface{}{o.ID, o.Name}
+	return []gorpmapper.CanonicalForm{
+		"{{.ID}}{{.Name}}",
+	}
+}
diff --git a/engine/api/rbac/rule_global.go b/engine/api/rbac/rule_global.go
index 380187a25..6ea0a6bc0 100644
--- a/engine/api/rbac/rule_global.go
+++ b/engine/api/rbac/rule_global.go
@@ -30,7 +30,15 @@ func hasGlobalRole(ctx context.Context, auth *sdk.AuthConsumer, _ cache.Store, d
 	return nil
 }
 
-// PermissionManage return nil if the current AuthConsumer have the RoleManage on current project KEY
+// PermissionManage return nil if the current AuthConsumer have the ProjectRoleManage on current project KEY
 func PermissionManage(ctx context.Context, auth *sdk.AuthConsumer, store cache.Store, db gorp.SqlExecutor, _ map[string]string) error {
-	return hasGlobalRole(ctx, auth, store, db, sdk.RoleManagePermission)
+	return hasGlobalRole(ctx, auth, store, db, sdk.GlobalRoleManagePermission)
+}
+
+func OrganizationManage(ctx context.Context, auth *sdk.AuthConsumer, store cache.Store, db gorp.SqlExecutor, _ map[string]string) error {
+	return hasGlobalRole(ctx, auth, store, db, sdk.GlobalRoleManageOrganization)
+}
+
+func RegionManage(ctx context.Context, auth *sdk.AuthConsumer, store cache.Store, db gorp.SqlExecutor, _ map[string]string) error {
+	return hasGlobalRole(ctx, auth, store, db, sdk.GlobalRoleManageRegion)
 }
diff --git a/engine/api/rbac/rule_project.go b/engine/api/rbac/rule_project.go
index 2b9835796..d31726563 100644
--- a/engine/api/rbac/rule_project.go
+++ b/engine/api/rbac/rule_project.go
@@ -35,14 +35,14 @@ func hasRoleOnProject(ctx context.Context, auth *sdk.AuthConsumer, store cache.S
 	return nil
 }
 
-// ProjectManage return nil if the current AuthConsumer have the RoleManage on current project KEY
+// ProjectManage return nil if the current AuthConsumer have the ProjectRoleManage on current project KEY
 func ProjectManage(ctx context.Context, auth *sdk.AuthConsumer, store cache.Store, db gorp.SqlExecutor, vars map[string]string) error {
 	projectKey := vars["projectKey"]
-	return hasRoleOnProject(ctx, auth, store, db, projectKey, sdk.RoleManage)
+	return hasRoleOnProject(ctx, auth, store, db, projectKey, sdk.ProjectRoleManage)
 }
 
-// ProjectRead return nil if the current AuthConsumer have the RoleRead on current project KEY
+// ProjectRead return nil if the current AuthConsumer have the ProjectRoleRead on current project KEY
 func ProjectRead(ctx context.Context, auth *sdk.AuthConsumer, store cache.Store, db gorp.SqlExecutor, vars map[string]string) error {
 	projectKey := vars["projectKey"]
-	return hasRoleOnProject(ctx, auth, store, db, projectKey, sdk.RoleRead)
+	return hasRoleOnProject(ctx, auth, store, db, projectKey, sdk.ProjectRoleRead)
 }
diff --git a/engine/api/region/dao_region.go b/engine/api/region/dao_region.go
new file mode 100644
index 000000000..d2f1f046e
--- /dev/null
+++ b/engine/api/region/dao_region.go
@@ -0,0 +1,84 @@
+package region
+
+import (
+	"context"
+
+	"github.com/go-gorp/gorp"
+	"github.com/rockbears/log"
+
+	"github.com/ovh/cds/engine/api/database/gorpmapping"
+	"github.com/ovh/cds/engine/gorpmapper"
+	"github.com/ovh/cds/sdk"
+)
+
+func Insert(ctx context.Context, db gorpmapper.SqlExecutorWithTx, region *sdk.Region) error {
+	region.ID = sdk.UUID()
+	dbData := &dbRegion{Region: *region}
+	if err := gorpmapping.InsertAndSign(ctx, db, dbData); err != nil {
+		return err
+	}
+	*region = dbData.Region
+	return nil
+}
+
+func Delete(db gorpmapper.SqlExecutorWithTx, regionID string) error {
+	_, err := db.Exec("DELETE FROM region WHERE id = $1", regionID)
+	return sdk.WrapError(err, "cannot delete region %s", regionID)
+}
+
+func getRegion(ctx context.Context, db gorp.SqlExecutor, query gorpmapping.Query) (*sdk.Region, error) {
+	var dbRegion dbRegion
+	found, err := gorpmapping.Get(ctx, db, query, &dbRegion)
+	if err != nil {
+		return nil, err
+	}
+	if !found {
+		return nil, sdk.WithStack(sdk.ErrNotFound)
+	}
+
+	isValid, err := gorpmapping.CheckSignature(dbRegion, dbRegion.Signature)
+	if err != nil {
+		return nil, err
+	}
+	if !isValid {
+		log.Error(ctx, "region %s / %s data corrupted", dbRegion.ID, dbRegion.Name)
+		return nil, sdk.WithStack(sdk.ErrNotFound)
+	}
+	return &dbRegion.Region, nil
+}
+
+func getAllRegions(ctx context.Context, db gorp.SqlExecutor, query gorpmapping.Query) ([]sdk.Region, error) {
+	var res []dbRegion
+	if err := gorpmapping.GetAll(ctx, db, query, &res); err != nil {
+		return nil, err
+	}
+
+	regions := make([]sdk.Region, 0, len(res))
+	for _, r := range res {
+		isValid, err := gorpmapping.CheckSignature(r, r.Signature)
+		if err != nil {
+			return nil, err
+		}
+		if !isValid {
+			log.Error(ctx, "region %d / %s data corrupted", r.ID, r.Name)
+			continue
+		}
+		regions = append(regions, r.Region)
+	}
+	return regions, nil
+}
+
+func LoadAllRegions(ctx context.Context, db gorp.SqlExecutor) ([]sdk.Region, error) {
+	query := gorpmapping.NewQuery(`SELECT region.* FROM region`)
+	return getAllRegions(ctx, db, query)
+}
+
+func LoadRegionByName(ctx context.Context, db gorp.SqlExecutor, name string) (*sdk.Region, error) {
+	query := gorpmapping.NewQuery(`SELECT region.* FROM region WHERE region.name = $1`).Args(name)
+	return getRegion(ctx, db, query)
+}
+
+func LoadRegionByID(ctx context.Context, db gorp.SqlExecutor, ID string) (*sdk.Region, error) {
+	query := gorpmapping.NewQuery(`SELECT region.* FROM region WHERE region.id = $1`).Args(ID)
+	return getRegion(ctx, db, query)
+}
diff --git a/engine/api/region/gorp_model.go b/engine/api/region/gorp_model.go
new file mode 100644
index 000000000..7a032d199
--- /dev/null
+++ b/engine/api/region/gorp_model.go
@@ -0,0 +1,23 @@
+package region
+
+import (
+	"github.com/ovh/cds/engine/api/database/gorpmapping"
+	"github.com/ovh/cds/engine/gorpmapper"
+	"github.com/ovh/cds/sdk"
+)
+
+func init() {
+	gorpmapping.Register(gorpmapping.New(dbRegion{}, "region", false, "id"))
+}
+
+type dbRegion struct {
+	sdk.Region
+	gorpmapper.SignedEntity
+}
+
+func (o dbRegion) Canonical() gorpmapper.CanonicalForms {
+	_ = []interface{}{o.ID, o.Name}
+	return []gorpmapper.CanonicalForm{
+		"{{.ID}}{{.Name}}",
+	}
+}
diff --git a/engine/api/router_middleware_auth_permission.go b/engine/api/router_middleware_auth_permission.go
index 5ea5c1fbd..04def9b96 100644
--- a/engine/api/router_middleware_auth_permission.go
+++ b/engine/api/router_middleware_auth_permission.go
@@ -3,6 +3,7 @@ package api
 import (
 	"context"
 	"net/http"
+	"net/url"
 	"strconv"
 
 	"github.com/rockbears/log"
@@ -406,7 +407,12 @@ func (api *API) checkActionPermissions(ctx context.Context, w http.ResponseWrite
 		return err
 	}
 
-	a, err := action.LoadTypeDefaultByNameAndGroupID(ctx, api.mustDB(), actionName, g.ID)
+	name, err := url.PathUnescape(actionName)
+	if err != nil {
+		return sdk.NewErrorFrom(sdk.ErrWrongRequest, "%s", err)
+	}
+
+	a, err := action.LoadTypeDefaultByNameAndGroupID(ctx, api.mustDB(), name, g.ID)
 	if err != nil {
 		return err
 	}
@@ -422,7 +428,12 @@ func (api *API) checkActionBuiltinPermissions(ctx context.Context, w http.Respon
 		return sdk.WrapError(sdk.ErrWrongRequest, "invalid given action name")
 	}
 
-	a, err := action.LoadByTypesAndName(ctx, api.mustDB(), []string{sdk.BuiltinAction, sdk.PluginAction}, actionName)
+	name, err := url.PathUnescape(actionName)
+	if err != nil {
+		return sdk.NewErrorFrom(sdk.ErrWrongRequest, "%s", err)
+	}
+
+	a, err := action.LoadByTypesAndName(ctx, api.mustDB(), []string{sdk.BuiltinAction, sdk.PluginAction}, name)
 	if err != nil {
 		return err
 	}
diff --git a/engine/api/user/dao.go b/engine/api/user/dao.go
index a37e48b27..0582286a6 100644
--- a/engine/api/user/dao.go
+++ b/engine/api/user/dao.go
@@ -80,6 +80,14 @@ func Get(ctx context.Context, db gorp.SqlExecutor, q gorpmapping.Query, opts ...
 	return &au, nil
 }
 
+func LoadUsersWithoutOrganization(ctx context.Context, db gorp.SqlExecutor) ([]sdk.AuthentifiedUser, error) {
+	query := gorpmapping.NewQuery(`
+		SELECT au.* FROM authentified_user au
+		LEFT JOIN authentified_user_organization auo ON auo.authentified_user_id = au.id
+		WHERE auo.authentified_user_id is null`)
+	return getAll(ctx, db, query)
+}
+
 // LoadAll returns all users from database.
 func LoadAll(ctx context.Context, db gorp.SqlExecutor, opts ...LoadOptionFunc) (sdk.AuthentifiedUsers, error) {
 	query := gorpmapping.NewQuery("SELECT * FROM authentified_user")
diff --git a/engine/api/v2_organization.go b/engine/api/v2_organization.go
new file mode 100644
index 000000000..683b36d1f
--- /dev/null
+++ b/engine/api/v2_organization.go
@@ -0,0 +1,101 @@
+package api
+
+import (
+	"context"
+	"net/http"
+
+	"github.com/gorilla/mux"
+
+	"github.com/ovh/cds/engine/api/organization"
+	"github.com/ovh/cds/engine/api/rbac"
+	"github.com/ovh/cds/engine/service"
+	"github.com/ovh/cds/sdk"
+)
+
+func (api *API) getOrganizationByIdentifier(ctx context.Context, orgaIdentifier string) (*sdk.Organization, error) {
+	var orga *sdk.Organization
+	var err error
+	if sdk.IsValidUUID(orgaIdentifier) {
+		orga, err = organization.LoadOrganizationByID(ctx, api.mustDB(), orgaIdentifier)
+	} else {
+		orga, err = organization.LoadOrganizationByName(ctx, api.mustDB(), orgaIdentifier)
+	}
+	if err != nil {
+		return nil, err
+	}
+	return orga, nil
+}
+
+func (api *API) postOrganizationHandler() ([]service.RbacChecker, service.Handler) {
+	return service.RBAC(rbac.OrganizationManage),
+		func(ctx context.Context, w http.ResponseWriter, req *http.Request) error {
+
+			var org sdk.Organization
+			if err := service.UnmarshalBody(req, &org); err != nil {
+				return err
+			}
+
+			tx, err := api.mustDB().Begin()
+			if err != nil {
+				return sdk.WithStack(err)
+			}
+			defer tx.Rollback() // nolint
+
+			if err := organization.Insert(ctx, tx, &org); err != nil {
+				return err
+			}
+			if err := tx.Commit(); err != nil {
+				return sdk.WithStack(err)
+			}
+			return service.WriteMarshal(w, req, nil, http.StatusCreated)
+		}
+}
+
+func (api *API) getOrganizationsHandler() ([]service.RbacChecker, service.Handler) {
+	return service.RBAC(rbac.OrganizationManage),
+		func(ctx context.Context, w http.ResponseWriter, req *http.Request) error {
+			orgas, err := organization.LoadAllOrganizations(ctx, api.mustDB())
+			if err != nil {
+				return err
+			}
+			return service.WriteMarshal(w, req, orgas, http.StatusOK)
+		}
+}
+
+func (api *API) getOrganizationHandler() ([]service.RbacChecker, service.Handler) {
+	return service.RBAC(rbac.OrganizationManage),
+		func(ctx context.Context, w http.ResponseWriter, req *http.Request) error {
+			vars := mux.Vars(req)
+			orgaIdentifier := vars["organizationIdentifier"]
+
+			orga, err := api.getOrganizationByIdentifier(ctx, orgaIdentifier)
+			if err != nil {
+				return err
+			}
+			return service.WriteMarshal(w, req, orga, http.StatusOK)
+		}
+}
+
+func (api *API) deleteOrganizationHandler() ([]service.RbacChecker, service.Handler) {
+	return service.RBAC(rbac.OrganizationManage),
+		func(ctx context.Context, w http.ResponseWriter, req *http.Request) error {
+			vars := mux.Vars(req)
+			orgaIdentifier := vars["organizationIdentifier"]
+
+			orga, err := api.getOrganizationByIdentifier(ctx, orgaIdentifier)
+			if err != nil {
+				return err
+			}
+
+			tx, err := api.mustDB().Begin()
+			if err != nil {
+				return sdk.WithStack(err)
+			}
+			defer tx.Rollback() // nolint
+
+			if err := organization.Delete(tx, orga.ID); err != nil {
+				return err
+			}
+			return sdk.WithStack(tx.Commit())
+		}
+}
diff --git a/engine/api/v2_region.go b/engine/api/v2_region.go
new file mode 100644
index 000000000..e5197f9af
--- /dev/null
+++ b/engine/api/v2_region.go
@@ -0,0 +1,101 @@
+package api
+
+import (
+	"context"
+	"net/http"
+
+	"github.com/gorilla/mux"
+
+	"github.com/ovh/cds/engine/api/rbac"
+	"github.com/ovh/cds/engine/api/region"
+	"github.com/ovh/cds/engine/service"
+	"github.com/ovh/cds/sdk"
+)
+
+func (api *API) getRegionByIdentifier(ctx context.Context, regionIdentifier string) (*sdk.Region, error) {
+	var reg *sdk.Region
+	var err error
+	if sdk.IsValidUUID(regionIdentifier) {
+		reg, err = region.LoadRegionByID(ctx, api.mustDB(), regionIdentifier)
+	} else {
+		reg, err = region.LoadRegionByName(ctx, api.mustDB(), regionIdentifier)
+	}
+	if err != nil {
+		return nil, err
+	}
+	return reg, nil
+}
+
+func (api *API) postRegionHandler() ([]service.RbacChecker, service.Handler) {
+	return service.RBAC(rbac.RegionManage),
+		func(ctx context.Context, w http.ResponseWriter, req *http.Request) error {
+
+			var reg sdk.Region
+			if err := service.UnmarshalBody(req, &reg); err != nil {
+				return err
+			}
+
+			tx, err := api.mustDB().Begin()
+			if err != nil {
+				return sdk.WithStack(err)
+			}
+			defer tx.Rollback() // nolint
+
+			if err := region.Insert(ctx, tx, &reg); err != nil {
+				return err
+			}
+			if err := tx.Commit(); err != nil {
+				return sdk.WithStack(err)
+			}
+			return service.WriteMarshal(w, req, nil, http.StatusCreated)
+		}
+}
+
+func (api *API) getRegionsHandler() ([]service.RbacChecker, service.Handler) {
+	return nil,
+		func(ctx context.Context, w http.ResponseWriter, req *http.Request) error {
+			regions, err := region.LoadAllRegions(ctx, api.mustDB())
+			if err != nil {
+				return err
+			}
+			return service.WriteMarshal(w, req, regions, http.StatusOK)
+		}
+}
+
+func (api *API) getRegionHandler() ([]service.RbacChecker, service.Handler) {
+	return nil,
+		func(ctx context.Context, w http.ResponseWriter, req *http.Request) error {
+			vars := mux.Vars(req)
+			regionIdentifier := vars["regionIdentifier"]
+
+			reg, err := api.getRegionByIdentifier(ctx, regionIdentifier)
+			if err != nil {
+				return err
+			}
+			return service.WriteMarshal(w, req, reg, http.StatusOK)
+		}
+}
+
+func (api *API) deleteRegionHandler() ([]service.RbacChecker, service.Handler) {
+	return service.RBAC(rbac.RegionManage),
+		func(ctx context.Context, w http.ResponseWriter, req *http.Request) error {
+			vars := mux.Vars(req)
+			regionIdentifier := vars["regionIdentifier"]
+
+			reg, err := api.getRegionByIdentifier(ctx, regionIdentifier)
+			if err != nil {
+				return err
+			}
+
+			tx, err := api.mustDB().Begin()
+			if err != nil {
+				return sdk.WithStack(err)
+			}
+			defer tx.Rollback() // nolint
+
+			if err := region.Delete(tx, reg.ID); err != nil {
+				return err
+			}
+			return sdk.WithStack(tx.Commit())
+		}
+}
diff --git a/engine/api/v2_repository_analyze.go b/engine/api/v2_repository_analyze.go
index 1ca73c4fb..199ad6694 100644
--- a/engine/api/v2_repository_analyze.go
+++ b/engine/api/v2_repository_analyze.go
@@ -325,7 +325,7 @@ func (api *API) analyzeRepository(ctx context.Context, projectRepoID string, ana
 			analysis.Data.CDSUserName = cdsUser.Username
 
 			// Check user right
-			b, err := rbac.HasRoleOnProjectAndUserID(ctx, api.mustDB(), sdk.RoleManage, cdsUser.ID, analysis.ProjectKey)
+			b, err := rbac.HasRoleOnProjectAndUserID(ctx, api.mustDB(), sdk.ProjectRoleManage, cdsUser.ID, analysis.ProjectKey)
 			if err != nil {
 				return api.stopAnalysis(ctx, analysis, err)
 			}
diff --git a/engine/api/workflow.go b/engine/api/workflow.go
index 066678fea..65fa89098 100644
--- a/engine/api/workflow.go
+++ b/engine/api/workflow.go
@@ -920,6 +920,7 @@ func (api *API) getSearchWorkflowHandler() service.Handler {
 		dao.Filters.WorkflowName = FormString(r, "name")
 		dao.Filters.VCSServer = FormString(r, "vcs")
 		dao.Filters.ApplicationRepository = FormString(r, "repository")
+		dao.Filters.AsCode = service.FormBool(r, "ascode")
 		dao.Loaders.WithRuns = service.FormInt(r, "runs")
 		dao.Loaders.WithFavoritesForUserID = getAPIConsumer(ctx).AuthentifiedUserID
 
diff --git a/engine/api/workflow/factory_dao.go b/engine/api/workflow/factory_dao.go
index 5079b176c..72e32bb9b 100644
--- a/engine/api/workflow/factory_dao.go
+++ b/engine/api/workflow/factory_dao.go
@@ -3,6 +3,7 @@ package workflow
 import (
 	"context"
 	"fmt"
+	"strings"
 	"time"
 
 	"github.com/go-gorp/gorp"
@@ -72,6 +73,7 @@ type LoadAllWorkflowsOptionsFilters struct {
 	GroupIDs                     []int64
 	WorkflowIDs                  []int64
 	DisableFilterDeletedWorkflow bool
+	AsCode                       bool
 }
 
 type LoadAllWorkflowsOptionsLoaders struct {
@@ -169,17 +171,21 @@ func (dao WorkflowDAO) Query() gorpmapping.Query {
 		args = append(args, pq.Int64Array(dao.Filters.WorkflowIDs))
 	}
 
+	queryFilters := make([]string, 0, len(filters))
 	for i, f := range filters {
-		if i == 0 {
-			queryString += " WHERE "
-		} else {
-			queryString += " AND "
-		}
-		queryString += fmt.Sprintf(f, i+1)
+		queryFilters = append(queryFilters, fmt.Sprintf(f, i+1))
 	}
 
 	if !dao.Filters.DisableFilterDeletedWorkflow {
-		queryString += " AND workflow.to_delete  = false"
+		queryFilters = append(queryFilters, "workflow.to_delete  = false")
+	}
+
+	if dao.Filters.AsCode {
+		queryFilters = append(queryFilters, "workflow.from_repository <> ''")
+	}
+
+	if len(queryFilters) > 0 {
+		queryString += "WHERE " + strings.Join(queryFilters, " AND ")
 	}
 
 	var order = " ORDER BY selected_workflow.projectkey, selected_workflow.workflow_name "
diff --git a/engine/repositories/cleaner.go b/engine/repositories/cleaner.go
index 69a4bd6d1..2e1f00a6c 100644
--- a/engine/repositories/cleaner.go
+++ b/engine/repositories/cleaner.go
@@ -87,7 +87,7 @@ func (s *Service) vacuumFileSystemCleanerFunc(ctx context.Context, repoUUID stri
 
 	if !s.dao.isExpired(ctx, repoUUID) {
 		log.Debug(ctx, "vacuumFileSystemCleanerFunc> %s is not expired. skipping", repoUUID)
-		_ = s.dao.unlock(ctx, repoUUID, 24*time.Hour*time.Duration(s.Cfg.RepositoriesRetention))
+		_ = s.dao.unlock(ctx, repoUUID)
 		return nil
 	}
 
diff --git a/engine/repositories/dao.go b/engine/repositories/dao.go
index 43133ce02..87686eaec 100644
--- a/engine/repositories/dao.go
+++ b/engine/repositories/dao.go
@@ -80,13 +80,10 @@ func (d *dao) deleteLock(ctx context.Context, uuid string) error {
 	return nil
 }
 
-func (d *dao) unlock(ctx context.Context, uuid string, retention time.Duration) error {
+func (d *dao) unlock(ctx context.Context, uuid string) error {
 	if err := d.store.Unlock(cache.Key(locksKey, uuid)); err != nil {
 		log.Error(ctx, "error on unlock uuid %s: %v", uuid, err)
 	}
-	if _, err := d.store.Lock(cache.Key(lastAccessKey, uuid), retention, -1, -1); err != nil {
-		return sdk.WrapError(err, "error on cache.lock uuid:%s", uuid)
-	}
 	return nil
 }
 
diff --git a/engine/repositories/processor.go b/engine/repositories/processor.go
index 31b366c40..3fc37981f 100644
--- a/engine/repositories/processor.go
+++ b/engine/repositories/processor.go
@@ -4,9 +4,11 @@ import (
 	"context"
 	"time"
 
+	"github.com/rockbears/log"
+
+	"github.com/ovh/cds/engine/cache"
 	"github.com/ovh/cds/sdk"
 	cdslog "github.com/ovh/cds/sdk/log"
-	"github.com/rockbears/log"
 )
 
 func (s *Service) processor(ctx context.Context) error {
@@ -51,7 +53,10 @@ func (s *Service) do(ctx context.Context, op sdk.Operation) error {
 	if s.dao.lock(r.ID()) == errLockUnavailable {
 		return errLockUnavailable
 	}
-	defer s.dao.unlock(ctx, r.ID(), 24*time.Hour*time.Duration(s.Cfg.RepositoriesRetention)) // nolint
+	defer func() {
+		s.dao.unlock(ctx, r.ID())
+		s.dao.store.Lock(cache.Key(lastAccessKey, r.ID()), 24*time.Hour*time.Duration(s.Cfg.RepositoriesRetention), -1, -1)
+	}()
 
 	switch {
 	// Load workflow as code file
diff --git a/engine/sql/api/253_organization.sql b/engine/sql/api/253_organization.sql
new file mode 100644
index 000000000..ebaf82665
--- /dev/null
+++ b/engine/sql/api/253_organization.sql
@@ -0,0 +1,12 @@
+-- +migrate Up
+CREATE TABLE IF NOT EXISTS "organization" (
+    "id" uuid PRIMARY KEY,
+    "name" TEXT,
+    "sig"           BYTEA,
+    "signer"        TEXT
+);
+SELECT create_unique_index('organization', 'idx_unq_organization_name', 'name');
+
+
+-- +migrate Down
+DROP TABLE organization;
diff --git a/engine/sql/api/254_region.sql b/engine/sql/api/254_region.sql
new file mode 100644
index 000000000..37a842272
--- /dev/null
+++ b/engine/sql/api/254_region.sql
@@ -0,0 +1,11 @@
+-- +migrate Up
+CREATE TABLE IF NOT EXISTS "region" (
+    "id" uuid PRIMARY KEY,
+    "name" TEXT,
+    "sig"           BYTEA,
+    "signer"        TEXT
+);
+SELECT create_unique_index('region', 'idx_unq_region_name', 'name');
+
+-- +migrate Down
+DROP TABLE region;
diff --git a/engine/types.go b/engine/types.go
index 8623da5ea..be73f88f7 100644
--- a/engine/types.go
+++ b/engine/types.go
@@ -22,7 +22,7 @@ import (
 type Configuration struct {
 	// common
 	Log struct {
-		Level   string `toml:"level" default:"warning" comment:"Log Level: debug, info, warning, notice, critical" json:"level"`
+		Level   string `toml:"level" default:"warning" comment:"Log Level: debug, info, warning, notice, error" json:"level"`
 		Format  string `toml:"format" default:"text" comment:"Stdout format: text, json, discard" json:"format"`
 		Graylog struct {
 			Host       string `toml:"host" comment:"Example: thot.ovh.com" json:"host"`
diff --git a/engine/vcs/bitbucketcloud/client_hook.go b/engine/vcs/bitbucketcloud/client_hook.go
index 8b52ccedd..f3e8f73ca 100644
--- a/engine/vcs/bitbucketcloud/client_hook.go
+++ b/engine/vcs/bitbucketcloud/client_hook.go
@@ -145,6 +145,9 @@ func (client *bitbucketcloudClient) UpdateHook(ctx context.Context, repo string,
 	}
 
 	bitbucketHook.Events = hook.Events
+	bitbucketHook.URL = hook.URL
+	bitbucketHook.Description = "CDS webhook"
+	bitbucketHook.Active = true
 	b, err := json.Marshal(bitbucketHook)
 	if err != nil {
 		return sdk.WrapError(err, "cannot marshal body %+v", bitbucketHook)
diff --git a/engine/worker/cmd.go b/engine/worker/cmd.go
index 6bbfc5d8f..e2ade251b 100644
--- a/engine/worker/cmd.go
+++ b/engine/worker/cmd.go
@@ -53,7 +53,7 @@ func initFlagsRun(cmd *cobra.Command) {
 	flags.String(flagGraylogPort, "", "Ex: --graylog-port=12202")
 	flags.String(flagGraylogExtraKey, "", "Ex: --graylog-extra-key=xxxx-yyyy")
 	flags.String(flagGraylogExtraValue, "", "Ex: --graylog-extra-value=xxxx-yyyy")
-	flags.String(flagLogLevel, "notice", "Log Level: debug, info, notice, warning, critical")
+	flags.String(flagLogLevel, "notice", "Log Level: debug, info, notice, warning, error")
 	flags.String(flagAPI, "", "URL of CDS API")
 	flags.Bool(flagInsecure, false, `(SSL) This option explicitly allows curl to perform "insecure" SSL connections and transfers.`)
 	flags.String(flagToken, "", "CDS Token")
diff --git a/sdk/cdsclient/client_admin.go b/sdk/cdsclient/client_admin.go
index 631eee5d9..e0d05172b 100644
--- a/sdk/cdsclient/client_admin.go
+++ b/sdk/cdsclient/client_admin.go
@@ -14,6 +14,35 @@ import (
 	"github.com/ovh/cds/sdk"
 )
 
+func (c *client) AdminOrganizationMigrateUser(ctx context.Context, orgaIdentifier string) error {
+	if _, err := c.PostJSON(ctx, fmt.Sprintf("/admin/organization/%s/migrate-user", orgaIdentifier), nil, nil); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (c *client) AdminOrganizationCreate(ctx context.Context, orga sdk.Organization) error {
+	if _, err := c.PostJSON(ctx, "/admin/organization", &orga, nil); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (c *client) AdminOrganizationList(ctx context.Context) ([]sdk.Organization, error) {
+	var orgas []sdk.Organization
+	if _, err := c.GetJSON(ctx, "/admin/organization", &orgas); err != nil {
+		return nil, err
+	}
+	return orgas, nil
+}
+
+func (c *client) AdminOrganizationDelete(ctx context.Context, orgaIdentifier string) error {
+	if _, err := c.DeleteJSON(ctx, "/admin/organization/"+orgaIdentifier, nil); err != nil {
+		return err
+	}
+	return nil
+}
+
 func (c *client) AdminDatabaseMigrationDelete(service string, id string) error {
 	path := fmt.Sprintf("/admin/database/migration/delete/%s", url.QueryEscape(id))
 	var f = c.switchServiceCallFunc(service, http.MethodDelete, path, nil, nil)
diff --git a/sdk/cdsclient/client_organization.go b/sdk/cdsclient/client_organization.go
new file mode 100644
index 000000000..4c89284d8
--- /dev/null
+++ b/sdk/cdsclient/client_organization.go
@@ -0,0 +1,37 @@
+package cdsclient
+
+import (
+	"context"
+
+	"github.com/ovh/cds/sdk"
+)
+
+func (c *client) OrganizationAdd(ctx context.Context, orga sdk.Organization) error {
+	if _, err := c.PostJSON(ctx, "/v2/organization", &orga, nil); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (c *client) OrganizationGet(ctx context.Context, organizationIdentifier string) (sdk.Organization, error) {
+	var orga sdk.Organization
+	if _, err := c.GetJSON(ctx, "/v2/organization/"+organizationIdentifier, &orga, nil); err != nil {
+		return orga, err
+	}
+	return orga, nil
+}
+
+func (c *client) OrganizationList(ctx context.Context) ([]sdk.Organization, error) {
+	var orgs []sdk.Organization
+	if _, err := c.GetJSON(ctx, "/v2/organization", &orgs, nil); err != nil {
+		return nil, err
+	}
+	return orgs, nil
+}
+
+func (c *client) OrganizationDelete(ctx context.Context, organizationIdentifier string) error {
+	if _, err := c.DeleteJSON(ctx, "/v2/organization/"+organizationIdentifier, nil, nil); err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/sdk/cdsclient/client_region.go b/sdk/cdsclient/client_region.go
new file mode 100644
index 000000000..cfa079cfc
--- /dev/null
+++ b/sdk/cdsclient/client_region.go
@@ -0,0 +1,37 @@
+package cdsclient
+
+import (
+	"context"
+
+	"github.com/ovh/cds/sdk"
+)
+
+func (c *client) RegionAdd(ctx context.Context, orga sdk.Region) error {
+	if _, err := c.PostJSON(ctx, "/v2/region", &orga, nil); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (c *client) RegionGet(ctx context.Context, regionIdentifier string) (sdk.Region, error) {
+	var reg sdk.Region
+	if _, err := c.GetJSON(ctx, "/v2/region/"+regionIdentifier, &reg, nil); err != nil {
+		return reg, err
+	}
+	return reg, nil
+}
+
+func (c *client) RegionList(ctx context.Context) ([]sdk.Region, error) {
+	var regions []sdk.Region
+	if _, err := c.GetJSON(ctx, "/v2/region", &regions, nil); err != nil {
+		return nil, err
+	}
+	return regions, nil
+}
+
+func (c *client) RegionDelete(ctx context.Context, regionIdentifier string) error {
+	if _, err := c.DeleteJSON(ctx, "/v2/region/"+regionIdentifier, nil, nil); err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/sdk/cdsclient/interface.go b/sdk/cdsclient/interface.go
index 7dc8ed192..fc6f2d6ef 100644
--- a/sdk/cdsclient/interface.go
+++ b/sdk/cdsclient/interface.go
@@ -50,6 +50,10 @@ type Admin interface {
 	AdminCDSMigrationCancel(id int64) error
 	AdminCDSMigrationReset(id int64) error
 	AdminWorkflowUpdateMaxRuns(projectKey string, workflowName string, maxRuns int64) error
+	AdminOrganizationCreate(ctx context.Context, orga sdk.Organization) error
+	AdminOrganizationList(ctx context.Context) ([]sdk.Organization, error)
+	AdminOrganizationDelete(ctx context.Context, orgaIdentifier string) error
+	AdminOrganizationMigrateUser(ctx context.Context, orgaIdentifier string) error
 	Features() ([]sdk.Feature, error)
 	FeatureCreate(f sdk.Feature) error
 	FeatureDelete(name sdk.FeatureName) error
@@ -198,6 +202,20 @@ type MaintenanceClient interface {
 	Maintenance(enable bool, hooks bool) error
 }
 
+type OrganizationClient interface {
+	OrganizationAdd(ctx context.Context, organization sdk.Organization) error
+	OrganizationGet(ctx context.Context, organizationIdentifier string) (sdk.Organization, error)
+	OrganizationList(ctx context.Context) ([]sdk.Organization, error)
+	OrganizationDelete(ctx context.Context, organizationIdentifier string) error
+}
+
+type RegionClient interface {
+	RegionAdd(ctx context.Context, region sdk.Region) error
+	RegionGet(ctx context.Context, regionIdentifier string) (sdk.Region, error)
+	RegionList(ctx context.Context) ([]sdk.Region, error)
+	RegionDelete(ctx context.Context, regionIdentifier string) error
+}
+
 // ProjectClient exposes project related functions
 type ProjectClient interface {
 	ProjectCreate(proj *sdk.Project) error
@@ -415,6 +433,8 @@ type Interface interface {
 	IntegrationClient
 	ProjectClient
 	RBACClient
+	OrganizationClient
+	RegionClient
 	QueueClient
 	Navbar() ([]sdk.NavbarProjectData, error)
 	Requirements() ([]sdk.Requirement, error)
diff --git a/sdk/cdsclient/mock_cdsclient/interface_mock.go b/sdk/cdsclient/mock_cdsclient/interface_mock.go
index 5b8913caa..7c529d7ad 100644
--- a/sdk/cdsclient/mock_cdsclient/interface_mock.go
+++ b/sdk/cdsclient/mock_cdsclient/interface_mock.go
@@ -393,6 +393,63 @@ func (mr *MockAdminMockRecorder) AdminDatabaseSignaturesRollEntity(service, e, i
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AdminDatabaseSignaturesRollEntity", reflect.TypeOf((*MockAdmin)(nil).AdminDatabaseSignaturesRollEntity), service, e, idx)
 }
 
+// AdminOrganizationCreate mocks base method.
+func (m *MockAdmin) AdminOrganizationCreate(ctx context.Context, orga sdk.Organization) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "AdminOrganizationCreate", ctx, orga)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// AdminOrganizationCreate indicates an expected call of AdminOrganizationCreate.
+func (mr *MockAdminMockRecorder) AdminOrganizationCreate(ctx, orga interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AdminOrganizationCreate", reflect.TypeOf((*MockAdmin)(nil).AdminOrganizationCreate), ctx, orga)
+}
+
+// AdminOrganizationDelete mocks base method.
+func (m *MockAdmin) AdminOrganizationDelete(ctx context.Context, orgaIdentifier string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "AdminOrganizationDelete", ctx, orgaIdentifier)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// AdminOrganizationDelete indicates an expected call of AdminOrganizationDelete.
+func (mr *MockAdminMockRecorder) AdminOrganizationDelete(ctx, orgaIdentifier interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AdminOrganizationDelete", reflect.TypeOf((*MockAdmin)(nil).AdminOrganizationDelete), ctx, orgaIdentifier)
+}
+
+// AdminOrganizationList mocks base method.
+func (m *MockAdmin) AdminOrganizationList(ctx context.Context) ([]sdk.Organization, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "AdminOrganizationList", ctx)
+	ret0, _ := ret[0].([]sdk.Organization)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// AdminOrganizationList indicates an expected call of AdminOrganizationList.
+func (mr *MockAdminMockRecorder) AdminOrganizationList(ctx interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AdminOrganizationList", reflect.TypeOf((*MockAdmin)(nil).AdminOrganizationList), ctx)
+}
+
+// AdminOrganizationMigrateUser mocks base method.
+func (m *MockAdmin) AdminOrganizationMigrateUser(ctx context.Context, orgaIdentifier string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "AdminOrganizationMigrateUser", ctx, orgaIdentifier)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// AdminOrganizationMigrateUser indicates an expected call of AdminOrganizationMigrateUser.
+func (mr *MockAdminMockRecorder) AdminOrganizationMigrateUser(ctx, orgaIdentifier interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AdminOrganizationMigrateUser", reflect.TypeOf((*MockAdmin)(nil).AdminOrganizationMigrateUser), ctx, orgaIdentifier)
+}
+
 // AdminWorkflowUpdateMaxRuns mocks base method.
 func (m *MockAdmin) AdminWorkflowUpdateMaxRuns(projectKey, workflowName string, maxRuns int64) error {
 	m.ctrl.T.Helper()
@@ -2299,6 +2356,168 @@ func (mr *MockMaintenanceClientMockRecorder) Maintenance(enable, hooks interface
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Maintenance", reflect.TypeOf((*MockMaintenanceClient)(nil).Maintenance), enable, hooks)
 }
 
+// MockOrganizationClient is a mock of OrganizationClient interface.
+type MockOrganizationClient struct {
+	ctrl     *gomock.Controller
+	recorder *MockOrganizationClientMockRecorder
+}
+
+// MockOrganizationClientMockRecorder is the mock recorder for MockOrganizationClient.
+type MockOrganizationClientMockRecorder struct {
+	mock *MockOrganizationClient
+}
+
+// NewMockOrganizationClient creates a new mock instance.
+func NewMockOrganizationClient(ctrl *gomock.Controller) *MockOrganizationClient {
+	mock := &MockOrganizationClient{ctrl: ctrl}
+	mock.recorder = &MockOrganizationClientMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockOrganizationClient) EXPECT() *MockOrganizationClientMockRecorder {
+	return m.recorder
+}
+
+// OrganizationAdd mocks base method.
+func (m *MockOrganizationClient) OrganizationAdd(ctx context.Context, organization sdk.Organization) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "OrganizationAdd", ctx, organization)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// OrganizationAdd indicates an expected call of OrganizationAdd.
+func (mr *MockOrganizationClientMockRecorder) OrganizationAdd(ctx, organization interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrganizationAdd", reflect.TypeOf((*MockOrganizationClient)(nil).OrganizationAdd), ctx, organization)
+}
+
+// OrganizationDelete mocks base method.
+func (m *MockOrganizationClient) OrganizationDelete(ctx context.Context, organizationIdentifier string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "OrganizationDelete", ctx, organizationIdentifier)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// OrganizationDelete indicates an expected call of OrganizationDelete.
+func (mr *MockOrganizationClientMockRecorder) OrganizationDelete(ctx, organizationIdentifier interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrganizationDelete", reflect.TypeOf((*MockOrganizationClient)(nil).OrganizationDelete), ctx, organizationIdentifier)
+}
+
+// OrganizationGet mocks base method.
+func (m *MockOrganizationClient) OrganizationGet(ctx context.Context, organizationIdentifier string) (sdk.Organization, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "OrganizationGet", ctx, organizationIdentifier)
+	ret0, _ := ret[0].(sdk.Organization)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// OrganizationGet indicates an expected call of OrganizationGet.
+func (mr *MockOrganizationClientMockRecorder) OrganizationGet(ctx, organizationIdentifier interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrganizationGet", reflect.TypeOf((*MockOrganizationClient)(nil).OrganizationGet), ctx, organizationIdentifier)
+}
+
+// OrganizationList mocks base method.
+func (m *MockOrganizationClient) OrganizationList(ctx context.Context) ([]sdk.Organization, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "OrganizationList", ctx)
+	ret0, _ := ret[0].([]sdk.Organization)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// OrganizationList indicates an expected call of OrganizationList.
+func (mr *MockOrganizationClientMockRecorder) OrganizationList(ctx interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrganizationList", reflect.TypeOf((*MockOrganizationClient)(nil).OrganizationList), ctx)
+}
+
+// MockRegionClient is a mock of RegionClient interface.
+type MockRegionClient struct {
+	ctrl     *gomock.Controller
+	recorder *MockRegionClientMockRecorder
+}
+
+// MockRegionClientMockRecorder is the mock recorder for MockRegionClient.
+type MockRegionClientMockRecorder struct {
+	mock *MockRegionClient
+}
+
+// NewMockRegionClient creates a new mock instance.
+func NewMockRegionClient(ctrl *gomock.Controller) *MockRegionClient {
+	mock := &MockRegionClient{ctrl: ctrl}
+	mock.recorder = &MockRegionClientMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockRegionClient) EXPECT() *MockRegionClientMockRecorder {
+	return m.recorder
+}
+
+// RegionAdd mocks base method.
+func (m *MockRegionClient) RegionAdd(ctx context.Context, region sdk.Region) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "RegionAdd", ctx, region)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// RegionAdd indicates an expected call of RegionAdd.
+func (mr *MockRegionClientMockRecorder) RegionAdd(ctx, region interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegionAdd", reflect.TypeOf((*MockRegionClient)(nil).RegionAdd), ctx, region)
+}
+
+// RegionDelete mocks base method.
+func (m *MockRegionClient) RegionDelete(ctx context.Context, regionIdentifier string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "RegionDelete", ctx, regionIdentifier)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// RegionDelete indicates an expected call of RegionDelete.
+func (mr *MockRegionClientMockRecorder) RegionDelete(ctx, regionIdentifier interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegionDelete", reflect.TypeOf((*MockRegionClient)(nil).RegionDelete), ctx, regionIdentifier)
+}
+
+// RegionGet mocks base method.
+func (m *MockRegionClient) RegionGet(ctx context.Context, regionIdentifier string) (sdk.Region, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "RegionGet", ctx, regionIdentifier)
+	ret0, _ := ret[0].(sdk.Region)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// RegionGet indicates an expected call of RegionGet.
+func (mr *MockRegionClientMockRecorder) RegionGet(ctx, regionIdentifier interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegionGet", reflect.TypeOf((*MockRegionClient)(nil).RegionGet), ctx, regionIdentifier)
+}
+
+// RegionList mocks base method.
+func (m *MockRegionClient) RegionList(ctx context.Context) ([]sdk.Region, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "RegionList", ctx)
+	ret0, _ := ret[0].([]sdk.Region)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// RegionList indicates an expected call of RegionList.
+func (mr *MockRegionClientMockRecorder) RegionList(ctx interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegionList", reflect.TypeOf((*MockRegionClient)(nil).RegionList), ctx)
+}
+
 // MockProjectClient is a mock of ProjectClient interface.
 type MockProjectClient struct {
 	ctrl     *gomock.Controller
@@ -5246,6 +5465,63 @@ func (mr *MockInterfaceMockRecorder) AdminDatabaseSignaturesRollEntity(service,
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AdminDatabaseSignaturesRollEntity", reflect.TypeOf((*MockInterface)(nil).AdminDatabaseSignaturesRollEntity), service, e, idx)
 }
 
+// AdminOrganizationCreate mocks base method.
+func (m *MockInterface) AdminOrganizationCreate(ctx context.Context, orga sdk.Organization) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "AdminOrganizationCreate", ctx, orga)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// AdminOrganizationCreate indicates an expected call of AdminOrganizationCreate.
+func (mr *MockInterfaceMockRecorder) AdminOrganizationCreate(ctx, orga interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AdminOrganizationCreate", reflect.TypeOf((*MockInterface)(nil).AdminOrganizationCreate), ctx, orga)
+}
+
+// AdminOrganizationDelete mocks base method.
+func (m *MockInterface) AdminOrganizationDelete(ctx context.Context, orgaIdentifier string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "AdminOrganizationDelete", ctx, orgaIdentifier)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// AdminOrganizationDelete indicates an expected call of AdminOrganizationDelete.
+func (mr *MockInterfaceMockRecorder) AdminOrganizationDelete(ctx, orgaIdentifier interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AdminOrganizationDelete", reflect.TypeOf((*MockInterface)(nil).AdminOrganizationDelete), ctx, orgaIdentifier)
+}
+
+// AdminOrganizationList mocks base method.
+func (m *MockInterface) AdminOrganizationList(ctx context.Context) ([]sdk.Organization, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "AdminOrganizationList", ctx)
+	ret0, _ := ret[0].([]sdk.Organization)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// AdminOrganizationList indicates an expected call of AdminOrganizationList.
+func (mr *MockInterfaceMockRecorder) AdminOrganizationList(ctx interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AdminOrganizationList", reflect.TypeOf((*MockInterface)(nil).AdminOrganizationList), ctx)
+}
+
+// AdminOrganizationMigrateUser mocks base method.
+func (m *MockInterface) AdminOrganizationMigrateUser(ctx context.Context, orgaIdentifier string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "AdminOrganizationMigrateUser", ctx, orgaIdentifier)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// AdminOrganizationMigrateUser indicates an expected call of AdminOrganizationMigrateUser.
+func (mr *MockInterfaceMockRecorder) AdminOrganizationMigrateUser(ctx, orgaIdentifier interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AdminOrganizationMigrateUser", reflect.TypeOf((*MockInterface)(nil).AdminOrganizationMigrateUser), ctx, orgaIdentifier)
+}
+
 // AdminWorkflowUpdateMaxRuns mocks base method.
 func (m *MockInterface) AdminWorkflowUpdateMaxRuns(projectKey, workflowName string, maxRuns int64) error {
 	m.ctrl.T.Helper()
@@ -6483,6 +6759,64 @@ func (mr *MockInterfaceMockRecorder) Navbar() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Navbar", reflect.TypeOf((*MockInterface)(nil).Navbar))
 }
 
+// OrganizationAdd mocks base method.
+func (m *MockInterface) OrganizationAdd(ctx context.Context, organization sdk.Organization) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "OrganizationAdd", ctx, organization)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// OrganizationAdd indicates an expected call of OrganizationAdd.
+func (mr *MockInterfaceMockRecorder) OrganizationAdd(ctx, organization interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrganizationAdd", reflect.TypeOf((*MockInterface)(nil).OrganizationAdd), ctx, organization)
+}
+
+// OrganizationDelete mocks base method.
+func (m *MockInterface) OrganizationDelete(ctx context.Context, organizationIdentifier string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "OrganizationDelete", ctx, organizationIdentifier)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// OrganizationDelete indicates an expected call of OrganizationDelete.
+func (mr *MockInterfaceMockRecorder) OrganizationDelete(ctx, organizationIdentifier interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrganizationDelete", reflect.TypeOf((*MockInterface)(nil).OrganizationDelete), ctx, organizationIdentifier)
+}
+
+// OrganizationGet mocks base method.
+func (m *MockInterface) OrganizationGet(ctx context.Context, organizationIdentifier string) (sdk.Organization, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "OrganizationGet", ctx, organizationIdentifier)
+	ret0, _ := ret[0].(sdk.Organization)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// OrganizationGet indicates an expected call of OrganizationGet.
+func (mr *MockInterfaceMockRecorder) OrganizationGet(ctx, organizationIdentifier interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrganizationGet", reflect.TypeOf((*MockInterface)(nil).OrganizationGet), ctx, organizationIdentifier)
+}
+
+// OrganizationList mocks base method.
+func (m *MockInterface) OrganizationList(ctx context.Context) ([]sdk.Organization, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "OrganizationList", ctx)
+	ret0, _ := ret[0].([]sdk.Organization)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// OrganizationList indicates an expected call of OrganizationList.
+func (mr *MockInterfaceMockRecorder) OrganizationList(ctx interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrganizationList", reflect.TypeOf((*MockInterface)(nil).OrganizationList), ctx)
+}
+
 // PipelineCreate mocks base method.
 func (m *MockInterface) PipelineCreate(projectKey string, pip *sdk.Pipeline) error {
 	m.ctrl.T.Helper()
@@ -7615,6 +7949,64 @@ func (mr *MockInterfaceMockRecorder) RBACImport(ctx, content interface{}, mods .
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RBACImport", reflect.TypeOf((*MockInterface)(nil).RBACImport), varargs...)
 }
 
+// RegionAdd mocks base method.
+func (m *MockInterface) RegionAdd(ctx context.Context, region sdk.Region) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "RegionAdd", ctx, region)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// RegionAdd indicates an expected call of RegionAdd.
+func (mr *MockInterfaceMockRecorder) RegionAdd(ctx, region interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegionAdd", reflect.TypeOf((*MockInterface)(nil).RegionAdd), ctx, region)
+}
+
+// RegionDelete mocks base method.
+func (m *MockInterface) RegionDelete(ctx context.Context, regionIdentifier string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "RegionDelete", ctx, regionIdentifier)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// RegionDelete indicates an expected call of RegionDelete.
+func (mr *MockInterfaceMockRecorder) RegionDelete(ctx, regionIdentifier interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegionDelete", reflect.TypeOf((*MockInterface)(nil).RegionDelete), ctx, regionIdentifier)
+}
+
+// RegionGet mocks base method.
+func (m *MockInterface) RegionGet(ctx context.Context, regionIdentifier string) (sdk.Region, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "RegionGet", ctx, regionIdentifier)
+	ret0, _ := ret[0].(sdk.Region)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// RegionGet indicates an expected call of RegionGet.
+func (mr *MockInterfaceMockRecorder) RegionGet(ctx, regionIdentifier interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegionGet", reflect.TypeOf((*MockInterface)(nil).RegionGet), ctx, regionIdentifier)
+}
+
+// RegionList mocks base method.
+func (m *MockInterface) RegionList(ctx context.Context) ([]sdk.Region, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "RegionList", ctx)
+	ret0, _ := ret[0].([]sdk.Region)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// RegionList indicates an expected call of RegionList.
+func (mr *MockInterfaceMockRecorder) RegionList(ctx interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegionList", reflect.TypeOf((*MockInterface)(nil).RegionList), ctx)
+}
+
 // RepositoriesList mocks base method.
 func (m *MockInterface) RepositoriesList(projectKey, repoManager string, resync bool) ([]sdk.VCSRepo, error) {
 	m.ctrl.T.Helper()
diff --git a/sdk/doc/route.go b/sdk/doc/route.go
index 7c20ea4d9..bffafacd4 100644
--- a/sdk/doc/route.go
+++ b/sdk/doc/route.go
@@ -103,6 +103,10 @@ func CleanURLParameter(u string) string {
 		u = "gpg-key-id"
 	case "analysisID":
 		u = "analysis-id"
+	case "organizationIdentifier":
+		u = "organization-identifier"
+	case "regionIdentifier":
+		u = "region-identifier"
 	}
 	return u
 }
diff --git a/sdk/organization.go b/sdk/organization.go
new file mode 100644
index 000000000..82dededed
--- /dev/null
+++ b/sdk/organization.go
@@ -0,0 +1,6 @@
+package sdk
+
+type Organization struct {
+	ID   string `json:"id" db:"id" cli:"id"`
+	Name string `json:"name" db:"name" cli:"name"`
+}
diff --git a/sdk/rbac.go b/sdk/rbac.go
index 8a5c517bd..9dfb236e8 100644
--- a/sdk/rbac.go
+++ b/sdk/rbac.go
@@ -3,11 +3,17 @@ package sdk
 import "time"
 
 const (
-	RoleRead             = "read"
-	RoleManage           = "manage"
-	RoleDelete           = "delete"
-	RoleCreateProject    = "create-project"
-	RoleManagePermission = "manage-permission"
+	// Global Role
+	GlobalRoleManagePermission   = "manage-permission"
+	GlobalRoleManageOrganization = "manage-organization"
+	GlobalRoleManageRegion       = "manage-region"
+	GlobalRoleManageUser         = "manage-user"
+	GlobalRoleManageGroup        = "manage-group"
+	GlobalRoleProjectCreate      = "create-project"
+
+	// Project Role
+	ProjectRoleRead   = "read"
+	ProjectRoleManage = "manage"
 )
 
 type RBAC struct {
diff --git a/sdk/rbac_global.go b/sdk/rbac_global.go
index db0e0c07d..ca835ab47 100644
--- a/sdk/rbac_global.go
+++ b/sdk/rbac_global.go
@@ -1,7 +1,7 @@
 package sdk
 
 var (
-	GlobalRoles = []string{RoleCreateProject, RoleManagePermission}
+	GlobalRoles = []string{GlobalRoleProjectCreate, GlobalRoleManagePermission, GlobalRoleManageOrganization, GlobalRoleManageRegion, GlobalRoleManageUser, GlobalRoleManageGroup}
 )
 
 type RBACGlobal struct {
@@ -19,14 +19,8 @@ func isValidRBACGlobal(rbacName string, rg RBACGlobal) error {
 	if rg.Role == "" {
 		return NewErrorFrom(ErrInvalidData, "rbac %s: role for global permission cannot be empty", rbacName)
 	}
-	roleFound := false
-	for _, r := range GlobalRoles {
-		if rg.Role == r {
-			roleFound = true
-			break
-		}
-	}
-	if !roleFound {
+
+	if !IsInArray(rg.Role, GlobalRoles) {
 		return NewErrorFrom(ErrInvalidData, "rbac %s: role %s is not allowed on a global permission", rbacName, rg.Role)
 	}
 	return nil
diff --git a/sdk/rbac_project.go b/sdk/rbac_project.go
index 7fb93d765..d23c42b0f 100644
--- a/sdk/rbac_project.go
+++ b/sdk/rbac_project.go
@@ -1,7 +1,7 @@
 package sdk
 
 var (
-	ProjectRoles = []string{RoleRead, RoleManage, RoleDelete}
+	ProjectRoles = []string{ProjectRoleRead, ProjectRoleManage}
 )
 
 type RBACProject struct {
diff --git a/sdk/region.go b/sdk/region.go
new file mode 100644
index 000000000..444b7c092
--- /dev/null
+++ b/sdk/region.go
@@ -0,0 +1,6 @@
+package sdk
+
+type Region struct {
+	ID   string `json:"id" db:"id" cli:"id"`
+	Name string `json:"name" db:"name" cli:"name"`
+}


Test patch:
diff --git a/engine/api/admin_test.go b/engine/api/admin_test.go
index 202b07810..f2a57ffaf 100644
--- a/engine/api/admin_test.go
+++ b/engine/api/admin_test.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"github.com/ovh/cds/engine/api/organization"
 	"testing"
 	"time"
 
@@ -28,6 +29,50 @@ import (
 	"github.com/ovh/cds/sdk"
 )
 
+func Test_getAdminOrganizationCRUD(t *testing.T) {
+	api, db, _ := newTestAPI(t)
+
+	_, jwt := assets.InsertAdminUser(t, db)
+
+	_, err := db.Exec("DELETE FROM organization")
+	require.NoError(t, err)
+
+	orga := sdk.Organization{
+		Name: sdk.RandomString(10),
+	}
+	uri := api.Router.GetRoute("POST", api.postAdminOrganizationHandler, nil)
+	req := assets.NewJWTAuthentifiedRequest(t, jwt, "POST", uri, &orga)
+
+	// Do the request
+	w := httptest.NewRecorder()
+	api.Router.Mux.ServeHTTP(w, req)
+	assert.Equal(t, 201, w.Code)
+
+	uriList := api.Router.GetRoute("GET", api.getAdminOrganizationsHandler, nil)
+	reqList := assets.NewJWTAuthentifiedRequest(t, jwt, "GET", uriList, nil)
+	wList := httptest.NewRecorder()
+	api.Router.Mux.ServeHTTP(wList, reqList)
+
+	var orgs []sdk.Organization
+	assert.Equal(t, 200, wList.Code)
+	body := wList.Body.Bytes()
+	require.NoError(t, json.Unmarshal(body, &orgs))
+
+	require.Equal(t, 1, len(orgs))
+	require.Equal(t, orga.Name, orgs[0].Name)
+
+	uriDelete := api.Router.GetRoute("DELETE", api.deleteAdminOrganizationsHandler, map[string]string{"organizationIdentifier": orgs[0].Name})
+	reqDelete := assets.NewJWTAuthentifiedRequest(t, jwt, "DELETE", uriDelete, nil)
+	wDelete := httptest.NewRecorder()
+	api.Router.Mux.ServeHTTP(wDelete, reqDelete)
+	require.Equal(t, 204, wDelete.Code)
+
+	orgsDb, err := organization.LoadAllOrganizations(context.TODO(), db)
+	require.NoError(t, err)
+	require.Equal(t, 0, len(orgsDb))
+
+}
+
 func Test_getAdminDatabaseSignatureResume(t *testing.T) {
 	api, db, _ := newTestAPI(t)
 
diff --git a/engine/api/api_test.go b/engine/api/api_test.go
index 7f57b7e52..af97afcd0 100644
--- a/engine/api/api_test.go
+++ b/engine/api/api_test.go
@@ -38,7 +38,7 @@ func newTestAPI(t *testing.T, bootstrapFunc ...test.Bootstrapf) (*API, *test.Fak
 		Cache:               store,
 	}
 	api.AuthenticationDrivers = make(map[sdk.AuthConsumerType]sdk.AuthDriver)
-	api.AuthenticationDrivers[sdk.ConsumerLocal] = local.NewDriver(context.TODO(), false, "http://localhost:8080", "")
+	api.AuthenticationDrivers[sdk.ConsumerLocal] = local.NewDriver(context.TODO(), false, "http://localhost:8080", "", "")
 	api.AuthenticationDrivers[sdk.ConsumerBuiltin] = builtin.NewDriver()
 	api.AuthenticationDrivers[sdk.ConsumerTest] = authdrivertest.NewDriver(t)
 	api.AuthenticationDrivers[sdk.ConsumerTest2] = authdrivertest.NewDriver(t)
@@ -88,7 +88,7 @@ func newTestServer(t *testing.T, bootstrapFunc ...test.Bootstrapf) (*API, *test.
 		Cache:               cache,
 	}
 	api.AuthenticationDrivers = make(map[sdk.AuthConsumerType]sdk.AuthDriver)
-	api.AuthenticationDrivers[sdk.ConsumerLocal] = local.NewDriver(context.TODO(), false, "http://localhost:8080", "")
+	api.AuthenticationDrivers[sdk.ConsumerLocal] = local.NewDriver(context.TODO(), false, "http://localhost:8080", "", "")
 	api.AuthenticationDrivers[sdk.ConsumerBuiltin] = builtin.NewDriver()
 	api.GoRoutines = sdk.NewGoRoutines(context.TODO())
 
diff --git a/engine/api/authentication/local/driver_test.go b/engine/api/authentication/local/driver_test.go
index 2c213fd5e..be8a13dae 100644
--- a/engine/api/authentication/local/driver_test.go
+++ b/engine/api/authentication/local/driver_test.go
@@ -66,7 +66,7 @@ func TestIsAllowedDomain(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			d := NewDriver(context.TODO(), false, "http://localhost:8080", tt.args.allowedDomains)
+			d := NewDriver(context.TODO(), false, "http://localhost:8080", tt.args.allowedDomains, "")
 			l := d.(*AuthDriver)
 			if got := l.isAllowedDomain(tt.args.email); got != tt.want {
 				t.Errorf("IsAllowedDomain() = %v, want %v", got, tt.want)
diff --git a/engine/api/rbac/dao_rbac_test.go b/engine/api/rbac/dao_rbac_test.go
index 312cd7127..e4ce1ba3d 100644
--- a/engine/api/rbac/dao_rbac_test.go
+++ b/engine/api/rbac/dao_rbac_test.go
@@ -57,13 +57,13 @@ projects:
 
 	require.NoError(t, rbac.Insert(context.Background(), db, &r))
 
-	projectKeysForUser1, err := rbac.LoadProjectKeysByRoleAndUserID(context.TODO(), db, sdk.RoleRead, user1.ID)
+	projectKeysForUser1, err := rbac.LoadProjectKeysByRoleAndUserID(context.TODO(), db, sdk.ProjectRoleRead, user1.ID)
 	require.NoError(t, err)
 	t.Logf("%+v", projectKeysForUser1)
 	require.Len(t, projectKeysForUser1, 1)
 	require.Equal(t, projectKeysForUser1[0], proj1.Key)
 
-	projectKeysForUser2, err := rbac.LoadProjectKeysByRoleAndUserID(context.TODO(), db, sdk.RoleManage, user2.ID)
+	projectKeysForUser2, err := rbac.LoadProjectKeysByRoleAndUserID(context.TODO(), db, sdk.ProjectRoleManage, user2.ID)
 	require.NoError(t, err)
 	require.Len(t, projectKeysForUser2, 1)
 	require.Equal(t, projectKeysForUser2[0], proj2.Key)
diff --git a/engine/api/v2_organization_test.go b/engine/api/v2_organization_test.go
new file mode 100644
index 000000000..e0086b030
--- /dev/null
+++ b/engine/api/v2_organization_test.go
@@ -0,0 +1,75 @@
+package api
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"github.com/ovh/cds/engine/api/rbac"
+	"github.com/rockbears/yaml"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/ovh/cds/engine/api/test"
+	"github.com/ovh/cds/engine/api/test/assets"
+	"github.com/ovh/cds/sdk"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_crudOrganization(t *testing.T) {
+	api, db, _ := newTestAPI(t)
+
+	u, pass := assets.InsertLambdaUser(t, db)
+
+	// Insert rbac
+	perm := fmt.Sprintf(`name: perm-orga-%s
+globals:
+  - role: %s
+    users: [%s]
+`, sdk.RandomString(10), sdk.GlobalRoleManageOrganization, u.Username)
+
+	var rb sdk.RBAC
+	require.NoError(t, yaml.Unmarshal([]byte(perm), &rb))
+	rb.Globals[0].RBACUsersIDs = []string{u.ID}
+	require.NoError(t, rbac.Insert(context.TODO(), db, &rb))
+
+	orga := sdk.Organization{Name: sdk.RandomString(10)}
+
+	uri := api.Router.GetRouteV2("POST", api.postOrganizationHandler, nil)
+	test.NotEmpty(t, uri)
+	req := assets.NewAuthentifiedRequest(t, u, pass, "POST", uri, &orga)
+	w := httptest.NewRecorder()
+	api.Router.Mux.ServeHTTP(w, req)
+	require.Equal(t, 201, w.Code)
+
+	// Then Get the organization
+	uriGet := api.Router.GetRouteV2("GET", api.getOrganizationHandler, map[string]string{"organizationIdentifier": orga.Name})
+	test.NotEmpty(t, uriGet)
+	reqGet := assets.NewAuthentifiedRequest(t, u, pass, "GET", uriGet, nil)
+	wGet := httptest.NewRecorder()
+	api.Router.Mux.ServeHTTP(wGet, reqGet)
+	require.Equal(t, 200, wGet.Code)
+
+	var orgaGet sdk.Organization
+	require.NoError(t, json.Unmarshal(wGet.Body.Bytes(), &orgaGet))
+	require.Equal(t, orga.Name, orgaGet.Name)
+
+	// Then Delete Organization
+	uriDelete := api.Router.GetRouteV2("DELETE", api.deleteOrganizationHandler, map[string]string{"organizationIdentifier": orga.Name})
+	test.NotEmpty(t, uriDelete)
+	reqDelete := assets.NewAuthentifiedRequest(t, u, pass, "DELETE", uriDelete, nil)
+	wDelete := httptest.NewRecorder()
+	api.Router.Mux.ServeHTTP(wDelete, reqDelete)
+	require.Equal(t, 204, wDelete.Code)
+
+	// Then check if organization has been deleted
+	uriList := api.Router.GetRouteV2("GET", api.getOrganizationsHandler, nil)
+	test.NotEmpty(t, uriList)
+	wList := httptest.NewRecorder()
+	reqList := assets.NewAuthentifiedRequest(t, u, pass, "GET", uriList, nil)
+	api.Router.Mux.ServeHTTP(wList, reqList)
+	require.Equal(t, 200, wList.Code)
+
+	var orgs []sdk.Organization
+	require.NoError(t, json.Unmarshal(wList.Body.Bytes(), &orgs))
+	require.Len(t, orgs, 0)
+}
diff --git a/engine/api/v2_region_test.go b/engine/api/v2_region_test.go
new file mode 100644
index 000000000..58b1341ae
--- /dev/null
+++ b/engine/api/v2_region_test.go
@@ -0,0 +1,75 @@
+package api
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"github.com/ovh/cds/engine/api/rbac"
+	"github.com/rockbears/yaml"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/ovh/cds/engine/api/test"
+	"github.com/ovh/cds/engine/api/test/assets"
+	"github.com/ovh/cds/sdk"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_crudRegion(t *testing.T) {
+	api, db, _ := newTestAPI(t)
+
+	u, pass := assets.InsertLambdaUser(t, db)
+
+	// Insert rbac
+	perm := fmt.Sprintf(`name: perm-region-%s
+globals:
+  - role: %s
+    users: [%s]
+`, sdk.RandomString(10), sdk.GlobalRoleManageRegion, u.Username)
+
+	var rb sdk.RBAC
+	require.NoError(t, yaml.Unmarshal([]byte(perm), &rb))
+	rb.Globals[0].RBACUsersIDs = []string{u.ID}
+	require.NoError(t, rbac.Insert(context.TODO(), db, &rb))
+
+	reg := sdk.Region{Name: sdk.RandomString(10)}
+
+	uri := api.Router.GetRouteV2("POST", api.postRegionHandler, nil)
+	test.NotEmpty(t, uri)
+	req := assets.NewAuthentifiedRequest(t, u, pass, "POST", uri, &reg)
+	w := httptest.NewRecorder()
+	api.Router.Mux.ServeHTTP(w, req)
+	require.Equal(t, 201, w.Code)
+
+	// Then Get the region
+	uriGet := api.Router.GetRouteV2("GET", api.getRegionHandler, map[string]string{"regionIdentifier": reg.Name})
+	test.NotEmpty(t, uriGet)
+	reqGet := assets.NewAuthentifiedRequest(t, u, pass, "GET", uriGet, nil)
+	wGet := httptest.NewRecorder()
+	api.Router.Mux.ServeHTTP(wGet, reqGet)
+	require.Equal(t, 200, wGet.Code)
+
+	var regionGet sdk.Organization
+	require.NoError(t, json.Unmarshal(wGet.Body.Bytes(), &regionGet))
+	require.Equal(t, regionGet.Name, regionGet.Name)
+
+	// Then Delete Region
+	uriDelete := api.Router.GetRouteV2("DELETE", api.deleteRegionHandler, map[string]string{"regionIdentifier": reg.Name})
+	test.NotEmpty(t, uriDelete)
+	reqDelete := assets.NewAuthentifiedRequest(t, u, pass, "DELETE", uriDelete, nil)
+	wDelete := httptest.NewRecorder()
+	api.Router.Mux.ServeHTTP(wDelete, reqDelete)
+	require.Equal(t, 204, wDelete.Code)
+
+	// Then check if region has been deleted
+	uriList := api.Router.GetRouteV2("GET", api.getRegionsHandler, nil)
+	test.NotEmpty(t, uriList)
+	wList := httptest.NewRecorder()
+	reqList := assets.NewAuthentifiedRequest(t, u, pass, "GET", uriList, nil)
+	api.Router.Mux.ServeHTTP(wList, reqList)
+	require.Equal(t, 200, wList.Code)
+
+	var regions []sdk.Region
+	require.NoError(t, json.Unmarshal(wList.Body.Bytes(), &regions))
+	require.Len(t, regions, 0)
+}
diff --git a/engine/api/v2_repository_analyze_test.go b/engine/api/v2_repository_analyze_test.go
index fb6f3a882..3f1a6cdc6 100644
--- a/engine/api/v2_repository_analyze_test.go
+++ b/engine/api/v2_repository_analyze_test.go
@@ -572,7 +572,7 @@ GDFkaTe3nUJdYV4=
 	}
 	require.NoError(t, user.InsertGPGKey(ctx, db, userKey))
 
-	assets.InsertRBAcProject(t, db, sdk.RoleManage, proj1.Key, *u)
+	assets.InsertRBAcProject(t, db, sdk.ProjectRoleManage, proj1.Key, *u)
 
 	// Create VCS
 	vcsProject := assets.InsertTestVCSProject(t, db, proj1.ID, "vcs-server", "github")
diff --git a/engine/test/database.go b/engine/test/database.go
index 93436f688..ee24fe000 100644
--- a/engine/test/database.go
+++ b/engine/test/database.go
@@ -91,7 +91,7 @@ func SetupPGToCancel(t require.TestingT, m *gorpmapper.Mapper, serviceType strin
 	redisHost := cfg["redisHost"]
 	redisPassword := cfg["redisPassword"]
 	redisDbIndex, err := strconv.ParseInt(cfg["redisDbIndex"], 10, 64)
-	require.NoError(t, err, "error when unmarshal config")
+	require.NoError(t, err, "error when unmarshal redisDbIndex config")
 
 	sigKeys := database.RollingKeyConfig{
 		Cipher: "hmac",
diff --git a/sdk/rbac_global_test.go b/sdk/rbac_global_test.go
index caffbd2fc..d22714cf8 100644
--- a/sdk/rbac_global_test.go
+++ b/sdk/rbac_global_test.go
@@ -17,7 +17,7 @@ func TestRBACGlobalInvalidGlobalRole(t *testing.T) {
 }
 func TestRBACGlobalInvalidGroupAndUsers(t *testing.T) {
 	rb := RBACGlobal{
-		Role:          RoleCreateProject,
+		Role:          GlobalRoleProjectCreate,
 		RBACGroupsIDs: []int64{},
 		RBACUsersIDs:  []string{},
 	}
diff --git a/sdk/rbac_project_test.go b/sdk/rbac_project_test.go
index 87a969bf9..e8c90b652 100644
--- a/sdk/rbac_project_test.go
+++ b/sdk/rbac_project_test.go
@@ -11,19 +11,19 @@ func TestRBACProjectInvalidRole(t *testing.T) {
 	rb := RBACProject{
 		RBACProjectKeys: []string{"foo"},
 		All:             false,
-		Role:            RoleCreateProject,
+		Role:            GlobalRoleProjectCreate,
 		RBACGroupsIDs:   []int64{1},
 		RBACUsersIDs:    []string{"aa-aa-aa"},
 	}
 	err := isValidRBACProject("myRule", rb)
 	require.Error(t, err)
-	require.Contains(t, err.Error(), fmt.Sprintf("rbac myRule: role %s is not allowed on a project permission", RoleCreateProject))
+	require.Contains(t, err.Error(), fmt.Sprintf("rbac myRule: role %s is not allowed on a project permission", GlobalRoleProjectCreate))
 }
 func TestRBACProjectInvalidGroupAndUsers(t *testing.T) {
 	rb := RBACProject{
 		RBACProjectKeys: []string{"foo"},
 		All:             false,
-		Role:            RoleRead,
+		Role:            ProjectRoleRead,
 		RBACGroupsIDs:   []int64{},
 		RBACUsersIDs:    []string{},
 	}
@@ -35,7 +35,7 @@ func TestRBACProjectInvalidProjectKeys(t *testing.T) {
 	rb := RBACProject{
 		RBACProjectKeys: []string{},
 		All:             false,
-		Role:            RoleRead,
+		Role:            ProjectRoleRead,
 		RBACGroupsIDs:   []int64{1},
 		RBACUsersIDs:    []string{},
 	}
@@ -59,7 +59,7 @@ func TestRBACProjectInvalidAllAndListOfProject(t *testing.T) {
 	rb := RBACProject{
 		RBACProjectKeys: []string{"foo"},
 		All:             true,
-		Role:            RoleRead,
+		Role:            ProjectRoleRead,
 		RBACGroupsIDs:   []int64{1},
 		RBACUsersIDs:    []string{},
 	}
diff --git a/tests/03_clictl_admin_organization.yml b/tests/03_clictl_admin_organization.yml
new file mode 100644
index 000000000..2b09c49d2
--- /dev/null
+++ b/tests/03_clictl_admin_organization.yml
@@ -0,0 +1,14 @@
+name: Database Commands TestSuite
+testcases:
+- name: Add an organization
+  steps:
+  - script: {{.cdsctl}} -f {{.cdsctl.config}} admin organization add my-org
+- name: List all organization
+  steps:
+  - script: {{.cdsctl}} -f {{.cdsctl.config}} admin organization list --format json
+    assertions:
+    - result.systemoutjson.systemoutjson0.name ShouldEqual "my-org"
+- name: Remove an organization
+  steps:
+  - script: {{.cdsctl}} -f {{.cdsctl.config}} admin organization delete my-org --force
+
diff --git a/tests/08_v2_analyze.yml b/tests/08_v2_analyze.yml
index a742b8db0..0c30e9155 100644
--- a/tests/08_v2_analyze.yml
+++ b/tests/08_v2_analyze.yml
@@ -1,4 +1,4 @@
-name: Project VCS Command TestSuite
+name: Project Analyze testSuite
 testcases:
 - name: prepare test
   steps:
diff --git a/tests/08_v2_cli_organization.yml b/tests/08_v2_cli_organization.yml
new file mode 100644
index 000000000..c170c9390
--- /dev/null
+++ b/tests/08_v2_cli_organization.yml
@@ -0,0 +1,24 @@
+name: Project Organization Command testSuite
+testcases:
+- name: prepare test
+  steps:
+  - script: {{.cdsctl}} -f {{.cdsctl.config}} experimental organization remove myorg --force
+
+- name: add new organization
+  steps:
+  - script: {{.cdsctl}} -f {{.cdsctl.config}} experimental organization add myorg
+
+- name: get and list organization
+  steps:
+    - script: {{.cdsctl}} -f {{.cdsctl.config}} experimental organization get myorg --format json
+      assertions:
+        - result.code ShouldEqual 0
+        - result.systemoutjson.name ShouldEqual myorg
+    - script: {{.cdsctl}} -f {{.cdsctl.config}} experimental organization list --format json
+      assertions:
+        - result.code ShouldEqual 0
+        - result.systemoutjson.systemoutjson0.name ShouldEqual myorg
+
+- name: remove organization
+  steps:
+    - script: {{.cdsctl}} -f {{.cdsctl.config}} experimental organization delete myorg --force
diff --git a/tests/08_v2_cli_region.yml b/tests/08_v2_cli_region.yml
new file mode 100644
index 000000000..338e7977b
--- /dev/null
+++ b/tests/08_v2_cli_region.yml
@@ -0,0 +1,24 @@
+name: Project Region Command TestSuite
+testcases:
+- name: prepare test
+  steps:
+  - script: {{.cdsctl}} -f {{.cdsctl.config}} experimental region remove my-region --force
+
+- name: add new region
+  steps:
+  - script: {{.cdsctl}} -f {{.cdsctl.config}} experimental region add my-region
+
+- name: get and list region
+  steps:
+    - script: {{.cdsctl}} -f {{.cdsctl.config}} experimental region get my-region --format json
+      assertions:
+        - result.code ShouldEqual 0
+        - result.systemoutjson.name ShouldEqual my-region
+    - script: {{.cdsctl}} -f {{.cdsctl.config}} experimental region list --format json
+      assertions:
+        - result.code ShouldEqual 0
+        - result.systemoutjson.systemoutjson0.name ShouldEqual "my-region"
+
+- name: remove region
+  steps:
+    - script: {{.cdsctl}} -f {{.cdsctl.config}} experimental region delete my-region --force