mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
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:
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -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
32
coderd/rbac/error_test.go
Normal 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))
|
||||
})
|
||||
}
|
@ -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"
|
||||
|
Reference in New Issue
Block a user