-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #38 from salsadigitalauorg/feature/DEVOPS-280
feat: Add check for permissions of a specific role.
- Loading branch information
Showing
6 changed files
with
482 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package drupal | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"github.com/salsadigitalauorg/shipshape/pkg/config" | ||
"github.com/salsadigitalauorg/shipshape/pkg/result" | ||
"github.com/salsadigitalauorg/shipshape/pkg/utils" | ||
"io/fs" | ||
"os/exec" | ||
"strings" | ||
) | ||
|
||
const RolePermissions config.CheckType = "drupal-role-permissions" | ||
|
||
// RolePermissionsCheck checks the permissions of a role. | ||
type RolePermissionsCheck struct { | ||
config.CheckBase `yaml:",inline"` | ||
DrushCommand `yaml:",inline"` | ||
// The Role ID to check. | ||
RoleId string `yaml:"rid"` | ||
// List permissions the above role is required to have. | ||
RequiredPermissions []string `yaml:"required-permissions"` | ||
// List permissions the above role must not have. | ||
DisallowedPermissions []string `yaml:"disallowed-permissions"` | ||
} | ||
|
||
// Init implementation for the drush-based role permissions check. | ||
func (c *RolePermissionsCheck) Init(ct config.CheckType) { | ||
c.CheckBase.Init(ct) | ||
c.RequiresDb = true | ||
} | ||
|
||
// GetRolePermissions get the permissions of the role. | ||
func (c *RolePermissionsCheck) GetRolePermissions() []string { | ||
// Command: drush role:list --filter=id=anonymous --fields=perms --format=json | ||
cmd := []string{"role:list", "--filter=id=" + c.RoleId, "--fields=perms", "--format=json"} | ||
|
||
drushOutput, err := Drush(c.DrushPath, c.Alias, cmd).Exec() | ||
|
||
var pathError *fs.PathError | ||
if err != nil && errors.As(err, &pathError) { | ||
c.AddFail(pathError.Path + ": " + pathError.Err.Error()) | ||
c.AddBreach(result.ValueBreach{ | ||
Value: pathError.Path + ": " + pathError.Err.Error()}) | ||
} else if err != nil { | ||
msg := string(err.(*exec.ExitError).Stderr) | ||
c.AddFail(strings.ReplaceAll(strings.TrimSpace(msg), " \n ", "")) | ||
c.AddBreach(result.ValueBreach{ | ||
Value: strings.ReplaceAll(strings.TrimSpace(msg), " \n ", "")}) | ||
} else { | ||
// Unmarshal role:list JSON. | ||
// { | ||
// "anonymous": { | ||
// "perms": [ | ||
// "access content", | ||
// "search content", | ||
// "view media", | ||
// "view securitytxt" | ||
// ] | ||
// } | ||
//} | ||
rolePermissionsMap := map[string]map[string][]string{} | ||
err = json.Unmarshal(drushOutput, &rolePermissionsMap) | ||
var syntaxError *json.SyntaxError | ||
if err != nil && errors.As(err, &syntaxError) { | ||
c.AddFail(err.Error()) | ||
c.AddBreach(result.ValueBreach{Value: err.Error()}) | ||
} | ||
|
||
if len(rolePermissionsMap[c.RoleId]["perms"]) > 0 { | ||
return rolePermissionsMap[c.RoleId]["perms"] | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// HasData implementation for RolePermissionsCheck check. | ||
func (c *RolePermissionsCheck) HasData(failCheck bool) bool { | ||
return true | ||
} | ||
|
||
// Merge implementation for RolePermissionsCheck check. | ||
func (c *RolePermissionsCheck) Merge(mergeCheck config.Check) error { | ||
return nil | ||
} | ||
|
||
// RunCheck implements the Check logic for role permissions. | ||
func (c *RolePermissionsCheck) RunCheck() { | ||
if c.RoleId == "" { | ||
c.AddFail("no role ID provided") | ||
c.AddBreach(result.ValueBreach{Value: "no role ID provided"}) | ||
return | ||
} | ||
|
||
rolePermissions := c.GetRolePermissions() | ||
// Check for required permissions. | ||
diff := utils.StringSlicesInterdiffUnique(rolePermissions, c.RequiredPermissions) | ||
if len(diff) > 0 { | ||
c.AddFail(fmt.Sprintf("The role [%s] does not have all required permissions.", c.RoleId)) | ||
c.AddBreach(result.ValueBreach{ | ||
ValueLabel: "Missing permissions", | ||
Value: strings.Join(diff, ", "), | ||
}) | ||
} | ||
|
||
// Check for disallowed permissions. | ||
diff = utils.StringSlicesIntersectUnique(rolePermissions, c.DisallowedPermissions) | ||
if len(diff) > 0 { | ||
c.AddFail(fmt.Sprintf("The role [%s] has disallowed permissions.", c.RoleId)) | ||
c.AddBreach(result.ValueBreach{ | ||
ValueLabel: "Disallowed permissions", | ||
Value: strings.Join(diff, ", ")}, | ||
) | ||
} | ||
|
||
if len(c.Result.Failures) == 0 { | ||
c.Result.Status = result.Pass | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
package drupal_test | ||
|
||
import ( | ||
"github.com/salsadigitalauorg/shipshape/pkg/checks/drupal" | ||
"github.com/salsadigitalauorg/shipshape/pkg/command" | ||
"github.com/salsadigitalauorg/shipshape/pkg/internal" | ||
"github.com/salsadigitalauorg/shipshape/pkg/result" | ||
"github.com/stretchr/testify/assert" | ||
"os/exec" | ||
"testing" | ||
) | ||
|
||
func TestRolePermissionsCheck_Init(t *testing.T) { | ||
c := drupal.RolePermissionsCheck{ | ||
RoleId: "authenticated", | ||
RequiredPermissions: []string{"setup own tfa"}, | ||
DisallowedPermissions: []string{}, | ||
} | ||
c.Init(drupal.RolePermissions) | ||
assert.True(t, c.RequiresDb) | ||
assert.Equal(t, "authenticated", c.RoleId) | ||
} | ||
|
||
func TestRolePermissionsCheck_Merge(t *testing.T) { | ||
c := drupal.RolePermissionsCheck{} | ||
c.Init(drupal.RolePermissions) | ||
assert.Nil(t, c.Merge(&c)) | ||
} | ||
|
||
func TestRolePermissionsCheck_HasData(t *testing.T) { | ||
c := drupal.RolePermissionsCheck{} | ||
c.Init(drupal.RolePermissions) | ||
assert.True(t, c.HasData(true)) | ||
} | ||
|
||
func TestRolePermissionsCheck_RunCheck(t *testing.T) { | ||
assertions := assert.New(t) | ||
curShellCommander := command.ShellCommander | ||
defer func() { command.ShellCommander = curShellCommander }() | ||
|
||
t.Run("failOnNoRoleProvided", func(t *testing.T) { | ||
c := drupal.RolePermissionsCheck{} | ||
c.RunCheck() | ||
assertions.Equal(result.Fail, c.Result.Status) | ||
assertions.EqualValues([]string{"no role ID provided"}, c.Result.Failures) | ||
}) | ||
|
||
t.Run("failOnDrushNotFound", func(t *testing.T) { | ||
c := drupal.RolePermissionsCheck{ | ||
RoleId: "authenticated", | ||
} | ||
c.RunCheck() | ||
assertions.Equal(result.Fail, c.Result.Status) | ||
assertions.EqualValues([]string{"vendor/drush/drush/drush: no such file or directory"}, c.Result.Failures) | ||
}) | ||
|
||
t.Run("failOnDrushError", func(t *testing.T) { | ||
c := drupal.RolePermissionsCheck{ | ||
RoleId: "authenticated", | ||
} | ||
c.Init(drupal.RolePermissions) | ||
assertions.True(c.RequiresDb) | ||
|
||
command.ShellCommander = internal.ShellCommanderMaker( | ||
nil, | ||
&exec.ExitError{Stderr: []byte("Unexpected error")}, | ||
nil, | ||
) | ||
c.RunCheck() | ||
assertions.Empty(c.Result.Passes) | ||
assertions.ElementsMatch( | ||
[]string{"Unexpected error"}, | ||
c.Result.Failures, | ||
) | ||
}) | ||
|
||
t.Run("failOnDrushInvalidResponse", func(t *testing.T) { | ||
c := drupal.RolePermissionsCheck{ | ||
RoleId: "authenticated", | ||
} | ||
c.Init(drupal.RolePermissions) | ||
assertions.True(c.RequiresDb) | ||
|
||
stdout := "Unexpected error" | ||
command.ShellCommander = internal.ShellCommanderMaker( | ||
&stdout, | ||
nil, | ||
nil, | ||
) | ||
c.RunCheck() | ||
assertions.Equal(result.Fail, c.Result.Status) | ||
assertions.Empty(c.Result.Passes) | ||
assertions.ElementsMatch( | ||
[]string{"invalid character 'U' looking for beginning of value"}, | ||
c.Result.Failures, | ||
) | ||
}) | ||
|
||
t.Run("failOnPermissions", func(t *testing.T) { | ||
c := drupal.RolePermissionsCheck{ | ||
RoleId: "authenticated", | ||
RequiredPermissions: []string{"setup own tfa"}, | ||
DisallowedPermissions: []string{"administer users"}, | ||
} | ||
c.Init(drupal.RolePermissions) | ||
assertions.True(c.RequiresDb) | ||
|
||
stdout := ` | ||
{ | ||
"authenticated": { | ||
"perms": [ | ||
"access content", | ||
"administer users", | ||
"opt-in or out of google analytics tracking", | ||
"search content", | ||
"use text format webform_default", | ||
"view media", | ||
"view securitytxt" | ||
] | ||
} | ||
} | ||
` | ||
command.ShellCommander = internal.ShellCommanderMaker( | ||
&stdout, | ||
nil, | ||
nil, | ||
) | ||
c.RunCheck() | ||
assertions.Equal(result.Fail, c.Result.Status) | ||
assertions.Empty(c.Result.Passes) | ||
assertions.ElementsMatch( | ||
[]string{ | ||
"The role [authenticated] does not have all required permissions.", | ||
"The role [authenticated] has disallowed permissions.", | ||
}, | ||
c.Result.Failures, | ||
) | ||
assertions.Equal("Missing permissions", (c.Result.Breaches[0]).(result.ValueBreach).ValueLabel) | ||
assertions.Equal("setup own tfa", (c.Result.Breaches[0]).(result.ValueBreach).Value) | ||
assertions.Equal("Disallowed permissions", (c.Result.Breaches[1]).(result.ValueBreach).ValueLabel) | ||
assertions.Equal("administer users", (c.Result.Breaches[1]).(result.ValueBreach).Value) | ||
}) | ||
|
||
t.Run("passOnPermissions", func(t *testing.T) { | ||
c := drupal.RolePermissionsCheck{ | ||
RoleId: "authenticated", | ||
RequiredPermissions: []string{"setup own tfa"}, | ||
} | ||
c.Init(drupal.RolePermissions) | ||
assertions.True(c.RequiresDb) | ||
|
||
stdout := ` | ||
{ | ||
"authenticated": { | ||
"perms": [ | ||
"access content", | ||
"opt-in or out of google analytics tracking", | ||
"search content", | ||
"setup own tfa", | ||
"use text format webform_default", | ||
"view media", | ||
"view securitytxt" | ||
] | ||
} | ||
} | ||
` | ||
command.ShellCommander = internal.ShellCommanderMaker( | ||
&stdout, | ||
nil, | ||
nil, | ||
) | ||
c.RunCheck() | ||
assertions.Equal(result.Pass, c.Result.Status) | ||
assertions.Empty(c.Result.Failures) | ||
}) | ||
} |
Oops, something went wrong.