Repository: ovh/cds
Pull request #6289 (2022-09-19 10:16:29)

feat(api,cli): add organization
1. Description
1. Related issues
1. About tests
1. Mentions

@ovh/cds


ovh-cds: 
CDS Report build-all-cds#17937.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 ✔ 

ovh-cds: 
CDS Report build-all-cds#17939.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 ✔ 

ovh-cds: 
CDS Report build-all-cds#17940.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/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..ed719be33 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,6 +438,8 @@ 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/repository/analyze", Scope(sdk.AuthConsumerScopeHooks), r.POSTv2(api.postRepositoryAnalysisHandler))
diff --git a/engine/api/auth_local.go b/engine/api/auth_local.go
index 62baca4f3..df4f13c53 100644
--- a/engine/api/auth_local.go
+++ b/engine/api/auth_local.go
@@ -213,6 +213,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 +362,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..c6776e4ae 100644
--- a/engine/api/rbac/rule_global.go
+++ b/engine/api/rbac/rule_global.go
@@ -30,7 +30,11 @@ 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)
 }
+
+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)
+}
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/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_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/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/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/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/interface.go b/sdk/cdsclient/interface.go
index 7dc8ed192..2184de58d 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
diff --git a/sdk/cdsclient/mock_cdsclient/interface_mock.go b/sdk/cdsclient/mock_cdsclient/interface_mock.go
index 5b8913caa..50d024d43 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()
@@ -5246,6 +5303,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()
diff --git a/sdk/doc/route.go b/sdk/doc/route.go
index 7c20ea4d9..ed368096b 100644
--- a/sdk/doc/route.go
+++ b/sdk/doc/route.go
@@ -103,6 +103,8 @@ func CleanURLParameter(u string) string {
 		u = "gpg-key-id"
 	case "analysisID":
 		u = "analysis-id"
+	case "organizationIdentifier":
+		u = "organization-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..32be26a29 100644
--- a/sdk/rbac.go
+++ b/sdk/rbac.go
@@ -3,11 +3,14 @@ package sdk
 import "time"
 
 const (
-	RoleRead             = "read"
-	RoleManage           = "manage"
-	RoleDelete           = "delete"
-	RoleCreateProject    = "create-project"
-	RoleManagePermission = "manage-permission"
+	// Global Role
+	RoleManagePermission         = "manage-permission"
+	GlobalRoleManageOrganization = "manage-organization"
+	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..17f01881b 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, RoleManagePermission}
 )
 
 type RBACGlobal struct {
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 {


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_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
+