mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 19:17:58 +00:00
Postgres revocation sql, beta mode (#1972)
This commit is contained in:
@@ -262,7 +262,7 @@ func testAccStepRole(t *testing.T, wildCard bool) logicaltest.TestStep {
|
|||||||
} else {
|
} else {
|
||||||
pathData = map[string]interface{}{
|
pathData = map[string]interface{}{
|
||||||
"sql": testRoleHost,
|
"sql": testRoleHost,
|
||||||
"revoke_sql": testRevokeSQL,
|
"revocation_sql": testRevocationSQL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +362,7 @@ const testRoleHost = `
|
|||||||
CREATE USER '{{name}}'@'10.1.1.2' IDENTIFIED BY '{{password}}';
|
CREATE USER '{{name}}'@'10.1.1.2' IDENTIFIED BY '{{password}}';
|
||||||
GRANT SELECT ON *.* TO '{{name}}'@'10.1.1.2';
|
GRANT SELECT ON *.* TO '{{name}}'@'10.1.1.2';
|
||||||
`
|
`
|
||||||
const testRevokeSQL = `
|
const testRevocationSQL = `
|
||||||
REVOKE ALL PRIVILEGES, GRANT OPTION FROM '{{name}}'@'10.1.1.2';
|
REVOKE ALL PRIVILEGES, GRANT OPTION FROM '{{name}}'@'10.1.1.2';
|
||||||
DROP USER '{{name}}'@'10.1.1.2';
|
DROP USER '{{name}}'@'10.1.1.2';
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func pathRoles(b *backend) *framework.Path {
|
|||||||
Description: "SQL string to create a user. See help for more info.",
|
Description: "SQL string to create a user. See help for more info.",
|
||||||
},
|
},
|
||||||
|
|
||||||
"revoke_sql": {
|
"revocation_sql": {
|
||||||
Type: framework.TypeString,
|
Type: framework.TypeString,
|
||||||
Description: "SQL string to revoke a user. See help for more info.",
|
Description: "SQL string to revoke a user. See help for more info.",
|
||||||
},
|
},
|
||||||
@@ -118,7 +118,7 @@ func (b *backend) pathRoleRead(
|
|||||||
return &logical.Response{
|
return &logical.Response{
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"sql": role.SQL,
|
"sql": role.SQL,
|
||||||
"revoke_sql": role.RevokeSQL,
|
"revocation_sql": role.RevocationSQL,
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -165,7 +165,7 @@ func (b *backend) pathRoleCreate(
|
|||||||
// Store it
|
// Store it
|
||||||
entry, err := logical.StorageEntryJSON("role/"+name, &roleEntry{
|
entry, err := logical.StorageEntryJSON("role/"+name, &roleEntry{
|
||||||
SQL: sql,
|
SQL: sql,
|
||||||
RevokeSQL: data.Get("revoke_sql").(string),
|
RevocationSQL: data.Get("revocation_sql").(string),
|
||||||
UsernameLength: data.Get("username_length").(int),
|
UsernameLength: data.Get("username_length").(int),
|
||||||
DisplaynameLength: data.Get("displayname_length").(int),
|
DisplaynameLength: data.Get("displayname_length").(int),
|
||||||
RolenameLength: data.Get("rolename_length").(int),
|
RolenameLength: data.Get("rolename_length").(int),
|
||||||
@@ -181,7 +181,7 @@ func (b *backend) pathRoleCreate(
|
|||||||
|
|
||||||
type roleEntry struct {
|
type roleEntry struct {
|
||||||
SQL string `json:"sql" mapstructure:"sql" structs:"sql"`
|
SQL string `json:"sql" mapstructure:"sql" structs:"sql"`
|
||||||
RevokeSQL string `json:"revoke_sql" mapstructure:"revoke_sql" structs:"revoke_sql"`
|
RevocationSQL string `json:"revocation_sql" mapstructure:"revocation_sql" structs:"revocation_sql"`
|
||||||
UsernameLength int `json:"username_length" mapstructure:"username_length" structs:"username_length"`
|
UsernameLength int `json:"username_length" mapstructure:"username_length" structs:"username_length"`
|
||||||
DisplaynameLength int `json:"displayname_length" mapstructure:"displayname_length" structs:"displayname_length"`
|
DisplaynameLength int `json:"displayname_length" mapstructure:"displayname_length" structs:"displayname_length"`
|
||||||
RolenameLength int `json:"rolename_length" mapstructure:"rolename_length" structs:"rolename_length"`
|
RolenameLength int `json:"rolename_length" mapstructure:"rolename_length" structs:"rolename_length"`
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import (
|
|||||||
|
|
||||||
const SecretCredsType = "creds"
|
const SecretCredsType = "creds"
|
||||||
|
|
||||||
// defaultRevokeSQL is a default SQL statement for revoking a user. Revoking
|
// defaultRevocationSQL is a default SQL statement for revoking a user. Revoking
|
||||||
// permissions for the user is done before the drop, because MySQL explicitly
|
// permissions for the user is done before the drop, because MySQL explicitly
|
||||||
// documents that open user connections will not be closed. By revoking all
|
// documents that open user connections will not be closed. By revoking all
|
||||||
// grants, at least we ensure that the open connection is useless. Dropping the
|
// grants, at least we ensure that the open connection is useless. Dropping the
|
||||||
// user will only affect the next connection.
|
// user will only affect the next connection.
|
||||||
const defaultRevokeSQL = `
|
const defaultRevocationSQL = `
|
||||||
REVOKE ALL PRIVILEGES, GRANT OPTION FROM '{{name}}'@'%';
|
REVOKE ALL PRIVILEGES, GRANT OPTION FROM '{{name}}'@'%';
|
||||||
DROP USER '{{name}}'@'%'
|
DROP USER '{{name}}'@'%'
|
||||||
`
|
`
|
||||||
@@ -34,11 +34,6 @@ func secretCreds(b *backend) *framework.Secret {
|
|||||||
Type: framework.TypeString,
|
Type: framework.TypeString,
|
||||||
Description: "Password",
|
Description: "Password",
|
||||||
},
|
},
|
||||||
|
|
||||||
"role": &framework.FieldSchema{
|
|
||||||
Type: framework.TypeString,
|
|
||||||
Description: "Role",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
Renew: b.secretCredsRenew,
|
Renew: b.secretCredsRenew,
|
||||||
@@ -93,10 +88,10 @@ func (b *backend) secretCredsRevoke(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use a default SQL statement for revocation if one cannot be fetched from the role
|
// Use a default SQL statement for revocation if one cannot be fetched from the role
|
||||||
revokeSQL := defaultRevokeSQL
|
revocationSQL := defaultRevocationSQL
|
||||||
|
|
||||||
if role != nil && role.RevokeSQL != "" {
|
if role != nil && role.RevocationSQL != "" {
|
||||||
revokeSQL = role.RevokeSQL
|
revocationSQL = role.RevocationSQL
|
||||||
} else {
|
} else {
|
||||||
if resp == nil {
|
if resp == nil {
|
||||||
resp = &logical.Response{}
|
resp = &logical.Response{}
|
||||||
@@ -111,7 +106,7 @@ func (b *backend) secretCredsRevoke(
|
|||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
for _, query := range strutil.ParseArbitraryStringSlice(revokeSQL, ";") {
|
for _, query := range strutil.ParseArbitraryStringSlice(revocationSQL, ";") {
|
||||||
query = strings.TrimSpace(query)
|
query = strings.TrimSpace(query)
|
||||||
if len(query) == 0 {
|
if len(query) == 0 {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -233,6 +233,39 @@ func TestBackend_roleReadOnly(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBackend_roleReadOnly_revocationSQL(t *testing.T) {
|
||||||
|
config := logical.TestBackendConfig()
|
||||||
|
config.StorageView = &logical.InmemStorage{}
|
||||||
|
b, err := Factory(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cid, connURL := prepareTestContainer(t, config.StorageView, b)
|
||||||
|
if cid != "" {
|
||||||
|
defer cleanupTestContainer(t, cid)
|
||||||
|
}
|
||||||
|
connData := map[string]interface{}{
|
||||||
|
"connection_url": connURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
logicaltest.Test(t, logicaltest.TestCase{
|
||||||
|
Backend: b,
|
||||||
|
Steps: []logicaltest.TestStep{
|
||||||
|
testAccStepConfig(t, connData, false),
|
||||||
|
testAccStepCreateRoleWithRevocationSQL(t, "web", testRole, defaultRevocationSQL, false),
|
||||||
|
testAccStepCreateRoleWithRevocationSQL(t, "web-readonly", testReadOnlyRole, defaultRevocationSQL, false),
|
||||||
|
testAccStepReadRole(t, "web-readonly", testReadOnlyRole),
|
||||||
|
testAccStepCreateTable(t, b, config.StorageView, "web", connURL),
|
||||||
|
testAccStepReadCreds(t, b, config.StorageView, "web-readonly", connURL),
|
||||||
|
testAccStepDropTable(t, b, config.StorageView, "web", connURL),
|
||||||
|
testAccStepDeleteRole(t, "web-readonly"),
|
||||||
|
testAccStepDeleteRole(t, "web"),
|
||||||
|
testAccStepReadRole(t, "web-readonly", ""),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func testAccStepConfig(t *testing.T, d map[string]interface{}, expectError bool) logicaltest.TestStep {
|
func testAccStepConfig(t *testing.T, d map[string]interface{}, expectError bool) logicaltest.TestStep {
|
||||||
return logicaltest.TestStep{
|
return logicaltest.TestStep{
|
||||||
Operation: logical.UpdateOperation,
|
Operation: logical.UpdateOperation,
|
||||||
@@ -273,6 +306,18 @@ func testAccStepCreateRole(t *testing.T, name string, sql string, expectFail boo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testAccStepCreateRoleWithRevocationSQL(t *testing.T, name, sql, revocationSQL string, expectFail bool) logicaltest.TestStep {
|
||||||
|
return logicaltest.TestStep{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: path.Join("roles", name),
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"sql": sql,
|
||||||
|
"revocation_sql": revocationSQL,
|
||||||
|
},
|
||||||
|
ErrorOk: expectFail,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testAccStepDeleteRole(t *testing.T, name string) logicaltest.TestStep {
|
func testAccStepDeleteRole(t *testing.T, name string) logicaltest.TestStep {
|
||||||
return logicaltest.TestStep{
|
return logicaltest.TestStep{
|
||||||
Operation: logical.DeleteOperation,
|
Operation: logical.DeleteOperation,
|
||||||
@@ -341,6 +386,7 @@ func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, na
|
|||||||
InternalData: map[string]interface{}{
|
InternalData: map[string]interface{}{
|
||||||
"secret_type": "creds",
|
"secret_type": "creds",
|
||||||
"username": d.Username,
|
"username": d.Username,
|
||||||
|
"role": name,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -543,7 +589,8 @@ GRANT CONNECT ON DATABASE "postgres" TO "{{name}}";
|
|||||||
`
|
`
|
||||||
|
|
||||||
var testBlockStatementRoleSlice = []string{
|
var testBlockStatementRoleSlice = []string{
|
||||||
`DO $$
|
`
|
||||||
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
IF NOT EXISTS (SELECT * FROM pg_catalog.pg_roles WHERE rolname='foo-role') THEN
|
IF NOT EXISTS (SELECT * FROM pg_catalog.pg_roles WHERE rolname='foo-role') THEN
|
||||||
CREATE ROLE "foo-role";
|
CREATE ROLE "foo-role";
|
||||||
@@ -556,9 +603,18 @@ BEGIN
|
|||||||
GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA foo TO "foo-role";
|
GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA foo TO "foo-role";
|
||||||
END IF;
|
END IF;
|
||||||
END
|
END
|
||||||
$$`,
|
$$
|
||||||
|
`,
|
||||||
`CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';`,
|
`CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';`,
|
||||||
`GRANT "foo-role" TO "{{name}}";`,
|
`GRANT "foo-role" TO "{{name}}";`,
|
||||||
`ALTER ROLE "{{name}}" SET search_path = foo;`,
|
`ALTER ROLE "{{name}}" SET search_path = foo;`,
|
||||||
`GRANT CONNECT ON DATABASE "postgres" TO "{{name}}";`,
|
`GRANT CONNECT ON DATABASE "postgres" TO "{{name}}";`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultRevocationSQL = `
|
||||||
|
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM {{name}};
|
||||||
|
REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM {{name}};
|
||||||
|
REVOKE USAGE ON SCHEMA public FROM {{name}};
|
||||||
|
|
||||||
|
DROP ROLE IF EXISTS {{name}};
|
||||||
|
`
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ func (b *backend) pathRoleCreateRead(
|
|||||||
"password": password,
|
"password": password,
|
||||||
}, map[string]interface{}{
|
}, map[string]interface{}{
|
||||||
"username": username,
|
"username": username,
|
||||||
|
"role": name,
|
||||||
})
|
})
|
||||||
resp.Secret.TTL = lease.Lease
|
resp.Secret.TTL = lease.Lease
|
||||||
return resp, nil
|
return resp, nil
|
||||||
|
|||||||
@@ -26,15 +26,20 @@ func pathRoles(b *backend) *framework.Path {
|
|||||||
return &framework.Path{
|
return &framework.Path{
|
||||||
Pattern: "roles/" + framework.GenericNameRegex("name"),
|
Pattern: "roles/" + framework.GenericNameRegex("name"),
|
||||||
Fields: map[string]*framework.FieldSchema{
|
Fields: map[string]*framework.FieldSchema{
|
||||||
"name": &framework.FieldSchema{
|
"name": {
|
||||||
Type: framework.TypeString,
|
Type: framework.TypeString,
|
||||||
Description: "Name of the role.",
|
Description: "Name of the role.",
|
||||||
},
|
},
|
||||||
|
|
||||||
"sql": &framework.FieldSchema{
|
"sql": {
|
||||||
Type: framework.TypeString,
|
Type: framework.TypeString,
|
||||||
Description: "SQL string to create a user. See help for more info.",
|
Description: "SQL string to create a user. See help for more info.",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"revocation_sql": {
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: "SQL string to revoke a user. This is in beta; use with caution.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||||
@@ -85,11 +90,19 @@ func (b *backend) pathRoleRead(
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &logical.Response{
|
resp := &logical.Response{
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"sql": role.SQL,
|
"sql": role.SQL,
|
||||||
},
|
},
|
||||||
}, nil
|
}
|
||||||
|
|
||||||
|
// TODO: This is separate because this is in beta in 0.6.2, so we don't
|
||||||
|
// want it to show up in the general case.
|
||||||
|
if role.RevocationSQL != "" {
|
||||||
|
resp.Data["revocation_sql"] = role.RevocationSQL
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) pathRoleList(
|
func (b *backend) pathRoleList(
|
||||||
@@ -135,6 +148,7 @@ func (b *backend) pathRoleCreate(
|
|||||||
// Store it
|
// Store it
|
||||||
entry, err := logical.StorageEntryJSON("role/"+name, &roleEntry{
|
entry, err := logical.StorageEntryJSON("role/"+name, &roleEntry{
|
||||||
SQL: sql,
|
SQL: sql,
|
||||||
|
RevocationSQL: data.Get("revocation_sql").(string),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -147,7 +161,8 @@ func (b *backend) pathRoleCreate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
type roleEntry struct {
|
type roleEntry struct {
|
||||||
SQL string `json:"sql"`
|
SQL string `json:"sql" mapstructure:"sql" structs:"sql"`
|
||||||
|
RevocationSQL string `json:"revocation_sql" mapstructure:"revocation_sql" structs:"revocation_sql"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathRoleHelpSyn = `
|
const pathRoleHelpSyn = `
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package postgresql
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/helper/strutil"
|
||||||
"github.com/hashicorp/vault/logical"
|
"github.com/hashicorp/vault/logical"
|
||||||
"github.com/hashicorp/vault/logical/framework"
|
"github.com/hashicorp/vault/logical/framework"
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
@@ -91,12 +93,36 @@ func (b *backend) secretCredsRevoke(
|
|||||||
}
|
}
|
||||||
username, ok := usernameRaw.(string)
|
username, ok := usernameRaw.(string)
|
||||||
|
|
||||||
|
var revocationSQL string
|
||||||
|
var resp *logical.Response
|
||||||
|
|
||||||
|
roleNameRaw, ok := req.Secret.InternalData["role"]
|
||||||
|
if ok {
|
||||||
|
role, err := b.Role(req.Storage, roleNameRaw.(string))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if role == nil {
|
||||||
|
if resp == nil {
|
||||||
|
resp = &logical.Response{}
|
||||||
|
}
|
||||||
|
resp.AddWarning(fmt.Sprintf("Role %q cannot be found. Using default revocation SQL.", roleNameRaw.(string)))
|
||||||
|
} else {
|
||||||
|
revocationSQL = role.RevocationSQL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get our connection
|
// Get our connection
|
||||||
db, err := b.DB(req.Storage)
|
db, err := b.DB(req.Storage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch revocationSQL {
|
||||||
|
|
||||||
|
// This is the default revocation logic. If revocation SQL is provided it
|
||||||
|
// is simply executed as-is.
|
||||||
|
case "":
|
||||||
// Check if the role exists
|
// Check if the role exists
|
||||||
var exists bool
|
var exists bool
|
||||||
err = db.QueryRow("SELECT exists (SELECT rolname FROM pg_roles WHERE rolname=$1);", username).Scan(&exists)
|
err = db.QueryRow("SELECT exists (SELECT rolname FROM pg_roles WHERE rolname=$1);", username).Scan(&exists)
|
||||||
@@ -105,7 +131,7 @@ func (b *backend) secretCredsRevoke(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if exists == false {
|
if exists == false {
|
||||||
return nil, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query for permissions; we need to revoke permissions before we can drop
|
// Query for permissions; we need to revoke permissions before we can drop
|
||||||
@@ -206,5 +232,39 @@ func (b *backend) secretCredsRevoke(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
// We have revocation SQL, execute directly, within a transaction
|
||||||
|
default:
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
tx.Rollback()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, query := range strutil.ParseArbitraryStringSlice(revocationSQL, ";") {
|
||||||
|
query = strings.TrimSpace(query)
|
||||||
|
if len(query) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt, err := tx.Prepare(Query(query, map[string]string{
|
||||||
|
"name": pq.QuoteIdentifier(username),
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
if _, err := stmt.Exec(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,12 +248,20 @@ the default on versions prior to that.
|
|||||||
<li>
|
<li>
|
||||||
<span class="param">sql</span>
|
<span class="param">sql</span>
|
||||||
<span class="param-flags">required</span>
|
<span class="param-flags">required</span>
|
||||||
The SQL statements executed to create and configure the role. Must be
|
The SQL statements executed to create and configure a user. Must be a
|
||||||
a semicolon-separated string, a base64-encoded semicolon-separated
|
semicolon-separated string, a base64-encoded semicolon-separated
|
||||||
string, a serialized JSON string array, or a base64-encoded serialized
|
string, a serialized JSON string array, or a base64-encoded serialized
|
||||||
JSON string array. The '{{name}}' and '{{password}}' values will be
|
JSON string array. The '{{name}}' and '{{password}}' values will be
|
||||||
substituted.
|
substituted.
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="param">revocation_sql</span>
|
||||||
|
<span class="param-flags">optional</span>
|
||||||
|
The SQL statements executed to revoke a user. Must be a
|
||||||
|
semicolon-separated string, a base64-encoded semicolon-separated
|
||||||
|
string, a serialized JSON string array, or a base64-encoded serialized
|
||||||
|
JSON string array. The '{{name}}' value will be substituted.
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="param">rolename_length</span>
|
<span class="param">rolename_length</span>
|
||||||
<span class="param-flags">optional</span>
|
<span class="param-flags">optional</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user