Repository: ovh/cds
Pull request #6265 (2022-09-02 09:23:02)

feat(api): add RBAC import handler
1. Description
1. Related issues
1. About tests
1. Mentions

@ovh/cds



Patch:
diff --git a/cli/cdsctl/experimental.go b/cli/cdsctl/experimental.go
index a615d152a..fea6a5800 100644
--- a/cli/cdsctl/experimental.go
+++ b/cli/cdsctl/experimental.go
@@ -15,6 +15,7 @@ var experimentalCmd = cli.Command{
 
 func experimentalCommands() []*cobra.Command {
 	return []*cobra.Command{
+		experimentalRbac(),
 		experimentalProject(),
 	}
 }
diff --git a/cli/cdsctl/experimental_rbac.go b/cli/cdsctl/experimental_rbac.go
new file mode 100644
index 000000000..03f44f803
--- /dev/null
+++ b/cli/cdsctl/experimental_rbac.go
@@ -0,0 +1,50 @@
+package main
+
+import (
+	"context"
+	"os"
+
+	"github.com/spf13/cobra"
+
+	"github.com/ovh/cds/cli"
+	"github.com/ovh/cds/sdk/cdsclient"
+)
+
+var experimentalRbacCmd = cli.Command{
+	Name:  "rbac",
+	Short: "CDS Experimental rbac commands",
+}
+
+func experimentalRbac() *cobra.Command {
+	return cli.NewCommand(experimentalRbacCmd, nil, []*cobra.Command{
+		cli.NewCommand(rbacImportCmd, rbacImportFunc, nil, withAllCommandModifiers()...),
+	})
+}
+
+var rbacImportCmd = cli.Command{
+	Name:    "import",
+	Short:   "Import a rbac rule from a yaml file",
+	Example: "cdsctl rbac import file.yml",
+	Ctx:     []cli.Arg{},
+	Args: []cli.Arg{
+		{Name: "filename"},
+	},
+	Flags: []cli.Flag{
+		{Name: "force", Type: cli.FlagBool},
+	},
+}
+
+func rbacImportFunc(v cli.Values) error {
+	f, err := os.Open(v.GetString("filename"))
+	if err != nil {
+		return cli.WrapError(err, "unable to open file %s", v.GetString("filename"))
+	}
+	defer f.Close() // nolint
+
+	var mods []cdsclient.RequestModifier
+	if v.GetBool("force") {
+		mods = append(mods, cdsclient.Force())
+	}
+	_, err = client.RBACImport(context.Background(), f, mods...)
+	return err
+}
diff --git a/contrib/grpcplugins/action/plugin-archive/go.mod b/contrib/grpcplugins/action/plugin-archive/go.mod
index caad49611..3b610ae35 100644
--- a/contrib/grpcplugins/action/plugin-archive/go.mod
+++ b/contrib/grpcplugins/action/plugin-archive/go.mod
@@ -8,7 +8,7 @@ require (
 	github.com/golang/protobuf v1.5.2
 	github.com/mholt/archiver/v3 v3.5.1-0.20210618180617-81fac4ba96e4
 	github.com/ovh/cds v0.0.0-00010101000000-000000000000
-	github.com/stretchr/testify v1.7.0
+	github.com/stretchr/testify v1.8.0
 )
 
 require (
@@ -75,5 +75,5 @@ require (
 	google.golang.org/grpc v1.43.0 // indirect
 	google.golang.org/protobuf v1.27.1 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
-	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/contrib/grpcplugins/action/plugin-archive/go.sum b/contrib/grpcplugins/action/plugin-archive/go.sum
index ce283252c..2ffbda87b 100644
--- a/contrib/grpcplugins/action/plugin-archive/go.sum
+++ b/contrib/grpcplugins/action/plugin-archive/go.sum
@@ -291,13 +291,16 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
@@ -671,8 +674,9 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/contrib/grpcplugins/action/plugin-artifactory-release-bundle-create/go.mod b/contrib/grpcplugins/action/plugin-artifactory-release-bundle-create/go.mod
index 9c33f11d4..5f5d378e5 100644
--- a/contrib/grpcplugins/action/plugin-artifactory-release-bundle-create/go.mod
+++ b/contrib/grpcplugins/action/plugin-artifactory-release-bundle-create/go.mod
@@ -12,7 +12,7 @@ require (
 	github.com/jfrog/jfrog-client-go v1.5.1
 	github.com/ovh/cds v0.0.0-00010101000000-000000000000
 	github.com/pkg/errors v0.9.1
-	github.com/stretchr/testify v1.7.0
+	github.com/stretchr/testify v1.8.0
 )
 
 require (
@@ -99,5 +99,5 @@ require (
 	gopkg.in/ini.v1 v1.66.4 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
-	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/contrib/grpcplugins/action/plugin-artifactory-release-bundle-create/go.sum b/contrib/grpcplugins/action/plugin-artifactory-release-bundle-create/go.sum
index db0df132f..30a958212 100644
--- a/contrib/grpcplugins/action/plugin-artifactory-release-bundle-create/go.sum
+++ b/contrib/grpcplugins/action/plugin-artifactory-release-bundle-create/go.sum
@@ -463,13 +463,16 @@ github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk=
 github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8qy1rU=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@@ -917,8 +920,9 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/contrib/grpcplugins/action/plugin-artifactory-release-bundle-distribute/go.sum b/contrib/grpcplugins/action/plugin-artifactory-release-bundle-distribute/go.sum
index 881b16b5f..23d0fc25c 100644
--- a/contrib/grpcplugins/action/plugin-artifactory-release-bundle-distribute/go.sum
+++ b/contrib/grpcplugins/action/plugin-artifactory-release-bundle-distribute/go.sum
@@ -295,8 +295,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
@@ -669,8 +669,8 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/contrib/grpcplugins/action/plugin-download/go.mod b/contrib/grpcplugins/action/plugin-download/go.mod
index fe751e385..31b2f8444 100644
--- a/contrib/grpcplugins/action/plugin-download/go.mod
+++ b/contrib/grpcplugins/action/plugin-download/go.mod
@@ -7,7 +7,7 @@ go 1.18
 require (
 	github.com/golang/protobuf v1.5.2
 	github.com/ovh/cds v0.0.0-00010101000000-000000000000
-	github.com/stretchr/testify v1.7.0
+	github.com/stretchr/testify v1.8.0
 )
 
 require (
@@ -75,5 +75,5 @@ require (
 	google.golang.org/grpc v1.43.0 // indirect
 	google.golang.org/protobuf v1.27.1 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
-	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/contrib/grpcplugins/action/plugin-download/go.sum b/contrib/grpcplugins/action/plugin-download/go.sum
index f70cf5c69..b5e6a9f32 100644
--- a/contrib/grpcplugins/action/plugin-download/go.sum
+++ b/contrib/grpcplugins/action/plugin-download/go.sum
@@ -289,13 +289,16 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
@@ -668,8 +671,9 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/contrib/grpcplugins/action/plugin-group-tmpl/go.sum b/contrib/grpcplugins/action/plugin-group-tmpl/go.sum
index f70cf5c69..9132a168c 100644
--- a/contrib/grpcplugins/action/plugin-group-tmpl/go.sum
+++ b/contrib/grpcplugins/action/plugin-group-tmpl/go.sum
@@ -294,8 +294,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
@@ -668,8 +668,8 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/contrib/grpcplugins/action/plugin-kafka-publish/go.mod b/contrib/grpcplugins/action/plugin-kafka-publish/go.mod
index 19ddf53e0..e29b93512 100644
--- a/contrib/grpcplugins/action/plugin-kafka-publish/go.mod
+++ b/contrib/grpcplugins/action/plugin-kafka-publish/go.mod
@@ -13,7 +13,7 @@ require (
 	github.com/golang/protobuf v1.5.2
 	github.com/ovh/cds v0.0.0-00010101000000-000000000000
 	github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee
-	github.com/stretchr/testify v1.7.0
+	github.com/stretchr/testify v1.8.0
 	gopkg.in/urfave/cli.v1 v1.20.0
 )
 
@@ -96,6 +96,7 @@ require (
 	github.com/prometheus/procfs v0.0.8 // indirect
 	github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
 	github.com/rockbears/log v0.6.0 // indirect
+	github.com/rockbears/yaml v0.1.1-0.20220901090137-13dadb408625 // indirect
 	github.com/rubenv/sql-migrate v0.0.0-20160620083229-6f4757563362 // indirect
 	github.com/sergi/go-diff v1.1.0 // indirect
 	github.com/sguiheux/go-coverage v0.0.0-20190710153556-287b082a7197 // indirect
@@ -128,5 +129,5 @@ require (
 	gopkg.in/square/go-jose.v2 v2.3.1 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
-	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/contrib/grpcplugins/action/plugin-kafka-publish/go.sum b/contrib/grpcplugins/action/plugin-kafka-publish/go.sum
index 7c77de5a4..2055160fe 100644
--- a/contrib/grpcplugins/action/plugin-kafka-publish/go.sum
+++ b/contrib/grpcplugins/action/plugin-kafka-publish/go.sum
@@ -507,6 +507,8 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5X
 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rockbears/log v0.6.0 h1:Wzpu7nbrZFFJy14ku41jI2/UEh3d0CJwvIED9/FQZKQ=
 github.com/rockbears/log v0.6.0/go.mod h1:z46IOEOh914gJvg256Vm3F4s8K7D3ePaEAzuMYcfk98=
+github.com/rockbears/yaml v0.1.1-0.20220901090137-13dadb408625 h1:HQAWw/D9RItCYSoWFs8E7IrGKrX9ivqAlCq47DM3IVU=
+github.com/rockbears/yaml v0.1.1-0.20220901090137-13dadb408625/go.mod h1:8cDJx2PWQJMtfGgsRCvHVbIB61SV3dvy8o6EGv2cIpg=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -547,13 +549,16 @@ github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q
 github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@@ -1066,8 +1071,9 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/contrib/grpcplugins/action/plugin-marathon/go.mod b/contrib/grpcplugins/action/plugin-marathon/go.mod
index 2d6121739..7a0798ea7 100644
--- a/contrib/grpcplugins/action/plugin-marathon/go.mod
+++ b/contrib/grpcplugins/action/plugin-marathon/go.mod
@@ -9,7 +9,7 @@ require (
 	github.com/golang/protobuf v1.5.2
 	github.com/ovh/cds v0.0.0-00010101000000-000000000000
 	github.com/ovh/cds/sdk/interpolate v0.0.0-20191126072910-b8d81d038865
-	github.com/stretchr/testify v1.7.0
+	github.com/stretchr/testify v1.8.0
 	github.com/xeipuuv/gojsonschema v1.2.0
 )
 
@@ -76,6 +76,7 @@ require (
 	github.com/prometheus/common v0.9.1 // indirect
 	github.com/prometheus/procfs v0.0.8 // indirect
 	github.com/rockbears/log v0.6.0 // indirect
+	github.com/rockbears/yaml v0.1.1-0.20220901090137-13dadb408625 // indirect
 	github.com/sergi/go-diff v1.1.0 // indirect
 	github.com/sguiheux/go-coverage v0.0.0-20190710153556-287b082a7197 // indirect
 	github.com/sirupsen/logrus v1.8.1 // indirect
@@ -106,5 +107,5 @@ require (
 	gopkg.in/AlecAivazis/survey.v1 v1.7.1 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
-	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/contrib/grpcplugins/action/plugin-marathon/go.sum b/contrib/grpcplugins/action/plugin-marathon/go.sum
index cc3d83336..8315df88b 100644
--- a/contrib/grpcplugins/action/plugin-marathon/go.sum
+++ b/contrib/grpcplugins/action/plugin-marathon/go.sum
@@ -454,6 +454,8 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rockbears/log v0.6.0 h1:Wzpu7nbrZFFJy14ku41jI2/UEh3d0CJwvIED9/FQZKQ=
 github.com/rockbears/log v0.6.0/go.mod h1:z46IOEOh914gJvg256Vm3F4s8K7D3ePaEAzuMYcfk98=
+github.com/rockbears/yaml v0.1.1-0.20220901090137-13dadb408625 h1:HQAWw/D9RItCYSoWFs8E7IrGKrX9ivqAlCq47DM3IVU=
+github.com/rockbears/yaml v0.1.1-0.20220901090137-13dadb408625/go.mod h1:8cDJx2PWQJMtfGgsRCvHVbIB61SV3dvy8o6EGv2cIpg=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -492,13 +494,16 @@ github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q
 github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@@ -993,8 +998,9 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/contrib/grpcplugins/action/plugin-npm-audit-parser/go.sum b/contrib/grpcplugins/action/plugin-npm-audit-parser/go.sum
index f70cf5c69..9132a168c 100644
--- a/contrib/grpcplugins/action/plugin-npm-audit-parser/go.sum
+++ b/contrib/grpcplugins/action/plugin-npm-audit-parser/go.sum
@@ -294,8 +294,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
@@ -668,8 +668,8 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/contrib/grpcplugins/action/plugin-ssh-cmd/go.sum b/contrib/grpcplugins/action/plugin-ssh-cmd/go.sum
index f70cf5c69..9132a168c 100644
--- a/contrib/grpcplugins/action/plugin-ssh-cmd/go.sum
+++ b/contrib/grpcplugins/action/plugin-ssh-cmd/go.sum
@@ -294,8 +294,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
@@ -668,8 +668,8 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/contrib/grpcplugins/action/plugin-tmpl/go.sum b/contrib/grpcplugins/action/plugin-tmpl/go.sum
index f70cf5c69..9132a168c 100644
--- a/contrib/grpcplugins/action/plugin-tmpl/go.sum
+++ b/contrib/grpcplugins/action/plugin-tmpl/go.sum
@@ -294,8 +294,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
@@ -668,8 +668,8 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/contrib/grpcplugins/action/plugin-venom/go.mod b/contrib/grpcplugins/action/plugin-venom/go.mod
index bb8e6ecf3..f785d0304 100644
--- a/contrib/grpcplugins/action/plugin-venom/go.mod
+++ b/contrib/grpcplugins/action/plugin-venom/go.mod
@@ -8,7 +8,7 @@ require (
 	github.com/golang/protobuf v1.5.2
 	github.com/ovh/cds v0.0.0-00010101000000-000000000000
 	github.com/ovh/venom v0.26.0
-	github.com/stretchr/testify v1.7.0
+	github.com/stretchr/testify v1.8.0
 	gopkg.in/yaml.v2 v2.4.0
 )
 
@@ -108,5 +108,5 @@ require (
 	gopkg.in/ini.v1 v1.66.4 // indirect
 	gopkg.in/testfixtures.v2 v2.6.0 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
-	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/contrib/grpcplugins/action/plugin-venom/go.sum b/contrib/grpcplugins/action/plugin-venom/go.sum
index 480bb4733..96e598a77 100644
--- a/contrib/grpcplugins/action/plugin-venom/go.sum
+++ b/contrib/grpcplugins/action/plugin-venom/go.sum
@@ -365,13 +365,16 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
@@ -761,8 +764,9 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/contrib/integrations/artifactory/artifactory.go b/contrib/integrations/artifactory/artifactory.go
index 82842c9e8..102ad882e 100644
--- a/contrib/integrations/artifactory/artifactory.go
+++ b/contrib/integrations/artifactory/artifactory.go
@@ -17,8 +17,8 @@ import (
 	"github.com/jfrog/jfrog-client-go/config"
 	"github.com/jfrog/jfrog-client-go/distribution"
 	authdistrib "github.com/jfrog/jfrog-client-go/distribution/auth"
-	"github.com/jfrog/jfrog-client-go/utils/log"
 	"github.com/pkg/errors"
+	"github.com/rockbears/log"
 
 	"github.com/ovh/cds/engine/api/integration/artifact_manager"
 	"github.com/ovh/cds/sdk"
@@ -236,7 +236,7 @@ type BuildInfoRequest struct {
 
 func PrepareBuildInfo(ctx context.Context, artiClient artifact_manager.ArtifactManager, r BuildInfoRequest) (*buildinfo.BuildInfo, error) {
 	buildInfoName := fmt.Sprintf("%s/%s/%s", r.BuildInfoPrefix, r.ProjectKey, r.WorkflowName)
-	log.Debug(ctx, "PrepareBuildInfo %q", buildInfoName)
+	log.Debug(ctx, "PrepareBuildInfo %q maturity:%q", buildInfoName, r.LowMaturitySuffix)
 
 	buildInfoRequest := &buildinfo.BuildInfo{
 		Properties: map[string]string{},
diff --git a/engine/api/api_routes.go b/engine/api/api_routes.go
index 4afb64315..a4e1ae39b 100644
--- a/engine/api/api_routes.go
+++ b/engine/api/api_routes.go
@@ -321,6 +321,7 @@ func (api *API) InitRouter() {
 	r.Handle("/queue/workflows/{permJobID}/coverage", Scope(sdk.AuthConsumerScopeRunExecution), r.POSTEXECUTE(api.postWorkflowJobCoverageResultsHandler, MaintenanceAware()))
 	r.Handle("/queue/workflows/{permJobID}/run/results", Scope(sdk.AuthConsumerScopeRunExecution), r.POSTEXECUTE(api.postWorkflowRunResultsHandler))
 	r.Handle("/queue/workflows/{permJobID}/run/results/check", Scope(sdk.AuthConsumerScopeRunExecution), r.POSTEXECUTE(api.workflowRunResultCheckUploadHandler))
+	r.Handle("/queue/workflows/{permJobID}/run/results/promote", Scope(sdk.AuthConsumerScopeRunExecution), r.POSTEXECUTE(api.workflowRunResultPromoteHandler))
 	r.Handle("/queue/workflows/{permJobID}/run/results/release", Scope(sdk.AuthConsumerScopeRunExecution), r.POSTEXECUTE(api.workflowRunResultReleaseHandler))
 	r.Handle("/queue/workflows/{permJobID}/test", Scope(sdk.AuthConsumerScopeRunExecution), r.POSTEXECUTE(api.postWorkflowJobTestsResultsHandler, MaintenanceAware()))
 	r.Handle("/queue/workflows/{permJobID}/tag", Scope(sdk.AuthConsumerScopeRunExecution), r.POSTEXECUTE(api.postWorkflowJobTagsHandler, MaintenanceAware()))
@@ -432,6 +433,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/rbac/import", nil, r.POSTv2(api.postImportRbacHandler))
+
 	r.Handle("/v2/repository/analyze", Scope(sdk.AuthConsumerScopeHooks), r.POSTv2(api.postRepositoryAnalysisHandler))
 	r.Handle("/v2/project/repositories", Scope(sdk.AuthConsumerScopeHooks), r.GETv2(api.getAllRepositoriesHandler))
 	r.Handle("/v2/project/repositories/{repositoryIdentifier}/hook", Scope(sdk.AuthConsumerScopeHooks), r.GETv2(api.getRepositoryHookHandler))
diff --git a/engine/api/integration/artifact_manager/artifactory/client.go b/engine/api/integration/artifact_manager/artifactory/client.go
index 9eb217f1a..2b04aef3e 100644
--- a/engine/api/integration/artifact_manager/artifactory/client.go
+++ b/engine/api/integration/artifact_manager/artifactory/client.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
+	"path"
 	"strconv"
 	"strings"
 
@@ -27,6 +28,11 @@ func (c *Client) GetFileInfo(repoName string, filePath string) (sdk.FileInfo, er
 		return fi, sdk.NewErrorFrom(sdk.ErrUnknownError, "unable to get repository %s: %v", repoName, err)
 	}
 
+	// To get FileInfo for a docker image, we have to check the manifest file
+	if repoDetails.PackageType == "docker" && !strings.HasSuffix(filePath, "manifest.json") {
+		filePath = path.Join(filePath, "manifest.json")
+	}
+
 	fileInfoURL := fmt.Sprintf("%sapi/storage/%s/%s", c.Asm.GetConfig().GetServiceDetails().GetUrl(), repoName, filePath)
 	httpDetails := c.Asm.GetConfig().GetServiceDetails().CreateHttpClientDetails()
 	re, body, _, err := c.Asm.Client().SendGet(fileInfoURL, true, &httpDetails)
diff --git a/engine/api/purge/purge.go b/engine/api/purge/purge.go
index e46f93773..36e12ea3d 100644
--- a/engine/api/purge/purge.go
+++ b/engine/api/purge/purge.go
@@ -331,7 +331,7 @@ func DeleteArtifactsFromRepositoryManager(ctx context.Context, db gorp.SqlExecut
 	)
 
 	// Load the token from secrets
-	secrets, err := workflow.LoadDecryptSecrets(ctx, db, wr, nil)
+	secrets, err := workflow.LoadDecryptSecrets(ctx, db, wr, wr.RootRun())
 	if err != nil {
 		return err
 	}
diff --git a/engine/api/rbac/dao_rbac.go b/engine/api/rbac/dao_rbac.go
index a2f8e60ab..4b94d4a9c 100644
--- a/engine/api/rbac/dao_rbac.go
+++ b/engine/api/rbac/dao_rbac.go
@@ -12,14 +12,14 @@ import (
 	"github.com/ovh/cds/sdk"
 )
 
-func LoadRbacByName(ctx context.Context, db gorp.SqlExecutor, name string, opts ...LoadOptionFunc) (sdk.RBAC, error) {
+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 {
+	if err := sdk.IsValidRBAC(rb); err != nil {
 		return err
 	}
 	if rb.ID == "" {
@@ -39,7 +39,7 @@ func Insert(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rb *sdk.RBAC)
 			RbacID:     dbRb.ID,
 			RBACGlobal: rb.Globals[i],
 		}
-		if err := insertRbacGlobal(ctx, db, &dbRbGlobal); err != nil {
+		if err := insertRBACGlobal(ctx, db, &dbRbGlobal); err != nil {
 			return err
 		}
 	}
@@ -48,7 +48,7 @@ func Insert(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rb *sdk.RBAC)
 			RbacID:      dbRb.ID,
 			RBACProject: rb.Projects[i],
 		}
-		if err := insertRbacProject(ctx, db, &dbRbProject); err != nil {
+		if err := insertRBACProject(ctx, db, &dbRbProject); err != nil {
 			return err
 		}
 	}
@@ -71,30 +71,30 @@ func Delete(_ context.Context, db gorpmapper.SqlExecutorWithTx, rb sdk.RBAC) err
 	return nil
 }
 
-func get(ctx context.Context, db gorp.SqlExecutor, q gorpmapping.Query, opts ...LoadOptionFunc) (sdk.RBAC, error) {
+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
+		return nil, err
 	}
 	if !found {
-		return r, sdk.WithStack(sdk.ErrNotFound)
+		return nil, 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.ID)
+		return nil, sdk.WrapError(err, "error when checking signature for rbac %s", rbacDB.ID)
 	}
 	if !isValid {
 		log.Error(ctx, "rbac.get> rbac %s (%s) data corrupted", rbacDB.Name, rbacDB.ID)
-		return r, sdk.WithStack(sdk.ErrNotFound)
+		return nil, sdk.WithStack(sdk.ErrNotFound)
 	}
 	for _, f := range opts {
 		if err := f(ctx, db, &rbacDB); err != nil {
-			return r, err
+			return nil, err
 		}
 	}
 	r = rbacDB.RBAC
-	return r, nil
+	return &r, nil
 }
diff --git a/engine/api/rbac/dao_rbac_global.go b/engine/api/rbac/dao_rbac_global.go
index 85cccd4eb..72ffb2a69 100644
--- a/engine/api/rbac/dao_rbac_global.go
+++ b/engine/api/rbac/dao_rbac_global.go
@@ -2,34 +2,35 @@ package rbac
 
 import (
 	"context"
-
-	"github.com/go-gorp/gorp"
+	"github.com/lib/pq"
+	"github.com/ovh/cds/sdk/telemetry"
 	"github.com/rockbears/log"
 
+	"github.com/go-gorp/gorp"
 	"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 {
+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 {
+		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 {
+		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 {
+func insertRBACGlobalUser(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rbacGlobalID int64, userID string) error {
 	rgu := rbacGlobalUser{
 		RbacGlobalID:     rbacGlobalID,
 		RbacGlobalUserID: userID,
@@ -40,7 +41,7 @@ func insertRbacGlobalUser(ctx context.Context, db gorpmapper.SqlExecutorWithTx,
 	return nil
 }
 
-func insertRbacGlobalGroup(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rbacGlobalID int64, groupID int64) error {
+func insertRBACGlobalGroup(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rbacGlobalID int64, groupID int64) error {
 	rgu := rbacGlobalGroup{
 		RbacGlobalID:      rbacGlobalID,
 		RbacGlobalGroupID: groupID,
@@ -51,44 +52,59 @@ func insertRbacGlobalGroup(ctx context.Context, db gorpmapper.SqlExecutorWithTx,
 	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
+func HasGlobalRole(ctx context.Context, db gorp.SqlExecutor, role string, userID string) (bool, error) {
+	ctx, next := telemetry.Span(ctx, "rbac.HasGlobalRole")
+	defer next()
+
+	// Get rbac_global_groups
+	rbacGlobalGroups, err := loadRBACGlobalGroupsByUserID(ctx, db, userID)
+	if err != nil {
+		return false, 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)
+	// Get rbac_global_users
+	rbacGlobalUsers, err := loadRBACGlobalUsersByUserID(ctx, db, userID)
+	if err != nil {
+		return false, err
 	}
-	return nil
+
+	rbacGlobalIDs := make(sdk.Int64Slice, 0)
+	for _, rgg := range rbacGlobalGroups {
+		rbacGlobalIDs = append(rbacGlobalIDs, rgg.RbacGlobalID)
+	}
+	for _, rgu := range rbacGlobalUsers {
+		rbacGlobalIDs = append(rbacGlobalIDs, rgu.RbacGlobalID)
+	}
+	rbacGlobalIDs.Unique()
+
+	rgs, err := loadRBACGlobalsByRoleAndIDs(ctx, db, role, rbacGlobalIDs)
+	if err != nil {
+		return false, err
+	}
+
+	return len(rgs) > 0, 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
+func loadRBACGlobalsByRoleAndIDs(ctx context.Context, db gorp.SqlExecutor, role string, rbacGlobalIDs []int64) ([]rbacGlobal, error) {
+	q := gorpmapping.NewQuery(`SELECT * from rbac_global WHERE role = $1 AND id = ANY($2)`).Args(role, pq.Int64Array(rbacGlobalIDs))
+	return getAllRBACGlobals(ctx, db, q)
+}
+
+func getAllRBACGlobals(ctx context.Context, db gorp.SqlExecutor, q gorpmapping.Query) ([]rbacGlobal, error) {
+	var rbacGlobals []rbacGlobal
+	if err := gorpmapping.GetAll(ctx, db, q, &rbacGlobals); err != nil {
+		return nil, err
 	}
-	rbacGlobal.RBACGlobal.RBACGroupsIDs = make([]int64, 0, len(rbacGroupIDs))
-	for _, rbacGroups := range rbacGroupIDs {
-		isValid, err := gorpmapping.CheckSignature(rbacGroups, rbacGroups.Signature)
+
+	for _, rg := range rbacGlobals {
+		isValid, err := gorpmapping.CheckSignature(rg, rg.Signature)
 		if err != nil {
-			return sdk.WrapError(err, "error when checking signature for rbac_global_groups %d", rbacGroups.ID)
+			return nil, sdk.WrapError(err, "error when checking signature for rbac_global %d", rg.ID)
 		}
 		if !isValid {
-			log.Error(ctx, "rbac.getAllRBACGlobalGroups> rbac_global_groups %d data corrupted", rbacGroups.ID)
+			log.Error(ctx, "rbac.getAllRBACGlobals> rbac_global %d data corrupted", rg.ID)
 			continue
 		}
-		rbacGlobal.RBACGlobal.RBACGroupsIDs = append(rbacGlobal.RBACGlobal.RBACGroupsIDs, rbacGroups.RbacGlobalGroupID)
+		rbacGlobals = append(rbacGlobals, rg)
 	}
-	return nil
+	return rbacGlobals, nil
 }
diff --git a/engine/api/rbac/dao_rbac_global_group.go b/engine/api/rbac/dao_rbac_global_group.go
new file mode 100644
index 000000000..2a7e6dead
--- /dev/null
+++ b/engine/api/rbac/dao_rbac_global_group.go
@@ -0,0 +1,61 @@
+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 loadRBACGlobalGroupsByUserID(ctx context.Context, db gorp.SqlExecutor, userID string) ([]rbacGlobalGroup, 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 loadRBACGlobalGroupsByGroupIDs(ctx, db, groupIDs)
+}
+
+func loadRBACGlobalGroupsByGroupIDs(ctx context.Context, db gorp.SqlExecutor, groupIDs []int64) ([]rbacGlobalGroup, error) {
+	q := gorpmapping.NewQuery("SELECT * FROM rbac_global_groups WHERE group_id = ANY ($1)").Args(pq.Int64Array(groupIDs))
+	return getAllRBACGlobalGroups(ctx, db, q)
+}
+
+func getAllRBACGlobalGroups(ctx context.Context, db gorp.SqlExecutor, q gorpmapping.Query) ([]rbacGlobalGroup, error) {
+	var rbacGroups []rbacGlobalGroup
+	if err := gorpmapping.GetAll(ctx, db, q, &rbacGroups); err != nil {
+		return nil, err
+	}
+	for _, rg := range rbacGroups {
+		isValid, err := gorpmapping.CheckSignature(rg, rg.Signature)
+		if err != nil {
+			return nil, sdk.WrapError(err, "error when checking signature for rbac_global_groups %d", rg.ID)
+		}
+		if !isValid {
+			log.Error(ctx, "rbac.getAllRBACGlobalGroups> rbac_global_groups %d data corrupted", rg.ID)
+			continue
+		}
+	}
+	return rbacGroups, nil
+}
+
+func loadRBACGlobalGroups(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)
+	rbacGlobalGroups, err := getAllRBACGlobalGroups(ctx, db, q)
+	if err != nil {
+		return err
+	}
+	rbacGlobal.RBACGroupsIDs = make([]int64, 0, len(rbacGlobalGroups))
+	for _, rgg := range rbacGlobalGroups {
+		rbacGlobal.RBACGroupsIDs = append(rbacGlobal.RBACGroupsIDs, rgg.RbacGlobalGroupID)
+	}
+	return nil
+}
diff --git a/engine/api/rbac/dao_rbac_global_user.go b/engine/api/rbac/dao_rbac_global_user.go
new file mode 100644
index 000000000..ef0790336
--- /dev/null
+++ b/engine/api/rbac/dao_rbac_global_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 loadRBACGlobalUsersByUserID(ctx context.Context, db gorp.SqlExecutor, userID string) ([]rbacGlobalUser, error) {
+	q := gorpmapping.NewQuery("SELECT * FROM rbac_global_users WHERE user_id = $1").Args(userID)
+	return getAllRBACGlobalUsers(ctx, db, q)
+}
+
+func loadRBACGlobalUsers(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)
+	rbacUserIDS, err := getAllRBACGlobalUsers(ctx, db, q)
+	if err != nil {
+		return err
+	}
+	rbacGlobal.RBACUsersIDs = make([]string, 0, len(rbacUserIDS))
+	for _, rbacUsers := range rbacUserIDS {
+		rbacGlobal.RBACUsersIDs = append(rbacGlobal.RBACUsersIDs, rbacUsers.RbacGlobalUserID)
+	}
+	return nil
+}
+
+func getAllRBACGlobalUsers(ctx context.Context, db gorp.SqlExecutor, q gorpmapping.Query) ([]rbacGlobalUser, error) {
+	var rbacGlobalUsers []rbacGlobalUser
+	if err := gorpmapping.GetAll(ctx, db, q, &rbacGlobalUsers); err != nil {
+		return nil, err
+	}
+
+	usersFiltered := make([]rbacGlobalUser, 0, len(rbacGlobalUsers))
+	for _, rbacUsers := range rbacGlobalUsers {
+		isValid, err := gorpmapping.CheckSignature(rbacUsers, rbacUsers.Signature)
+		if err != nil {
+			return nil, 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
+		}
+		usersFiltered = append(usersFiltered, rbacUsers)
+	}
+	return usersFiltered, nil
+}
diff --git a/engine/api/rbac/dao_rbac_project.go b/engine/api/rbac/dao_rbac_project.go
index 063f281fb..55ae6defb 100644
--- a/engine/api/rbac/dao_rbac_project.go
+++ b/engine/api/rbac/dao_rbac_project.go
@@ -12,30 +12,30 @@ import (
 	"github.com/ovh/cds/sdk"
 )
 
-func insertRbacProject(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rbacProject *rbacProject) error {
+func insertRBACProject(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rbacProject *rbacProject) error {
 	if err := gorpmapping.InsertAndSign(ctx, db, rbacProject); err != nil {
 		return err
 	}
 
 	for _, projectKey := range rbacProject.RBACProjectKeys {
-		if err := insertRbacProjectKey(ctx, db, rbacProject.ID, projectKey); err != nil {
+		if err := insertRBACProjectKey(ctx, db, rbacProject.ID, projectKey); err != nil {
 			return err
 		}
 	}
 	for _, rbUserID := range rbacProject.RBACUsersIDs {
-		if err := insertRbacProjectUser(ctx, db, rbacProject.ID, rbUserID); err != nil {
+		if err := insertRBACProjectUser(ctx, db, rbacProject.ID, rbUserID); err != nil {
 			return err
 		}
 	}
 	for _, rbGroupID := range rbacProject.RBACGroupsIDs {
-		if err := insertRbacProjectGroup(ctx, db, rbacProject.ID, rbGroupID); err != nil {
+		if err := insertRBACProjectGroup(ctx, db, rbacProject.ID, rbGroupID); err != nil {
 			return err
 		}
 	}
 	return nil
 }
 
-func insertRbacProjectKey(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rbacParentID int64, projectKey string) error {
+func insertRBACProjectKey(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rbacParentID int64, projectKey string) error {
 	rpk := rbacProjectKey{
 		RbacProjectID: rbacParentID,
 		ProjectKey:    projectKey,
@@ -46,7 +46,7 @@ func insertRbacProjectKey(ctx context.Context, db gorpmapper.SqlExecutorWithTx,
 	return nil
 }
 
-func insertRbacProjectUser(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rbacProjectID int64, userID string) error {
+func insertRBACProjectUser(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rbacProjectID int64, userID string) error {
 	rgu := rbacProjectUser{
 		RbacProjectID:     rbacProjectID,
 		RbacProjectUserID: userID,
@@ -57,7 +57,7 @@ func insertRbacProjectUser(ctx context.Context, db gorpmapper.SqlExecutorWithTx,
 	return nil
 }
 
-func insertRbacProjectGroup(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rbacProjectID int64, groupID int64) error {
+func insertRBACProjectGroup(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rbacProjectID int64, groupID int64) error {
 	rgu := rbacProjectGroup{
 		RbacProjectID:      rbacProjectID,
 		RbacProjectGroupID: groupID,
@@ -68,7 +68,7 @@ func insertRbacProjectGroup(ctx context.Context, db gorpmapper.SqlExecutorWithTx
 	return nil
 }
 
-func getAllRbacProjects(ctx context.Context, db gorp.SqlExecutor, q gorpmapping.Query) ([]rbacProject, error) {
+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
@@ -81,7 +81,7 @@ func getAllRbacProjects(ctx context.Context, db gorp.SqlExecutor, q gorpmapping.
 			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)
+			log.Error(ctx, "rbac.getAllRBACProjects> rbac_project %d data corrupted", projectDatas.ID)
 			continue
 		}
 		projectsFiltered = append(projectsFiltered, projectDatas)
@@ -89,7 +89,7 @@ func getAllRbacProjects(ctx context.Context, db gorp.SqlExecutor, q gorpmapping.
 	return projectsFiltered, nil
 }
 
-func loadRbacProjectsByRoleAndIDs(ctx context.Context, db gorp.SqlExecutor, role string, rbacProjectIDs []int64) ([]rbacProject, error) {
+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)
+	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
index a125032f2..d5cd22e9f 100644
--- a/engine/api/rbac/dao_rbac_project_group.go
+++ b/engine/api/rbac/dao_rbac_project_group.go
@@ -12,7 +12,7 @@ import (
 	"github.com/ovh/cds/sdk"
 )
 
-func loadRbacProjectGroupsByUserID(ctx context.Context, db gorp.SqlExecutor, userID string) ([]rbacProjectGroup, error) {
+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
@@ -21,10 +21,10 @@ func loadRbacProjectGroupsByUserID(ctx context.Context, db gorp.SqlExecutor, use
 	for _, g := range groups {
 		groupIDs = append(groupIDs, g.ID)
 	}
-	return loadRbacProjectGroupsByGroupIDs(ctx, db, groupIDs)
+	return loadRBACProjectGroupsByGroupIDs(ctx, db, groupIDs)
 }
 
-func loadRbacProjectGroupsByGroupIDs(ctx context.Context, db gorp.SqlExecutor, groupIDs []int64) ([]rbacProjectGroup, error) {
+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)
 }
diff --git a/engine/api/rbac/dao_rbac_project_key.go b/engine/api/rbac/dao_rbac_project_key.go
index e891d240c..39c4a0508 100644
--- a/engine/api/rbac/dao_rbac_project_key.go
+++ b/engine/api/rbac/dao_rbac_project_key.go
@@ -46,7 +46,7 @@ func loadRBACProjectKeys(ctx context.Context, db gorp.SqlExecutor, rbacProject *
 	return nil
 }
 
-func loadRRBACProjectKeys(ctx context.Context, db gorp.SqlExecutor, rbacProjectIDs []int64) ([]rbacProjectKey, error) {
+func loadAllRBACProjectKeys(ctx context.Context, db gorp.SqlExecutor, rbacProjectIDs []int64) ([]rbacProjectKey, error) {
 	query := gorpmapping.NewQuery(`SELECT * FROM rbac_project_keys WHERE rbac_project_id = ANY($1)`).Args(pq.Int64Array(rbacProjectIDs))
 	return getAllRBACProjectKeys(ctx, db, query)
 }
@@ -63,32 +63,28 @@ func HasRoleOnProjectAndUserID(ctx context.Context, db gorp.SqlExecutor, role st
 
 func LoadProjectKeysByRoleAndUserID(ctx context.Context, db gorp.SqlExecutor, role string, userID string) ([]string, error) {
 	// Get rbac_project_groups
-	rbacProjectGroups, err := loadRbacProjectGroupsByUserID(ctx, db, userID)
+	rbacProjectGroups, err := loadRBACProjectGroupsByUserID(ctx, db, userID)
 	if err != nil {
 		return nil, err
 	}
 	// Get rbac_project_users
-	rbacProjectUsers, err := loadRbacProjectUsersByUserID(ctx, db, userID)
+	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)
+	rbacProjectIDs := make(sdk.Int64Slice, 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)
-		}
+		rbacProjectIDs = append(rbacProjectIDs, rpu.RbacProjectID)
 	}
+	rbacProjectIDs.Unique()
 
 	// Get rbac_project
-	rbacProjects, err := loadRbacProjectsByRoleAndIDs(ctx, db, role, rbacProjectIDs)
+	rbacProjects, err := loadRBACProjectsByRoleAndIDs(ctx, db, role, rbacProjectIDs)
 	if err != nil {
 		return nil, err
 	}
@@ -98,7 +94,7 @@ func LoadProjectKeysByRoleAndUserID(ctx context.Context, db gorp.SqlExecutor, ro
 	for _, rp := range rbacProjects {
 		rbacProjectIDs = append(rbacProjectIDs, rp.ID)
 	}
-	rbacProjectKeys, err := loadRRBACProjectKeys(ctx, db, rbacProjectIDs)
+	rbacProjectKeys, err := loadAllRBACProjectKeys(ctx, db, rbacProjectIDs)
 	if err != nil {
 		return nil, err
 	}
diff --git a/engine/api/rbac/dao_rbac_project_user.go b/engine/api/rbac/dao_rbac_project_user.go
index 0ab5890c2..42574471c 100644
--- a/engine/api/rbac/dao_rbac_project_user.go
+++ b/engine/api/rbac/dao_rbac_project_user.go
@@ -10,7 +10,7 @@ import (
 	"github.com/ovh/cds/sdk"
 )
 
-func loadRbacProjectUsersByUserID(ctx context.Context, db gorp.SqlExecutor, userID string) ([]rbacProjectUser, error) {
+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)
 }
diff --git a/engine/api/rbac/loader.go b/engine/api/rbac/loader.go
index aa0de1ba1..079ddfd9a 100644
--- a/engine/api/rbac/loader.go
+++ b/engine/api/rbac/loader.go
@@ -16,25 +16,25 @@ 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
+	LoadRBACGlobal  LoadOptionFunc
+	LoadRBACProject LoadOptionFunc
 }{
 	Default:         loadDefault,
-	LoadRbacGlobal:  loadRbacGlobal,
-	LoadRbacProject: loadRbacProject,
+	LoadRBACGlobal:  loadRBACGlobal,
+	LoadRBACProject: loadRBACProject,
 }
 
 func loadDefault(ctx context.Context, db gorp.SqlExecutor, rbac *rbac) error {
-	if err := loadRbacGlobal(ctx, db, rbac); err != nil {
+	if err := loadRBACGlobal(ctx, db, rbac); err != nil {
 		return err
 	}
-	if err := loadRbacProject(ctx, db, rbac); err != nil {
+	if err := loadRBACProject(ctx, db, rbac); err != nil {
 		return err
 	}
 	return nil
 }
 
-func loadRbacProject(ctx context.Context, db gorp.SqlExecutor, rbac *rbac) error {
+func loadRBACProject(ctx context.Context, db gorp.SqlExecutor, rbac *rbac) error {
 	query := "SELECT * FROM rbac_project WHERE rbac_id = $1"
 	var rbacProjects []rbacProject
 	if err := gorpmapping.GetAll(ctx, db, gorpmapping.NewQuery(query).Args(rbac.ID), &rbacProjects); err != nil {
@@ -67,7 +67,7 @@ func loadRbacProject(ctx context.Context, db gorp.SqlExecutor, rbac *rbac) error
 	return nil
 }
 
-func loadRbacGlobal(ctx context.Context, db gorp.SqlExecutor, rbac *rbac) error {
+func loadRBACGlobal(ctx context.Context, db gorp.SqlExecutor, rbac *rbac) error {
 	query := "SELECT * FROM rbac_global WHERE rbac_id = $1"
 	var rbacGbl []rbacGlobal
 	if err := gorpmapping.GetAll(ctx, db, gorpmapping.NewQuery(query).Args(rbac.ID), &rbacGbl); err != nil {
@@ -81,13 +81,13 @@ func loadRbacGlobal(ctx context.Context, db gorp.SqlExecutor, rbac *rbac) error
 			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)
+			log.Error(ctx, "rbac.loadRBACGlobal> rbac_global %d data corrupted", rg.ID)
 			continue
 		}
-		if err := getAllRBACGlobalUsers(ctx, db, rg); err != nil {
+		if err := loadRBACGlobalUsers(ctx, db, rg); err != nil {
 			return err
 		}
-		if err := getAllRBACGlobalGroups(ctx, db, rg); err != nil {
+		if err := loadRBACGlobalGroups(ctx, db, rg); err != nil {
 			return err
 		}
 		rbac.Globals = append(rbac.Globals, rg.RBACGlobal)
diff --git a/engine/api/rbac/rbac.go b/engine/api/rbac/rbac.go
index 9b9089c8e..edb433de5 100644
--- a/engine/api/rbac/rbac.go
+++ b/engine/api/rbac/rbac.go
@@ -11,32 +11,34 @@ import (
 
 func FillWithIDs(ctx context.Context, db gorp.SqlExecutor, r *sdk.RBAC) error {
 	// Check existing permission
-	rbacDB, err := LoadRbacByName(ctx, db, r.Name)
+	rbacDB, err := LoadRBACByName(ctx, db, r.Name)
 	if err != nil {
 		if !sdk.ErrorIs(err, sdk.ErrNotFound) {
 			return err
 		}
 	}
-	r.ID = rbacDB.ID
+	if rbacDB != nil {
+		r.ID = rbacDB.ID
+	}
 
 	userCache := make(map[string]string)
 	groupCache := make(map[string]int64)
 	for gID := range r.Globals {
 		rbacGbl := &r.Globals[gID]
-		if err := fillRbacGlobalWithID(ctx, db, rbacGbl, userCache, groupCache); err != nil {
+		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, userCache, groupCache); err != nil {
+		if err := fillRBACProjectWithID(ctx, db, rbacPrj, userCache, groupCache); err != nil {
 			return err
 		}
 	}
 	return nil
 }
 
-func fillRbacProjectWithID(ctx context.Context, db gorp.SqlExecutor, rbacPrj *sdk.RBACProject, userCache map[string]string, groupCache map[string]int64) error {
+func fillRBACProjectWithID(ctx context.Context, db gorp.SqlExecutor, rbacPrj *sdk.RBACProject, userCache map[string]string, groupCache map[string]int64) error {
 	rbacPrj.RBACUsersIDs = make([]string, 0, len(rbacPrj.RBACUsersName))
 	for _, userName := range rbacPrj.RBACUsersName {
 		userID := userCache[userName]
@@ -66,7 +68,7 @@ func fillRbacProjectWithID(ctx context.Context, db gorp.SqlExecutor, rbacPrj *sd
 	return nil
 }
 
-func fillRbacGlobalWithID(ctx context.Context, db gorp.SqlExecutor, rbacGbl *sdk.RBACGlobal, userCache map[string]string, groupCache map[string]int64) error {
+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]
diff --git a/engine/api/rbac/rule_global.go b/engine/api/rbac/rule_global.go
new file mode 100644
index 000000000..380187a25
--- /dev/null
+++ b/engine/api/rbac/rule_global.go
@@ -0,0 +1,36 @@
+package rbac
+
+import (
+	"context"
+
+	"github.com/go-gorp/gorp"
+	"github.com/rockbears/log"
+
+	"github.com/ovh/cds/engine/cache"
+	"github.com/ovh/cds/sdk"
+)
+
+func hasGlobalRole(ctx context.Context, auth *sdk.AuthConsumer, _ cache.Store, db gorp.SqlExecutor, role string) error {
+	if auth == nil {
+		return sdk.WithStack(sdk.ErrForbidden)
+	}
+
+	hasRole, err := HasGlobalRole(ctx, db, role, auth.AuthentifiedUser.ID)
+	if err != nil {
+		return err
+	}
+
+	log.RegisterField(LogFieldRole)
+	ctx = context.WithValue(ctx, LogFieldRole, role)
+	log.Info(ctx, "hasRole:%t", hasRole)
+
+	if !hasRole {
+		return sdk.WithStack(sdk.ErrForbidden)
+	}
+	return nil
+}
+
+// PermissionManage return nil if the current AuthConsumer have the RoleManage on current project KEY
+func PermissionManage(ctx context.Context, auth *sdk.AuthConsumer, store cache.Store, db gorp.SqlExecutor, _ map[string]string) error {
+	return hasGlobalRole(ctx, auth, store, db, sdk.RoleManagePermission)
+}
diff --git a/engine/api/v2_rbac.go b/engine/api/v2_rbac.go
new file mode 100644
index 000000000..9e7a3c74b
--- /dev/null
+++ b/engine/api/v2_rbac.go
@@ -0,0 +1,56 @@
+package api
+
+import (
+	"context"
+	"net/http"
+
+	"github.com/ovh/cds/sdk"
+
+	"github.com/ovh/cds/engine/api/rbac"
+	"github.com/ovh/cds/engine/service"
+)
+
+func (api *API) postImportRbacHandler() ([]service.RbacChecker, service.Handler) {
+	return service.RBAC(rbac.PermissionManage),
+		func(ctx context.Context, w http.ResponseWriter, req *http.Request) error {
+			force := service.FormBool(req, "force")
+
+			var rbacRule sdk.RBAC
+			if err := service.UnmarshalRequest(ctx, req, &rbacRule); err != nil {
+				return err
+			}
+
+			existingRule, err := rbac.LoadRBACByName(ctx, api.mustDB(), rbacRule.Name)
+			if err != nil && !sdk.ErrorIs(err, sdk.ErrNotFound) {
+				return err
+			}
+
+			if err := rbac.FillWithIDs(ctx, api.mustDB(), &rbacRule); err != nil {
+				return err
+			}
+
+			tx, err := api.mustDB().Begin()
+			if err != nil {
+				return sdk.WithStack(err)
+			}
+			defer tx.Rollback() // nolint
+
+			if existingRule != nil && !force {
+				return sdk.NewErrorFrom(sdk.ErrForbidden, "unable to override existing permission")
+			}
+			if existingRule != nil {
+				if err := rbac.Delete(ctx, tx, *existingRule); err != nil {
+					return err
+				}
+			}
+
+			if err := rbac.Insert(ctx, tx, &rbacRule); err != nil {
+				return err
+			}
+
+			if err := tx.Commit(); err != nil {
+				return sdk.WithStack(err)
+			}
+			return service.WriteMarshal(w, req, nil, http.StatusCreated)
+		}
+}
diff --git a/engine/api/workflow/workflow_run_results.go b/engine/api/workflow/workflow_run_results.go
index a6de2000f..f168a6a42 100644
--- a/engine/api/workflow/workflow_run_results.go
+++ b/engine/api/workflow/workflow_run_results.go
@@ -9,6 +9,7 @@ import (
 	"time"
 
 	"github.com/go-gorp/gorp"
+	"github.com/lib/pq"
 	"github.com/rockbears/log"
 
 	art "github.com/ovh/cds/contrib/integrations/artifactory"
@@ -175,7 +176,7 @@ func AddResult(ctx context.Context, db *gorp.DbMap, store cache.Store, wr *sdk.W
 	return sdk.WithStack(store.Delete(cacheKey))
 }
 
-// Check validity of the request + complete runResuklt with md5,size,type
+// Check validity of the request + complete runResult with md5,size,type
 func verifyAddResultArtifactManager(ctx context.Context, db gorp.SqlExecutor, store cache.Store, wr *sdk.WorkflowRun, newRunResult *sdk.WorkflowRunResult) (string, error) {
 	artNewResult, err := newRunResult.GetArtifactManager()
 	if err != nil {
@@ -369,6 +370,11 @@ func getAll(ctx context.Context, db gorp.SqlExecutor, query gorpmapping.Query) (
 	return results, nil
 }
 
+func LoadRunResultsByRunIDFilterByIDs(ctx context.Context, db gorp.SqlExecutor, runID int64, resultIDs ...string) (sdk.WorkflowRunResults, error) {
+	query := gorpmapping.NewQuery("SELECT * FROM workflow_run_result WHERE workflow_run_id = $1 AND id = ANY($2) ORDER BY sub_num DESC").Args(runID, pq.StringArray(resultIDs))
+	return getAll(ctx, db, query)
+}
+
 func LoadRunResultsByRunID(ctx context.Context, db gorp.SqlExecutor, runID int64) (sdk.WorkflowRunResults, error) {
 	query := gorpmapping.NewQuery("SELECT * FROM workflow_run_result WHERE workflow_run_id = $1 ORDER BY sub_num DESC").Args(runID)
 	return getAll(ctx, db, query)
@@ -413,8 +419,19 @@ func ResyncWorkflowRunResultsRoutine(ctx context.Context, DBFunc func() *gorp.Db
 					continue
 				}
 				for _, id := range ids {
-					if err := SyncRunResultArtifactManagerByRunID(ctx, DBFunc(), id); err != nil {
+					tx, err := DBFunc().Begin()
+					if err != nil {
+						log.ErrorWithStackTrace(ctx, err)
+						continue
+					}
+					if err := SyncRunResultArtifactManagerByRunID(ctx, tx, id); err != nil {
+						log.ErrorWithStackTrace(ctx, err)
+						tx.Rollback()
+						continue
+					}
+					if err := tx.Commit(); err != nil {
 						log.ErrorWithStackTrace(ctx, err)
+						tx.Rollback()
 						continue
 					}
 				}
@@ -432,14 +449,17 @@ func FindOldestWorkflowRunsWithResultToSync(ctx context.Context, dbmap *gorp.DbM
 	return results, nil
 }
 
-func SyncRunResultArtifactManagerByRunID(ctx context.Context, dbmap *gorp.DbMap, id int64) error {
-	log.Info(ctx, "sync run result for workflow run id %d", id)
-	db, err := dbmap.Begin()
-	if err != nil {
-		return sdk.WithStack(err)
+func UpdateRunResult(ctx context.Context, db gorp.SqlExecutor, result *sdk.WorkflowRunResult) error {
+	dbResult := dbRunResult(*result)
+	log.Debug(ctx, "updating run result %s: %v", dbResult.ID, result)
+	if err := gorpmapping.Update(db, &dbResult); err != nil {
+		return err
 	}
-	defer db.Rollback() // nolint
+	return nil
+}
 
+func SyncRunResultArtifactManagerByRunID(ctx context.Context, db gorp.SqlExecutor, id int64) error {
+	log.Info(ctx, "sync run result for workflow run id %d", id)
 	wr, err := LoadAndLockRunByID(ctx, db, id, LoadRunOptions{})
 	if err != nil {
 		return err
@@ -459,12 +479,12 @@ func SyncRunResultArtifactManagerByRunID(ctx context.Context, dbmap *gorp.DbMap,
 		result := allRunResults[i]
 		// If the result is not an artifact manager, we do nothing but we consider it as synchronized
 		if result.Type != sdk.WorkflowRunResultTypeArtifactManager {
-			dbResult := dbRunResult(result)
-			dbResult.DataSync = new(sdk.WorkflowRunResultSync)
-			dbResult.DataSync.Link = ""
-			dbResult.DataSync.Sync = true
-			log.Debug(ctx, "updating run result %s", dbResult.ID)
-			if err := gorpmapping.Update(db, &dbResult); err != nil {
+			if result.DataSync == nil {
+				result.DataSync = new(sdk.WorkflowRunResultSync)
+			}
+			result.DataSync.Link = ""
+			result.DataSync.Sync = true
+			if err := UpdateRunResult(ctx, db, &result); err != nil {
 				return err
 			}
 		} else {
@@ -474,9 +494,6 @@ func SyncRunResultArtifactManagerByRunID(ctx context.Context, dbmap *gorp.DbMap,
 
 	// Nothing more to do with artifact manager
 	if len(runResults) == 0 {
-		if err := db.Commit(); err != nil {
-			return err
-		}
 		return nil
 	}
 
@@ -495,30 +512,27 @@ func SyncRunResultArtifactManagerByRunID(ctx context.Context, dbmap *gorp.DbMap,
 		for i := range runResults {
 			result := runResults[i]
 			// If the result is not an artifact manager, we do nothing but we consider it as synchronized
-			dbResult := dbRunResult(result)
-			dbResult.DataSync = new(sdk.WorkflowRunResultSync)
-			dbResult.DataSync.Sync = false
-			dbResult.DataSync.Error = err.Error()
-			log.Debug(ctx, "updating run result %s", dbResult.ID)
-			if err := gorpmapping.Update(db, &dbResult); err != nil {
+			if result.DataSync == nil {
+				result.DataSync = new(sdk.WorkflowRunResultSync)
+			}
+			result.DataSync.Sync = false
+			result.DataSync.Error = err.Error()
+			if err := UpdateRunResult(ctx, db, &result); err != nil {
 				return err
 			}
 		}
-		if err := db.Commit(); err != nil {
-			return err
-		}
 		return nil
 	}
 
 	log.Info(ctx, "artifact manager %q found for workflow run", artifactManagerInteg.ProjectIntegration.Name)
 
 	var (
-		rtName                = artifactManagerInteg.ProjectIntegration.Config[sdk.ArtifactoryConfigPlatform].Value
-		rtURL                 = artifactManagerInteg.ProjectIntegration.Config[sdk.ArtifactoryConfigURL].Value
-		buildInfoPrefix       = artifactManagerInteg.ProjectIntegration.Config[sdk.ArtifactoryConfigBuildInfoPrefix].Value
-		tokenName             = artifactManagerInteg.ProjectIntegration.Config[sdk.ArtifactoryConfigTokenName].Value
-		lowMaturitySuffix     = artifactManagerInteg.ProjectIntegration.Config[sdk.ArtifactoryConfigPromotionLowMaturity].Value
-		artifactoryProjectKey = artifactManagerInteg.ProjectIntegration.Config[sdk.ArtifactoryConfigProjectKey].Value
+		rtName                      = artifactManagerInteg.ProjectIntegration.Config[sdk.ArtifactoryConfigPlatform].Value
+		rtURL                       = artifactManagerInteg.ProjectIntegration.Config[sdk.ArtifactoryConfigURL].Value
+		buildInfoPrefix             = artifactManagerInteg.ProjectIntegration.Config[sdk.ArtifactoryConfigBuildInfoPrefix].Value
+		tokenName                   = artifactManagerInteg.ProjectIntegration.Config[sdk.ArtifactoryConfigTokenName].Value
+		lowMaturitySuffixFromConfig = artifactManagerInteg.ProjectIntegration.Config[sdk.ArtifactoryConfigPromotionLowMaturity].Value
+		artifactoryProjectKey       = artifactManagerInteg.ProjectIntegration.Config[sdk.ArtifactoryConfigProjectKey].Value
 	)
 
 	// Load the token from secrets
@@ -539,18 +553,15 @@ func SyncRunResultArtifactManagerByRunID(ctx context.Context, dbmap *gorp.DbMap,
 		log.ErrorWithStackTrace(ctx, err)
 		for i := range runResults {
 			result := runResults[i]
-			dbResult := dbRunResult(result)
-			dbResult.DataSync = new(sdk.WorkflowRunResultSync)
-			dbResult.DataSync.Sync = false
-			dbResult.DataSync.Error = err.Error()
-			log.Debug(ctx, "updating run result %s", dbResult.ID)
-			if err := gorpmapping.Update(db, &dbResult); err != nil {
+			if result.DataSync == nil {
+				result.DataSync = new(sdk.WorkflowRunResultSync)
+			}
+			result.DataSync.Sync = false
+			result.DataSync.Error = err.Error()
+			if err := UpdateRunResult(ctx, db, &result); err != nil {
 				return err
 			}
 		}
-		if err := db.Commit(); err != nil {
-			return err
-		}
 		return nil
 	}
 
@@ -591,6 +602,27 @@ func SyncRunResultArtifactManagerByRunID(ctx context.Context, dbmap *gorp.DbMap,
 	nodeRunURL := parameters["cds.ui.pipeline.run"][0]
 	runURL := nodeRunURL[0:strings.Index(nodeRunURL, "/node/")]
 
+	var lowMaturitySuffix string
+	for i := range runResults {
+		log.Debug(ctx, "checking for earlier maturity in %+v", runResults[i].DataSync)
+		if runResults[i].DataSync != nil {
+			p := runResults[i].DataSync.LatestPromotionOrRelease()
+			if p != nil {
+				if lowMaturitySuffix != "" {
+					if p.FromMaturity != lowMaturitySuffix {
+						return sdk.NewErrorFrom(sdk.ErrWrongRequest, "several maturities (%q, %q) detected among your artifacts", p.ToMaturity, lowMaturitySuffix)
+					}
+				}
+				log.Debug(ctx, "low maturity is %+v", p.FromMaturity)
+				lowMaturitySuffix = p.FromMaturity
+				// we don't break the loop to let us to check all the maturities
+			}
+		}
+	}
+	if lowMaturitySuffix == "" {
+		lowMaturitySuffix = lowMaturitySuffixFromConfig
+	}
+
 	buildInfoRequest, err := art.PrepareBuildInfo(ctx, artifactClient, art.BuildInfoRequest{
 		BuildInfoPrefix:   buildInfoPrefix,
 		ProjectKey:        wr.Workflow.ProjectKey,
@@ -630,15 +662,15 @@ func SyncRunResultArtifactManagerByRunID(ctx context.Context, dbmap *gorp.DbMap,
 	}
 
 	for _, result := range runResults {
-		dbResult := dbRunResult(result)
-		dbResult.DataSync = new(sdk.WorkflowRunResultSync)
-		dbResult.DataSync.Link = buildInfoRequest.Name + "/" + buildInfoRequest.Number
-		dbResult.DataSync.Sync = true
-		log.Debug(ctx, "updating run result %s", dbResult.ID)
-		if err := gorpmapping.Update(db, &dbResult); err != nil {
+		if result.DataSync == nil {
+			result.DataSync = new(sdk.WorkflowRunResultSync)
+		}
+		result.DataSync.Link = buildInfoRequest.Name + "/" + buildInfoRequest.Number
+		result.DataSync.Sync = true
+		if err := UpdateRunResult(ctx, db, &result); err != nil {
 			return err
 		}
 	}
 
-	return sdk.WithStack(db.Commit())
+	return nil
 }
diff --git a/engine/api/workflow_queue.go b/engine/api/workflow_queue.go
index ee1940927..6a3f26d7f 100644
--- a/engine/api/workflow_queue.go
+++ b/engine/api/workflow_queue.go
@@ -1121,6 +1121,67 @@ func (api *API) workflowRunResultCheckUploadHandler() service.Handler {
 	}
 }
 
+func (api *API) workflowRunResultPromoteHandler() service.Handler {
+	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+		if !isWorker(ctx) {
+			return sdk.WrapError(sdk.ErrForbidden, "only workers can call this route")
+		}
+
+		jobID, err := requestVarInt(r, "permJobID")
+		if err != nil {
+			return err
+		}
+
+		var promotionRequest sdk.WorkflowRunResultPromotionRequest
+		if err := service.UnmarshalBody(r, &promotionRequest); err != nil {
+			return sdk.WithStack(err)
+		}
+
+		tx, err := api.mustDBWithCtx(ctx).Begin()
+		if err != nil {
+			return sdk.WithStack(err)
+		}
+		defer tx.Rollback()
+
+		wr, err := workflow.LoadRunByJobID(ctx, tx, jobID, workflow.LoadRunOptions{DisableDetailledNodeRun: true})
+		if err != nil {
+			return err
+		}
+
+		runResults, err := workflow.LoadRunResultsByRunIDFilterByIDs(ctx, tx, wr.ID, promotionRequest.IDs...)
+		if err != nil {
+			return err
+		}
+
+		if len(runResults) == 0 {
+			return sdk.NewErrorFrom(sdk.ErrWrongRequest, "unable to find any run results among %v", promotionRequest.IDs)
+		}
+
+		promotionRequest.WorkflowRunResultPromotion.Date = time.Now()
+		for i := range runResults {
+			r := &runResults[i]
+			for _, id := range promotionRequest.IDs {
+				if id == r.ID {
+					if r.DataSync == nil {
+						r.DataSync = new(sdk.WorkflowRunResultSync)
+					}
+					log.Debug(ctx, "adding promotion data: %+v", promotionRequest)
+					r.DataSync.Promotions = append(r.DataSync.Promotions, promotionRequest.WorkflowRunResultPromotion)
+				}
+			}
+			if err := workflow.UpdateRunResult(ctx, tx, r); err != nil {
+				return err
+			}
+		}
+
+		if err := workflow.SyncRunResultArtifactManagerByRunID(ctx, tx, wr.ID); err != nil {
+			return err
+		}
+
+		return sdk.WithStack(tx.Commit())
+	}
+}
+
 func (api *API) workflowRunResultReleaseHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 		if !isWorker(ctx) {
@@ -1132,16 +1193,53 @@ func (api *API) workflowRunResultReleaseHandler() service.Handler {
 			return err
 		}
 
-		wr, err := workflow.LoadRunByJobID(ctx, api.mustDBWithCtx(ctx), jobID, workflow.LoadRunOptions{DisableDetailledNodeRun: true})
+		var releaseRequest sdk.WorkflowRunResultPromotionRequest
+		if err := service.UnmarshalBody(r, &releaseRequest); err != nil {
+			return sdk.WithStack(err)
+		}
+
+		tx, err := api.mustDBWithCtx(ctx).Begin()
+		if err != nil {
+			return sdk.WithStack(err)
+		}
+		defer tx.Rollback()
+
+		wr, err := workflow.LoadRunByJobID(ctx, tx, jobID, workflow.LoadRunOptions{DisableDetailledNodeRun: true})
 		if err != nil {
 			return err
 		}
 
-		if err := workflow.SyncRunResultArtifactManagerByRunID(ctx, api.mustDBWithCtx(ctx), wr.ID); err != nil {
+		runResults, err := workflow.LoadRunResultsByRunIDFilterByIDs(ctx, tx, wr.ID, releaseRequest.IDs...)
+		if err != nil {
 			return err
 		}
 
-		return nil
+		if len(runResults) == 0 {
+			return sdk.NewErrorFrom(sdk.ErrWrongRequest, "unable to find any run results among %v", releaseRequest.IDs)
+		}
+
+		releaseRequest.WorkflowRunResultPromotion.Date = time.Now()
+		for i := range runResults {
+			r := &runResults[i]
+			for _, id := range releaseRequest.IDs {
+				if id == r.ID {
+					if r.DataSync == nil {
+						r.DataSync = new(sdk.WorkflowRunResultSync)
+					}
+					log.Debug(ctx, "adding release data: %+v", releaseRequest)
+					r.DataSync.Releases = append(r.DataSync.Releases, releaseRequest.WorkflowRunResultPromotion)
+				}
+			}
+			if err := workflow.UpdateRunResult(ctx, tx, r); err != nil {
+				return err
+			}
+		}
+
+		if err := workflow.SyncRunResultArtifactManagerByRunID(ctx, tx, wr.ID); err != nil {
+			return err
+		}
+
+		return sdk.WithStack(tx.Commit())
 	}
 }
 
diff --git a/engine/worker/internal/action/builtin_promote.go b/engine/worker/internal/action/builtin_promote.go
index ad0d2577b..7da4e97e3 100644
--- a/engine/worker/internal/action/builtin_promote.go
+++ b/engine/worker/internal/action/builtin_promote.go
@@ -7,6 +7,7 @@ import (
 	"strings"
 
 	"github.com/golang/protobuf/ptypes/empty"
+	"github.com/rockbears/log"
 
 	"github.com/ovh/cds/engine/worker/pkg/workerruntime"
 	"github.com/ovh/cds/sdk"
@@ -14,6 +15,24 @@ import (
 )
 
 func RunPromote(ctx context.Context, wk workerruntime.Runtime, a sdk.Action, _ []sdk.Variable) (sdk.Result, error) {
+	jobID, err := workerruntime.JobID(ctx)
+	if err != nil {
+		return sdk.Result{Status: sdk.StatusFail}, err
+	}
+
+	promotedRunResultIDs, err := RunReleaseActionPrepare(ctx, wk, a)
+	if err != nil {
+		return sdk.Result{Status: sdk.StatusFail}, err
+	}
+
+	log.Info(ctx, "RunPromote> preparing run result %+v for promotion", promotedRunResultIDs)
+	if err := wk.Client().QueueWorkflowRunResultsPromote(ctx,
+		jobID, promotedRunResultIDs,
+		sdk.ParameterValue(a.Parameters, "srcMaturity"), sdk.ParameterValue(a.Parameters, "destMaturity"),
+	); err != nil {
+		return sdk.Result{Status: sdk.StatusFail}, err
+	}
+
 	pfName := sdk.ParameterFind(wk.Parameters(), "cds.integration.artifact_manager")
 	if pfName == nil {
 		return sdk.Result{}, errors.New("unable to retrieve artifact manager integration... Aborting")
diff --git a/engine/worker/internal/action/builtin_release.go b/engine/worker/internal/action/builtin_release.go
index 0d8c777a6..1ea84b5cf 100644
--- a/engine/worker/internal/action/builtin_release.go
+++ b/engine/worker/internal/action/builtin_release.go
@@ -2,11 +2,13 @@ package action
 
 import (
 	"context"
-	"errors"
 	"fmt"
+	"regexp"
+	"strconv"
 	"strings"
 
 	"github.com/golang/protobuf/ptypes/empty"
+	"github.com/pkg/errors"
 	"github.com/rockbears/log"
 
 	"github.com/ovh/cds/engine/worker/pkg/workerruntime"
@@ -14,14 +16,72 @@ import (
 	"github.com/ovh/cds/sdk/grpcplugin/integrationplugin"
 )
 
+func RunReleaseActionPrepare(ctx context.Context, wk workerruntime.Runtime, a sdk.Action) ([]string, error) {
+	artifactList := sdk.ParameterValue(a.Parameters, "artifacts")
+	artSplitted := strings.Split(artifactList, ",")
+	artRegs := make([]*regexp.Regexp, 0, len(artSplitted))
+	for _, arti := range artSplitted {
+		r, err := regexp.Compile(arti)
+		if err != nil {
+			return nil, sdk.Errorf("unable to compile regexp in artifact list: %v", err)
+		}
+		artRegs = append(artRegs, r)
+	}
+
+	projectKey := sdk.ParameterValue(wk.Parameters(), "cds.project")
+	wName := sdk.ParameterValue(wk.Parameters(), "cds.workflow")
+	runNumberString := sdk.ParameterValue(wk.Parameters(), "cds.run.number")
+	runNumber, err := strconv.ParseInt(runNumberString, 10, 64)
+	if err != nil {
+		newError := sdk.NewError(sdk.ErrWrongRequest, fmt.Errorf("cannot parse '%s' as run number: %s", runNumberString, err))
+		return nil, newError
+	}
+	runResult, err := wk.Client().WorkflowRunResultsList(ctx, projectKey, wName, runNumber)
+	if err != nil {
+		return nil, sdk.Errorf("unable to get run result list: %v", err)
+	}
+
+	promotedRunResultIDs := make([]string, 0)
+	for _, r := range runResult {
+		// static-file type does not need to be released
+		if r.Type == sdk.WorkflowRunResultTypeStaticFile {
+			continue
+		}
+		rData, err := r.GetArtifactManager()
+		if err != nil {
+			return nil, sdk.Errorf("unable to read artifacts data: %v", err)
+		}
+		skip := true
+		for _, reg := range artRegs {
+			if reg.MatchString(rData.Name) {
+				skip = false
+				break
+			}
+		}
+		if !skip {
+			promotedRunResultIDs = append(promotedRunResultIDs, r.ID)
+		}
+	}
+
+	return promotedRunResultIDs, nil
+}
+
 func RunRelease(ctx context.Context, wk workerruntime.Runtime, a sdk.Action, _ []sdk.Variable) (sdk.Result, error) {
 	jobID, err := workerruntime.JobID(ctx)
 	if err != nil {
 		return sdk.Result{Status: sdk.StatusFail}, err
 	}
 
-	log.Debug(ctx, "RunRelease> preparing run result for release ")
-	if err := wk.Client().QueueWorkflowRunResultsRelease(ctx, jobID); err != nil {
+	promotedRunResultIDs, err := RunReleaseActionPrepare(ctx, wk, a)
+	if err != nil {
+		return sdk.Result{Status: sdk.StatusFail}, err
+	}
+
+	log.Info(ctx, "RunRelease> preparing run result %+v for release", promotedRunResultIDs)
+	if err := wk.Client().QueueWorkflowRunResultsRelease(ctx,
+		jobID, promotedRunResultIDs,
+		sdk.ParameterValue(a.Parameters, "srcMaturity"), sdk.ParameterValue(a.Parameters, "destMaturity"),
+	); err != nil {
 		return sdk.Result{Status: sdk.StatusFail}, err
 	}
 
diff --git a/engine/worker/internal/handler_cache.go b/engine/worker/internal/handler_cache.go
index ced93fe85..ef20a6f1c 100644
--- a/engine/worker/internal/handler_cache.go
+++ b/engine/worker/internal/handler_cache.go
@@ -122,7 +122,7 @@ func cachePushHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc {
 			writeError(w, r, err)
 			return
 		}
-		duration, err := wk.client.CDNItemUpload(ctx, wk.cdnHttpAddr, sig, wk.BaseDir(), tarF.Name())
+		duration, err := wk.client.CDNItemUpload(ctx, wk.cfg.CDNEndpoint, sig, wk.BaseDir(), tarF.Name())
 		if err != nil {
 			log.Error(ctx, "%v", err)
 			writeError(w, r, err)
@@ -177,7 +177,7 @@ func cachePullHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc {
 			writeError(w, req, err)
 			return
 		}
-		if err := wk.client.CDNItemDownload(ctx, wk.cdnHttpAddr, items.Items[0].APIRefHash, sdk.CDNTypeItemWorkerCache, items.Items[0].MD5, f); err != nil {
+		if err := wk.client.CDNItemDownload(ctx, wk.cfg.CDNEndpoint, items.Items[0].APIRefHash, sdk.CDNTypeItemWorkerCache, items.Items[0].MD5, f); err != nil {
 			_ = f.Close()
 			err = sdk.Error{
 				Message: "worker cache pull > Cannot pull cache: " + err.Error(),
diff --git a/engine/worker/internal/take.go b/engine/worker/internal/take.go
index 1832bdab8..48e2de937 100644
--- a/engine/worker/internal/take.go
+++ b/engine/worker/internal/take.go
@@ -46,8 +46,6 @@ func (w *CurrentWorker) Take(ctx context.Context, job sdk.WorkflowNodeJobRun) er
 	// Reset build variables
 	w.currentJob.newVariables = nil
 
-	w.cdnHttpAddr = info.CDNHttpAddr
-
 	secretKey := make([]byte, 32)
 	if _, err := base64.StdEncoding.Decode(secretKey, []byte(info.SigningKey)); err != nil {
 		return sdk.WithStack(err)
@@ -58,11 +56,11 @@ func (w *CurrentWorker) Take(ctx context.Context, job sdk.WorkflowNodeJobRun) er
 	}
 	w.currentJob.signer = signer
 
-	log.Info(ctx, "Setup step logger %s", info.GelfServiceAddr)
+	log.Info(ctx, "Setup step logger %s", w.cfg.GelfServiceAddr)
 	throttlePolicy := hook.NewDefaultThrottlePolicy()
 
 	var graylogCfg = &hook.Config{
-		Addr:     info.GelfServiceAddr,
+		Addr:     w.cfg.GelfServiceAddr,
 		Protocol: "tcp",
 		ThrottlePolicy: &hook.ThrottlePolicyConfig{
 			Amount: 100,
@@ -71,8 +69,8 @@ func (w *CurrentWorker) Take(ctx context.Context, job sdk.WorkflowNodeJobRun) er
 		},
 	}
 
-	if info.GelfServiceAddrEnableTLS {
-		tcpCDNUrl := info.GelfServiceAddr
+	if w.cfg.GelfServiceAddrEnableTLS {
+		tcpCDNUrl := w.cfg.GelfServiceAddr
 		// Check if the url has a scheme
 		// We have to remove if to retrieve the hostname
 		if i := strings.Index(tcpCDNUrl, "://"); i > -1 {
diff --git a/engine/worker/internal/types.go b/engine/worker/internal/types.go
index 57dbdfeeb..455dec285 100644
--- a/engine/worker/internal/types.go
+++ b/engine/worker/internal/types.go
@@ -42,7 +42,6 @@ type CurrentWorker struct {
 	basedir     afero.Fs
 	manualExit  bool
 	gelfLogger  *logger
-	cdnHttpAddr string // FIXME get CDN information from worker config
 	stepLogLine int64
 	httpPort    int32
 	currentJob  struct {
@@ -194,7 +193,7 @@ func (wk *CurrentWorker) SendLog(ctx context.Context, level workerruntime.Level,
 }
 
 func (wk *CurrentWorker) CDNHttpURL() string {
-	return wk.cdnHttpAddr
+	return wk.cfg.CDNEndpoint
 }
 
 func (wk *CurrentWorker) prepareLog(ctx context.Context, level workerruntime.Level, s string) (cdslog.Message, string, error) {
diff --git a/go.mod b/go.mod
index 9c0400243..118f8716b 100644
--- a/go.mod
+++ b/go.mod
@@ -67,6 +67,7 @@ require (
 	github.com/pkg/browser v0.0.0-20170505125900-c90ca0c84f15
 	github.com/pkg/errors v0.9.1
 	github.com/rockbears/log v0.6.0
+	github.com/rockbears/yaml v0.1.1-0.20220901090137-13dadb408625
 	github.com/rubenv/sql-migrate v0.0.0-20160620083229-6f4757563362
 	github.com/sguiheux/go-coverage v0.0.0-20190710153556-287b082a7197
 	github.com/shirou/gopsutil v0.0.0-20170406131756-e49a95f3d5f8
@@ -76,7 +77,7 @@ require (
 	github.com/spf13/cobra v1.1.1
 	github.com/spf13/viper v1.10.1
 	github.com/streadway/amqp v0.0.0-20180528204448-e5adc2ada8b8
-	github.com/stretchr/testify v1.7.0
+	github.com/stretchr/testify v1.8.0
 	github.com/studio-b12/gowebdav v0.0.0-20200303150724-9380631c29a1
 	github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5
 	github.com/urfave/cli v1.20.0
@@ -252,7 +253,7 @@ require (
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/ini.v1 v1.66.4 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
-	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 	gotest.tools v2.1.0+incompatible // indirect
 	k8s.io/klog/v2 v2.8.0 // indirect
 	k8s.io/utils v0.0.0-20201110183641-67b214c5f920 // indirect
diff --git a/go.sum b/go.sum
index af1394abe..755e76f2c 100644
--- a/go.sum
+++ b/go.sum
@@ -756,6 +756,8 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5X
 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rockbears/log v0.6.0 h1:Wzpu7nbrZFFJy14ku41jI2/UEh3d0CJwvIED9/FQZKQ=
 github.com/rockbears/log v0.6.0/go.mod h1:z46IOEOh914gJvg256Vm3F4s8K7D3ePaEAzuMYcfk98=
+github.com/rockbears/yaml v0.1.1-0.20220901090137-13dadb408625 h1:HQAWw/D9RItCYSoWFs8E7IrGKrX9ivqAlCq47DM3IVU=
+github.com/rockbears/yaml v0.1.1-0.20220901090137-13dadb408625/go.mod h1:8cDJx2PWQJMtfGgsRCvHVbIB61SV3dvy8o6EGv2cIpg=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -816,16 +818,19 @@ github.com/streadway/amqp v0.0.0-20180528204448-e5adc2ada8b8 h1:l6epF6yBwuejBfhG
 github.com/streadway/amqp v0.0.0-20180528204448-e5adc2ada8b8/go.mod h1:1WNBiOZtZQLpVAyu0iTduoJL9hEsMloAK5XWrtW0xdY=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
 github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/studio-b12/gowebdav v0.0.0-20200303150724-9380631c29a1 h1:TPyHV/OgChqNcnYqCoCvIFjR9TU60gFXXBKnhOBzVEI=
 github.com/studio-b12/gowebdav v0.0.0-20200303150724-9380631c29a1/go.mod h1:gCcfDlA1Y7GqOaeEKw5l9dOGx1VLdc/HuQSlQAaZ30s=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
@@ -1418,8 +1423,9 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools v2.1.0+incompatible h1:5USw7CrJBYKqjg9R7QlA6jzqZKEAtvW82aNmsxxGPxw=
 gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/sdk/cdsclient/client_queue.go b/sdk/cdsclient/client_queue.go
index 5a4223d91..8a62100d8 100644
--- a/sdk/cdsclient/client_queue.go
+++ b/sdk/cdsclient/client_queue.go
@@ -271,8 +271,28 @@ func (c *client) QueueJobSetVersion(ctx context.Context, jobID int64, version sd
 	return err
 }
 
-func (c *client) QueueWorkflowRunResultsRelease(ctx context.Context, permJobID int64) error {
+func (c *client) QueueWorkflowRunResultsRelease(ctx context.Context, permJobID int64, runResultIDs []string, from, to string) error {
+	req := sdk.WorkflowRunResultPromotionRequest{
+		IDs: runResultIDs,
+		WorkflowRunResultPromotion: sdk.WorkflowRunResultPromotion{
+			FromMaturity: from,
+			ToMaturity:   to,
+		},
+	}
 	uri := fmt.Sprintf("/queue/workflows/%d/run/results/release", permJobID)
-	_, err := c.PostJSON(ctx, uri, nil, nil)
+	_, err := c.PostJSON(ctx, uri, req, nil)
+	return err
+}
+
+func (c *client) QueueWorkflowRunResultsPromote(ctx context.Context, permJobID int64, runResultIDs []string, from, to string) error {
+	req := sdk.WorkflowRunResultPromotionRequest{
+		IDs: runResultIDs,
+		WorkflowRunResultPromotion: sdk.WorkflowRunResultPromotion{
+			FromMaturity: from,
+			ToMaturity:   to,
+		},
+	}
+	uri := fmt.Sprintf("/queue/workflows/%d/run/results/promote", permJobID)
+	_, err := c.PostJSON(ctx, uri, req, nil)
 	return err
 }
diff --git a/sdk/cdsclient/client_rbac.go b/sdk/cdsclient/client_rbac.go
new file mode 100644
index 000000000..627d1ecbb
--- /dev/null
+++ b/sdk/cdsclient/client_rbac.go
@@ -0,0 +1,27 @@
+package cdsclient
+
+import (
+	"context"
+	"io"
+
+	"github.com/rockbears/yaml"
+
+	"github.com/ovh/cds/sdk"
+)
+
+func (c *client) RBACImport(ctx context.Context, content io.Reader, mods ...RequestModifier) (sdk.RBAC, error) {
+	var rbacRule sdk.RBAC
+
+	body, err := io.ReadAll(content)
+	if err != nil {
+		return rbacRule, err
+	}
+
+	if err := yaml.Unmarshal(body, &rbacRule); err != nil {
+		return rbacRule, err
+	}
+
+	path := "/v2/rbac/import"
+	_, err = c.PostJSON(ctx, path, &rbacRule, &rbacRule, mods...)
+	return rbacRule, err
+}
diff --git a/sdk/cdsclient/interface.go b/sdk/cdsclient/interface.go
index 7e239c8f8..3291ee86b 100644
--- a/sdk/cdsclient/interface.go
+++ b/sdk/cdsclient/interface.go
@@ -231,6 +231,10 @@ type ProjectClient interface {
 	ProjectRepositoryAnalysisGet(ctx context.Context, projectKey string, vcsIdentifier string, repositoryIdentifier string, analysisID string) (sdk.ProjectRepositoryAnalysis, error)
 }
 
+type RBACClient interface {
+	RBACImport(ctx context.Context, content io.Reader, mods ...RequestModifier) (sdk.RBAC, error)
+}
+
 // ProjectKeysClient exposes project keys related functions
 type ProjectKeysClient interface {
 	ProjectKeysList(projectKey string) ([]sdk.ProjectKey, error)
@@ -270,7 +274,8 @@ type QueueClient interface {
 	QueueWorkerCacheLink(ctx context.Context, jobID int64, tag string) (sdk.CDNItemLinks, error)
 	QueueWorkflowRunResultsAdd(ctx context.Context, jobID int64, addRequest sdk.WorkflowRunResult) error
 	QueueWorkflowRunResultCheck(ctx context.Context, jobID int64, runResultCheck sdk.WorkflowRunResultCheck) (int, error)
-	QueueWorkflowRunResultsRelease(ctx context.Context, permJobID int64) error
+	QueueWorkflowRunResultsRelease(ctx context.Context, permJobID int64, runResultIDs []string, from, to string) error
+	QueueWorkflowRunResultsPromote(ctx context.Context, permJobID int64, runResultIDs []string, from, to string) error
 }
 
 // UserClient exposes users functions
@@ -405,6 +410,7 @@ type Interface interface {
 	PipelineClient
 	IntegrationClient
 	ProjectClient
+	RBACClient
 	QueueClient
 	Navbar() ([]sdk.NavbarProjectData, error)
 	Requirements() ([]sdk.Requirement, error)
diff --git a/sdk/cdsclient/mock_cdsclient/interface_mock.go b/sdk/cdsclient/mock_cdsclient/interface_mock.go
index d4ca5c618..ae4b5fd71 100644
--- a/sdk/cdsclient/mock_cdsclient/interface_mock.go
+++ b/sdk/cdsclient/mock_cdsclient/interface_mock.go
@@ -2894,6 +2894,49 @@ func (mr *MockProjectClientMockRecorder) VariableListEncrypt(projectKey interfac
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VariableListEncrypt", reflect.TypeOf((*MockProjectClient)(nil).VariableListEncrypt), projectKey)
 }
 
+// MockRbacClient is a mock of RBACClient interface.
+type MockRbacClient struct {
+	ctrl     *gomock.Controller
+	recorder *MockRbacClientMockRecorder
+}
+
+// MockRbacClientMockRecorder is the mock recorder for MockRbacClient.
+type MockRbacClientMockRecorder struct {
+	mock *MockRbacClient
+}
+
+// NewMockRbacClient creates a new mock instance.
+func NewMockRbacClient(ctrl *gomock.Controller) *MockRbacClient {
+	mock := &MockRbacClient{ctrl: ctrl}
+	mock.recorder = &MockRbacClientMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockRbacClient) EXPECT() *MockRbacClientMockRecorder {
+	return m.recorder
+}
+
+// RbacImport mocks base method.
+func (m *MockRbacClient) RBACImport(ctx context.Context, content io.Reader, mods ...cdsclient.RequestModifier) (sdk.RBAC, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{ctx, content}
+	for _, a := range mods {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "RBACImport", varargs...)
+	ret0, _ := ret[0].(sdk.RBAC)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// RbacImport indicates an expected call of RbacImport.
+func (mr *MockRbacClientMockRecorder) RbacImport(ctx, content interface{}, mods ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{ctx, content}, mods...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RBACImport", reflect.TypeOf((*MockRbacClient)(nil).RBACImport), varargs...)
+}
+
 // MockProjectKeysClient is a mock of ProjectKeysClient interface.
 type MockProjectKeysClient struct {
 	ctrl     *gomock.Controller
@@ -3390,18 +3433,32 @@ func (mr *MockQueueClientMockRecorder) QueueWorkflowRunResultsAdd(ctx, jobID, ad
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueueWorkflowRunResultsAdd", reflect.TypeOf((*MockQueueClient)(nil).QueueWorkflowRunResultsAdd), ctx, jobID, addRequest)
 }
 
+// QueueWorkflowRunResultsPromote mocks base method.
+func (m *MockQueueClient) QueueWorkflowRunResultsPromote(ctx context.Context, permJobID int64, runResultIDs []string, from, to string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "QueueWorkflowRunResultsPromote", ctx, permJobID, runResultIDs, from, to)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// QueueWorkflowRunResultsPromote indicates an expected call of QueueWorkflowRunResultsPromote.
+func (mr *MockQueueClientMockRecorder) QueueWorkflowRunResultsPromote(ctx, permJobID, runResultIDs, from, to interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueueWorkflowRunResultsPromote", reflect.TypeOf((*MockQueueClient)(nil).QueueWorkflowRunResultsPromote), ctx, permJobID, runResultIDs, from, to)
+}
+
 // QueueWorkflowRunResultsRelease mocks base method.
-func (m *MockQueueClient) QueueWorkflowRunResultsRelease(ctx context.Context, permJobID int64) error {
+func (m *MockQueueClient) QueueWorkflowRunResultsRelease(ctx context.Context, permJobID int64, runResultIDs []string, from, to string) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "QueueWorkflowRunResultsRelease", ctx, permJobID)
+	ret := m.ctrl.Call(m, "QueueWorkflowRunResultsRelease", ctx, permJobID, runResultIDs, from, to)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
 // QueueWorkflowRunResultsRelease indicates an expected call of QueueWorkflowRunResultsRelease.
-func (mr *MockQueueClientMockRecorder) QueueWorkflowRunResultsRelease(ctx, permJobID interface{}) *gomock.Call {
+func (mr *MockQueueClientMockRecorder) QueueWorkflowRunResultsRelease(ctx, permJobID, runResultIDs, from, to interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueueWorkflowRunResultsRelease", reflect.TypeOf((*MockQueueClient)(nil).QueueWorkflowRunResultsRelease), ctx, permJobID)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueueWorkflowRunResultsRelease", reflect.TypeOf((*MockQueueClient)(nil).QueueWorkflowRunResultsRelease), ctx, permJobID, runResultIDs, from, to)
 }
 
 // MockUserClient is a mock of UserClient interface.
@@ -7466,18 +7523,52 @@ func (mr *MockInterfaceMockRecorder) QueueWorkflowRunResultsAdd(ctx, jobID, addR
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueueWorkflowRunResultsAdd", reflect.TypeOf((*MockInterface)(nil).QueueWorkflowRunResultsAdd), ctx, jobID, addRequest)
 }
 
+// QueueWorkflowRunResultsPromote mocks base method.
+func (m *MockInterface) QueueWorkflowRunResultsPromote(ctx context.Context, permJobID int64, runResultIDs []string, from, to string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "QueueWorkflowRunResultsPromote", ctx, permJobID, runResultIDs, from, to)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// QueueWorkflowRunResultsPromote indicates an expected call of QueueWorkflowRunResultsPromote.
+func (mr *MockInterfaceMockRecorder) QueueWorkflowRunResultsPromote(ctx, permJobID, runResultIDs, from, to interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueueWorkflowRunResultsPromote", reflect.TypeOf((*MockInterface)(nil).QueueWorkflowRunResultsPromote), ctx, permJobID, runResultIDs, from, to)
+}
+
 // QueueWorkflowRunResultsRelease mocks base method.
-func (m *MockInterface) QueueWorkflowRunResultsRelease(ctx context.Context, permJobID int64) error {
+func (m *MockInterface) QueueWorkflowRunResultsRelease(ctx context.Context, permJobID int64, runResultIDs []string, from, to string) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "QueueWorkflowRunResultsRelease", ctx, permJobID)
+	ret := m.ctrl.Call(m, "QueueWorkflowRunResultsRelease", ctx, permJobID, runResultIDs, from, to)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
 // QueueWorkflowRunResultsRelease indicates an expected call of QueueWorkflowRunResultsRelease.
-func (mr *MockInterfaceMockRecorder) QueueWorkflowRunResultsRelease(ctx, permJobID interface{}) *gomock.Call {
+func (mr *MockInterfaceMockRecorder) QueueWorkflowRunResultsRelease(ctx, permJobID, runResultIDs, from, to interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueueWorkflowRunResultsRelease", reflect.TypeOf((*MockInterface)(nil).QueueWorkflowRunResultsRelease), ctx, permJobID)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueueWorkflowRunResultsRelease", reflect.TypeOf((*MockInterface)(nil).QueueWorkflowRunResultsRelease), ctx, permJobID, runResultIDs, from, to)
+}
+
+// RbacImport mocks base method.
+func (m *MockInterface) RBACImport(ctx context.Context, content io.Reader, mods ...cdsclient.RequestModifier) (sdk.RBAC, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{ctx, content}
+	for _, a := range mods {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "RBACImport", varargs...)
+	ret0, _ := ret[0].(sdk.RBAC)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// RbacImport indicates an expected call of RbacImport.
+func (mr *MockInterfaceMockRecorder) RbacImport(ctx, content interface{}, mods ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{ctx, content}, mods...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RBACImport", reflect.TypeOf((*MockInterface)(nil).RBACImport), varargs...)
 }
 
 // RepositoriesList mocks base method.
@@ -9583,18 +9674,32 @@ func (mr *MockWorkerInterfaceMockRecorder) QueueWorkflowRunResultsAdd(ctx, jobID
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueueWorkflowRunResultsAdd", reflect.TypeOf((*MockWorkerInterface)(nil).QueueWorkflowRunResultsAdd), ctx, jobID, addRequest)
 }
 
+// QueueWorkflowRunResultsPromote mocks base method.
+func (m *MockWorkerInterface) QueueWorkflowRunResultsPromote(ctx context.Context, permJobID int64, runResultIDs []string, from, to string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "QueueWorkflowRunResultsPromote", ctx, permJobID, runResultIDs, from, to)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// QueueWorkflowRunResultsPromote indicates an expected call of QueueWorkflowRunResultsPromote.
+func (mr *MockWorkerInterfaceMockRecorder) QueueWorkflowRunResultsPromote(ctx, permJobID, runResultIDs, from, to interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueueWorkflowRunResultsPromote", reflect.TypeOf((*MockWorkerInterface)(nil).QueueWorkflowRunResultsPromote), ctx, permJobID, runResultIDs, from, to)
+}
+
 // QueueWorkflowRunResultsRelease mocks base method.
-func (m *MockWorkerInterface) QueueWorkflowRunResultsRelease(ctx context.Context, permJobID int64) error {
+func (m *MockWorkerInterface) QueueWorkflowRunResultsRelease(ctx context.Context, permJobID int64, runResultIDs []string, from, to string) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "QueueWorkflowRunResultsRelease", ctx, permJobID)
+	ret := m.ctrl.Call(m, "QueueWorkflowRunResultsRelease", ctx, permJobID, runResultIDs, from, to)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
 // QueueWorkflowRunResultsRelease indicates an expected call of QueueWorkflowRunResultsRelease.
-func (mr *MockWorkerInterfaceMockRecorder) QueueWorkflowRunResultsRelease(ctx, permJobID interface{}) *gomock.Call {
+func (mr *MockWorkerInterfaceMockRecorder) QueueWorkflowRunResultsRelease(ctx, permJobID, runResultIDs, from, to interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueueWorkflowRunResultsRelease", reflect.TypeOf((*MockWorkerInterface)(nil).QueueWorkflowRunResultsRelease), ctx, permJobID)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueueWorkflowRunResultsRelease", reflect.TypeOf((*MockWorkerInterface)(nil).QueueWorkflowRunResultsRelease), ctx, permJobID, runResultIDs, from, to)
 }
 
 // Requirements mocks base method.
diff --git a/sdk/rbac.go b/sdk/rbac.go
index 4e2bd49d8..8a5c517bd 100644
--- a/sdk/rbac.go
+++ b/sdk/rbac.go
@@ -19,17 +19,17 @@ type RBAC struct {
 	Projects     []RBACProject `json:"projects" db:"-"`
 }
 
-func IsValidRbac(rbac *RBAC) error {
+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 {
+		if err := isValidRBACGlobal(rbac.Name, g); err != nil {
 			return err
 		}
 	}
 	for _, p := range rbac.Projects {
-		if err := isValidRbacProject(rbac.Name, p); err != nil {
+		if err := isValidRBACProject(rbac.Name, p); err != nil {
 			return err
 		}
 	}
diff --git a/sdk/rbac_global.go b/sdk/rbac_global.go
index 40dad46ab..db0e0c07d 100644
--- a/sdk/rbac_global.go
+++ b/sdk/rbac_global.go
@@ -12,7 +12,7 @@ type RBACGlobal struct {
 	RBACGroupsIDs  []int64  `json:"-" db:"-"`
 }
 
-func isValidRbacGlobal(rbacName string, rg RBACGlobal) error {
+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)
 	}
diff --git a/sdk/rbac_project.go b/sdk/rbac_project.go
index 5cb731fe9..7fb93d765 100644
--- a/sdk/rbac_project.go
+++ b/sdk/rbac_project.go
@@ -15,7 +15,7 @@ type RBACProject struct {
 	RBACGroupsIDs []int64  `json:"-" db:"-"`
 }
 
-func isValidRbacProject(rbacName string, rbacProject RBACProject) error {
+func isValidRBACProject(rbacName string, rbacProject RBACProject) error {
 	// Check empty group and users
 	if len(rbacProject.RBACGroupsIDs) == 0 && len(rbacProject.RBACUsersIDs) == 0 {
 		return NewErrorFrom(ErrInvalidData, "rbac %s: missing groups or users on project permission", rbacName)
diff --git a/sdk/workflow_run_result.go b/sdk/workflow_run_result.go
index 3907bc45c..ba5713b5f 100644
--- a/sdk/workflow_run_result.go
+++ b/sdk/workflow_run_result.go
@@ -53,9 +53,38 @@ type WorkflowRunResult struct {
 }
 
 type WorkflowRunResultSync struct {
-	Sync  bool   `json:"sync"`
-	Link  string `json:"link"`
-	Error string `json:"error"`
+	Sync       bool                         `json:"sync"`
+	Link       string                       `json:"link"`
+	Error      string                       `json:"error"`
+	Promotions []WorkflowRunResultPromotion `json:"promotions"`
+	Releases   []WorkflowRunResultPromotion `json:"releases"`
+}
+
+type WorkflowRunResultPromotionRequest struct {
+	WorkflowRunResultPromotion
+	IDs []string
+}
+
+type WorkflowRunResultPromotion struct {
+	FromMaturity string    `json:"from_maturity"`
+	ToMaturity   string    `json:"to_maturity"`
+	Date         time.Time `json:"date"`
+}
+
+func (s *WorkflowRunResultSync) LatestPromotionOrRelease() *WorkflowRunResultPromotion {
+	sort.Slice(s.Promotions, func(i, j int) bool {
+		return s.Promotions[i].Date.Before(s.Promotions[j].Date)
+	})
+	sort.Slice(s.Releases, func(i, j int) bool {
+		return s.Releases[i].Date.Before(s.Releases[j].Date)
+	})
+	if len(s.Releases) > 0 {
+		return &s.Releases[len(s.Releases)-1]
+	}
+	if len(s.Promotions) > 0 {
+		return &s.Promotions[len(s.Promotions)-1]
+	}
+	return nil
 }
 
 // Value returns driver.Value from WorkflowRunResultSync


Test patch:
diff --git a/engine/api/rbac/dao_rbac_test.go b/engine/api/rbac/dao_rbac_test.go
index b2c0bd7ba..312cd7127 100644
--- a/engine/api/rbac/dao_rbac_test.go
+++ b/engine/api/rbac/dao_rbac_test.go
@@ -107,7 +107,7 @@ globals:
 
 	require.NoError(t, rbac.Insert(context.Background(), db, &r))
 
-	rbacDB, err := rbac.LoadRbacByName(context.TODO(), db, r.Name, rbac.LoadOptions.Default)
+	rbacDB, err := rbac.LoadRBACByName(context.TODO(), db, r.Name, rbac.LoadOptions.Default)
 	require.NoError(t, err)
 
 	// Global part
@@ -174,7 +174,7 @@ projects:
 
 	require.NoError(t, rbac.Insert(context.Background(), db, &r))
 
-	rbacDB, err := rbac.LoadRbacByName(context.TODO(), db, r.Name, rbac.LoadOptions.Default)
+	rbacDB, err := rbac.LoadRBACByName(context.TODO(), db, r.Name, rbac.LoadOptions.Default)
 	require.NoError(t, err)
 
 	require.Equal(t, "read", rbacDB.Projects[0].Role)
@@ -195,7 +195,7 @@ projects:
 
 	require.NoError(t, rbac.Update(context.TODO(), db, &rUpdated))
 
-	rbacDBUpdate, err := rbac.LoadRbacByName(context.TODO(), db, r.Name, rbac.LoadOptions.Default)
+	rbacDBUpdate, err := rbac.LoadRBACByName(context.TODO(), db, r.Name, rbac.LoadOptions.Default)
 	require.NoError(t, err)
 	require.Equal(t, "manage", rbacDBUpdate.Projects[0].Role)
 
diff --git a/engine/api/v2_rbac_test.go b/engine/api/v2_rbac_test.go
new file mode 100644
index 000000000..cad834dc7
--- /dev/null
+++ b/engine/api/v2_rbac_test.go
@@ -0,0 +1,69 @@
+package api
+
+import (
+	"context"
+	"fmt"
+	"github.com/ovh/cds/engine/api/rbac"
+	"io"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"github.com/ovh/cds/engine/api/test"
+	"github.com/ovh/cds/engine/api/test/assets"
+	"github.com/ovh/cds/sdk"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_postImportRbacHandler(t *testing.T) {
+	api, db, _ := newTestAPI(t)
+
+	_, err := db.Exec("DELETE FROM rbac")
+	require.NoError(t, err)
+
+	p := assets.InsertTestProject(t, db, api.Cache, sdk.RandomString(10), sdk.RandomString(10))
+	u, pass := assets.InsertAdminUser(t, db)
+	g := assets.InsertTestGroup(t, db, sdk.RandomString(10))
+
+	vars := map[string]string{}
+	uri := api.Router.GetRouteV2("POST", api.postImportRbacHandler, vars)
+	test.NotEmpty(t, uri)
+	req := assets.NewAuthentifiedRequest(t, u, pass, "POST", uri, nil)
+
+	body := fmt.Sprintf(`name: perm-test
+projects:
+  - role: read
+    projects: [%s]
+    users: [%s]
+    groups: [%s]
+globals:
+ - role: manage-permission
+   users: [%s]
+   groups: [%s]
+`, p.Key, u.Username, g.Name, u.Username, g.Name)
+
+	// Here, we insert the vcs server as a CDS administrator
+	req.Body = io.NopCloser(strings.NewReader(body))
+	req.Header.Set("Content-Type", "application/x-yaml")
+
+	w := httptest.NewRecorder()
+	api.Router.Mux.ServeHTTP(w, req)
+	require.Equal(t, 201, w.Code)
+
+	rbacDB, err := rbac.LoadRBACByName(context.TODO(), db, "perm-test", rbac.LoadOptions.LoadRBACProject, rbac.LoadOptions.LoadRBACGlobal)
+	require.NoError(t, err)
+
+	require.Equal(t, 1, len(rbacDB.Globals))
+	require.Equal(t, "manage-permission", rbacDB.Globals[0].Role)
+	require.Equal(t, 1, len(rbacDB.Globals[0].RBACUsersIDs))
+	require.Equal(t, 1, len(rbacDB.Globals[0].RBACGroupsIDs))
+	require.Equal(t, u.ID, rbacDB.Globals[0].RBACUsersIDs[0])
+	require.Equal(t, g.ID, rbacDB.Globals[0].RBACGroupsIDs[0])
+
+	require.Equal(t, 1, len(rbacDB.Projects))
+	require.Equal(t, "read", rbacDB.Projects[0].Role)
+	require.Equal(t, 1, len(rbacDB.Projects[0].RBACUsersIDs))
+	require.Equal(t, 1, len(rbacDB.Projects[0].RBACGroupsIDs))
+	require.Equal(t, u.ID, rbacDB.Projects[0].RBACUsersIDs[0])
+	require.Equal(t, g.ID, rbacDB.Projects[0].RBACGroupsIDs[0])
+}
diff --git a/engine/worker/internal/handler_cache_test.go b/engine/worker/internal/handler_cache_test.go
index 7b81ca258..b7deab675 100644
--- a/engine/worker/internal/handler_cache_test.go
+++ b/engine/worker/internal/handler_cache_test.go
@@ -107,8 +107,8 @@ func Test_cachePushPullHandler(t *testing.T) {
 	m := mock_cdsclient.NewMockWorkerInterface(ctrl)
 	wkPush.client = m
 	wkPull.client = m
-	wkPush.cdnHttpAddr = "https://cdn.local"
-	wkPull.cdnHttpAddr = "https://cdn.local"
+	wkPush.cfg = &workerruntime.WorkerConfig{CDNEndpoint: "https://cdn.local"}
+	wkPull.cfg = &workerruntime.WorkerConfig{CDNEndpoint: "https://cdn.local"}
 
 	var bodyBytes []byte
 	m.EXPECT().CDNItemUpload(gomock.Any(), "https://cdn.local", gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
diff --git a/engine/worker/internal/start_test.go b/engine/worker/internal/start_test.go
index 4e994b42e..6a30db0a9 100644
--- a/engine/worker/internal/start_test.go
+++ b/engine/worker/internal/start_test.go
@@ -357,6 +357,7 @@ export FOO_FROM_HOOK=BAR`,
 		Model:               "my-model",
 		Region:              "local-test",
 		Basedir:             basedir,
+		GelfServiceAddr:     "localhost:8090",
 	}
 	if err := w.Init(cfg, afero.NewBasePathFs(fs, basedir)); err != nil {
 		t.Fatalf("worker init failed: %v", err)
diff --git a/sdk/rbac_global_test.go b/sdk/rbac_global_test.go
index 1186bb5b0..caffbd2fc 100644
--- a/sdk/rbac_global_test.go
+++ b/sdk/rbac_global_test.go
@@ -5,34 +5,34 @@ import (
 	"testing"
 )
 
-func TestRbacGlobalInvalidGlobalRole(t *testing.T) {
+func TestRBACGlobalInvalidGlobalRole(t *testing.T) {
 	rb := RBACGlobal{
 		Role:          "runWorkflow",
 		RBACGroupsIDs: []int64{1},
 		RBACUsersIDs:  []string{"aa-aa-aa"},
 	}
-	err := isValidRbacGlobal("myRule", rb)
+	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) {
+func TestRBACGlobalInvalidGroupAndUsers(t *testing.T) {
 	rb := RBACGlobal{
 		Role:          RoleCreateProject,
 		RBACGroupsIDs: []int64{},
 		RBACUsersIDs:  []string{},
 	}
-	err := isValidRbacGlobal("myRule", rb)
+	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) {
+func TestRBACGlobalEmptyRole(t *testing.T) {
 	rb := RBACGlobal{
 		Role:          "",
 		RBACGroupsIDs: []int64{1},
 		RBACUsersIDs:  []string{},
 	}
-	err := isValidRbacGlobal("myRule", rb)
+	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
index 6179ecfe6..87a969bf9 100644
--- a/sdk/rbac_project_test.go
+++ b/sdk/rbac_project_test.go
@@ -7,7 +7,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func TestRbacProjectInvalidRole(t *testing.T) {
+func TestRBACProjectInvalidRole(t *testing.T) {
 	rb := RBACProject{
 		RBACProjectKeys: []string{"foo"},
 		All:             false,
@@ -15,11 +15,11 @@ func TestRbacProjectInvalidRole(t *testing.T) {
 		RBACGroupsIDs:   []int64{1},
 		RBACUsersIDs:    []string{"aa-aa-aa"},
 	}
-	err := isValidRbacProject("myRule", rb)
+	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) {
+func TestRBACProjectInvalidGroupAndUsers(t *testing.T) {
 	rb := RBACProject{
 		RBACProjectKeys: []string{"foo"},
 		All:             false,
@@ -27,11 +27,11 @@ func TestRbacProjectInvalidGroupAndUsers(t *testing.T) {
 		RBACGroupsIDs:   []int64{},
 		RBACUsersIDs:    []string{},
 	}
-	err := isValidRbacProject("myRule", rb)
+	err := isValidRBACProject("myRule", rb)
 	require.Error(t, err)
 	require.Contains(t, err.Error(), "rbac myRule: missing groups or users on project permission")
 }
-func TestRbacProjectInvalidProjectKeys(t *testing.T) {
+func TestRBACProjectInvalidProjectKeys(t *testing.T) {
 	rb := RBACProject{
 		RBACProjectKeys: []string{},
 		All:             false,
@@ -39,11 +39,11 @@ func TestRbacProjectInvalidProjectKeys(t *testing.T) {
 		RBACGroupsIDs:   []int64{1},
 		RBACUsersIDs:    []string{},
 	}
-	err := isValidRbacProject("myRule", rb)
+	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) {
+func TestRBACProjectEmptyRole(t *testing.T) {
 	rb := RBACProject{
 		RBACProjectKeys: []string{"foo"},
 		All:             false,
@@ -51,11 +51,11 @@ func TestRbacProjectEmptyRole(t *testing.T) {
 		RBACGroupsIDs:   []int64{1},
 		RBACUsersIDs:    []string{},
 	}
-	err := isValidRbacProject("myRule", rb)
+	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) {
+func TestRBACProjectInvalidAllAndListOfProject(t *testing.T) {
 	rb := RBACProject{
 		RBACProjectKeys: []string{"foo"},
 		All:             true,
@@ -63,7 +63,7 @@ func TestRbacProjectInvalidAllAndListOfProject(t *testing.T) {
 		RBACGroupsIDs:   []int64{1},
 		RBACUsersIDs:    []string{},
 	}
-	err := isValidRbacProject("myRule", rb)
+	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")
 }
diff --git a/tests/06_sc_workflow_with_artifactory.yml b/tests/06_sc_workflow_with_artifactory.yml
index 8692c1859..c9fd62894 100644
--- a/tests/06_sc_workflow_with_artifactory.yml
+++ b/tests/06_sc_workflow_with_artifactory.yml
@@ -23,10 +23,14 @@ testcases:
     info: "{{.result.systemout}}"
     assertions:
     - result.code ShouldEqual 0
-    - "result.systemout ShouldContainSubstring Workflowtest-artifactory-multiple-maturity #1 has been launched"
+    - "result.systemout ShouldContainSubstring has been launched"
+  - script:  {{.cdsctl}} -f {{.cdsctl.config}} workflow history {{.projectKey}} test-artifactory-multiple-maturity --format json # get the latest run number
+    vars:
+      runNumber:
+        from: result.systemoutjson.systemoutjson0.num
 - name: check_workflow_is_success
   steps:
-  - script: {{.cdsctl}} -f {{.cdsctl.config}} workflow status {{.projectKey}}  test-artifactory-multiple-maturity 1 --format json # check the workflow status
+  - script: {{.cdsctl}} -f {{.cdsctl.config}} workflow status {{.projectKey}}  test-artifactory-multiple-maturity {{.run_workflow.runNumber}} --format json # check the workflow status
     retry: 120
     delay: 5
     info: "{{.result.systemout}}"
diff --git a/tests/08_v2_analyze.yml b/tests/08_v2_analyze.yml
index 5d8c3ba8c..e986ab4a3 100644
--- a/tests/08_v2_analyze.yml
+++ b/tests/08_v2_analyze.yml
@@ -15,7 +15,7 @@ testcases:
   - script: gpg --import ./fixtures/gpg/key.priv
   - script: gpg --import ./fixtures/gpg/key.pub
   - script: 'curl -X POST {{.git.host}}/api/v1/user/gpg_keys -u "{{.git.user}}:{{.git.password}}" -H "Content-type: application/json" -d @./fixtures/gpg/key.pub.json'
-
+  - script: {{.cdsctl}} -f {{.cdsctl.config}} experimental rbac import ./fixtures/rbac/rbac_admin.yml --force
 
 - name: import / export vcs server
   steps:
@@ -73,9 +73,8 @@ testcases:
       delay: 2
       assertions:
         - result.code ShouldEqual 0
-        - 'result.systemout ShouldContainSubstring "status: Skipped"'
+        - 'result.systemout ShouldContainSubstring "status: Success"'
         - 'result.systemout ShouldContainSubstring "key_sign_id: E027B500E97E52E7"'
-        - result.systemout ShouldContainSubstring "doesn't have enough right on project ITCLIPRJVCS"
 
 - name: delete repository on project
   steps:
diff --git a/tests/fixtures/04SCWorkflowRunSimplePlugin/go.sum b/tests/fixtures/04SCWorkflowRunSimplePlugin/go.sum
index f70cf5c69..9132a168c 100644
--- a/tests/fixtures/04SCWorkflowRunSimplePlugin/go.sum
+++ b/tests/fixtures/04SCWorkflowRunSimplePlugin/go.sum
@@ -294,8 +294,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
@@ -668,8 +668,8 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/tests/fixtures/rbac/rbac_admin.yml b/tests/fixtures/rbac/rbac_admin.yml
new file mode 100644
index 000000000..b1cea20e7
--- /dev/null
+++ b/tests/fixtures/rbac/rbac_admin.yml
@@ -0,0 +1,8 @@
+name: perm-test
+projects:
+  - role: manage
+    projects: [ITCLIPRJVCS]
+    users: [cds.integration.tests.rw]
+globals:
+  - role: manage-permission
+    users: [cds.integration.tests.rw]