Skip to content

Commit

Permalink
Merge pull request #38 from salsadigitalauorg/feature/DEVOPS-280
Browse files Browse the repository at this point in the history
feat: Add check for permissions of a specific role.
  • Loading branch information
steveworley authored Nov 30, 2023
2 parents 3ad425c + 372a15e commit 3220c71
Show file tree
Hide file tree
Showing 6 changed files with 482 additions and 1 deletion.
24 changes: 23 additions & 1 deletion docs/src/config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ The following check types are available:
- [drupal-file-module](#drupal-file-module)
- [drupal-db-module](#drupal-db-module)
- [drupal-db-permissions](#drupal-db-permissions)
- [drupal-role-permissions](#drupal-role-permissions)
- [drupal-user-forbidden](#drupal-user-forbidden)
- [phpstan](#phpstan)
Expand Down Expand Up @@ -295,6 +296,28 @@ documentation coming soon...
### drupal-db-permissions
documentation coming soon...

### drupal-role-permissions
Checks for permissions of a specific role.

| Field | Default | Required | Description |
|------------------------| :-----: |:--------:|--------------------------------|
| rid | - | Yes | Role ID, eg. authenticated |
| required-permissions | - | No | List of required permissions |
| disallowed-permissions | - | No | List of disallowed permissions |

Examples:
```yaml
checks:
drupal-role-permissions:
- name: '[DATABASE] Authenticated role check'
severity: high
rid: 'authenticated'
required-permissions:
- 'setup own tfa'
disallowed-permissions:
- 'administer users'
```

### drupal-user-forbidden

Checks if a forbidden user is active.
Expand All @@ -312,7 +335,6 @@ checks:
- name: '[DATABASE] Active user 2 check'
severity: medium
uid: 2
```

### phpstan
Expand Down
1 change: 1 addition & 0 deletions pkg/checks/drupal/drupal.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func RegisterChecks() {
config.ChecksRegistry[FileModule] = func() config.Check { return &FileModuleCheck{} }
config.ChecksRegistry[DbModule] = func() config.Check { return &DbModuleCheck{} }
config.ChecksRegistry[DbPermissions] = func() config.Check { return &DbPermissionsCheck{} }
config.ChecksRegistry[RolePermissions] = func() config.Check { return &RolePermissionsCheck{} }
config.ChecksRegistry[TrackingCode] = func() config.Check { return &TrackingCodeCheck{} }
config.ChecksRegistry[UserRole] = func() config.Check { return &UserRoleCheck{} }
config.ChecksRegistry[AdminUser] = func() config.Check { return &AdminUserCheck{} }
Expand Down
122 changes: 122 additions & 0 deletions pkg/checks/drupal/rolepermissionscheck.go
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
}
}
176 changes: 176 additions & 0 deletions pkg/checks/drupal/rolepermissionscheck_test.go
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)
})
}
Loading

0 comments on commit 3220c71

Please sign in to comment.