feat: Add initial AuthzQuerier implementation (#5919)

feat: Add initial AuthzQuerier implementation
- Adds package database/dbauthz that adds a database.Store implementation where each method goes through AuthZ checks
- Implements all database.Store methods on AuthzQuerier
- Updates and fixes unit tests where required
- Updates coderd initialization to use AuthzQuerier if codersdk.ExperimentAuthzQuerier is enabled
This commit is contained in:
Steven Masley
2023-02-14 08:27:06 -06:00
committed by GitHub
parent ebdfdc749d
commit 6fb8aff6d0
59 changed files with 5013 additions and 136 deletions

View File

@ -1039,7 +1039,6 @@ func testAuthorize(t *testing.T, name string, subject Subject, sets ...[]authTes
}
}
func must[T any](value T, err error) T {
if err != nil {
panic(err)

View File

@ -133,6 +133,8 @@ var (
ResourceWorkspace.Type: {ActionRead},
// CRUD to provisioner daemons for now.
ResourceProvisionerDaemon.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
// Needs to read all organizations since
ResourceOrganization.Type: {ActionRead},
}),
Org: map[string][]Permission{},
User: []Permission{},
@ -217,6 +219,12 @@ var (
// The first key is the actor role, the second is the roles they can assign.
// map[actor_role][assign_role]<can_assign>
assignRoles = map[string]map[string]bool{
"system": {
owner: true,
member: true,
orgAdmin: true,
orgMember: true,
},
owner: {
owner: true,
auditor: true,

View File

@ -10,7 +10,7 @@ import (
// BenchmarkRBACValueAllocation benchmarks the cost of allocating a rego input
// value. By default, `ast.InterfaceToValue` is used to convert the input,
// which uses json marshalling under the hood.
// which uses json marshaling under the hood.
//
// Currently ast.Object.insert() is the slowest part of the process and allocates
// the most amount of bytes. This general approach copies all of our struct

View File

@ -19,6 +19,7 @@ type authSubject struct {
Actor rbac.Subject
}
// TODO: add the SYSTEM to the MATRIX
func TestRolePermissions(t *testing.T) {
t.Parallel()
@ -183,8 +184,8 @@ func TestRolePermissions(t *testing.T) {
Actions: []rbac.Action{rbac.ActionRead},
Resource: rbac.ResourceOrganization.WithID(orgID).InOrg(orgID),
AuthorizeMap: map[bool][]authSubject{
true: {owner, orgAdmin, orgMemberMe},
false: {otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
true: {owner, orgAdmin, orgMemberMe, templateAdmin},
false: {otherOrgAdmin, otherOrgMember, memberMe, userAdmin},
},
},
{

View File

@ -1,6 +1,10 @@
package rbac
import "github.com/open-policy-agent/opa/rego"
import (
"errors"
"github.com/open-policy-agent/opa/rego"
)
const (
// errUnauthorized is the error message that should be returned to
@ -24,6 +28,12 @@ type UnauthorizedError struct {
output rego.ResultSet
}
// IsUnauthorizedError is a convenience function to check if err is UnauthorizedError.
// It is equivalent to errors.As(err, &UnauthorizedError{}).
func IsUnauthorizedError(err error) bool {
return errors.As(err, &UnauthorizedError{})
}
// ForbiddenWithInternal creates a new error that will return a simple
// "forbidden" to the client, logging internally the more detailed message
// provided.
@ -37,6 +47,10 @@ func ForbiddenWithInternal(internal error, subject Subject, action Action, objec
}
}
func (e UnauthorizedError) Unwrap() error {
return e.internal
}
// Error implements the error interface.
func (UnauthorizedError) Error() string {
return errUnauthorized
@ -47,6 +61,10 @@ func (e *UnauthorizedError) Internal() error {
return e.internal
}
func (e *UnauthorizedError) SetInternal(err error) {
e.internal = err
}
func (e *UnauthorizedError) Input() map[string]interface{} {
return map[string]interface{}{
"subject": e.subject,
@ -59,3 +77,11 @@ func (e *UnauthorizedError) Input() map[string]interface{} {
func (e *UnauthorizedError) Output() rego.ResultSet {
return e.output
}
// As implements the errors.As interface.
func (*UnauthorizedError) As(target interface{}) bool {
if _, ok := target.(*UnauthorizedError); ok {
return true
}
return false
}

32
coderd/rbac/error_test.go Normal file
View File

@ -0,0 +1,32 @@
package rbac_test
import (
"testing"
"github.com/coder/coder/coderd/rbac"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
)
func TestIsUnauthorizedError(t *testing.T) {
t.Parallel()
t.Run("NotWrapped", func(t *testing.T) {
t.Parallel()
errFunc := func() error {
return rbac.UnauthorizedError{}
}
err := errFunc()
require.True(t, rbac.IsUnauthorizedError(err))
})
t.Run("Wrapped", func(t *testing.T) {
t.Parallel()
errFunc := func() error {
return xerrors.Errorf("test error: %w", rbac.UnauthorizedError{})
}
err := errFunc()
require.True(t, rbac.IsUnauthorizedError(err))
})
}

View File

@ -3,6 +3,8 @@ package rbac
import (
"fmt"
"github.com/google/uuid"
"golang.org/x/xerrors"
)
@ -41,6 +43,29 @@ func (s Scope) Name() string {
return s.Role.Name
}
// WorkspaceAgentScope returns a scope that is the same as ScopeAll but can only
// affect resources in the allow list. Only a scope is returned as the roles
// should come from the workspace owner.
func WorkspaceAgentScope(workspaceID, ownerID uuid.UUID) Scope {
allScope, err := ScopeAll.Expand()
if err != nil {
panic("failed to expand scope all, this should never happen")
}
return Scope{
// TODO: We want to limit the role too to be extra safe.
// Even though the allowlist blocks anything else, it is still good
// incase we change the behavior of the allowlist. The allowlist is new
// and evolving.
Role: allScope.Role,
// This prevents the agent from being able to access any other resource.
AllowIDList: []string{
workspaceID.String(),
ownerID.String(),
// TODO: Might want to include the template the workspace uses too?
},
}
}
const (
ScopeAll ScopeName = "all"
ScopeApplicationConnect ScopeName = "application_connect"