Repository: ovh/cds
Pull request #6104 (2022-03-03 09:12:59)

fix(api): raise error if git.branch and git.tag are set


@ovh/cds


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


Patch:
diff --git a/engine/api/api_routes.go b/engine/api/api_routes.go
index 618694678..57862e782 100644
--- a/engine/api/api_routes.go
+++ b/engine/api/api_routes.go
@@ -29,7 +29,7 @@ func (api *API) InitRouter() {
 	api.Router.SetHeaderFunc = service.DefaultHeaders
 	api.Router.Middlewares = append(api.Router.Middlewares, api.tracingMiddleware, api.jwtMiddleware)
 	api.Router.DefaultAuthMiddleware = api.authMiddleware
-	api.Router.PostAuthMiddlewares = append(api.Router.PostAuthMiddlewares, api.xsrfMiddleware, api.maintenanceMiddleware)
+	api.Router.PostAuthMiddlewares = append(api.Router.PostAuthMiddlewares, api.xsrfMiddleware, api.maintenanceMiddleware, api.rbacMiddleware)
 	api.Router.PostMiddlewares = append(api.Router.PostMiddlewares, service.TracingPostMiddleware)
 
 	r := api.Router
@@ -449,6 +449,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/project/{projectKey}/vcs", nil, r.POSTv2(api.postVCSOnProjectHandler))
+
 	//Not Found handler
 	r.Mux.NotFoundHandler = http.HandlerFunc(r.NotFoundHandler)
 
diff --git a/engine/api/rbac/dao_rbac.go b/engine/api/rbac/dao_rbac.go
new file mode 100644
index 000000000..72ba97ddd
--- /dev/null
+++ b/engine/api/rbac/dao_rbac.go
@@ -0,0 +1,100 @@
+package rbac
+
+import (
+	"context"
+	"time"
+
+	"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 LoadRbacByName(ctx context.Context, db gorp.SqlExecutor, name string, opts ...LoadOptionFunc) (sdk.RBAC, error) {
+	query := `SELECT * FROM rbac WHERE name = $1`
+	return get(ctx, db, gorpmapping.NewQuery(query).Args(name), opts...)
+}
+
+// Insert a RBAC permission in database
+func Insert(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rb *sdk.RBAC) error {
+	if err := sdk.IsValidRbac(rb); err != nil {
+		return err
+	}
+	if rb.UUID == "" {
+		rb.UUID = sdk.UUID()
+	}
+	if rb.Created.IsZero() {
+		rb.Created = time.Now()
+	}
+	rb.LastModified = time.Now()
+	dbRb := rbac{RBAC: *rb}
+	if err := gorpmapping.InsertAndSign(ctx, db, &dbRb); err != nil {
+		return err
+	}
+
+	for i := range rb.Globals {
+		dbRbGlobal := rbacGlobal{
+			RbacUUID:   dbRb.UUID,
+			RBACGlobal: rb.Globals[i],
+		}
+		if err := insertRbacGlobal(ctx, db, &dbRbGlobal); err != nil {
+			return err
+		}
+	}
+	for i := range rb.Projects {
+		dbRbProject := rbacProject{
+			RbacUUID:    dbRb.UUID,
+			RBACProject: rb.Projects[i],
+		}
+		if err := insertRbacProject(ctx, db, &dbRbProject); err != nil {
+			return err
+		}
+	}
+	*rb = dbRb.RBAC
+	return nil
+}
+
+func Update(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rb *sdk.RBAC) error {
+	if err := Delete(ctx, db, *rb); err != nil {
+		return err
+	}
+	return Insert(ctx, db, rb)
+}
+
+func Delete(_ context.Context, db gorpmapper.SqlExecutorWithTx, rb sdk.RBAC) error {
+	dbRb := rbac{RBAC: rb}
+	if err := gorpmapping.Delete(db, &dbRb); err != nil {
+		return err
+	}
+	return nil
+}
+
+func get(ctx context.Context, db gorp.SqlExecutor, q gorpmapping.Query, opts ...LoadOptionFunc) (sdk.RBAC, error) {
+	var r sdk.RBAC
+	var rbacDB rbac
+	found, err := gorpmapping.Get(ctx, db, q, &rbacDB)
+	if err != nil {
+		return r, err
+	}
+	if !found {
+		return r, sdk.WithStack(sdk.ErrNotFound)
+	}
+
+	isValid, err := gorpmapping.CheckSignature(rbacDB, rbacDB.Signature)
+	if err != nil {
+		return r, sdk.WrapError(err, "error when checking signature for rbac %s", rbacDB.UUID)
+	}
+	if !isValid {
+		log.Error(ctx, "rbac.get> rbac %s (%s) data corrupted", rbacDB.Name, rbacDB.UUID)
+		return r, sdk.WithStack(sdk.ErrNotFound)
+	}
+	for _, f := range opts {
+		if err := f(ctx, db, &rbacDB); err != nil {
+			return r, err
+		}
+	}
+	r = rbacDB.RBAC
+	return r, nil
+}
diff --git a/engine/api/rbac/dao_rbac_global.go b/engine/api/rbac/dao_rbac_global.go
new file mode 100644
index 000000000..85cccd4eb
--- /dev/null
+++ b/engine/api/rbac/dao_rbac_global.go
@@ -0,0 +1,94 @@
+package rbac
+
+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 insertRbacGlobal(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rg *rbacGlobal) error {
+	if err := gorpmapping.InsertAndSign(ctx, db, rg); err != nil {
+		return err
+	}
+
+	for _, userID := range rg.RBACUsersIDs {
+		if err := insertRbacGlobalUser(ctx, db, rg.ID, userID); err != nil {
+			return err
+		}
+	}
+	for _, groupID := range rg.RBACGroupsIDs {
+		if err := insertRbacGlobalGroup(ctx, db, rg.ID, groupID); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func insertRbacGlobalUser(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rbacGlobalID int64, userID string) error {
+	rgu := rbacGlobalUser{
+		RbacGlobalID:     rbacGlobalID,
+		RbacGlobalUserID: userID,
+	}
+	if err := gorpmapping.InsertAndSign(ctx, db, &rgu); err != nil {
+		return err
+	}
+	return nil
+}
+
+func insertRbacGlobalGroup(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rbacGlobalID int64, groupID int64) error {
+	rgu := rbacGlobalGroup{
+		RbacGlobalID:      rbacGlobalID,
+		RbacGlobalGroupID: groupID,
+	}
+	if err := gorpmapping.InsertAndSign(ctx, db, &rgu); err != nil {
+		return err
+	}
+	return nil
+}
+
+func getAllRBACGlobalUsers(ctx context.Context, db gorp.SqlExecutor, rbacGlobal *rbacGlobal) error {
+	q := gorpmapping.NewQuery("SELECT * FROM  rbac_global_users WHERE rbac_global_id = $1").Args(rbacGlobal.ID)
+	var rbacUserIDS []rbacGlobalUser
+	if err := gorpmapping.GetAll(ctx, db, q, &rbacUserIDS); err != nil {
+		return err
+	}
+	rbacGlobal.RBACGlobal.RBACUsersIDs = make([]string, 0, len(rbacUserIDS))
+	for _, rbacUsers := range rbacUserIDS {
+		isValid, err := gorpmapping.CheckSignature(rbacUsers, rbacUsers.Signature)
+		if err != nil {
+			return sdk.WrapError(err, "error when checking signature for rbac_global_users %d", rbacUsers.ID)
+		}
+		if !isValid {
+			log.Error(ctx, "rbac.getAllRBACGlobalUsers> rbac_global_users %d data corrupted", rbacUsers.ID)
+			continue
+		}
+		rbacGlobal.RBACGlobal.RBACUsersIDs = append(rbacGlobal.RBACGlobal.RBACUsersIDs, rbacUsers.RbacGlobalUserID)
+	}
+	return nil
+}
+
+func getAllRBACGlobalGroups(ctx context.Context, db gorp.SqlExecutor, rbacGlobal *rbacGlobal) error {
+	q := gorpmapping.NewQuery("SELECT * FROM rbac_global_groups WHERE rbac_global_id = $1").Args(rbacGlobal.ID)
+	var rbacGroupIDs []rbacGlobalGroup
+	if err := gorpmapping.GetAll(ctx, db, q, &rbacGroupIDs); err != nil {
+		return err
+	}
+	rbacGlobal.RBACGlobal.RBACGroupsIDs = make([]int64, 0, len(rbacGroupIDs))
+	for _, rbacGroups := range rbacGroupIDs {
+		isValid, err := gorpmapping.CheckSignature(rbacGroups, rbacGroups.Signature)
+		if err != nil {
+			return sdk.WrapError(err, "error when checking signature for rbac_global_groups %d", rbacGroups.ID)
+		}
+		if !isValid {
+			log.Error(ctx, "rbac.getAllRBACGlobalGroups> rbac_global_groups %d data corrupted", rbacGroups.ID)
+			continue
+		}
+		rbacGlobal.RBACGlobal.RBACGroupsIDs = append(rbacGlobal.RBACGlobal.RBACGroupsIDs, rbacGroups.RbacGlobalGroupID)
+	}
+	return nil
+}
diff --git a/engine/api/rbac/dao_rbac_project.go b/engine/api/rbac/dao_rbac_project.go
new file mode 100644
index 000000000..778c2b361
--- /dev/null
+++ b/engine/api/rbac/dao_rbac_project.go
@@ -0,0 +1,95 @@
+package rbac
+
+import (
+	"context"
+
+	"github.com/go-gorp/gorp"
+	"github.com/lib/pq"
+	"github.com/rockbears/log"
+
+	"github.com/ovh/cds/engine/api/database/gorpmapping"
+	"github.com/ovh/cds/engine/gorpmapper"
+	"github.com/ovh/cds/sdk"
+)
+
+func insertRbacProject(ctx context.Context, db gorpmapper.SqlExecutorWithTx, dbRP *rbacProject) error {
+	if err := gorpmapping.InsertAndSign(ctx, db, dbRP); err != nil {
+		return err
+	}
+
+	for _, rbProjectID := range dbRP.RBACProjectsIDs {
+		if err := insertRbacProjectIdentifiers(ctx, db, dbRP.ID, rbProjectID); err != nil {
+			return err
+		}
+	}
+	for _, rbUserID := range dbRP.RBACUsersIDs {
+		if err := insertRbacProjectUser(ctx, db, dbRP.ID, rbUserID); err != nil {
+			return err
+		}
+	}
+	for _, rbGroupID := range dbRP.RBACGroupsIDs {
+		if err := insertRbacProjectGroup(ctx, db, dbRP.ID, rbGroupID); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func insertRbacProjectIdentifiers(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rbacParentID int64, projectID int64) error {
+	identifier := rbacProjectIdentifiers{
+		RbacProjectID: rbacParentID,
+		ProjectID:     projectID,
+	}
+	if err := gorpmapping.InsertAndSign(ctx, db, &identifier); err != nil {
+		return err
+	}
+	return nil
+}
+
+func insertRbacProjectUser(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rbacProjectID int64, userID string) error {
+	rgu := rbacProjectUser{
+		RbacProjectID:     rbacProjectID,
+		RbacProjectUserID: userID,
+	}
+	if err := gorpmapping.InsertAndSign(ctx, db, &rgu); err != nil {
+		return err
+	}
+	return nil
+}
+
+func insertRbacProjectGroup(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rbacProjectID int64, groupID int64) error {
+	rgu := rbacProjectGroup{
+		RbacProjectID:      rbacProjectID,
+		RbacProjectGroupID: groupID,
+	}
+	if err := gorpmapping.InsertAndSign(ctx, db, &rgu); err != nil {
+		return err
+	}
+	return nil
+}
+
+func getAllRbacProjects(ctx context.Context, db gorp.SqlExecutor, q gorpmapping.Query) ([]rbacProject, error) {
+	var rbacProjects []rbacProject
+	if err := gorpmapping.GetAll(ctx, db, q, &rbacProjects); err != nil {
+		return nil, err
+	}
+
+	projectsFiltered := make([]rbacProject, 0, len(rbacProjects))
+	for _, projectDatas := range rbacProjects {
+		isValid, err := gorpmapping.CheckSignature(projectDatas, projectDatas.Signature)
+		if err != nil {
+			return nil, sdk.WrapError(err, "error when checking signature for rbac_project %d", projectDatas.ID)
+		}
+		if !isValid {
+			log.Error(ctx, "rbac.getAllRbacProjects> rbac_project %d data corrupted", projectDatas.ID)
+			continue
+		}
+		projectsFiltered = append(projectsFiltered, projectDatas)
+	}
+	return projectsFiltered, nil
+}
+
+func loadRbacProjectsByRoleAndIDs(ctx context.Context, db gorp.SqlExecutor, role string, rbacProjectIDs []int64) ([]rbacProject, error) {
+	q := gorpmapping.NewQuery(`SELECT * from rbac_project WHERE role = $1 AND id = ANY($2)`).Args(role, pq.Int64Array(rbacProjectIDs))
+	return getAllRbacProjects(ctx, db, q)
+}
diff --git a/engine/api/rbac/dao_rbac_project_group.go b/engine/api/rbac/dao_rbac_project_group.go
new file mode 100644
index 000000000..a125032f2
--- /dev/null
+++ b/engine/api/rbac/dao_rbac_project_group.go
@@ -0,0 +1,64 @@
+package rbac
+
+import (
+	"context"
+
+	"github.com/go-gorp/gorp"
+	"github.com/lib/pq"
+	"github.com/rockbears/log"
+
+	"github.com/ovh/cds/engine/api/database/gorpmapping"
+	"github.com/ovh/cds/engine/api/group"
+	"github.com/ovh/cds/sdk"
+)
+
+func loadRbacProjectGroupsByUserID(ctx context.Context, db gorp.SqlExecutor, userID string) ([]rbacProjectGroup, error) {
+	groups, err := group.LoadAllByUserID(ctx, db, userID)
+	if err != nil {
+		return nil, err
+	}
+	groupIDs := make([]int64, 0, len(groups))
+	for _, g := range groups {
+		groupIDs = append(groupIDs, g.ID)
+	}
+	return loadRbacProjectGroupsByGroupIDs(ctx, db, groupIDs)
+}
+
+func loadRbacProjectGroupsByGroupIDs(ctx context.Context, db gorp.SqlExecutor, groupIDs []int64) ([]rbacProjectGroup, error) {
+	q := gorpmapping.NewQuery("SELECT * FROM rbac_project_groups WHERE group_id = ANY ($1)").Args(pq.Int64Array(groupIDs))
+	return getAllRBACProjectGroups(ctx, db, q)
+}
+
+func loadRBACProjectGroups(ctx context.Context, db gorp.SqlExecutor, rbacProject *rbacProject) error {
+	q := gorpmapping.NewQuery("SELECT * FROM rbac_project_groups WHERE rbac_project_id = $1").Args(rbacProject.ID)
+	rbacProjectGroups, err := getAllRBACProjectGroups(ctx, db, q)
+	if err != nil {
+		return err
+	}
+	rbacProject.RBACProject.RBACGroupsIDs = make([]int64, 0, len(rbacProjectGroups))
+	for _, g := range rbacProjectGroups {
+		rbacProject.RBACProject.RBACGroupsIDs = append(rbacProject.RBACProject.RBACGroupsIDs, g.RbacProjectGroupID)
+	}
+	return nil
+}
+
+func getAllRBACProjectGroups(ctx context.Context, db gorp.SqlExecutor, q gorpmapping.Query) ([]rbacProjectGroup, error) {
+	var rbacGroupIDs []rbacProjectGroup
+	if err := gorpmapping.GetAll(ctx, db, q, &rbacGroupIDs); err != nil {
+		return nil, err
+	}
+
+	groupsFiltered := make([]rbacProjectGroup, 0, len(rbacGroupIDs))
+	for _, rbacGroups := range rbacGroupIDs {
+		isValid, err := gorpmapping.CheckSignature(rbacGroups, rbacGroups.Signature)
+		if err != nil {
+			return nil, sdk.WrapError(err, "error when checking signature for rbac_project_groups %d", rbacGroups.ID)
+		}
+		if !isValid {
+			log.Error(ctx, "rbac.getAllRBACProjectGroups> rbac_project_groups %d data corrupted", rbacGroups.ID)
+			continue
+		}
+		groupsFiltered = append(groupsFiltered, rbacGroups)
+	}
+	return groupsFiltered, nil
+}
diff --git a/engine/api/rbac/dao_rbac_project_project.go b/engine/api/rbac/dao_rbac_project_project.go
new file mode 100644
index 000000000..e0bf2e520
--- /dev/null
+++ b/engine/api/rbac/dao_rbac_project_project.go
@@ -0,0 +1,104 @@
+package rbac
+
+import (
+	"context"
+	"github.com/lib/pq"
+
+	"github.com/go-gorp/gorp"
+	"github.com/rockbears/log"
+
+	"github.com/ovh/cds/engine/api/database/gorpmapping"
+	"github.com/ovh/cds/sdk"
+)
+
+func getAllRBACProjectIdentifiers(ctx context.Context, db gorp.SqlExecutor, q gorpmapping.Query) ([]rbacProjectIdentifiers, error) {
+	var rbacProjectIdentifier []rbacProjectIdentifiers
+	if err := gorpmapping.GetAll(ctx, db, q, &rbacProjectIdentifier); err != nil {
+		return nil, err
+	}
+	rbacProjectIdentifierFiltered := make([]rbacProjectIdentifiers, 0, len(rbacProjectIdentifier))
+	for _, projectDatas := range rbacProjectIdentifier {
+		isValid, err := gorpmapping.CheckSignature(projectDatas, projectDatas.Signature)
+		if err != nil {
+			return nil, sdk.WrapError(err, "error when checking signature for rbac_project_projects %d", projectDatas.ID)
+		}
+		if !isValid {
+			log.Error(ctx, "rbac.getAllRBACProjectIdentifiers> rbac_project_projects %d data corrupted", projectDatas.ID)
+			continue
+		}
+		rbacProjectIdentifierFiltered = append(rbacProjectIdentifierFiltered, projectDatas)
+	}
+	return rbacProjectIdentifierFiltered, nil
+}
+
+func loadRBACProjectIdentifiers(ctx context.Context, db gorp.SqlExecutor, rbacProject *rbacProject) error {
+	q := gorpmapping.NewQuery("SELECT * FROM  rbac_project_projects WHERE rbac_project_id = $1").Args(rbacProject.ID)
+	rbacProjectIdentifiers, err := getAllRBACProjectIdentifiers(ctx, db, q)
+	if err != nil {
+		return err
+	}
+	rbacProject.RBACProject.RBACProjectsIDs = make([]int64, 0, len(rbacProjectIdentifiers))
+	for _, projectDatas := range rbacProjectIdentifiers {
+		rbacProject.RBACProject.RBACProjectsIDs = append(rbacProject.RBACProject.RBACProjectsIDs, projectDatas.ProjectID)
+	}
+	return nil
+}
+
+func loadRRBACProjectIdentifiers(ctx context.Context, db gorp.SqlExecutor, rbacProjectIDs []int64) ([]rbacProjectIdentifiers, error) {
+	query := gorpmapping.NewQuery(`SELECT * FROM rbac_project_projects WHERE rbac_project_id = ANY($1)`).Args(pq.Int64Array(rbacProjectIDs))
+	return getAllRBACProjectIdentifiers(ctx, db, query)
+}
+
+func LoadProjectIDsByRoleAndUserID(ctx context.Context, db gorp.SqlExecutor, role string, userID string) ([]int64, error) {
+	// Get rbac_project_groups
+	rbacProjectGroups, err := loadRbacProjectGroupsByUserID(ctx, db, userID)
+	if err != nil {
+		return nil, err
+	}
+	// Get rbac_project_users
+	rbacProjectUsers, err := loadRbacProjectUsersByUserID(ctx, db, userID)
+	if err != nil {
+		return nil, err
+	}
+
+	// Deduplicate rbac_project.id
+	mapRbacProjectID := make(map[int64]struct{})
+	rbacProjectIDs := make([]int64, 0)
+	for _, rpg := range rbacProjectGroups {
+		mapRbacProjectID[rpg.RbacProjectID] = struct{}{}
+		rbacProjectIDs = append(rbacProjectIDs, rpg.RbacProjectID)
+	}
+	for _, rpu := range rbacProjectUsers {
+		if _, has := mapRbacProjectID[rpu.RbacProjectID]; !has {
+			mapRbacProjectID[rpu.RbacProjectID] = struct{}{}
+			rbacProjectIDs = append(rbacProjectIDs, rpu.RbacProjectID)
+		}
+	}
+
+	// Get rbac_project
+	rbacProjects, err := loadRbacProjectsByRoleAndIDs(ctx, db, role, rbacProjectIDs)
+	if err != nil {
+		return nil, err
+	}
+
+	// Get rbac_project_projects
+	rbacProjectIDs = make([]int64, 0, len(rbacProjects))
+	for _, rp := range rbacProjects {
+		rbacProjectIDs = append(rbacProjectIDs, rp.ID)
+	}
+	rbacProjectsIdentifiers, err := loadRRBACProjectIdentifiers(ctx, db, rbacProjectIDs)
+	if err != nil {
+		return nil, err
+	}
+
+	// Deduplicate project ID
+	projectIDs := make([]int64, 0)
+	projectMap := make(map[int64]struct{})
+	for _, rpi := range rbacProjectsIdentifiers {
+		if _, has := projectMap[rpi.ProjectID]; !has {
+			projectMap[rpi.ProjectID] = struct{}{}
+			projectIDs = append(projectIDs, rpi.ProjectID)
+		}
+	}
+	return projectIDs, nil
+}
diff --git a/engine/api/rbac/dao_rbac_project_user.go b/engine/api/rbac/dao_rbac_project_user.go
new file mode 100644
index 000000000..0ab5890c2
--- /dev/null
+++ b/engine/api/rbac/dao_rbac_project_user.go
@@ -0,0 +1,50 @@
+package rbac
+
+import (
+	"context"
+
+	"github.com/go-gorp/gorp"
+	"github.com/rockbears/log"
+
+	"github.com/ovh/cds/engine/api/database/gorpmapping"
+	"github.com/ovh/cds/sdk"
+)
+
+func loadRbacProjectUsersByUserID(ctx context.Context, db gorp.SqlExecutor, userID string) ([]rbacProjectUser, error) {
+	q := gorpmapping.NewQuery("SELECT * FROM rbac_project_users WHERE user_id = $1").Args(userID)
+	return getAllRBACProjectUsers(ctx, db, q)
+}
+
+func loadRBACProjectUsers(ctx context.Context, db gorp.SqlExecutor, rbacProject *rbacProject) error {
+	q := gorpmapping.NewQuery("SELECT * FROM rbac_project_users WHERE rbac_project_id = $1").Args(rbacProject.ID)
+	rbacUserIDS, err := getAllRBACProjectUsers(ctx, db, q)
+	if err != nil {
+		return err
+	}
+	rbacProject.RBACProject.RBACUsersIDs = make([]string, 0, len(rbacUserIDS))
+	for _, rbacUsers := range rbacUserIDS {
+		rbacProject.RBACProject.RBACUsersIDs = append(rbacProject.RBACProject.RBACUsersIDs, rbacUsers.RbacProjectUserID)
+	}
+	return nil
+}
+
+func getAllRBACProjectUsers(ctx context.Context, db gorp.SqlExecutor, q gorpmapping.Query) ([]rbacProjectUser, error) {
+	var rbacProjectUsers []rbacProjectUser
+	if err := gorpmapping.GetAll(ctx, db, q, &rbacProjectUsers); err != nil {
+		return nil, err
+	}
+
+	usersFiltered := make([]rbacProjectUser, 0, len(rbacProjectUsers))
+	for _, rbacUsers := range rbacProjectUsers {
+		isValid, err := gorpmapping.CheckSignature(rbacUsers, rbacUsers.Signature)
+		if err != nil {
+			return nil, sdk.WrapError(err, "error when checking signature for rbac_project_users %d", rbacUsers.ID)
+		}
+		if !isValid {
+			log.Error(ctx, "rbac.getAllRBACProjectUsers> rbac_project_users %d data corrupted", rbacUsers.ID)
+			continue
+		}
+		usersFiltered = append(usersFiltered, rbacUsers)
+	}
+	return usersFiltered, nil
+}
diff --git a/engine/api/rbac/gorp_model.go b/engine/api/rbac/gorp_model.go
new file mode 100644
index 000000000..fa2b5fbff
--- /dev/null
+++ b/engine/api/rbac/gorp_model.go
@@ -0,0 +1,128 @@
+package rbac
+
+import (
+	"github.com/ovh/cds/engine/api/database/gorpmapping"
+	"github.com/ovh/cds/engine/gorpmapper"
+	"github.com/ovh/cds/sdk"
+)
+
+type rbac struct {
+	sdk.RBAC
+	gorpmapper.SignedEntity
+}
+
+func (r rbac) Canonical() gorpmapper.CanonicalForms {
+	_ = []interface{}{r.UUID, r.Name}
+	return []gorpmapper.CanonicalForm{
+		"{{.UUID}}{{.Name}}",
+	}
+}
+
+type rbacGlobal struct {
+	ID       int64  `db:"id"`
+	RbacUUID string `db:"rbac_uuid"`
+	sdk.RBACGlobal
+	gorpmapper.SignedEntity
+}
+
+func (rg rbacGlobal) Canonical() gorpmapper.CanonicalForms {
+	_ = []interface{}{rg.ID, rg.RbacUUID, rg.Role}
+	return []gorpmapper.CanonicalForm{
+		"{{.ID}}{{.RbacUUID}}{{.Role}}",
+	}
+}
+
+type rbacGlobalUser struct {
+	ID               int64  `db:"id"`
+	RbacGlobalID     int64  `db:"rbac_global_id"`
+	RbacGlobalUserID string `db:"user_id"`
+	gorpmapper.SignedEntity
+}
+
+func (rgu rbacGlobalUser) Canonical() gorpmapper.CanonicalForms {
+	_ = []interface{}{rgu.ID, rgu.RbacGlobalID, rgu.RbacGlobalUserID}
+	return []gorpmapper.CanonicalForm{
+		"{{.ID}}{{.RbacGlobalID}}{{.RbacGlobalUserID}}",
+	}
+}
+
+type rbacGlobalGroup struct {
+	ID                int64 `json:"-" db:"id" yaml:"-"`
+	RbacGlobalID      int64 `db:"rbac_global_id"`
+	RbacGlobalGroupID int64 `db:"group_id"`
+	gorpmapper.SignedEntity
+}
+
+func (rgg rbacGlobalGroup) Canonical() gorpmapper.CanonicalForms {
+	_ = []interface{}{rgg.ID, rgg.RbacGlobalID, rgg.RbacGlobalGroupID}
+	return []gorpmapper.CanonicalForm{
+		"{{.ID}}{{.RbacGlobalID}}{{.RbacGlobalGroupID}}",
+	}
+}
+
+type rbacProject struct {
+	ID       int64  `json:"-" db:"id" yaml:"-"`
+	RbacUUID string `json:"-" db:"rbac_uuid" yaml:"-"`
+	sdk.RBACProject
+	gorpmapper.SignedEntity
+}
+
+func (rp rbacProject) Canonical() gorpmapper.CanonicalForms {
+	_ = []interface{}{rp.ID, rp.RbacUUID, rp.Role, rp.All}
+	return []gorpmapper.CanonicalForm{
+		"{{.ID}}{{.RbacUUID}}{{.Role}}{{.All}}",
+	}
+}
+
+type rbacProjectIdentifiers struct {
+	ID            int64 `json:"-" db:"id" yaml:"-"`
+	RbacProjectID int64 `json:"-" db:"rbac_project_id" yaml:"-"`
+	ProjectID     int64 `json:"-" db:"project_id" yaml:"-"`
+	gorpmapper.SignedEntity
+}
+
+func (rpi rbacProjectIdentifiers) Canonical() gorpmapper.CanonicalForms {
+	_ = []interface{}{rpi.ID, rpi.RbacProjectID, rpi.ProjectID}
+	return []gorpmapper.CanonicalForm{
+		"{{.ID}}{{.RbacProjectID}}{{.ProjectID}}",
+	}
+}
+
+type rbacProjectUser struct {
+	ID                int64  `json:"-" db:"id" yaml:"-"`
+	RbacProjectID     int64  `db:"rbac_project_id"`
+	RbacProjectUserID string `db:"user_id"`
+	gorpmapper.SignedEntity
+}
+
+func (rgu rbacProjectUser) Canonical() gorpmapper.CanonicalForms {
+	_ = []interface{}{rgu.ID, rgu.RbacProjectID, rgu.RbacProjectUserID}
+	return []gorpmapper.CanonicalForm{
+		"{{.ID}}{{.RbacProjectID}}{{.RbacProjectUserID}}",
+	}
+}
+
+type rbacProjectGroup struct {
+	ID                 int64 `json:"-" db:"id" yaml:"-"`
+	RbacProjectID      int64 `db:"rbac_project_id"`
+	RbacProjectGroupID int64 `db:"group_id"`
+	gorpmapper.SignedEntity
+}
+
+func (rgg rbacProjectGroup) Canonical() gorpmapper.CanonicalForms {
+	_ = []interface{}{rgg.ID, rgg.RbacProjectID, rgg.RbacProjectGroupID}
+	return []gorpmapper.CanonicalForm{
+		"{{.ID}}{{.RbacProjectID}}{{.RbacProjectGroupID}}",
+	}
+}
+
+func init() {
+	gorpmapping.Register(gorpmapping.New(rbac{}, "rbac", false, "uuid"))
+	gorpmapping.Register(gorpmapping.New(rbacGlobal{}, "rbac_global", true, "id"))
+	gorpmapping.Register(gorpmapping.New(rbacGlobalUser{}, "rbac_global_users", true, "id"))
+	gorpmapping.Register(gorpmapping.New(rbacGlobalGroup{}, "rbac_global_groups", true, "id"))
+	gorpmapping.Register(gorpmapping.New(rbacProject{}, "rbac_project", true, "id"))
+	gorpmapping.Register(gorpmapping.New(rbacProjectIdentifiers{}, "rbac_project_projects", true, "id"))
+	gorpmapping.Register(gorpmapping.New(rbacProjectUser{}, "rbac_project_users", true, "id"))
+	gorpmapping.Register(gorpmapping.New(rbacProjectGroup{}, "rbac_project_groups", true, "id"))
+}
diff --git a/engine/api/rbac/loader.go b/engine/api/rbac/loader.go
new file mode 100644
index 000000000..af8b79cd6
--- /dev/null
+++ b/engine/api/rbac/loader.go
@@ -0,0 +1,96 @@
+package rbac
+
+import (
+	"context"
+
+	"github.com/go-gorp/gorp"
+	"github.com/rockbears/log"
+
+	"github.com/ovh/cds/engine/api/database/gorpmapping"
+	"github.com/ovh/cds/sdk"
+)
+
+// LoadOptionFunc is used as options to loadProject functions
+type LoadOptionFunc func(context.Context, gorp.SqlExecutor, *rbac) error
+
+// LoadOptions provides all options on rbac loads functions
+var LoadOptions = struct {
+	Default         LoadOptionFunc
+	LoadRbacGlobal  LoadOptionFunc
+	LoadRbacProject LoadOptionFunc
+}{
+	Default:         loadDefault,
+	LoadRbacGlobal:  loadRbacGlobal,
+	LoadRbacProject: loadRbacProject,
+}
+
+func loadDefault(ctx context.Context, db gorp.SqlExecutor, rbac *rbac) error {
+	if err := loadRbacGlobal(ctx, db, rbac); err != nil {
+		return err
+	}
+	if err := loadRbacProject(ctx, db, rbac); err != nil {
+		return err
+	}
+	return nil
+}
+
+func loadRbacProject(ctx context.Context, db gorp.SqlExecutor, rbac *rbac) error {
+	query := "SELECT * FROM rbac_project WHERE rbac_uuid = $1"
+	var rbacPrj []rbacProject
+	if err := gorpmapping.GetAll(ctx, db, gorpmapping.NewQuery(query).Args(rbac.UUID), &rbacPrj); err != nil {
+		return err
+	}
+	rbac.Projects = make([]sdk.RBACProject, 0, len(rbacPrj))
+	for i := range rbacPrj {
+		rp := &rbacPrj[i]
+		isValid, err := gorpmapping.CheckSignature(rp, rp.Signature)
+		if err != nil {
+			return sdk.WrapError(err, "error when checking signature for rbac_project %d", rp.ID)
+		}
+		if !isValid {
+			log.Error(ctx, "rbac_project.get> rbac_project %d data corrupted", rp.ID)
+			continue
+		}
+		if err := loadRBACProjectIdentifiers(ctx, db, rp); err != nil {
+			return err
+		}
+		if !rp.All {
+			if err := loadRBACProjectUsers(ctx, db, rp); err != nil {
+				return err
+			}
+			if err := loadRBACProjectGroups(ctx, db, rp); err != nil {
+				return err
+			}
+		}
+		rbac.Projects = append(rbac.Projects, rp.RBACProject)
+	}
+	return nil
+}
+
+func loadRbacGlobal(ctx context.Context, db gorp.SqlExecutor, rbac *rbac) error {
+	query := "SELECT * FROM rbac_global WHERE rbac_uuid = $1"
+	var rbacGbl []rbacGlobal
+	if err := gorpmapping.GetAll(ctx, db, gorpmapping.NewQuery(query).Args(rbac.UUID), &rbacGbl); err != nil {
+		return err
+	}
+	rbac.Globals = make([]sdk.RBACGlobal, 0, len(rbacGbl))
+	for i := range rbacGbl {
+		rg := &rbacGbl[i]
+		isValid, err := gorpmapping.CheckSignature(rg, rg.Signature)
+		if err != nil {
+			return sdk.WrapError(err, "error when checking signature for rbac_global %d", rg.ID)
+		}
+		if !isValid {
+			log.Error(ctx, "rbac.loadRbacGlobal> rbac_global %d data corrupted", rg.ID)
+			continue
+		}
+		if err := getAllRBACGlobalUsers(ctx, db, rg); err != nil {
+			return err
+		}
+		if err := getAllRBACGlobalGroups(ctx, db, rg); err != nil {
+			return err
+		}
+		rbac.Globals = append(rbac.Globals, rg.RBACGlobal)
+	}
+	return nil
+}
diff --git a/engine/api/rbac/rbac.go b/engine/api/rbac/rbac.go
new file mode 100644
index 000000000..ccc1abb9b
--- /dev/null
+++ b/engine/api/rbac/rbac.go
@@ -0,0 +1,113 @@
+package rbac
+
+import (
+	"context"
+
+	"github.com/go-gorp/gorp"
+	"github.com/ovh/cds/engine/api/group"
+	"github.com/ovh/cds/engine/api/project"
+	"github.com/ovh/cds/engine/api/user"
+	"github.com/ovh/cds/sdk"
+)
+
+func fillWithIDs(ctx context.Context, db gorp.SqlExecutor, r *sdk.RBAC) error {
+	// Check existing permission
+	rbacDB, err := LoadRbacByName(ctx, db, r.Name)
+	if err != nil {
+		if !sdk.ErrorIs(err, sdk.ErrNotFound) {
+			return err
+		}
+	}
+	r.UUID = rbacDB.UUID
+
+	userCache := make(map[string]string)
+	groupCache := make(map[string]int64)
+	projectCache := make(map[string]int64)
+	for gID := range r.Globals {
+		rbacGbl := &r.Globals[gID]
+		if err := fillRbacGlobalWithID(ctx, db, rbacGbl, userCache, groupCache); err != nil {
+			return err
+		}
+	}
+	for pID := range r.Projects {
+		rbacPrj := &r.Projects[pID]
+		if err := fillRbacProjectWithID(ctx, db, rbacPrj, projectCache, userCache, groupCache); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func fillRbacProjectWithID(ctx context.Context, db gorp.SqlExecutor, rbacPrj *sdk.RBACProject, projectCache map[string]int64, userCache map[string]string, groupCache map[string]int64) error {
+	rbacPrj.RBACProjectsIDs = make([]int64, 0, len(rbacPrj.RBACProjectKeys))
+	for _, projKey := range rbacPrj.RBACProjectKeys {
+		projectID := projectCache[projKey]
+		if projectID == 0 {
+			prj, err := project.Load(ctx, db, projKey)
+			if err != nil {
+				return err
+			}
+			projectID = prj.ID
+			projectCache[projKey] = prj.ID
+		}
+		rbacPrj.RBACProjectsIDs = append(rbacPrj.RBACProjectsIDs, projectID)
+	}
+	rbacPrj.RBACUsersIDs = make([]string, 0, len(rbacPrj.RBACUsersName))
+	for _, userName := range rbacPrj.RBACUsersName {
+		userID := userCache[userName]
+		if userID == "" {
+			authentifierUser, err := user.LoadByUsername(ctx, db, userName)
+			if err != nil {
+				return err
+			}
+			userID = authentifierUser.ID
+			userCache[userName] = userID
+		}
+		rbacPrj.RBACUsersIDs = append(rbacPrj.RBACUsersIDs, userID)
+	}
+	rbacPrj.RBACGroupsIDs = make([]int64, 0, len(rbacPrj.RBACGroupsName))
+	for _, groupName := range rbacPrj.RBACGroupsName {
+		groupID := groupCache[groupName]
+		if groupID == 0 {
+			groupDB, err := group.LoadByName(ctx, db, groupName)
+			if err != nil {
+				return err
+			}
+			groupID = groupDB.ID
+			groupCache[groupDB.Name] = groupID
+		}
+		rbacPrj.RBACGroupsIDs = append(rbacPrj.RBACGroupsIDs, groupID)
+	}
+	return nil
+}
+
+func fillRbacGlobalWithID(ctx context.Context, db gorp.SqlExecutor, rbacGbl *sdk.RBACGlobal, userCache map[string]string, groupCache map[string]int64) error {
+	rbacGbl.RBACUsersIDs = make([]string, 0, len(rbacGbl.RBACUsersName))
+	for _, rbacUserName := range rbacGbl.RBACUsersName {
+		userID := userCache[rbacUserName]
+		if userID == "" {
+			authentifierUser, err := user.LoadByUsername(ctx, db, rbacUserName)
+			if err != nil {
+				return err
+			}
+			userID = authentifierUser.ID
+			userCache[rbacUserName] = userID
+		}
+		rbacGbl.RBACUsersIDs = append(rbacGbl.RBACUsersIDs, userID)
+	}
+
+	rbacGbl.RBACGroupsIDs = make([]int64, 0, len(rbacGbl.RBACGroupsName))
+	for _, groupName := range rbacGbl.RBACGroupsName {
+		groupID := groupCache[groupName]
+		if groupID == 0 {
+			groupDB, err := group.LoadByName(ctx, db, groupName)
+			if err != nil {
+				return err
+			}
+			groupID = groupDB.ID
+			groupCache[groupDB.Name] = groupID
+		}
+		rbacGbl.RBACGroupsIDs = append(rbacGbl.RBACGroupsIDs, groupID)
+	}
+	return nil
+}
diff --git a/engine/api/rbac/rule_project.go b/engine/api/rbac/rule_project.go
new file mode 100644
index 000000000..8484a6cc5
--- /dev/null
+++ b/engine/api/rbac/rule_project.go
@@ -0,0 +1,30 @@
+package rbac
+
+import (
+	"context"
+
+	"github.com/go-gorp/gorp"
+	"github.com/rockbears/log"
+
+	"github.com/ovh/cds/engine/api/project"
+	"github.com/ovh/cds/sdk"
+)
+
+func ProjectExist(ctx context.Context, db gorp.SqlExecutor, vars map[string]string) error {
+	projectKey := vars["projectKey"]
+	exist, err := project.Exist(db, projectKey)
+	if err != nil {
+		return err
+	}
+	if !exist {
+		return sdk.WithStack(sdk.ErrNotFound)
+	}
+	return nil
+}
+
+func ProjectManage(ctx context.Context, db gorp.SqlExecutor, vars map[string]string) error {
+	projectKey := vars["projectKey"]
+	// TODO Check role manage project
+	log.Debug(ctx, "Checking manage project role on %s", projectKey)
+	return nil
+}
diff --git a/engine/api/router.go b/engine/api/router.go
index 19cdeab90..13e25fda4 100644
--- a/engine/api/router.go
+++ b/engine/api/router.go
@@ -263,10 +263,7 @@ func (r *Router) handle(uri string, scope HandlerScope, handlers ...*service.Han
 	for i := range handlers {
 		handlers[i].CleanURL = cleanURL
 		handlers[i].AllowedScopes = scope
-		name := runtime.FuncForPC(reflect.ValueOf(handlers[i].Handler).Pointer()).Name()
-		name = strings.Replace(name, ".func1", "", 1)
-		name = strings.Replace(name, ".1", "", 1)
-		name = strings.Replace(name, "github.com/ovh/cds/engine/", "", 1)
+		name := sdk.GetFuncName(handlers[i].Handler)
 		handlers[i].Name = name
 		cfg.Config[handlers[i].Method] = handlers[i]
 	}
@@ -490,6 +487,18 @@ func (r *Router) GET(h service.HandlerFunc, cfg ...service.HandlerConfigParam) *
 	return &rc
 }
 
+func (r *Router) POSTv2(h service.HandlerFuncV2, cfg ...service.HandlerConfigParam) *service.HandlerConfig {
+	var rc service.HandlerConfig
+	handler, rbacCheckers := h()
+	rc.Handler = handler
+	rc.RbacCheckers = rbacCheckers
+	rc.Method = "POST"
+	for _, c := range cfg {
+		c(&rc)
+	}
+	return &rc
+}
+
 // POST will set given handler only for POST request
 func (r *Router) POST(h service.HandlerFunc, cfg ...service.HandlerConfigParam) *service.HandlerConfig {
 	var rc service.HandlerConfig
diff --git a/engine/api/router_middleware_rbac.go b/engine/api/router_middleware_rbac.go
new file mode 100644
index 000000000..ba0771f20
--- /dev/null
+++ b/engine/api/router_middleware_rbac.go
@@ -0,0 +1,24 @@
+package api
+
+import (
+	"context"
+	"net/http"
+
+	"github.com/gorilla/mux"
+	"github.com/rockbears/log"
+
+	"github.com/ovh/cds/engine/service"
+	"github.com/ovh/cds/sdk"
+	cdslog "github.com/ovh/cds/sdk/log"
+)
+
+func (api *API) rbacMiddleware(ctx context.Context, _ http.ResponseWriter, req *http.Request, rc *service.HandlerConfig) (context.Context, error) {
+	for _, checker := range rc.RbacCheckers {
+		ctx := context.WithValue(ctx, cdslog.RbackCheckerName, sdk.GetFuncName(checker))
+		if err := checker(ctx, api.mustDB(), mux.Vars(req)); err != nil {
+			log.ErrorWithStackTrace(ctx, err)
+			return ctx, sdk.WithStack(sdk.ErrForbidden)
+		}
+	}
+	return ctx, nil
+}
diff --git a/engine/api/router_util.go b/engine/api/router_util.go
index 96b01a132..1fc4b0139 100644
--- a/engine/api/router_util.go
+++ b/engine/api/router_util.go
@@ -35,9 +35,7 @@ func writeNoContentPostMiddleware(ctx context.Context, w http.ResponseWriter, re
 	return ctx, nil
 }
 
-// GetRoute returns the routes given a handler
-func (r *Router) GetRoute(method string, handler service.HandlerFunc, vars map[string]string) string {
-	p1 := reflect.ValueOf(handler()).Pointer()
+func (r *Router) getRoute(method string, p1 uintptr, routeName string, vars map[string]string) string {
 	var url string
 	for uri, routerConfig := range r.mapRouterConfigs {
 		rc := routerConfig.Config[method]
@@ -59,12 +57,26 @@ func (r *Router) GetRoute(method string, handler service.HandlerFunc, vars map[s
 	}
 
 	if url == "" {
-		log.Debug(context.Background(), "Cant find route for Handler %s %v", method, handler)
-	}
 
+		log.Debug(context.Background(), "Cant find route for Handler %s %v", method, routeName)
+	}
 	return url
 }
 
+func (r *Router) GetRouteV2(method string, handler service.HandlerFuncV2, vars map[string]string) string {
+	routeHandler, _ := handler()
+	p1 := reflect.ValueOf(routeHandler).Pointer()
+	routeName := sdk.GetFuncName(routeHandler)
+	return r.getRoute(method, p1, routeName, vars)
+}
+
+// GetRoute returns the routes given a handler
+func (r *Router) GetRoute(method string, handler service.HandlerFunc, vars map[string]string) string {
+	p1 := reflect.ValueOf(handler()).Pointer()
+	routeName := sdk.GetFuncName(handler)
+	return r.getRoute(method, p1, routeName, vars)
+}
+
 // FormString return a string
 func FormString(r *http.Request, s string) string {
 	return r.FormValue(s)
diff --git a/engine/api/user/gorp_model.go b/engine/api/user/gorp_model.go
index 5f157a4cf..15a66bc93 100644
--- a/engine/api/user/gorp_model.go
+++ b/engine/api/user/gorp_model.go
@@ -12,6 +12,7 @@ type authentifiedUser struct {
 }
 
 func (u authentifiedUser) Canonical() gorpmapper.CanonicalForms {
+	_ = []interface{}{u.ID, u.Username, u.Fullname, u.Ring, u.Created}
 	return []gorpmapper.CanonicalForm{
 		"{{.ID}}{{.Username}}{{.Fullname}}{{.Ring}}{{printDate .Created}}",
 	}
@@ -23,6 +24,7 @@ type userContact struct {
 }
 
 func (c userContact) Canonical() gorpmapper.CanonicalForms {
+	_ = []interface{}{c.ID, c.UserID, c.Type, c.Value, c.Primary, c.Verified}
 	return []gorpmapper.CanonicalForm{
 		"{{.ID}}{{.UserID}}{{.Type}}{{.Value}}{{.Primary}}{{.Verified}}",
 	}
diff --git a/engine/api/v2_project.go b/engine/api/v2_project.go
new file mode 100644
index 000000000..f84e15bdc
--- /dev/null
+++ b/engine/api/v2_project.go
@@ -0,0 +1,28 @@
+package api
+
+import (
+	"context"
+	"github.com/gorilla/mux"
+	"github.com/ovh/cds/engine/api/rbac"
+	"github.com/rockbears/log"
+	"net/http"
+
+	"github.com/ovh/cds/engine/service"
+)
+
+func (api *API) postVCSOnProjectHandler() (service.Handler, []service.RbacChecker) {
+	handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+		vars := mux.Vars(r)
+		pKey := vars["projectKey"]
+
+		// my handler
+		log.Info(ctx, "My project: %s", pKey)
+
+		return nil
+	}
+	permChecker := []service.RbacChecker{
+		rbac.ProjectExist,
+		rbac.ProjectManage,
+	}
+	return handler, permChecker
+}
diff --git a/engine/api/workflow/process_node.go b/engine/api/workflow/process_node.go
index 678e10697..7eca0811a 100644
--- a/engine/api/workflow/process_node.go
+++ b/engine/api/workflow/process_node.go
@@ -245,6 +245,11 @@ func processNode(ctx context.Context, db gorpmapper.SqlExecutorWithTx, store cac
 			return nil, false, sdk.WrapError(sdk.ErrNoReposManagerClientAuth, "cannot get client %s %s got: %v", proj.Key, app.VCSServer, err)
 		}
 
+		// We can't have both git.branch and git.tag values
+		if currentJobGitValues["git.branch"] != "" && currentJobGitValues["git.tag"] != "" {
+			return nil, false, sdk.NewError(sdk.ErrWrongRequest, fmt.Errorf("invalid git variables"))
+		}
+
 		vcsInf, errVcs = getVCSInfos(ctx, db, store, proj.Key, vcsServer, currentJobGitValues, app.Name, app.VCSServer, app.RepositoryFullname)
 		if errVcs != nil {
 			AddWorkflowRunInfo(wr, sdk.SpawnMsgNew(*sdk.MsgWorkflowError, sdk.ExtractHTTPError(errVcs)))
diff --git a/engine/api/workflow_event.go b/engine/api/workflow_event.go
index 8beedc633..8daa76a49 100644
--- a/engine/api/workflow_event.go
+++ b/engine/api/workflow_event.go
@@ -102,6 +102,11 @@ func NewEventJobSummary(wr sdk.WorkflowRun, noderun sdk.WorkflowNodeRun, jobrun
 		GitCommit:            noderun.VCSHash,
 	}
 
+	node := wr.Workflow.WorkflowData.NodeByID(noderun.WorkflowNodeID)
+	if node != nil && node.Context != nil {
+		ejs.PipelineName = node.Context.PipelineName
+	}
+
 	if wr.Version != nil {
 		ejs.WorkflowRunVersion = *wr.Version
 	} else {
diff --git a/engine/hatchery/kubernetes/kill_workers.go b/engine/hatchery/kubernetes/kill_workers.go
index e931549b3..6ad02b311 100644
--- a/engine/hatchery/kubernetes/kill_workers.go
+++ b/engine/hatchery/kubernetes/kill_workers.go
@@ -2,6 +2,7 @@ package kubernetes
 
 import (
 	"context"
+	"fmt"
 	"strconv"
 	"strings"
 	"time"
@@ -17,20 +18,50 @@ import (
 )
 
 func (h *HatcheryKubernetes) killAwolWorkers(ctx context.Context) error {
-	pods, err := h.kubeClient.PodList(ctx, h.Config.Namespace, metav1.ListOptions{LabelSelector: LABEL_WORKER})
+	pods, err := h.kubeClient.PodList(ctx, h.Config.Namespace, metav1.ListOptions{
+		LabelSelector: fmt.Sprintf("%s=%s,%s", LABEL_HATCHERY_NAME, h.Config.Name, LABEL_WORKER_NAME),
+	})
 	if err != nil {
 		return err
 	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+	workers, err := h.CDSClient().WorkerList(ctx)
+	if err != nil {
+		return err
+	}
+
 	var globalErr error
 	for _, pod := range pods.Items {
 		annotations := pod.GetAnnotations()
 		labels := pod.GetLabels()
-		toDelete := false
+		if labels == nil {
+			continue
+		}
+
+		var toDelete bool
 		for _, container := range pod.Status.ContainerStatuses {
+			terminated := (container.State.Terminated != nil && (container.State.Terminated.Reason == "Completed" || container.State.Terminated.Reason == "Error"))
+			errImagePull := (container.State.Waiting != nil && container.State.Waiting.Reason == "ErrImagePull")
+			if terminated || errImagePull {
+				toDelete = true
+				log.Debug(ctx, "pod %s/%s is terminated or in error", pod.Namespace, pod.Name)
+				break
+			}
+		}
 
-			if (container.State.Terminated != nil && (container.State.Terminated.Reason == "Completed" || container.State.Terminated.Reason == "Error")) ||
-				(container.State.Waiting != nil && container.State.Waiting.Reason == "ErrImagePull") {
+		if !toDelete {
+			var found bool
+			for _, w := range workers {
+				if workerName, ok := labels[LABEL_WORKER_NAME]; ok && workerName == w.Name {
+					found = true
+					break
+				}
+			}
+			if !found && time.Since(pod.CreationTimestamp.Time) > 3*time.Minute {
 				toDelete = true
+				log.Debug(ctx, "pod %s/%s didn't match a registered worker and was started since %v", pod.Namespace, pod.Name, pod.CreationTimestamp.Time)
 			}
 		}
 
@@ -102,6 +133,7 @@ func (h *HatcheryKubernetes) killAwolWorkers(ctx context.Context) error {
 				globalErr = err
 				log.Error(ctx, "hatchery:kubernetes> killAwolWorkers> Cannot delete pod %s (%s)", pod.Name, err)
 			}
+			log.Debug(ctx, "pod %s/%s killed", pod.Namespace, pod.Name)
 		}
 	}
 	return globalErr
diff --git a/engine/hatchery/kubernetes/kubernetes.go b/engine/hatchery/kubernetes/kubernetes.go
index 870110959..f0fc5a353 100644
--- a/engine/hatchery/kubernetes/kubernetes.go
+++ b/engine/hatchery/kubernetes/kubernetes.go
@@ -24,6 +24,7 @@ import (
 	"github.com/ovh/cds/sdk"
 	"github.com/ovh/cds/sdk/cdsclient"
 	"github.com/ovh/cds/sdk/hatchery"
+	"github.com/ovh/cds/sdk/slug"
 )
 
 // New instanciates a new hatchery local
@@ -99,8 +100,12 @@ func (h *HatcheryKubernetes) ApplyConfiguration(cfg interface{}) error {
 // Status returns sdk.MonitoringStatus, implements interface service.Service
 func (h *HatcheryKubernetes) Status(ctx context.Context) *sdk.MonitoringStatus {
 	m := h.NewMonitoringStatus()
-	m.AddLine(sdk.MonitoringStatusLine{Component: "Workers", Value: fmt.Sprintf("%d/%d", len(h.WorkersStarted(ctx)), h.Config.Provision.MaxWorker), Status: sdk.MonitoringStatusOK})
-
+	ws, err := h.WorkersStarted(ctx)
+	if err != nil {
+		ctx = log.ContextWithStackTrace(ctx, err)
+		log.Warn(ctx, err.Error())
+	}
+	m.AddLine(sdk.MonitoringStatusLine{Component: "Workers", Value: fmt.Sprintf("%d/%d", len(ws), h.Config.Provision.MaxWorker), Status: sdk.MonitoringStatusOK})
 	return m
 }
 
@@ -171,11 +176,6 @@ func (h *HatcheryKubernetes) SpawnWorker(ctx context.Context, spawnArgs hatchery
 		return sdk.WithStack(fmt.Errorf("no job ID and no register"))
 	}
 
-	label := "execution"
-	if spawnArgs.RegisterOnly {
-		label = "register"
-	}
-
 	var logJob string
 	if spawnArgs.JobID > 0 {
 		logJob = fmt.Sprintf("for workflow job %d,", spawnArgs.JobID)
@@ -221,7 +221,6 @@ func (h *HatcheryKubernetes) SpawnWorker(ctx context.Context, spawnArgs hatchery
 	envsWm := workerConfig.InjectEnvVars
 	envsWm["CDS_MODEL_MEMORY"] = fmt.Sprintf("%d", memory)
 	envsWm["CDS_FROM_WORKER_IMAGE"] = "true"
-	envsWm["CDS_CONFIG"] = workerConfig.EncodeBase64()
 
 	for envName, envValue := range spawnArgs.Model.ModelDocker.Envs {
 		envsWm[envName] = envValue
@@ -239,6 +238,23 @@ func (h *HatcheryKubernetes) SpawnWorker(ctx context.Context, spawnArgs hatchery
 		pullPolicy = "Always"
 	}
 
+	// Create secret for worker config
+	configSecretName, err := h.createConfigSecret(ctx, workerConfig)
+	if err != nil {
+		return sdk.WrapError(err, "cannot create secret for config %s", workerConfig.Name)
+	}
+	envs = append(envs, apiv1.EnvVar{
+		Name: "CDS_CONFIG",
+		ValueFrom: &apiv1.EnvVarSource{
+			SecretKeyRef: &apiv1.SecretKeySelector{
+				LocalObjectReference: apiv1.LocalObjectReference{
+					Name: configSecretName,
+				},
+				Key: "CDS_CONFIG",
+			},
+		},
+	})
+
 	var gracePeriodSecs int64
 	podSchema := apiv1.Pod{
 		ObjectMeta: metav1.ObjectMeta{
@@ -246,9 +262,9 @@ func (h *HatcheryKubernetes) SpawnWorker(ctx context.Context, spawnArgs hatchery
 			Namespace:                  h.Config.Namespace,
 			DeletionGracePeriodSeconds: &gracePeriodSecs,
 			Labels: map[string]string{
-				LABEL_WORKER:        label,
-				LABEL_WORKER_MODEL:  strings.ToLower(spawnArgs.Model.Name),
-				LABEL_HATCHERY_NAME: h.Configuration().Name,
+				LABEL_HATCHERY_NAME:     h.Configuration().Name,
+				LABEL_WORKER_NAME:       workerConfig.Name,
+				LABEL_WORKER_MODEL_PATH: slug.Convert(spawnArgs.Model.Path()),
 			},
 			Annotations: map[string]string{},
 		},
@@ -273,6 +289,15 @@ func (h *HatcheryKubernetes) SpawnWorker(ctx context.Context, spawnArgs hatchery
 		},
 	}
 
+	// Check here to add secret if needed
+	if spawnArgs.Model.ModelDocker.Private {
+		secretRegistryName, err := h.createRegistrySecret(ctx, *spawnArgs.Model)
+		if err != nil {
+			return sdk.WrapError(err, "cannot create secret for model %s", spawnArgs.Model.Path())
+		}
+		podSchema.Spec.ImagePullSecrets = []apiv1.LocalObjectReference{{Name: secretRegistryName}}
+	}
+
 	var services []sdk.Requirement
 	for _, req := range spawnArgs.Requirements {
 		if req.Type == sdk.ServiceRequirement {
@@ -286,16 +311,6 @@ func (h *HatcheryKubernetes) SpawnWorker(ctx context.Context, spawnArgs hatchery
 		podSchema.Spec.HostAliases[0].Hostnames[0] = "worker"
 	}
 
-	// Check here to add secret if needed
-	secretName := "cds-credreg-" + spawnArgs.Model.Name
-	if spawnArgs.Model.ModelDocker.Private {
-		if err := h.createSecret(ctx, secretName, *spawnArgs.Model); err != nil {
-			return sdk.WrapError(err, "cannot create secret for model %s", spawnArgs.Model.Path())
-		}
-		podSchema.Spec.ImagePullSecrets = []apiv1.LocalObjectReference{{Name: secretName}}
-		podSchema.ObjectMeta.Labels[LABEL_SECRET] = secretName
-	}
-
 	for i, serv := range services {
 		//name= <alias> => the name of the host put in /etc/hosts of the worker
 		//value= "postgres:latest env_1=blabla env_2=blabla"" => we can add env variables in requirement name
@@ -345,7 +360,7 @@ func (h *HatcheryKubernetes) SpawnWorker(ctx context.Context, spawnArgs hatchery
 		podSchema.Spec.HostAliases[0].Hostnames[i+1] = strings.ToLower(serv.Name)
 	}
 
-	_, err := h.kubeClient.PodCreate(ctx, h.Config.Namespace, &podSchema, metav1.CreateOptions{})
+	_, err = h.kubeClient.PodCreate(ctx, h.Config.Namespace, &podSchema, metav1.CreateOptions{})
 	log.Debug(ctx, "hatchery> kubernetes> SpawnWorker> %s > Pod created", spawnArgs.WorkerName)
 	return sdk.WithStack(err)
 }
@@ -356,20 +371,18 @@ func (h *HatcheryKubernetes) GetLogger() *logrus.Logger {
 
 // WorkersStarted returns the number of instances started but
 // not necessarily register on CDS yet
-func (h *HatcheryKubernetes) WorkersStarted(ctx context.Context) []string {
-	list, err := h.kubeClient.PodList(ctx, h.Config.Namespace, metav1.ListOptions{LabelSelector: LABEL_HATCHERY_NAME})
+func (h *HatcheryKubernetes) WorkersStarted(ctx context.Context) ([]string, error) {
+	list, err := h.kubeClient.PodList(ctx, h.Config.Namespace, metav1.ListOptions{
+		LabelSelector: fmt.Sprintf("%s=%s,%s", LABEL_HATCHERY_NAME, h.Config.Name, LABEL_WORKER_NAME),
+	})
 	if err != nil {
-		log.Warn(ctx, "WorkersStarted> unable to list pods on namespace %s", h.Config.Namespace)
-		return nil
+		return nil, sdk.WrapError(err, "unable to list pods on namespace %s", h.Config.Namespace)
 	}
 	workerNames := make([]string, 0, list.Size())
 	for _, pod := range list.Items {
-		labels := pod.GetLabels()
-		if labels[LABEL_HATCHERY_NAME] == h.Configuration().Name {
-			workerNames = append(workerNames, pod.GetName())
-		}
+		workerNames = append(workerNames, pod.GetName())
 	}
-	return workerNames
+	return workerNames, nil
 }
 
 // NeedRegistration return true if worker model need regsitration
@@ -381,7 +394,7 @@ func (h *HatcheryKubernetes) NeedRegistration(_ context.Context, m *sdk.Model) b
 }
 
 func (h *HatcheryKubernetes) routines(ctx context.Context) {
-	ticker := time.NewTicker(10 * time.Minute)
+	ticker := time.NewTicker(10 * time.Second)
 	defer ticker.Stop()
 
 	for {
@@ -389,23 +402,25 @@ func (h *HatcheryKubernetes) routines(ctx context.Context) {
 		case <-ticker.C:
 			h.GoRoutines.Exec(ctx, "getCDNConfiguration", func(ctx context.Context) {
 				if err := h.Common.RefreshServiceLogger(ctx); err != nil {
-					log.Error(ctx, "hatchery> kubernetes> cannot get cdn configuration : %v", err)
+					log.ErrorWithStackTrace(ctx, sdk.WrapError(err, "cannot get cdn configuration"))
 				}
 			})
 
 			h.GoRoutines.Exec(ctx, "getServicesLogs", func(ctx context.Context) {
 				if err := h.getServicesLogs(ctx); err != nil {
-					log.Error(ctx, "Hatchery> Kubernetes> Cannot get service logs : %v", err)
+					log.ErrorWithStackTrace(ctx, sdk.WrapError(err, "cannot get service logs"))
 				}
 			})
 
 			h.GoRoutines.Exec(ctx, "killAwolWorker", func(ctx context.Context) {
-				_ = h.killAwolWorkers(ctx)
+				if err := h.killAwolWorkers(ctx); err != nil {
+					log.ErrorWithStackTrace(ctx, sdk.WrapError(err, "cannot delete awol worker"))
+				}
 			})
 
 			h.GoRoutines.Exec(ctx, "deleteSecrets", func(ctx context.Context) {
 				if err := h.deleteSecrets(ctx); err != nil {
-					log.Error(ctx, "hatchery> kubernetes> cannot handle secrets : %v", err)
+					log.ErrorWithStackTrace(ctx, sdk.WrapError(err, "cannot delete secrets"))
 				}
 			})
 		case <-ctx.Done():
diff --git a/engine/hatchery/kubernetes/secrets.go b/engine/hatchery/kubernetes/secrets.go
index 3e0c3bd40..22bae0715 100644
--- a/engine/hatchery/kubernetes/secrets.go
+++ b/engine/hatchery/kubernetes/secrets.go
@@ -8,32 +8,49 @@ import (
 	apiv1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
+	"github.com/ovh/cds/engine/worker/pkg/workerruntime"
 	"github.com/ovh/cds/sdk"
+	"github.com/ovh/cds/sdk/slug"
 )
 
+// Delete worker model registry and worker config secrets that are not used by any pods.
 func (h *HatcheryKubernetes) deleteSecrets(ctx context.Context) error {
-	pods, err := h.kubeClient.PodList(ctx, h.Config.Namespace, metav1.ListOptions{LabelSelector: LABEL_SECRET})
+	pods, err := h.kubeClient.PodList(ctx, h.Config.Namespace, metav1.ListOptions{
+		LabelSelector: fmt.Sprintf("%s=%s,%s", LABEL_HATCHERY_NAME, h.Config.Name, LABEL_WORKER_NAME),
+	})
 	if err != nil {
 		return sdk.WrapError(err, "cannot get pods with secret")
 	}
 
-	secrets, err := h.kubeClient.SecretList(ctx, h.Config.Namespace, metav1.ListOptions{LabelSelector: LABEL_SECRET})
+	secrets, err := h.kubeClient.SecretList(ctx, h.Config.Namespace, metav1.ListOptions{LabelSelector: LABEL_HATCHERY_NAME})
 	if err != nil {
 		return sdk.WrapError(err, "cannot get secrets")
 	}
 
 	for _, secret := range secrets.Items {
-		found := false
+		secretLabels := secret.GetLabels()
+		if secretLabels == nil {
+			continue
+		}
+		var found bool
 		for _, pod := range pods.Items {
-			labels := pod.GetLabels()
-			if labels != nil && labels[LABEL_SECRET] == secret.Name {
+			podLabels := pod.GetLabels()
+			if podLabels == nil {
+				continue
+			}
+			if wm, ok := secretLabels[LABEL_WORKER_MODEL_PATH]; ok && podLabels[LABEL_WORKER_MODEL_PATH] == wm {
+				found = true
+				break
+			}
+			if w, ok := secretLabels[LABEL_WORKER_NAME]; ok && podLabels[LABEL_WORKER_NAME] == w {
 				found = true
 				break
 			}
 		}
 		if !found {
+			log.Debug(ctx, "delete secret %q", secret.Name)
 			if err := h.kubeClient.SecretDelete(ctx, h.Config.Namespace, secret.Name, metav1.DeleteOptions{}); err != nil {
-				log.Error(ctx, "deleteSecrets> Cannot delete secret %s : %v", secret.Name, err)
+				log.ErrorWithStackTrace(ctx, sdk.WrapError(err, "cannot delete secret %s", secret.Name))
 			}
 		}
 	}
@@ -41,30 +58,58 @@ func (h *HatcheryKubernetes) deleteSecrets(ctx context.Context) error {
 	return nil
 }
 
-func (h *HatcheryKubernetes) createSecret(ctx context.Context, secretName string, model sdk.Model) error {
-	if _, err := h.kubeClient.SecretGet(ctx, h.Config.Namespace, secretName, metav1.GetOptions{}); err != nil {
-		registry := "https://index.docker.io/v1/"
-		if model.ModelDocker.Registry != "" {
-			registry = model.ModelDocker.Registry
-		}
-		dockerCfg := fmt.Sprintf(`{"auths":{"%s":{"username":"%s","password":"%s"}}}`, registry, model.ModelDocker.Username, model.ModelDocker.Password)
-		wmSecret := apiv1.Secret{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      secretName,
-				Namespace: h.Config.Namespace,
-				Labels: map[string]string{
-					LABEL_SECRET: model.Name,
-				},
+func (h *HatcheryKubernetes) createRegistrySecret(ctx context.Context, model sdk.Model) (string, error) {
+	secretName := slug.Convert("cds-worker-registry-" + model.Path())
+
+	_ = h.kubeClient.SecretDelete(ctx, h.Config.Namespace, secretName, metav1.DeleteOptions{})
+
+	registry := "https://index.docker.io/v1/"
+	if model.ModelDocker.Registry != "" {
+		registry = model.ModelDocker.Registry
+	}
+	dockerCfg := fmt.Sprintf(`{"auths":{"%s":{"username":"%s","password":"%s"}}}`, registry, model.ModelDocker.Username, model.ModelDocker.Password)
+
+	if _, err := h.kubeClient.SecretCreate(ctx, h.Config.Namespace, &apiv1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      secretName,
+			Namespace: h.Config.Namespace,
+			Labels: map[string]string{
+				LABEL_HATCHERY_NAME:     h.Configuration().Name,
+				LABEL_WORKER_MODEL_PATH: slug.Convert(model.Path()),
 			},
-			Type: apiv1.SecretTypeDockerConfigJson,
-			StringData: map[string]string{
-				apiv1.DockerConfigJsonKey: dockerCfg,
+		},
+		Type: apiv1.SecretTypeDockerConfigJson,
+		StringData: map[string]string{
+			apiv1.DockerConfigJsonKey: dockerCfg,
+		},
+	}, metav1.CreateOptions{}); err != nil {
+		return "", sdk.WrapError(err, "cannot create secret %s", secretName)
+	}
+
+	return secretName, nil
+}
+
+func (h *HatcheryKubernetes) createConfigSecret(ctx context.Context, workerConfig workerruntime.WorkerConfig) (string, error) {
+	secretName := slug.Convert("cds-worker-config-" + workerConfig.Name)
+
+	_ = h.kubeClient.SecretDelete(ctx, h.Config.Namespace, secretName, metav1.DeleteOptions{})
+
+	if _, err := h.kubeClient.SecretCreate(ctx, h.Config.Namespace, &apiv1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      secretName,
+			Namespace: h.Config.Namespace,
+			Labels: map[string]string{
+				LABEL_HATCHERY_NAME: h.Configuration().Name,
+				LABEL_WORKER_NAME:   workerConfig.Name,
 			},
-		}
-		if _, err := h.kubeClient.SecretCreate(ctx, h.Config.Namespace, &wmSecret, metav1.CreateOptions{}); err != nil {
-			return sdk.WrapError(err, "Cannot create secret %s", secretName)
-		}
+		},
+		Type: apiv1.SecretTypeOpaque,
+		StringData: map[string]string{
+			"CDS_CONFIG": workerConfig.EncodeBase64(),
+		},
+	}, metav1.CreateOptions{}); err != nil {
+		return "", sdk.WrapError(err, "cannot create secret %s", secretName)
 	}
 
-	return nil
+	return secretName, nil
 }
diff --git a/engine/hatchery/kubernetes/services.go b/engine/hatchery/kubernetes/services.go
index b87465c5e..fb27adf6c 100644
--- a/engine/hatchery/kubernetes/services.go
+++ b/engine/hatchery/kubernetes/services.go
@@ -2,6 +2,7 @@ package kubernetes
 
 import (
 	"context"
+	"fmt"
 	"strconv"
 	"strings"
 	"time"
@@ -31,7 +32,9 @@ func (h *HatcheryKubernetes) getServicesLogs(ctx context.Context) error {
 		apiWorkerNames[apiWorkers[i].Name] = struct{}{}
 	}
 
-	pods, err := h.kubeClient.PodList(ctx, h.Config.Namespace, metav1.ListOptions{LabelSelector: hatchery.LabelServiceJobID})
+	pods, err := h.kubeClient.PodList(ctx, h.Config.Namespace, metav1.ListOptions{
+		LabelSelector: fmt.Sprintf("%s=%s,%s", LABEL_HATCHERY_NAME, h.Config.Name, hatchery.LabelServiceJobID),
+	})
 	if err != nil {
 		return err
 	}
diff --git a/engine/hatchery/kubernetes/types.go b/engine/hatchery/kubernetes/types.go
index 9bc1cff17..5c08c4768 100644
--- a/engine/hatchery/kubernetes/types.go
+++ b/engine/hatchery/kubernetes/types.go
@@ -9,10 +9,9 @@ import (
 )
 
 const (
-	LABEL_HATCHERY_NAME = "CDS_HATCHERY_NAME"
-	LABEL_WORKER        = "CDS_WORKER"
-	LABEL_SECRET        = "CDS_SECRET"
-	LABEL_WORKER_MODEL  = "CDS_WORKER_MODEL"
+	LABEL_HATCHERY_NAME     = "CDS_HATCHERY_NAME"
+	LABEL_WORKER_NAME       = "CDS_WORKER_NAME"
+	LABEL_WORKER_MODEL_PATH = "CDS_WORKER_MODEL_PATH"
 )
 
 var containerServiceNameRegexp = regexp.MustCompile(`service-([0-9]+)-(.*)`)
diff --git a/engine/hatchery/local/local.go b/engine/hatchery/local/local.go
index 70d996ef8..77c225a9b 100644
--- a/engine/hatchery/local/local.go
+++ b/engine/hatchery/local/local.go
@@ -83,12 +83,16 @@ func (h *HatcheryLocal) ApplyConfiguration(cfg interface{}) error {
 // Status returns sdk.MonitoringStatus, implements interface service.Service
 func (h *HatcheryLocal) Status(ctx context.Context) *sdk.MonitoringStatus {
 	m := h.NewMonitoringStatus()
+	ws, err := h.WorkersStarted(ctx)
+	if err != nil {
+		ctx = log.ContextWithStackTrace(ctx, err)
+		log.Warn(ctx, err.Error())
+	}
 	m.AddLine(sdk.MonitoringStatusLine{
 		Component: "Workers",
-		Value:     fmt.Sprintf("%d/%d", len(h.WorkersStarted(ctx)), h.Config.Provision.MaxWorker),
+		Value:     fmt.Sprintf("%d/%d", len(ws), h.Config.Provision.MaxWorker),
 		Status:    sdk.MonitoringStatusOK,
 	})
-
 	return m
 }
 
@@ -237,7 +241,7 @@ func (h *HatcheryLocal) killWorker(ctx context.Context, name string, workerCmd w
 
 // WorkersStarted returns the number of instances started but
 // not necessarily register on CDS yet
-func (h *HatcheryLocal) WorkersStarted(ctx context.Context) []string {
+func (h *HatcheryLocal) WorkersStarted(ctx context.Context) ([]string, error) {
 	h.Mutex.Lock()
 	defer h.Mutex.Unlock()
 	workers := make([]string, len(h.workers))
@@ -246,7 +250,7 @@ func (h *HatcheryLocal) WorkersStarted(ctx context.Context) []string {
 		workers[i] = n
 		i++
 	}
-	return workers
+	return workers, nil
 }
 
 // InitHatchery register local hatchery with its worker model
diff --git a/engine/hatchery/marathon/marathon.go b/engine/hatchery/marathon/marathon.go
index 7b571d6b2..e901c9bbd 100644
--- a/engine/hatchery/marathon/marathon.go
+++ b/engine/hatchery/marathon/marathon.go
@@ -87,7 +87,12 @@ func (h *HatcheryMarathon) ApplyConfiguration(cfg interface{}) error {
 // Status returns sdk.MonitoringStatus, implements interface service.Service
 func (h *HatcheryMarathon) Status(ctx context.Context) *sdk.MonitoringStatus {
 	m := h.NewMonitoringStatus()
-	m.AddLine(sdk.MonitoringStatusLine{Component: "Workers", Value: fmt.Sprintf("%d/%d", len(h.WorkersStarted(ctx)), h.Config.Provision.MaxWorker), Status: sdk.MonitoringStatusOK})
+	ws, err := h.WorkersStarted(ctx)
+	if err != nil {
+		ctx = log.ContextWithStackTrace(ctx, err)
+		log.Warn(ctx, err.Error())
+	}
+	m.AddLine(sdk.MonitoringStatusLine{Component: "Workers", Value: fmt.Sprintf("%d/%d", len(ws), h.Config.Provision.MaxWorker), Status: sdk.MonitoringStatusOK})
 	return m
 }
 
@@ -419,18 +424,17 @@ func (h *HatcheryMarathon) listApplications(idPrefix string) ([]string, error) {
 
 // WorkersStarted returns the number of instances started but
 // not necessarily register on CDS yet
-func (h *HatcheryMarathon) WorkersStarted(ctx context.Context) []string {
+func (h *HatcheryMarathon) WorkersStarted(ctx context.Context) ([]string, error) {
 	apps, err := h.listApplications(h.Config.MarathonIDPrefix)
 	if err != nil {
-		log.Warn(ctx, "WorkersStarted> error on list applications err:%s", err)
-		return nil
+		return nil, sdk.WrapError(err, "error on list applications")
 	}
 	res := make([]string, len(apps))
 	for i, s := range apps {
 		res[i] = strings.Replace(s, h.Config.MarathonIDPrefix, "", 1)
 		res[i] = strings.TrimPrefix(res[i], "/")
 	}
-	return res
+	return res, nil
 }
 
 // InitHatchery only starts killing routine of worker not registered
diff --git a/engine/hatchery/openstack/openstack.go b/engine/hatchery/openstack/openstack.go
index 72b2f335e..2d9acbc73 100644
--- a/engine/hatchery/openstack/openstack.go
+++ b/engine/hatchery/openstack/openstack.go
@@ -91,7 +91,12 @@ func (h *HatcheryOpenstack) ApplyConfiguration(cfg interface{}) error {
 // Status returns sdk.MonitoringStatus, implements interface service.Service
 func (h *HatcheryOpenstack) Status(ctx context.Context) *sdk.MonitoringStatus {
 	m := h.NewMonitoringStatus()
-	m.AddLine(sdk.MonitoringStatusLine{Component: "Workers", Value: fmt.Sprintf("%d/%d", len(h.WorkersStarted(ctx)), h.Config.Provision.MaxWorker), Status: sdk.MonitoringStatusOK})
+	ws, err := h.WorkersStarted(ctx)
+	if err != nil {
+		ctx = log.ContextWithStackTrace(ctx, err)
+		log.Warn(ctx, err.Error())
+	}
+	m.AddLine(sdk.MonitoringStatusLine{Component: "Workers", Value: fmt.Sprintf("%d/%d", len(ws), h.Config.Provision.MaxWorker), Status: sdk.MonitoringStatusOK})
 	return m
 }
 
@@ -487,13 +492,13 @@ func (h *HatcheryOpenstack) deleteServer(ctx context.Context, s servers.Server)
 
 // WorkersStarted returns the number of instances started but
 // not necessarily register on CDS yet
-func (h *HatcheryOpenstack) WorkersStarted(ctx context.Context) []string {
+func (h *HatcheryOpenstack) WorkersStarted(ctx context.Context) ([]string, error) {
 	srvs := h.getServers(ctx)
 	res := make([]string, len(srvs))
 	for i, s := range srvs {
 		res[i] = s.Metadata["worker"]
 	}
-	return res
+	return res, nil
 }
 
 // NeedRegistration return true if worker model need regsitration
diff --git a/engine/hatchery/swarm/swarm.go b/engine/hatchery/swarm/swarm.go
index bd898bcad..85f800f81 100644
--- a/engine/hatchery/swarm/swarm.go
+++ b/engine/hatchery/swarm/swarm.go
@@ -489,23 +489,21 @@ func (h *HatcherySwarm) CanSpawn(ctx context.Context, model *sdk.Model, jobID in
 
 // WorkersStarted returns the number of instances started but
 // not necessarily register on CDS yet
-func (h *HatcherySwarm) WorkersStarted(ctx context.Context) []string {
+func (h *HatcherySwarm) WorkersStarted(ctx context.Context) ([]string, error) {
 	ctx, end := telemetry.Span(ctx, "hatchery.WorkersStarted")
 	defer end()
-
 	res := make([]string, 0)
 	for _, dockerClient := range h.dockerClients {
 		containers, err := h.getContainers(ctx, dockerClient, types.ContainerListOptions{All: true})
 		if err != nil {
-			log.Error(ctx, "hatchery> swarm> WorkersStarted> Unable to list containers: %s", err)
-			return nil
+			return nil, sdk.WrapError(err, "unable to list containers")
 		}
 		workers := containers.FilterWorkers()
 		for _, w := range workers {
 			res = append(res, w.Labels[LabelWorkerName])
 		}
 	}
-	return res
+	return res, nil
 }
 
 func (h *HatcherySwarm) GetLogger() *logrus.Logger {
diff --git a/engine/hatchery/swarm/swarm_conf.go b/engine/hatchery/swarm/swarm_conf.go
index 4146dcb83..1774224cc 100644
--- a/engine/hatchery/swarm/swarm_conf.go
+++ b/engine/hatchery/swarm/swarm_conf.go
@@ -61,7 +61,12 @@ func (h *HatcherySwarm) ApplyConfiguration(cfg interface{}) error {
 // Status returns sdk.MonitoringStatus, implements interface service.Service
 func (h *HatcherySwarm) Status(ctx context.Context) *sdk.MonitoringStatus {
 	m := h.NewMonitoringStatus()
-	m.AddLine(sdk.MonitoringStatusLine{Component: "Workers", Value: fmt.Sprintf("%d/%d", len(h.WorkersStarted(ctx)), h.Config.Provision.MaxWorker), Status: sdk.MonitoringStatusOK})
+	ws, err := h.WorkersStarted(ctx)
+	if err != nil {
+		ctx = log.ContextWithStackTrace(ctx, err)
+		log.Warn(ctx, err.Error())
+	}
+	m.AddLine(sdk.MonitoringStatusLine{Component: "Workers", Value: fmt.Sprintf("%d/%d", len(ws), h.Config.Provision.MaxWorker), Status: sdk.MonitoringStatusOK})
 	var nbErrorImageList, nbErrorGetContainers int
 	for dockerName, dockerClient := range h.dockerClients {
 		//Check images
diff --git a/engine/hatchery/vsphere/client.go b/engine/hatchery/vsphere/client.go
index d18a54978..e7f6e8e79 100644
--- a/engine/hatchery/vsphere/client.go
+++ b/engine/hatchery/vsphere/client.go
@@ -34,7 +34,6 @@ func (h *HatcheryVSphere) getVirtualMachines(ctx context.Context) []mo.VirtualMa
 		if isNotTemplate {
 			result = append(result, vms[i])
 		}
-
 	}
 	return result
 }
diff --git a/engine/hatchery/vsphere/hatchery.go b/engine/hatchery/vsphere/hatchery.go
index 72cf9edfd..ba9f1854c 100644
--- a/engine/hatchery/vsphere/hatchery.go
+++ b/engine/hatchery/vsphere/hatchery.go
@@ -87,7 +87,12 @@ func (h *HatcheryVSphere) ApplyConfiguration(cfg interface{}) error {
 // Status returns sdk.MonitoringStatus, implements interface service.Service
 func (h *HatcheryVSphere) Status(ctx context.Context) *sdk.MonitoringStatus {
 	m := h.NewMonitoringStatus()
-	m.AddLine(sdk.MonitoringStatusLine{Component: "Workers", Value: fmt.Sprintf("%d/%d", len(h.WorkersStarted(ctx)), h.Config.Provision.MaxWorker), Status: sdk.MonitoringStatusOK})
+	ws, err := h.WorkersStarted(ctx)
+	if err != nil {
+		ctx = log.ContextWithStackTrace(ctx, err)
+		log.Warn(ctx, err.Error())
+	}
+	m.AddLine(sdk.MonitoringStatusLine{Component: "Workers", Value: fmt.Sprintf("%d/%d", len(ws), h.Config.Provision.MaxWorker), Status: sdk.MonitoringStatusOK})
 	return m
 }
 
@@ -249,13 +254,13 @@ func (h *HatcheryVSphere) WorkerModelSecretList(m sdk.Model) (sdk.WorkerModelSec
 
 // WorkersStarted returns the list of workers started but
 // not necessarily register on CDS yet
-func (h *HatcheryVSphere) WorkersStarted(ctx context.Context) []string {
+func (h *HatcheryVSphere) WorkersStarted(ctx context.Context) ([]string, error) {
 	srvs := h.getVirtualMachines(ctx)
 	res := make([]string, 0, len(srvs))
 	for _, s := range srvs {
 		res = append(res, s.Name)
 	}
-	return res
+	return res, nil
 }
 
 // ModelType returns type of hatchery
diff --git a/engine/hatchery/vsphere/init.go b/engine/hatchery/vsphere/init.go
index 43e2faa1e..5df38078d 100644
--- a/engine/hatchery/vsphere/init.go
+++ b/engine/hatchery/vsphere/init.go
@@ -114,7 +114,7 @@ func (h *HatcheryVSphere) newGovmomiClient(ctx context.Context) (*govmomi.Client
 		return nil, sdk.WrapError(err, "cannot parse url")
 	}
 
-	log.Info(ctx, "initializing connection to %v...", u)
+  log.Info(ctx, "initializing connection to https://%v", h.Config.VSphereEndpoint)
 
 	// Connect and log in to ESX or vCenter
 	return govmomi.NewClient(ctx, u, false)
diff --git a/engine/service/http.go b/engine/service/http.go
index 35892dd33..8672c6468 100644
--- a/engine/service/http.go
+++ b/engine/service/http.go
@@ -12,6 +12,7 @@ import (
 	"sync"
 	"time"
 
+	"github.com/go-gorp/gorp"
 	"github.com/rockbears/log"
 	"gopkg.in/spacemonkeygo/httpsig.v0"
 
@@ -22,6 +23,7 @@ import (
 
 // Handler defines the HTTP handler used in CDS engine
 type Handler func(ctx context.Context, w http.ResponseWriter, r *http.Request) error
+type RbacChecker func(ctx context.Context, db gorp.SqlExecutor, vars map[string]string) error
 
 // AsynchronousHandler defines the HTTP asynchronous handler used in CDS engine
 type AsynchronousHandler func(ctx context.Context, r *http.Request) error
@@ -31,6 +33,7 @@ type Middleware func(ctx context.Context, w http.ResponseWriter, req *http.Reque
 
 // HandlerFunc defines the way to instantiate a handler
 type HandlerFunc func() Handler
+type HandlerFuncV2 func() (Handler, []RbacChecker)
 
 // AsynchronousHandlerFunc defines the way to instantiate a handler
 type AsynchronousHandlerFunc func() AsynchronousHandler
@@ -54,6 +57,7 @@ type HandlerConfig struct {
 	AllowedScopes          []sdk.AuthConsumerScope
 	PermissionLevel        int
 	CleanURL               string
+	RbacCheckers           []RbacChecker
 }
 
 // Accepted is a helper function used by asynchronous handlers
diff --git a/engine/sql/api/236_rbac.sql b/engine/sql/api/236_rbac.sql
new file mode 100644
index 000000000..489e2d8cc
--- /dev/null
+++ b/engine/sql/api/236_rbac.sql
@@ -0,0 +1,106 @@
+-- +migrate Up
+CREATE TABLE rbac
+(
+    "uuid"          uuid PRIMARY KEY,
+    "name"          VARCHAR(255) NOT NULL,
+    "created"       TIMESTAMP WITH TIME ZONE DEFAULT LOCALTIMESTAMP,
+    "last_modified" TIMESTAMP WITH TIME ZONE DEFAULT LOCALTIMESTAMP,
+    sig             BYTEA,
+    signer          TEXT
+);
+SELECT create_unique_index('rbac', 'idx_unq_rbac_name', 'name');
+
+-- GLOBAL PART
+CREATE TABLE rbac_global
+(
+    "id"        BIGSERIAL PRIMARY KEY,
+    "rbac_uuid" uuid         NOT NULL,
+    "role"      VARCHAR(255) NOT NULL,
+    sig         BYTEA,
+    signer      TEXT
+);
+SELECT create_foreign_key_idx_cascade('FK_rbac_global', 'rbac_global', 'rbac', 'rbac_uuid', 'uuid');
+SELECT create_index('rbac_global', 'idx_rbac_global_role', 'role');
+
+CREATE TABLE rbac_global_users
+(
+    "id"             BIGSERIAL PRIMARY KEY,
+    "rbac_global_id" BIGINT,
+    "user_id"        character varying(36),
+    sig              BYTEA,
+    signer           TEXT
+);
+SELECT create_foreign_key_idx_cascade('FK_rbac_global_users', 'rbac_global_users', 'rbac_global', 'rbac_global_id', 'id');
+SELECT create_foreign_key_idx_cascade('FK_rbac_global_users_ids', 'rbac_global_users', 'authentified_user', 'user_id', 'id');
+SELECT create_unique_index('rbac_global_users', 'idx_unq_rbac_global_users', 'rbac_global_id,user_id');
+
+CREATE TABLE rbac_global_groups
+(
+    "id"             BIGSERIAL PRIMARY KEY,
+    "rbac_global_id" BIGINT,
+    "group_id"       BIGINT,
+    sig              BYTEA,
+    signer           TEXT
+);
+SELECT create_foreign_key_idx_cascade('FK_rbac_global_groups', 'rbac_global_groups', 'rbac_global', 'rbac_global_id', 'id');
+SELECT create_foreign_key_idx_cascade('FK_rbac_global_groups_ids', 'rbac_global_groups', 'group', 'group_id', 'id');
+SELECT create_unique_index('rbac_global_groups', 'idx_unq_rbac_global_groups', 'rbac_global_id,group_id');
+
+-- PROJECT
+CREATE TABLE rbac_project
+(
+    "id"        BIGSERIAL PRIMARY KEY,
+    "rbac_uuid" uuid         NOT NULL,
+    "all"       BOOLEAN      NOT NULL DEFAULT FALSE,
+    "role"      VARCHAR(255) NOT NULL,
+    sig         BYTEA,
+    signer      TEXT
+);
+SELECT create_foreign_key_idx_cascade('FK_rbac_project', 'rbac_project', 'rbac', 'rbac_uuid', 'uuid');
+SELECT create_index('rbac_project', 'idx_rbac_project_role', 'role');
+
+CREATE TABLE rbac_project_projects
+(
+    "id"              BIGSERIAL PRIMARY KEY,
+    "rbac_project_id" BIGINT,
+    "project_id"      BIGINT,
+    sig               BYTEA,
+    signer            TEXT
+);
+SELECT create_foreign_key_idx_cascade('FK_rbac_project_projects', 'rbac_project_projects', 'rbac_project', 'rbac_project_id', 'id');
+SELECT create_foreign_key_idx_cascade('FK_rbac_project_projects_project', 'rbac_project_projects', 'project', 'project_id', 'id');
+SELECT create_unique_index('rbac_project_projects', 'idx_unq_rbac_project_projects', 'rbac_project_id,project_id');
+
+CREATE TABLE rbac_project_users
+(
+    "id"              BIGSERIAL PRIMARY KEY,
+    "rbac_project_id" BIGINT,
+    "user_id"         character varying(36),
+    sig               BYTEA,
+    signer            TEXT
+);
+SELECT create_foreign_key_idx_cascade('FK_rbac_project_users', 'rbac_project_users', 'rbac_project', 'rbac_project_id', 'id');
+SELECT create_foreign_key_idx_cascade('FK_rbac_project_users_id', 'rbac_project_users', 'authentified_user', 'user_id', 'id');
+SELECT create_unique_index('rbac_project_users', 'idx_unq_rbac_project_users', 'rbac_project_id,user_id');
+
+CREATE TABLE rbac_project_groups
+(
+    "id"              BIGSERIAL PRIMARY KEY,
+    "rbac_project_id" BIGINT,
+    "group_id"        BIGINT,
+    sig               BYTEA,
+    signer            TEXT
+);
+SELECT create_foreign_key_idx_cascade('FK_rbac_project_groups', 'rbac_project_groups', 'rbac_project', 'rbac_project_id', 'id');
+SELECT create_foreign_key_idx_cascade('FK_rbac_project_groups_ids', 'rbac_project_groups', 'group', 'group_id', 'id');
+SELECT create_unique_index('rbac_project_groups', 'idx_unq_rbac_project_groups', 'rbac_project_id,group_id');
+
+-- +migrate Down
+DROP TABLE rbac_project_groups;
+DROP TABLE rbac_project_users;
+DROP TABLE rbac_project_projects;
+DROP TABLE rbac_project;
+DROP TABLE rbac_global_groups;
+DROP TABLE rbac_global_users;
+DROP TABLE rbac_global;
+DROP TABLE rbac;
diff --git a/sdk/doc/route.go b/sdk/doc/route.go
index 4dd5ef535..cefc12ac4 100644
--- a/sdk/doc/route.go
+++ b/sdk/doc/route.go
@@ -29,7 +29,7 @@ func CleanURLParameter(u string) string {
 	switch u {
 	case "consumerType":
 		u = "consumer-type"
-	case "key", "permProjectKey":
+	case "key", "permProjectKey", "projectKey":
 		u = "project-key"
 	case "permWorkflowName", "permWorkflowNameAdvanced", "workflowName":
 		u = "workflow-name"
diff --git a/sdk/event.go b/sdk/event.go
index 92ae6a608..28cc5ace2 100644
--- a/sdk/event.go
+++ b/sdk/event.go
@@ -147,6 +147,7 @@ type EventJobSummary struct {
 	ProjectKey           string     `json:"project_key"`
 	Workflow             string     `json:"workflow"`
 	Pipeline             string     `json:"pipeline"`
+	PipelineName         string     `json:"pipeline_name"`
 	Job                  string     `json:"job"`
 	WorkflowRunVersion   string     `json:"workflow_run_version"`
 	WorkflowRunNumber    int        `json:"workflow_run_number"`
diff --git a/sdk/exportentities/v2/workflow.go b/sdk/exportentities/v2/workflow.go
index be7dcddfd..c69ded577 100644
--- a/sdk/exportentities/v2/workflow.go
+++ b/sdk/exportentities/v2/workflow.go
@@ -27,7 +27,7 @@ type VariableValue struct {
 type Workflow struct {
 	Name        string `json:"name" yaml:"name" jsonschema_description:"The name of the workflow."`
 	Description string `json:"description,omitempty" yaml:"description,omitempty"`
-	Version     string `json:"version,omitempty" yaml:"version,omitempty" jsonschema_description:"Version for the yaml syntax, latest is v1.0."`
+	Version     string `json:"version,omitempty" yaml:"version,omitempty" jsonschema_description:"Version for the yaml syntax, latest is v2.0."`
 
 	Workflow map[string]NodeEntry   `json:"workflow,omitempty" yaml:"workflow,omitempty" jsonschema_description:"Workflow nodes list."`
 	Hooks    map[string][]HookEntry `json:"hooks,omitempty" yaml:"hooks,omitempty" jsonschema_description:"Workflow hooks list."`
diff --git a/sdk/hatchery/mock_hatchery/interface_mock.go b/sdk/hatchery/mock_hatchery/interface_mock.go
index 44f9b15b7..f5fab427d 100644
--- a/sdk/hatchery/mock_hatchery/interface_mock.go
+++ b/sdk/hatchery/mock_hatchery/interface_mock.go
@@ -209,11 +209,12 @@ func (mr *MockInterfaceMockRecorder) Type() *gomock.Call {
 }
 
 // WorkersStarted mocks base method.
-func (m *MockInterface) WorkersStarted(ctx context.Context) []string {
+func (m *MockInterface) WorkersStarted(ctx context.Context) ([]string, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "WorkersStarted", ctx)
 	ret0, _ := ret[0].([]string)
-	return ret0
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
 }
 
 // WorkersStarted indicates an expected call of WorkersStarted.
@@ -472,11 +473,12 @@ func (mr *MockInterfaceWithModelsMockRecorder) WorkerModelsEnabled() *gomock.Cal
 }
 
 // WorkersStarted mocks base method.
-func (m *MockInterfaceWithModels) WorkersStarted(ctx context.Context) []string {
+func (m *MockInterfaceWithModels) WorkersStarted(ctx context.Context) ([]string, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "WorkersStarted", ctx)
 	ret0, _ := ret[0].([]string)
-	return ret0
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
 }
 
 // WorkersStarted indicates an expected call of WorkersStarted.
diff --git a/sdk/hatchery/pool.go b/sdk/hatchery/pool.go
index 56cb73789..7062cd008 100644
--- a/sdk/hatchery/pool.go
+++ b/sdk/hatchery/pool.go
@@ -32,7 +32,10 @@ func WorkerPool(ctx context.Context, h Interface, statusFilter ...string) ([]sdk
 	}
 
 	// Then: get all workers in the orchestrator queue
-	startedWorkers := h.WorkersStarted(ctx)
+	startedWorkers, err := h.WorkersStarted(ctx)
+	if err != nil {
+		return nil, err
+	}
 
 	// Make the union of the two slices
 	allWorkers := make([]sdk.Worker, 0, len(startedWorkers)+len(registeredWorkers))
diff --git a/sdk/hatchery/types.go b/sdk/hatchery/types.go
index edc303c85..0631410bb 100644
--- a/sdk/hatchery/types.go
+++ b/sdk/hatchery/types.go
@@ -106,7 +106,7 @@ type Interface interface {
 	InitHatchery(ctx context.Context) error
 	SpawnWorker(ctx context.Context, spawnArgs SpawnArguments) error
 	CanSpawn(ctx context.Context, model *sdk.Model, jobID int64, requirements []sdk.Requirement) bool
-	WorkersStarted(ctx context.Context) []string
+	WorkersStarted(ctx context.Context) ([]string, error)
 	Service() *sdk.Service
 	CDSClient() cdsclient.Interface
 	Configuration() service.HatcheryCommonConfiguration
diff --git a/sdk/log/fields.go b/sdk/log/fields.go
index de51136fd..f3ae5fd91 100644
--- a/sdk/log/fields.go
+++ b/sdk/log/fields.go
@@ -17,6 +17,7 @@ const (
 	AuthUserID         = log.Field("auth_user_id")
 	AuthUsername       = log.Field("auth_user_name")
 	AuthWorkerName     = log.Field("auth_worker_name")
+	RbackCheckerName   = log.Field("rbac_checker_name")
 	Deprecated         = log.Field("deprecated")
 	Duration           = log.Field("duration_milliseconds_num")
 	Goroutine          = log.Field("goroutine")
@@ -49,22 +50,23 @@ func init() {
 		AuthSessionID,
 		AuthSessionIAT,
 		AuthSessionTokenID,
-		IPAddress,
-		Method,
-		Route,
-		RequestURI,
 		Deprecated,
+		Duration,
+		Goroutine,
 		Handler,
+		IPAddress,
 		Latency,
 		LatencyNum,
-		Status,
-		StatusNum,
-		Goroutine,
+		Method,
+		RbackCheckerName,
+		Route,
 		RequestID,
+		RequestURI,
 		Service,
-		Stacktrace,
-		Duration,
 		Size,
+		Stacktrace,
+		Status,
+		StatusNum,
 		Sudo,
 	)
 }
diff --git a/sdk/project.go b/sdk/project.go
index 3e3cd0fff..93c11bbe0 100644
--- a/sdk/project.go
+++ b/sdk/project.go
@@ -10,6 +10,11 @@ import (
 	"github.com/mitchellh/hashstructure"
 )
 
+type ProjectIdentifiers struct {
+	ID  int64  `json:"-" yaml:"-" db:"id"`
+	Key string `json:"-" yaml:"-"db:"projectkey"`
+}
+
 type Projects []Project
 
 func (projects Projects) Keys() []string {
diff --git a/sdk/rbac.go b/sdk/rbac.go
new file mode 100644
index 000000000..7ef55aef6
--- /dev/null
+++ b/sdk/rbac.go
@@ -0,0 +1,37 @@
+package sdk
+
+import "time"
+
+const (
+	RoleRead             = "read"
+	RoleManage           = "manage"
+	RoleDelete           = "delete"
+	RoleCreateProject    = "create-project"
+	RoleManagePermission = "manage-permission"
+)
+
+type RBAC struct {
+	UUID         string        `json:"uuid" db:"uuid" yaml:"-"`
+	Name         string        `json:"name" db:"name" yaml:"name"`
+	Created      time.Time     `json:"created" db:"created" yaml:"-"`
+	LastModified time.Time     `json:"last_modified" db:"last_modified" yaml:"-"`
+	Globals      []RBACGlobal  `json:"globals" db:"-" yaml:"globals"`
+	Projects     []RBACProject `json:"projects" db:"-" yaml:"projects"`
+}
+
+func IsValidRbac(rbac *RBAC) error {
+	if rbac.Name == "" {
+		return WrapError(ErrInvalidData, "missing permission name")
+	}
+	for _, g := range rbac.Globals {
+		if err := isValidRbacGlobal(rbac.Name, g); err != nil {
+			return err
+		}
+	}
+	for _, p := range rbac.Projects {
+		if err := isValidRbacProject(rbac.Name, p); err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/sdk/rbac_global.go b/sdk/rbac_global.go
new file mode 100644
index 000000000..88e786d2e
--- /dev/null
+++ b/sdk/rbac_global.go
@@ -0,0 +1,33 @@
+package sdk
+
+var (
+	GlobalRoles = []string{RoleCreateProject, RoleManagePermission}
+)
+
+type RBACGlobal struct {
+	Role           string   `json:"role" db:"role" yaml:"role"`
+	RBACUsersName  []string `json:"users" db:"-" yaml:"users"`
+	RBACGroupsName []string `json:"groups" db:"-" yaml:"groups"`
+	RBACUsersIDs   []string `json:"-" db:"-" yaml:"-"`
+	RBACGroupsIDs  []int64  `json:"-" db:"-" yaml:"-"`
+}
+
+func isValidRbacGlobal(rbacName string, rg RBACGlobal) error {
+	if len(rg.RBACGroupsIDs) == 0 && len(rg.RBACUsersIDs) == 0 {
+		return NewErrorFrom(ErrInvalidData, "rbac %s: missing groups or users on global permission", rbacName)
+	}
+	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 {
+		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
new file mode 100644
index 000000000..1bf51f108
--- /dev/null
+++ b/sdk/rbac_project.go
@@ -0,0 +1,48 @@
+package sdk
+
+var (
+	ProjectRoles = []string{RoleRead, RoleManage, RoleDelete}
+)
+
+type RBACProject struct {
+	All             bool     `json:"all" db:"all" yaml:"all"`
+	Role            string   `json:"role" db:"role" yaml:"role"`
+	RBACProjectKeys []string `json:"projects" db:"-" yaml:"projects"`
+	RBACUsersName   []string `json:"users" db:"-" yaml:"users"`
+	RBACGroupsName  []string `json:"groups" db:"-" yaml:"groups"`
+
+	RBACProjectsIDs []int64  `json:"-" db:"-" yaml:"-"`
+	RBACUsersIDs    []string `json:"-" db:"-" yaml:"-"`
+	RBACGroupsIDs   []int64  `json:"-" db:"-" yaml:"-"`
+}
+
+func isValidRbacProject(rbacName string, rp RBACProject) error {
+	// Check empty group and users
+	if len(rp.RBACGroupsIDs) == 0 && len(rp.RBACUsersIDs) == 0 {
+		return NewErrorFrom(ErrInvalidData, "rbac %s: missing groups or users on project permission", rbacName)
+	}
+
+	// Check role
+	if rp.Role == "" {
+		return NewErrorFrom(ErrInvalidData, "rbac %s: role for project permission cannot be empty", rbacName)
+	}
+	roleFound := false
+	for _, r := range ProjectRoles {
+		if r == rp.Role {
+			roleFound = true
+			break
+		}
+	}
+	if !roleFound {
+		return NewErrorFrom(ErrInvalidData, "rbac %s: role %s is not allowed on a project permission", rbacName, rp.Role)
+	}
+
+	// Check project_ids and all flag
+	if len(rp.RBACProjectsIDs) == 0 && !rp.All {
+		return NewErrorFrom(ErrInvalidData, "rbac %s: must have at least 1 project on a project permission", rbacName)
+	}
+	if len(rp.RBACProjectsIDs) > 0 && rp.All {
+		return NewErrorFrom(ErrInvalidData, "rbac %s: you can't have a list of project and the all flag checked on a project permission", rbacName)
+	}
+	return nil
+}
diff --git a/sdk/reflect.go b/sdk/reflect.go
index 1d21e12ad..09e2e8361 100644
--- a/sdk/reflect.go
+++ b/sdk/reflect.go
@@ -2,9 +2,18 @@ package sdk
 
 import (
 	"reflect"
+	"runtime"
 	"strings"
 )
 
+func GetFuncName(i interface{}) string {
+	name := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
+	name = strings.Replace(name, ".func1", "", 1)
+	name = strings.Replace(name, ".1", "", 1)
+	name = strings.Replace(name, "github.com/ovh/cds/engine/", "", 1)
+	return name
+}
+
 // From https://github.com/fsamin/go-dump/blob/master/helper.go
 // Apache-2.0 License
 // https://github.com/fsamin/go-dump/blob/master/LICENSE.md


Test patch:
diff --git a/engine/api/rbac/dao_rbac_test.go b/engine/api/rbac/dao_rbac_test.go
new file mode 100644
index 000000000..dcbd040a6
--- /dev/null
+++ b/engine/api/rbac/dao_rbac_test.go
@@ -0,0 +1,205 @@
+package rbac
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"gopkg.in/yaml.v2"
+
+	"github.com/ovh/cds/engine/api/test"
+	"github.com/ovh/cds/engine/api/test/assets"
+	"github.com/ovh/cds/sdk"
+)
+
+func TestLoadRbacProject(t *testing.T) {
+	// user1  can read (proj1)
+	// Group 1 can read (proj1,prj2), manage(prj2)
+	// user2  in group 1
+	db, cache := test.SetupPG(t)
+
+	_, err := db.Exec("DELETE FROM rbac")
+	require.NoError(t, err)
+
+	key1 := sdk.RandomString(10)
+	proj1 := assets.InsertTestProject(t, db, cache, key1, key1)
+
+	key2 := sdk.RandomString(10)
+	proj2 := assets.InsertTestProject(t, db, cache, key2, key2)
+
+	grpName1 := sdk.RandomString(10)
+	group1 := assets.InsertTestGroup(t, db, grpName1)
+
+	users1, _ := assets.InsertLambdaUser(t, db)
+	users2, _ := assets.InsertLambdaUser(t, db, group1)
+
+	perm := fmt.Sprintf(`name: perm-test
+projects:
+  - role: read
+    projects: [%s]
+    users: [%s]
+    groups: [%s]
+  - role: read
+    projects: [%s]
+    groups: [%s]
+  - role: manage
+    projects: [%s]
+    groups: [%s]
+`, proj1.Key, users1.Username, group1.Name, proj2.Key, group1.Name, proj2.Key, group1.Name)
+
+	var r sdk.RBAC
+	require.NoError(t, yaml.Unmarshal([]byte(perm), &r))
+
+	err = fillWithIDs(context.TODO(), db, &r)
+	require.NoError(t, err)
+
+	require.NoError(t, Insert(context.Background(), db, &r))
+
+	prjusers1, err := LoadProjectIDsByRoleAndUserID(context.TODO(), db, sdk.RoleRead, users1.ID)
+	require.NoError(t, err)
+	t.Logf("%+v", prjusers1)
+	require.Len(t, prjusers1, 1)
+	require.Equal(t, prjusers1[0], proj1.ID)
+
+	prjusers2, err := LoadProjectIDsByRoleAndUserID(context.TODO(), db, sdk.RoleManage, users2.ID)
+	require.NoError(t, err)
+	require.Len(t, prjusers2, 1)
+	require.Equal(t, prjusers2[0], proj2.ID)
+}
+
+func TestLoadRbac(t *testing.T) {
+	db, cache := test.SetupPG(t)
+
+	_, err := db.Exec("DELETE FROM rbac WHERE name = 'perm-test'")
+	require.NoError(t, err)
+
+	key1 := sdk.RandomString(10)
+	proj1 := assets.InsertTestProject(t, db, cache, key1, key1)
+
+	key2 := sdk.RandomString(10)
+	proj2 := assets.InsertTestProject(t, db, cache, key2, key2)
+
+	grpName1 := sdk.RandomString(10)
+	group1 := assets.InsertTestGroup(t, db, grpName1)
+
+	users1, _ := assets.InsertLambdaUser(t, db)
+
+	perm := fmt.Sprintf(`name: perm-test
+projects:
+  - role: read
+    users: [%s]
+    groups: [%s]
+    projects: [%s]
+  - role: manage
+    groups: [%s]
+    projects: [%s]
+globals:
+  - role: create-project
+    users: [%s]
+`, users1.Username, group1.Name, proj1.Key, group1.Name, proj2.Key, users1.Username)
+
+	var r sdk.RBAC
+	require.NoError(t, yaml.Unmarshal([]byte(perm), &r))
+	err = fillWithIDs(context.TODO(), db, &r)
+	require.NoError(t, err)
+
+	require.NoError(t, Insert(context.Background(), db, &r))
+
+	rbacDB, err := LoadRbacByName(context.TODO(), db, r.Name, LoadOptions.Default)
+	require.NoError(t, err)
+
+	// Global part
+	require.Equal(t, len(r.Globals), len(rbacDB.Globals))
+	require.Equal(t, r.Globals[0].Role, rbacDB.Globals[0].Role)
+	require.Equal(t, users1.ID, rbacDB.Globals[0].RBACUsersIDs[0])
+
+	// Project part
+	require.Equal(t, len(r.Projects), len(rbacDB.Projects))
+
+	manageCheck := false
+	readCheck := false
+	for _, rp := range r.Projects {
+		if rp.Role == "manage" {
+			require.Equal(t, 1, len(rp.RBACGroupsName))
+			require.Equal(t, 1, len(rp.RBACGroupsIDs))
+			require.Equal(t, 1, len(rp.RBACProjectKeys))
+			require.Equal(t, 1, len(rp.RBACProjectsIDs))
+			require.Equal(t, proj2.Key, rp.RBACProjectKeys[0])
+			require.Equal(t, proj2.ID, rp.RBACProjectsIDs[0])
+			require.Equal(t, group1.Name, rp.RBACGroupsName[0])
+			require.Equal(t, group1.ID, rp.RBACGroupsIDs[0])
+			manageCheck = true
+		}
+		if rp.Role == "read" {
+			require.Equal(t, 1, len(rp.RBACGroupsName))
+			require.Equal(t, 1, len(rp.RBACGroupsIDs))
+			require.Equal(t, 1, len(rp.RBACUsersIDs))
+			require.Equal(t, 1, len(rp.RBACUsersName))
+			require.Equal(t, 1, len(rp.RBACProjectKeys))
+			require.Equal(t, 1, len(rp.RBACProjectsIDs))
+			require.Equal(t, proj1.Key, rp.RBACProjectKeys[0])
+			require.Equal(t, proj1.ID, rp.RBACProjectsIDs[0])
+			require.Equal(t, group1.Name, rp.RBACGroupsName[0])
+			require.Equal(t, group1.ID, rp.RBACGroupsIDs[0])
+			require.Equal(t, users1.Username, rp.RBACUsersName[0])
+			require.Equal(t, users1.ID, rp.RBACUsersIDs[0])
+			readCheck = true
+		}
+	}
+	require.True(t, manageCheck)
+	require.True(t, readCheck)
+}
+
+func TestUpdateRbac(t *testing.T) {
+	db, cache := test.SetupPG(t)
+
+	_, err := db.Exec("DELETE FROM rbac WHERE name = 'perm-test'")
+	require.NoError(t, err)
+
+	key1 := sdk.RandomString(10)
+	proj1 := assets.InsertTestProject(t, db, cache, key1, key1)
+
+	users1, _ := assets.InsertLambdaUser(t, db)
+
+	perm := fmt.Sprintf(`name: perm-test
+projects:
+  - role: read
+    users: [%s]
+    projects: [%s]
+
+`, users1.Username, proj1.Key)
+
+	var r sdk.RBAC
+	require.NoError(t, yaml.Unmarshal([]byte(perm), &r))
+	err = fillWithIDs(context.TODO(), db, &r)
+	require.NoError(t, err)
+
+	require.NoError(t, Insert(context.Background(), db, &r))
+
+	rbacDB, err := LoadRbacByName(context.TODO(), db, r.Name, LoadOptions.Default)
+	require.NoError(t, err)
+
+	require.Equal(t, "read", rbacDB.Projects[0].Role)
+
+	// Update change role
+	permUpdated := fmt.Sprintf(`name: perm-test
+projects:
+  - role: manage
+    users: [%s]
+    projects: [%s]
+
+`, users1.Username, proj1.Key)
+
+	var rUpdated sdk.RBAC
+	require.NoError(t, yaml.Unmarshal([]byte(permUpdated), &rUpdated))
+	err = fillWithIDs(context.TODO(), db, &rUpdated)
+	require.NoError(t, err)
+
+	require.NoError(t, Update(context.TODO(), db, &rUpdated))
+
+	rbacDBUpdate, err := LoadRbacByName(context.TODO(), db, r.Name, LoadOptions.Default)
+	require.NoError(t, err)
+	require.Equal(t, "manage", rbacDBUpdate.Projects[0].Role)
+
+}
diff --git a/engine/api/v2_project_test.go b/engine/api/v2_project_test.go
new file mode 100644
index 000000000..5edd8160f
--- /dev/null
+++ b/engine/api/v2_project_test.go
@@ -0,0 +1,30 @@
+package api
+
+import (
+	"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"
+	"net/http/httptest"
+	"testing"
+)
+
+func Test_addVCSOnProject(t *testing.T) {
+	api, db, _ := newTestAPI(t)
+
+	u, pass := assets.InsertLambdaUser(t, db)
+	proj := assets.InsertTestProject(t, db, api.Cache, sdk.RandomString(10), sdk.RandomString(10))
+
+	vars := map[string]string{
+		"projectKey": proj.Key,
+	}
+	uri := api.Router.GetRouteV2("POST", api.postVCSOnProjectHandler, vars)
+	test.NotEmpty(t, uri)
+	req := assets.NewAuthentifiedRequest(t, u, pass, "POST", uri, nil)
+
+	//Do the request
+	w := httptest.NewRecorder()
+	api.Router.Mux.ServeHTTP(w, req)
+	require.Equal(t, 204, w.Code)
+
+}
diff --git a/engine/hatchery/kubernetes/helper_test.go b/engine/hatchery/kubernetes/helper_test.go
index 4d6673057..05ca17ce1 100644
--- a/engine/hatchery/kubernetes/helper_test.go
+++ b/engine/hatchery/kubernetes/helper_test.go
@@ -23,12 +23,12 @@ func NewHatcheryKubernetesTest(t *testing.T) *HatcheryKubernetes {
 	h.kubeClient = &kubernetesClient{clientSet}
 	gock.InterceptClient(clientSet.CoreV1().RESTClient().(*rest.RESTClient).Client)
 
-	h.Config.Name = "kyubi"
-	h.Config.Namespace = "hachibi"
+	h.Config.Name = "my-hatchery"
+	h.Config.Namespace = "cds-workers"
 	h.ServiceInstance = &sdk.Service{
 		CanonicalService: sdk.CanonicalService{
 			ID:   1,
-			Name: "kyubi",
+			Name: "my-hatchery",
 		},
 	}
 	return h
diff --git a/engine/hatchery/kubernetes/kill_workers_test.go b/engine/hatchery/kubernetes/kill_workers_test.go
index e559d79ee..be0b71424 100644
--- a/engine/hatchery/kubernetes/kill_workers_test.go
+++ b/engine/hatchery/kubernetes/kill_workers_test.go
@@ -9,6 +9,8 @@ import (
 	"gopkg.in/h2non/gock.v1"
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/ovh/cds/sdk"
 )
 
 func TestHatcheryKubernetes_KillAwolWorkers(t *testing.T) {
@@ -19,8 +21,12 @@ func TestHatcheryKubernetes_KillAwolWorkers(t *testing.T) {
 		Items: []v1.Pod{
 			{
 				ObjectMeta: metav1.ObjectMeta{
-					Name:      "w1",
-					Namespace: "kyubi",
+					Name:      "worker-1",
+					Namespace: "cds-workers",
+					Labels: map[string]string{
+						LABEL_HATCHERY_NAME: "my-hatchery",
+						LABEL_WORKER_NAME:   "worker-1",
+					},
 				},
 				Status: v1.PodStatus{
 					ContainerStatuses: []v1.ContainerStatus{
@@ -36,15 +42,23 @@ func TestHatcheryKubernetes_KillAwolWorkers(t *testing.T) {
 			},
 			{
 				ObjectMeta: metav1.ObjectMeta{
-					Name:      "wrong",
-					Namespace: "kyubi",
+					Name:      "worker-2",
+					Namespace: "cds-workers",
+					Labels: map[string]string{
+						LABEL_HATCHERY_NAME: "my-hatchery",
+						LABEL_WORKER_NAME:   "worker-2",
+					},
 				},
 				Spec: v1.PodSpec{},
 			},
 			{
 				ObjectMeta: metav1.ObjectMeta{
-					Name:      "w2",
-					Namespace: "kyubi",
+					Name:      "worker-3",
+					Namespace: "cds-workers",
+					Labels: map[string]string{
+						LABEL_HATCHERY_NAME: "my-hatchery",
+						LABEL_WORKER_NAME:   "worker-3",
+					},
 				},
 				Status: v1.PodStatus{
 					ContainerStatuses: []v1.ContainerStatus{
@@ -60,8 +74,12 @@ func TestHatcheryKubernetes_KillAwolWorkers(t *testing.T) {
 			},
 			{
 				ObjectMeta: metav1.ObjectMeta{
-					Name:      "w3",
-					Namespace: "kyubi",
+					Name:      "worker-4",
+					Namespace: "cds-workers",
+					Labels: map[string]string{
+						LABEL_HATCHERY_NAME: "my-hatchery",
+						LABEL_WORKER_NAME:   "worker-4",
+					},
 				},
 				Status: v1.PodStatus{
 					ContainerStatuses: []v1.ContainerStatus{
@@ -75,13 +93,39 @@ func TestHatcheryKubernetes_KillAwolWorkers(t *testing.T) {
 					},
 				},
 			},
+			{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "worker-5",
+					Namespace: "cds-workers",
+					Labels: map[string]string{
+						LABEL_HATCHERY_NAME: "my-hatchery",
+						LABEL_WORKER_NAME:   "worker-5",
+					},
+				},
+			},
+			{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "worker-6",
+					Namespace: "cds-workers",
+					Labels: map[string]string{
+						LABEL_HATCHERY_NAME: "my-hatchery",
+						LABEL_WORKER_NAME:   "worker-6",
+					},
+					CreationTimestamp: metav1.Now(),
+				},
+			},
 		},
 	}
-	gock.New("http://lolcat.kube").Get("/api/v1/namespaces/hachibi/pods").Reply(http.StatusOK).JSON(podsList)
+	gock.New("http://lolcat.kube").Get("/api/v1/namespaces/cds-workers/pods").Reply(http.StatusOK).JSON(podsList)
+
+	gock.New("http://lolcat.kube").Delete("/api/v1/namespaces/cds-workers/pods/worker-1").Reply(http.StatusOK).JSON(nil)
+	gock.New("http://lolcat.kube").Delete("/api/v1/namespaces/cds-workers/pods/worker-2").Reply(http.StatusOK).JSON(nil)
+	gock.New("http://lolcat.kube").Delete("/api/v1/namespaces/cds-workers/pods/worker-3").Reply(http.StatusOK).JSON(nil)
+	gock.New("http://lolcat.kube").Delete("/api/v1/namespaces/cds-workers/pods/worker-4").Reply(http.StatusOK).JSON(nil)
 
-	gock.New("http://lolcat.kube").Delete("/api/v1/namespaces/kyubi/pods/w1").Reply(http.StatusOK).JSON(nil)
-	gock.New("http://lolcat.kube").Delete("/api/v1/namespaces/kyubi/pods/w2").Reply(http.StatusOK).JSON(nil)
-	gock.New("http://lolcat.kube").Delete("/api/v1/namespaces/kyubi/pods/w3").Reply(http.StatusOK).JSON(nil)
+	gock.New("http://lolcat.api").Get("/worker").Reply(http.StatusOK).JSON([]sdk.Worker{{
+		Name: "worker-5",
+	}})
 
 	err := h.killAwolWorkers(context.TODO())
 	require.NoError(t, err)
diff --git a/engine/hatchery/kubernetes/kubernetes_test.go b/engine/hatchery/kubernetes/kubernetes_test.go
index ebb2c2dda..9b879e610 100644
--- a/engine/hatchery/kubernetes/kubernetes_test.go
+++ b/engine/hatchery/kubernetes/kubernetes_test.go
@@ -5,9 +5,9 @@ import (
 	"encoding/json"
 	"io"
 	"net/http"
+	"strings"
 	"testing"
 
-	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"gopkg.in/h2non/gock.v1"
 	v1 "k8s.io/api/core/v1"
@@ -27,16 +27,7 @@ func TestHatcheryKubernetes_WorkersStarted(t *testing.T) {
 				ObjectMeta: metav1.ObjectMeta{
 					Name: "w1",
 					Labels: map[string]string{
-						LABEL_HATCHERY_NAME: "kyubi",
-					},
-				},
-				Spec: v1.PodSpec{},
-			},
-			{
-				ObjectMeta: metav1.ObjectMeta{
-					Name: "wrong",
-					Labels: map[string]string{
-						LABEL_HATCHERY_NAME: "jubi",
+						LABEL_HATCHERY_NAME: "my-hatchery",
 					},
 				},
 				Spec: v1.PodSpec{},
@@ -45,16 +36,17 @@ func TestHatcheryKubernetes_WorkersStarted(t *testing.T) {
 				ObjectMeta: metav1.ObjectMeta{
 					Name: "w2",
 					Labels: map[string]string{
-						LABEL_HATCHERY_NAME: "kyubi",
+						LABEL_HATCHERY_NAME: "my-hatchery",
 					},
 				},
 				Spec: v1.PodSpec{},
 			},
 		},
 	}
-	gock.New("http://lolcat.kube").Get("/api/v1/namespaces/hachibi/pods").Reply(http.StatusOK).JSON(podsList)
+	gock.New("http://lolcat.kube").Get("/api/v1/namespaces/cds-workers/pods").Reply(http.StatusOK).JSON(podsList)
 
-	ws := h.WorkersStarted(context.TODO())
+	ws, err := h.WorkersStarted(context.TODO())
+	require.NoError(t, err)
 	require.Equal(t, 2, len(ws))
 	require.Equal(t, "w1", ws[0])
 	require.Equal(t, "w2", ws[1])
@@ -65,7 +57,7 @@ func TestHatcheryKubernetes_Status(t *testing.T) {
 	defer gock.Off()
 	defer gock.Observe(nil)
 	h := NewHatcheryKubernetesTest(t)
-	h.Config.HatcheryCommonConfiguration.Provision.InjectEnvVars = []string{"ZZZZ=ZZZZ"}
+	h.Config.HatcheryCommonConfiguration.Provision.InjectEnvVars = []string{"PROVISION_ENV=MYVALUE"}
 
 	m := &sdk.Model{
 		Name: "model1",
@@ -74,49 +66,64 @@ func TestHatcheryKubernetes_Status(t *testing.T) {
 		},
 	}
 
-	podResponse := v1.Pod{}
-	gock.New("http://lolcat.kube").Post("/api/v1/namespaces/hachibi/pods").Reply(http.StatusOK).JSON(podResponse)
+	gock.New("http://lolcat.kube").Delete("/api/v1/namespaces/cds-workers/secrets/cds-worker-config-my-worker").Reply(http.StatusOK)
+	gock.New("http://lolcat.kube").Post("/api/v1/namespaces/cds-workers/secrets").Reply(http.StatusOK).JSON(v1.Pod{})
+	gock.New("http://lolcat.kube").Post("/api/v1/namespaces/cds-workers/pods").Reply(http.StatusOK).JSON(v1.Pod{})
 
-	var checkRequest gock.ObserverFunc = func(request *http.Request, mock gock.Mock) {
-		if request.Body == nil {
-			return
-		}
+	gock.Observe(func(request *http.Request, mock gock.Mock) {
+		t.Logf("%s %s", request.URL, request.Method)
 		bodyContent, err := io.ReadAll(request.Body)
-		assert.NoError(t, err)
-		var podRequest v1.Pod
-		require.NoError(t, json.Unmarshal(bodyContent, &podRequest))
-
+		require.NoError(t, err)
 		t.Logf("%s", string(bodyContent))
-		require.Equal(t, "hachibi", podRequest.ObjectMeta.Namespace)
-		require.Equal(t, "kyubi", podRequest.Labels["CDS_HATCHERY_NAME"])
-		require.Equal(t, "666", podRequest.Labels[hatchery.LabelServiceJobID])
-		require.Equal(t, "999", podRequest.Labels[hatchery.LabelServiceNodeRunID])
-		require.Equal(t, "execution", podRequest.Labels["CDS_WORKER"])
-		require.Equal(t, "model1", podRequest.Labels["CDS_WORKER_MODEL"])
-
-		require.Equal(t, 2, len(podRequest.Spec.Containers))
-		require.Equal(t, "k8s-toto", podRequest.Spec.Containers[0].Name)
-		require.Equal(t, int64(4096), podRequest.Spec.Containers[0].Resources.Requests.Memory().Value())
-		var zzzzFound bool
-		for _, env := range podRequest.Spec.Containers[0].Env {
-			if "ZZZZ" == env.Name && "ZZZZ" == env.Value {
-				zzzzFound = true
-				break
+
+		if request.Method == http.MethodPost && strings.HasPrefix(request.URL.String(), "http://lolcat.kube/api/v1/namespaces/cds-workers/secrets") {
+			var secretRequest v1.Secret
+			require.NoError(t, json.Unmarshal(bodyContent, &secretRequest))
+
+			require.Equal(t, "Secret", secretRequest.Kind)
+			require.Equal(t, "cds-worker-config-my-worker", secretRequest.Name)
+			require.Equal(t, "my-hatchery", secretRequest.Labels[LABEL_HATCHERY_NAME])
+			require.Equal(t, "my-worker", secretRequest.Labels[LABEL_WORKER_NAME])
+		}
+
+		if request.Method == http.MethodPost && strings.HasPrefix(request.URL.String(), "http://lolcat.kube/api/v1/namespaces/cds-workers/pods") {
+			var podRequest v1.Pod
+			require.NoError(t, json.Unmarshal(bodyContent, &podRequest))
+
+			require.Equal(t, "Pod", podRequest.Kind)
+			require.Equal(t, "cds-workers", podRequest.ObjectMeta.Namespace)
+			require.Equal(t, "my-hatchery", podRequest.Labels[LABEL_HATCHERY_NAME])
+			require.Equal(t, "666", podRequest.Labels[hatchery.LabelServiceJobID])
+			require.Equal(t, "999", podRequest.Labels[hatchery.LabelServiceNodeRunID])
+			require.Equal(t, "my-worker", podRequest.Labels[LABEL_WORKER_NAME])
+			require.Equal(t, "group-model1", podRequest.Labels[LABEL_WORKER_MODEL_PATH])
+
+			require.Equal(t, 2, len(podRequest.Spec.Containers))
+			require.Equal(t, "my-worker", podRequest.Spec.Containers[0].Name)
+			require.Equal(t, int64(4096), podRequest.Spec.Containers[0].Resources.Requests.Memory().Value())
+			var foundEnv, foundSecret bool
+			for _, env := range podRequest.Spec.Containers[0].Env {
+				if env.Name == "PROVISION_ENV" && env.Value == "MYVALUE" {
+					foundEnv = true
+				}
+				if env.Name == "CDS_CONFIG" && env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil && env.ValueFrom.SecretKeyRef.Name == "cds-worker-config-my-worker" {
+					foundSecret = true
+				}
 			}
+			require.True(t, foundEnv, "\"PROVISION_ENV\" not found in env variables")
+			require.True(t, foundSecret, "\"CDS_CONFIG\" not found in env variables")
+			require.Equal(t, "service-0-pg", podRequest.Spec.Containers[1].Name)
+			require.Equal(t, 1, len(podRequest.Spec.Containers[1].Env))
+			require.Equal(t, "PG_USERNAME", podRequest.Spec.Containers[1].Env[0].Name)
+			require.Equal(t, "username", podRequest.Spec.Containers[1].Env[0].Value)
 		}
-		require.True(t, zzzzFound, "\"ZZZZ\" not found in env variables")
-		require.Equal(t, "service-0-pg", podRequest.Spec.Containers[1].Name)
-		require.Equal(t, 1, len(podRequest.Spec.Containers[1].Env))
-		require.Equal(t, "PG_USERNAME", podRequest.Spec.Containers[1].Env[0].Name)
-		require.Equal(t, "toto", podRequest.Spec.Containers[1].Env[0].Value)
-	}
-	gock.Observe(checkRequest)
+	})
 
 	err := h.SpawnWorker(context.TODO(), hatchery.SpawnArguments{
 		JobID:      666,
 		NodeRunID:  999,
 		Model:      m,
-		WorkerName: "k8s-toto",
+		WorkerName: "my-worker",
 		Requirements: []sdk.Requirement{
 			{
 				Name:  "mem",
@@ -125,7 +132,7 @@ func TestHatcheryKubernetes_Status(t *testing.T) {
 			}, {
 				Name:  "pg",
 				Type:  sdk.ServiceRequirement,
-				Value: "postgresql:5.6.7 PG_USERNAME=toto",
+				Value: "postgresql:5.6.7 PG_USERNAME=username",
 			},
 		},
 	})
diff --git a/engine/hatchery/kubernetes/services_test.go b/engine/hatchery/kubernetes/services_test.go
index 981272f6b..8a751de40 100644
--- a/engine/hatchery/kubernetes/services_test.go
+++ b/engine/hatchery/kubernetes/services_test.go
@@ -41,7 +41,7 @@ func Test_serviceLogs(t *testing.T) {
 			{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "pod-name",
-					Namespace: "kyubi",
+					Namespace: "cds-workers",
 					Labels: map[string]string{
 						hatchery.LabelServiceJobID:        "666",
 						hatchery.LabelServiceNodeRunID:    "999",
@@ -65,12 +65,12 @@ func Test_serviceLogs(t *testing.T) {
 			},
 		},
 	}
-	gock.New("http://lolcat.kube").Get("/api/v1/namespaces/hachibi/pods").Reply(http.StatusOK).JSON(podsList)
+	gock.New("http://lolcat.kube").Get("/api/v1/namespaces/cds-workers/pods").Reply(http.StatusOK).JSON(podsList)
 
 	gock.New("http://lolcat.kube").AddMatcher(func(r *http.Request, rr *gock.Request) (bool, error) {
 		b, err := gock.MatchPath(r, rr)
-		assert.NoError(t, err)
-		if r.Method == http.MethodGet && strings.HasPrefix(r.URL.String(), "http://lolcat.kube/api/v1/namespaces/hachibi/pods/pod-name/log?container=service-666-blabla") {
+		require.NoError(t, err)
+		if r.Method == http.MethodGet && strings.HasPrefix(r.URL.String(), "http://lolcat.kube/api/v1/namespaces/cds-workers/pods/pod-name/log?container=service-666-blabla") {
 			if b {
 				return true, nil
 			}
diff --git a/engine/hatchery/marathon/marathon_test.go b/engine/hatchery/marathon/marathon_test.go
index e7002477d..05c58bfcf 100644
--- a/engine/hatchery/marathon/marathon_test.go
+++ b/engine/hatchery/marathon/marathon_test.go
@@ -14,6 +14,7 @@ import (
 	"github.com/ovh/cds/sdk"
 	"github.com/ovh/cds/sdk/hatchery"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	"gopkg.in/h2non/gock.v1"
 )
 
@@ -37,7 +38,8 @@ func TestWorkerStarted(t *testing.T) {
 	}
 
 	gock.New("http://mara.thon").Get("/v2/apps").Reply(200).JSON(apps)
-	wkrs := h.WorkersStarted(context.TODO())
+	wkrs, err := h.WorkersStarted(context.TODO())
+  require.NoError(t, err)
 	t.Logf("%+v", wkrs)
 	assert.Equal(t, 2, len(wkrs))
 	assert.Equal(t, "w1", wkrs[0])
@@ -179,7 +181,6 @@ func TestKillAwolWOrkers(t *testing.T) {
 	err := h.killAwolWorkers()
 	assert.NoError(t, err)
 	assert.True(t, gock.IsDone())
-
 }
 
 func TestSpawn(t *testing.T) {
diff --git a/engine/hatchery/swarm/swarm_test.go b/engine/hatchery/swarm/swarm_test.go
index 35c023b31..ff19eed16 100644
--- a/engine/hatchery/swarm/swarm_test.go
+++ b/engine/hatchery/swarm/swarm_test.go
@@ -260,7 +260,8 @@ func TestHatcherySwarm_WorkersStarted(t *testing.T) {
 	}
 	gock.New("https://lolcat.host").Get("/v6.66/containers/json").Reply(http.StatusOK).JSON(containers)
 
-	s := h.WorkersStarted(context.TODO())
+	s, err := h.WorkersStarted(context.TODO())
+	require.NoError(t, err)
 	require.Equal(t, 2, len(s))
 	require.Equal(t, "w1", s[0])
 	require.Equal(t, "w2", s[1])
diff --git a/sdk/hatchery/hatchery_test.go b/sdk/hatchery/hatchery_test.go
index f10c485af..3fcdff97c 100644
--- a/sdk/hatchery/hatchery_test.go
+++ b/sdk/hatchery/hatchery_test.go
@@ -80,7 +80,7 @@ func TestCreate(t *testing.T) {
 
 	// This calls are expected for each job received in the channel
 	mockCDSClient.EXPECT().WorkerList(gomock.Any()).Return(nil, nil).AnyTimes()
-	mockHatchery.EXPECT().WorkersStarted(gomock.Any()).Return(nil).AnyTimes()
+	mockHatchery.EXPECT().WorkersStarted(gomock.Any()).Return(nil, nil).AnyTimes()
 	mockHatchery.EXPECT().CanSpawn(gomock.Any(), gomock.Any(), int64(666), gomock.Any()).Return(true).AnyTimes()
 	mockCDSClient.EXPECT().QueueJobBook(gomock.Any(), int64(666)).Return(sdk.WorkflowNodeJobRunBooked{}, nil).AnyTimes()
 	mockCDSClient.EXPECT().QueueJobSendSpawnInfo(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
diff --git a/sdk/rbac_global_test.go b/sdk/rbac_global_test.go
new file mode 100644
index 000000000..1186bb5b0
--- /dev/null
+++ b/sdk/rbac_global_test.go
@@ -0,0 +1,38 @@
+package sdk
+
+import (
+	"github.com/stretchr/testify/require"
+	"testing"
+)
+
+func TestRbacGlobalInvalidGlobalRole(t *testing.T) {
+	rb := RBACGlobal{
+		Role:          "runWorkflow",
+		RBACGroupsIDs: []int64{1},
+		RBACUsersIDs:  []string{"aa-aa-aa"},
+	}
+	err := isValidRbacGlobal("myRule", rb)
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "rbac myRule: role runWorkflow is not allowed on a global permission")
+}
+func TestRbacGlobalInvalidGroupAndUsers(t *testing.T) {
+	rb := RBACGlobal{
+		Role:          RoleCreateProject,
+		RBACGroupsIDs: []int64{},
+		RBACUsersIDs:  []string{},
+	}
+	err := isValidRbacGlobal("myRule", rb)
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "rbac myRule: missing groups or users on global permission")
+}
+
+func TestRbacGlobalEmptyRole(t *testing.T) {
+	rb := RBACGlobal{
+		Role:          "",
+		RBACGroupsIDs: []int64{1},
+		RBACUsersIDs:  []string{},
+	}
+	err := isValidRbacGlobal("myRule", rb)
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "rbac myRule: role for global permission cannot be empty")
+}
diff --git a/sdk/rbac_project_test.go b/sdk/rbac_project_test.go
new file mode 100644
index 000000000..fcb87c103
--- /dev/null
+++ b/sdk/rbac_project_test.go
@@ -0,0 +1,68 @@
+package sdk
+
+import (
+	"fmt"
+	"github.com/stretchr/testify/require"
+	"testing"
+)
+
+func TestRbacProjectInvalidRole(t *testing.T) {
+	rb := RBACProject{
+		RBACProjectsIDs: []int64{1},
+		All:             false,
+		Role:            RoleCreateProject,
+		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))
+}
+func TestRbacProjectInvalidGroupAndUsers(t *testing.T) {
+	rb := RBACProject{
+		RBACProjectsIDs: []int64{1},
+		All:             false,
+		Role:            RoleRead,
+		RBACGroupsIDs:   []int64{},
+		RBACUsersIDs:    []string{},
+	}
+	err := isValidRbacProject("myRule", rb)
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "rbac myRule: missing groups or users on project permission")
+}
+func TestRbacProjectInvalidProjectIDs(t *testing.T) {
+	rb := RBACProject{
+		RBACProjectsIDs: []int64{},
+		All:             false,
+		Role:            RoleRead,
+		RBACGroupsIDs:   []int64{1},
+		RBACUsersIDs:    []string{},
+	}
+	err := isValidRbacProject("myRule", rb)
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "rbac myRule: must have at least 1 project on a project permission")
+}
+func TestRbacProjectEmptyRole(t *testing.T) {
+	rb := RBACProject{
+		RBACProjectsIDs: []int64{1},
+		All:             false,
+		Role:            "",
+		RBACGroupsIDs:   []int64{1},
+		RBACUsersIDs:    []string{},
+	}
+	err := isValidRbacProject("myRule", rb)
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "rbac myRule: role for project permission cannot be empty")
+}
+func TestRbacProjectInvalidAllAndListOfProject(t *testing.T) {
+	rb := RBACProject{
+		RBACProjectsIDs: []int64{1},
+		All:             true,
+		Role:            RoleRead,
+		RBACGroupsIDs:   []int64{1},
+		RBACUsersIDs:    []string{},
+	}
+	err := isValidRbacProject("myRule", rb)
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "rbac myRule: you can't have a list of project and the all flag checked on a project permission")
+}