feat: add auditing for groups (#4527)

- Clean up `database.TemplateACL` implementation.
This commit is contained in:
Jon Ayers 2022-10-19 02:00:45 -05:00 committed by GitHub
parent d4585fefb8
commit 0d1096da6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 405 additions and 213 deletions

View File

@ -15,7 +15,8 @@ type Auditable interface {
database.TemplateVersion |
database.User |
database.Workspace |
database.GitSSHKey
database.GitSSHKey |
database.Group
}
// Map is a map of changed fields in an audited resource. It maps field names to

View File

@ -45,6 +45,8 @@ func ResourceTarget[T Auditable](tgt T) string {
return typed.Name
case database.GitSSHKey:
return typed.PublicKey
case database.Group:
return typed.Name
default:
panic(fmt.Sprintf("unknown resource %T", tgt))
}
@ -64,6 +66,8 @@ func ResourceID[T Auditable](tgt T) uuid.UUID {
return typed.ID
case database.GitSSHKey:
return typed.UserID
case database.Group:
return typed.ID
default:
panic(fmt.Sprintf("unknown resource %T", tgt))
}
@ -83,6 +87,8 @@ func ResourceType[T Auditable](tgt T) database.ResourceType {
return database.ResourceTypeWorkspace
case database.GitSSHKey:
return database.ResourceTypeGitSshKey
case database.Group:
return database.ResourceTypeGroup
default:
panic(fmt.Sprintf("unknown resource %T", tgt))
}

View File

@ -1457,34 +1457,6 @@ func (q *fakeQuerier) GetTemplates(_ context.Context) ([]database.Template, erro
return templates, nil
}
func (q *fakeQuerier) UpdateTemplateUserACLByID(_ context.Context, id uuid.UUID, acl database.TemplateACL) error {
q.mutex.RLock()
defer q.mutex.RUnlock()
for i, t := range q.templates {
if t.ID == id {
t = t.SetUserACL(acl)
q.templates[i] = t
return nil
}
}
return sql.ErrNoRows
}
func (q *fakeQuerier) UpdateTemplateGroupACLByID(_ context.Context, id uuid.UUID, acl database.TemplateACL) error {
q.mutex.RLock()
defer q.mutex.RUnlock()
for i, t := range q.templates {
if t.ID == id {
t = t.SetGroupACL(acl)
q.templates[i] = t
return nil
}
}
return sql.ErrNoRows
}
func (q *fakeQuerier) GetTemplateUserRoles(_ context.Context, id uuid.UUID) ([]database.TemplateUser, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@ -1501,10 +1473,8 @@ func (q *fakeQuerier) GetTemplateUserRoles(_ context.Context, id uuid.UUID) ([]d
return nil, sql.ErrNoRows
}
acl := template.UserACL()
users := make([]database.TemplateUser, 0, len(acl))
for k, v := range acl {
users := make([]database.TemplateUser, 0, len(template.UserACL))
for k, v := range template.UserACL {
user, err := q.GetUserByID(context.Background(), uuid.MustParse(k))
if err != nil && xerrors.Is(err, sql.ErrNoRows) {
return nil, xerrors.Errorf("get user by ID: %w", err)
@ -1544,10 +1514,8 @@ func (q *fakeQuerier) GetTemplateGroupRoles(_ context.Context, id uuid.UUID) ([]
return nil, sql.ErrNoRows
}
acl := template.GroupACL()
groups := make([]database.TemplateGroup, 0, len(acl))
for k, v := range acl {
groups := make([]database.TemplateGroup, 0, len(template.GroupACL))
for k, v := range template.GroupACL {
group, err := q.GetGroupByID(context.Background(), uuid.MustParse(k))
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
return nil, xerrors.Errorf("get group by ID: %w", err)
@ -2047,11 +2015,9 @@ func (q *fakeQuerier) InsertTemplate(_ context.Context, arg database.InsertTempl
MaxTtl: arg.MaxTtl,
MinAutostartInterval: arg.MinAutostartInterval,
CreatedBy: arg.CreatedBy,
UserACL: arg.UserACL,
GroupACL: arg.GroupACL,
}
template = template.SetUserACL(database.TemplateACL{})
template = template.SetGroupACL(database.TemplateACL{
arg.OrganizationID.String(): []rbac.Action{rbac.ActionRead},
})
q.templates = append(q.templates, template)
return template, nil
}
@ -2470,6 +2436,23 @@ func (q *fakeQuerier) UpdateTemplateDeletedByID(_ context.Context, arg database.
return sql.ErrNoRows
}
func (q *fakeQuerier) UpdateTemplateACLByID(_ context.Context, arg database.UpdateTemplateACLByIDParams) (database.Template, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
for i, template := range q.templates {
if template.ID == arg.ID {
template.GroupACL = arg.GroupACL
template.UserACL = arg.UserACL
q.templates[i] = template
return template, nil
}
}
return database.Template{}, sql.ErrNoRows
}
func (q *fakeQuerier) UpdateTemplateVersionByID(_ context.Context, arg database.UpdateTemplateVersionByIDParams) error {
q.mutex.Lock()
defer q.mutex.Unlock()

View File

@ -24,3 +24,22 @@ func (a *Actions) Scan(src interface{}) error {
func (a *Actions) Value() (driver.Value, error) {
return json.Marshal(a)
}
// TemplateACL is a map of ids to permissions.
type TemplateACL map[string][]rbac.Action
func (t *TemplateACL) Scan(src interface{}) error {
switch v := src.(type) {
case string:
return json.Unmarshal([]byte(v), &t)
case []byte, json.RawMessage:
//nolint
return json.Unmarshal(v.([]byte), &t)
}
return xerrors.Errorf("unexpected type %T", src)
}
func (t TemplateACL) Value() (driver.Value, error) {
return json.Marshal(t)
}

View File

@ -87,7 +87,8 @@ CREATE TYPE resource_type AS ENUM (
'user',
'workspace',
'git_ssh_key',
'api_key'
'api_key',
'group'
);
CREATE TYPE user_status AS ENUM (

View File

@ -0,0 +1,2 @@
-- You cannot safely remove values from enums https://www.postgresql.org/docs/current/datatype-enum.html
-- You cannot create a new type and do a rename because objects depend on this type now.

View File

@ -0,0 +1,5 @@
BEGIN;
ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'group';
COMMIT;

View File

@ -1,65 +1,11 @@
package database
import (
"encoding/json"
"fmt"
"github.com/coder/coder/coderd/rbac"
)
const AllUsersGroup = "Everyone"
// TemplateACL is a map of user_ids to permissions.
type TemplateACL map[string][]rbac.Action
func (t Template) UserACL() TemplateACL {
var acl TemplateACL
if len(t.userACL) == 0 {
return acl
}
err := json.Unmarshal(t.userACL, &acl)
if err != nil {
panic(fmt.Sprintf("failed to unmarshal template.userACL: %v", err.Error()))
}
return acl
}
func (t Template) GroupACL() TemplateACL {
var acl TemplateACL
if len(t.groupACL) == 0 {
return acl
}
err := json.Unmarshal(t.groupACL, &acl)
if err != nil {
panic(fmt.Sprintf("failed to unmarshal template.userACL: %v", err.Error()))
}
return acl
}
func (t Template) SetGroupACL(acl TemplateACL) Template {
raw, err := json.Marshal(acl)
if err != nil {
panic(fmt.Sprintf("marshal user acl: %v", err))
}
t.groupACL = raw
return t
}
func (t Template) SetUserACL(acl TemplateACL) Template {
raw, err := json.Marshal(acl)
if err != nil {
panic(fmt.Sprintf("marshal user acl: %v", err))
}
t.userACL = raw
return t
}
func (s APIKeyScope) ToRBAC() rbac.Scope {
switch s {
case APIKeyScopeAll:
@ -74,8 +20,8 @@ func (s APIKeyScope) ToRBAC() rbac.Scope {
func (t Template) RBACObject() rbac.Object {
obj := rbac.ResourceTemplate
return obj.InOrg(t.OrganizationID).
WithACLUserList(t.UserACL()).
WithGroupACL(t.GroupACL())
WithACLUserList(t.UserACL).
WithGroupACL(t.GroupACL)
}
func (TemplateVersion) RBACObject(template Template) rbac.Object {

View File

@ -2,7 +2,6 @@ package database
import (
"context"
"encoding/json"
"fmt"
"strings"
@ -23,8 +22,6 @@ type customQuerier interface {
}
type templateQuerier interface {
UpdateTemplateUserACLByID(ctx context.Context, id uuid.UUID, acl TemplateACL) error
UpdateTemplateGroupACLByID(ctx context.Context, id uuid.UUID, acl TemplateACL) error
GetTemplateGroupRoles(ctx context.Context, id uuid.UUID) ([]TemplateGroup, error)
GetTemplateUserRoles(ctx context.Context, id uuid.UUID) ([]TemplateUser, error)
}
@ -34,28 +31,6 @@ type TemplateUser struct {
Actions Actions `db:"actions"`
}
func (q *sqlQuerier) UpdateTemplateUserACLByID(ctx context.Context, id uuid.UUID, acl TemplateACL) error {
raw, err := json.Marshal(acl)
if err != nil {
return xerrors.Errorf("marshal user acl: %w", err)
}
const query = `
UPDATE
templates
SET
user_acl = $2
WHERE
id = $1`
_, err = q.db.ExecContext(ctx, query, id.String(), raw)
if err != nil {
return xerrors.Errorf("update user acl: %w", err)
}
return nil
}
func (q *sqlQuerier) GetTemplateUserRoles(ctx context.Context, id uuid.UUID) ([]TemplateUser, error) {
const query = `
SELECT
@ -100,28 +75,6 @@ type TemplateGroup struct {
Actions Actions `db:"actions"`
}
func (q *sqlQuerier) UpdateTemplateGroupACLByID(ctx context.Context, id uuid.UUID, acl TemplateACL) error {
raw, err := json.Marshal(acl)
if err != nil {
return xerrors.Errorf("marshal user acl: %w", err)
}
const query = `
UPDATE
templates
SET
group_acl = $2
WHERE
id = $1`
_, err = q.db.ExecContext(ctx, query, id.String(), raw)
if err != nil {
return xerrors.Errorf("update user acl: %w", err)
}
return nil
}
func (q *sqlQuerier) GetTemplateGroupRoles(ctx context.Context, id uuid.UUID) ([]TemplateGroup, error) {
const query = `
SELECT

View File

@ -301,6 +301,7 @@ const (
ResourceTypeWorkspace ResourceType = "workspace"
ResourceTypeGitSshKey ResourceType = "git_ssh_key"
ResourceTypeApiKey ResourceType = "api_key"
ResourceTypeGroup ResourceType = "group"
)
func (e *ResourceType) Scan(src interface{}) error {
@ -573,8 +574,8 @@ type Template struct {
MinAutostartInterval int64 `db:"min_autostart_interval" json:"min_autostart_interval"`
CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
Icon string `db:"icon" json:"icon"`
userACL json.RawMessage `db:"user_acl" json:"user_acl"`
groupACL json.RawMessage `db:"group_acl" json:"group_acl"`
UserACL TemplateACL `db:"user_acl" json:"user_acl"`
GroupACL TemplateACL `db:"group_acl" json:"group_acl"`
}
type TemplateVersion struct {

View File

@ -162,6 +162,7 @@ type sqlcQuerier interface {
UpdateProvisionerJobWithCancelByID(ctx context.Context, arg UpdateProvisionerJobWithCancelByIDParams) error
UpdateProvisionerJobWithCompleteByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteByIDParams) error
UpdateReplica(ctx context.Context, arg UpdateReplicaParams) (Replica, error)
UpdateTemplateACLByID(ctx context.Context, arg UpdateTemplateACLByIDParams) (Template, error)
UpdateTemplateActiveVersionByID(ctx context.Context, arg UpdateTemplateActiveVersionByIDParams) error
UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTemplateDeletedByIDParams) error
UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) (Template, error)

View File

@ -2899,8 +2899,8 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
&i.MinAutostartInterval,
&i.CreatedBy,
&i.Icon,
&i.userACL,
&i.groupACL,
&i.UserACL,
&i.GroupACL,
)
return i, err
}
@ -2941,8 +2941,8 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
&i.MinAutostartInterval,
&i.CreatedBy,
&i.Icon,
&i.userACL,
&i.groupACL,
&i.UserACL,
&i.GroupACL,
)
return i, err
}
@ -2975,8 +2975,8 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
&i.MinAutostartInterval,
&i.CreatedBy,
&i.Icon,
&i.userACL,
&i.groupACL,
&i.UserACL,
&i.GroupACL,
); err != nil {
return nil, err
}
@ -3055,8 +3055,8 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate
&i.MinAutostartInterval,
&i.CreatedBy,
&i.Icon,
&i.userACL,
&i.groupACL,
&i.UserACL,
&i.GroupACL,
); err != nil {
return nil, err
}
@ -3085,10 +3085,12 @@ INSERT INTO
max_ttl,
min_autostart_interval,
created_by,
icon
icon,
user_acl,
group_acl
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl
`
type InsertTemplateParams struct {
@ -3104,6 +3106,8 @@ type InsertTemplateParams struct {
MinAutostartInterval int64 `db:"min_autostart_interval" json:"min_autostart_interval"`
CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
Icon string `db:"icon" json:"icon"`
UserACL TemplateACL `db:"user_acl" json:"user_acl"`
GroupACL TemplateACL `db:"group_acl" json:"group_acl"`
}
func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParams) (Template, error) {
@ -3120,6 +3124,8 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam
arg.MinAutostartInterval,
arg.CreatedBy,
arg.Icon,
arg.UserACL,
arg.GroupACL,
)
var i Template
err := row.Scan(
@ -3136,8 +3142,49 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam
&i.MinAutostartInterval,
&i.CreatedBy,
&i.Icon,
&i.userACL,
&i.groupACL,
&i.UserACL,
&i.GroupACL,
)
return i, err
}
const updateTemplateACLByID = `-- name: UpdateTemplateACLByID :one
UPDATE
templates
SET
group_acl = $1,
user_acl = $2
WHERE
id = $3
RETURNING
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl
`
type UpdateTemplateACLByIDParams struct {
GroupACL TemplateACL `db:"group_acl" json:"group_acl"`
UserACL TemplateACL `db:"user_acl" json:"user_acl"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateTemplateACLByID(ctx context.Context, arg UpdateTemplateACLByIDParams) (Template, error) {
row := q.db.QueryRowContext(ctx, updateTemplateACLByID, arg.GroupACL, arg.UserACL, arg.ID)
var i Template
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OrganizationID,
&i.Deleted,
&i.Name,
&i.Provisioner,
&i.ActiveVersionID,
&i.Description,
&i.MaxTtl,
&i.MinAutostartInterval,
&i.CreatedBy,
&i.Icon,
&i.UserACL,
&i.GroupACL,
)
return i, err
}
@ -3235,8 +3282,8 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl
&i.MinAutostartInterval,
&i.CreatedBy,
&i.Icon,
&i.userACL,
&i.groupACL,
&i.UserACL,
&i.GroupACL,
)
return i, err
}

View File

@ -68,10 +68,12 @@ INSERT INTO
max_ttl,
min_autostart_interval,
created_by,
icon
icon,
user_acl,
group_acl
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *;
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING *;
-- name: UpdateTemplateActiveVersionByID :exec
UPDATE
@ -106,6 +108,17 @@ WHERE
RETURNING
*;
-- name: UpdateTemplateACLByID :one
UPDATE
templates
SET
group_acl = $1,
user_acl = $2
WHERE
id = $3
RETURNING
*;
-- name: GetTemplateAverageBuildTime :one
WITH build_times AS (
SELECT

View File

@ -19,6 +19,12 @@ packages:
overrides:
- column: "users.rbac_roles"
go_type: "github.com/lib/pq.StringArray"
- column: "templates.user_acl"
go_type:
type: "TemplateACL"
- column: "templates.group_acl"
go_type:
type: "TemplateACL"
rename:
api_key: APIKey
@ -39,5 +45,5 @@ rename:
ip_addresses: IPAddresses
ids: IDs
jwt: JWT
user_acl: userACL
group_acl: groupACL
user_acl: UserACL
group_acl: GroupACL

View File

@ -257,6 +257,10 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
MaxTtl: int64(maxTTL),
MinAutostartInterval: int64(minAutostartInterval),
CreatedBy: apiKey.UserID,
UserACL: database.TemplateACL{},
GroupACL: database.TemplateACL{
organization.ID.String(): []rbac.Action{rbac.ActionRead},
},
})
if err != nil {
return xerrors.Errorf("insert template: %s", err)
@ -299,13 +303,6 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
}
}
err = tx.UpdateTemplateGroupACLByID(ctx, dbTemplate.ID, database.TemplateACL{
dbTemplate.OrganizationID.String(): []rbac.Action{rbac.ActionRead},
})
if err != nil {
return xerrors.Errorf("update template group acl: %w", err)
}
createdByNameMap, err := getCreatedByNamesByTemplateIDs(ctx, tx, []database.Template{dbTemplate})
if err != nil {
return xerrors.Errorf("get creator name: %w", err)
@ -683,6 +680,10 @@ func (api *API) autoImportTemplate(ctx context.Context, opts autoImportTemplateO
MaxTtl: int64(maxTTLDefault),
MinAutostartInterval: int64(minAutostartIntervalDefault),
CreatedBy: opts.userID,
UserACL: database.TemplateACL{},
GroupACL: database.TemplateACL{
opts.orgID.String(): []rbac.Action{rbac.ActionRead},
},
})
if err != nil {
return xerrors.Errorf("insert template: %w", err)
@ -718,13 +719,6 @@ func (api *API) autoImportTemplate(ctx context.Context, opts autoImportTemplateO
}
}
err = tx.UpdateTemplateGroupACLByID(ctx, template.ID, database.TemplateACL{
opts.orgID.String(): []rbac.Action{rbac.ActionRead},
})
if err != nil {
return xerrors.Errorf("update template group acl: %w", err)
}
return nil
})

View File

@ -21,6 +21,7 @@ const (
ResourceTypeWorkspace ResourceType = "workspace"
ResourceTypeGitSSHKey ResourceType = "git_ssh_key"
ResourceTypeAPIKey ResourceType = "api_key"
ResourceTypeGroup ResourceType = "group"
)
func (r ResourceType) FriendlyString() string {
@ -39,6 +40,8 @@ func (r ResourceType) FriendlyString() string {
return "git ssh key"
case ResourceTypeAPIKey:
return "api key"
case ResourceTypeGroup:
return "group"
default:
return "unknown"
}

View File

@ -8,6 +8,7 @@ import (
"github.com/google/uuid"
"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/database"
)
func structName(t reflect.Type) string {
@ -137,7 +138,8 @@ func convertDiffType(left, right any) (newLeft, newRight any, changed bool) {
}
return leftInt64Ptr, rightInt64Ptr, true
case database.TemplateACL:
return fmt.Sprintf("%+v", left), fmt.Sprintf("%+v", right), true
default:
return left, right, false
}

View File

@ -62,6 +62,8 @@ var AuditableResources = auditMap(map[any]map[string]Action{
"min_autostart_interval": ActionTrack,
"created_by": ActionTrack,
"is_private": ActionTrack,
"group_acl": ActionTrack,
"user_acl": ActionTrack,
},
&database.TemplateVersion{}: {
"id": ActionTrack,
@ -101,6 +103,12 @@ var AuditableResources = auditMap(map[any]map[string]Action{
"ttl": ActionTrack,
"last_used_at": ActionIgnore,
},
&database.Group{}: {
"id": ActionTrack,
"name": ActionTrack,
"organization_id": ActionTrack,
"avatar_url": ActionTrack,
},
})
// auditMap converts a map of struct pointers to a map of struct names as

View File

@ -14,6 +14,8 @@ import (
"github.com/coder/coder/cli/deployment"
"github.com/coder/coder/cryptorand"
"github.com/coder/coder/enterprise/audit"
"github.com/coder/coder/enterprise/audit/backends"
"github.com/coder/coder/enterprise/coderd"
"github.com/coder/coder/tailnet"
@ -48,6 +50,13 @@ func server() *cobra.Command {
}
options.DERPServer.SetMeshKey(meshKey)
if dflags.AuditLogging.Value {
options.Auditor = audit.NewAuditor(audit.DefaultFilter,
backends.NewPostgres(options.Database, true),
backends.NewSlog(options.Logger),
)
}
o := &coderd.Options{
AuditLogging: dflags.AuditLogging.Value,
BrowserOnly: dflags.BrowserOnly.Value,
@ -59,6 +68,7 @@ func server() *cobra.Command {
Options: options,
}
api, err := coderd.New(ctx, o)
if err != nil {
return nil, nil, err

View File

@ -22,8 +22,6 @@ import (
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/coderd/workspacequota"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/enterprise/audit"
"github.com/coder/coder/enterprise/audit/backends"
"github.com/coder/coder/enterprise/coderd/license"
"github.com/coder/coder/enterprise/derpmesh"
"github.com/coder/coder/enterprise/replicasync"
@ -241,11 +239,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
if changed, enabled := featureChanged(codersdk.FeatureAuditLog); changed {
auditor := agplaudit.NewNop()
if enabled {
auditor = audit.NewAuditor(
audit.DefaultFilter,
backends.NewPostgres(api.Database, true),
backends.NewSlog(api.Logger),
)
auditor = api.AGPL.Options.Auditor
}
api.AGPL.Auditor.Store(&auditor)
}

View File

@ -158,6 +158,9 @@ func TestAuditLogging(t *testing.T) {
t.Parallel()
client, _, api := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
AuditLogging: true,
Options: &coderdtest.Options{
Auditor: audit.NewAuditor(audit.DefaultFilter),
},
})
coderdtest.CreateFirstUser(t, client)
coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{

View File

@ -9,6 +9,7 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
@ -18,9 +19,17 @@ import (
func (api *API) postGroupByOrganization(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
org = httpmw.OrganizationParam(r)
ctx = r.Context()
org = httpmw.OrganizationParam(r)
auditor = api.AGPL.Auditor.Load()
aReq, commitAudit = audit.InitRequest[database.Group](rw, &audit.RequestParams{
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionCreate,
})
)
defer commitAudit()
if !api.Authorize(r, rbac.ActionCreate, rbac.ResourceGroup) {
http.NotFound(rw, r)
@ -55,15 +64,25 @@ func (api *API) postGroupByOrganization(rw http.ResponseWriter, r *http.Request)
httpapi.InternalServerError(rw, err)
return
}
aReq.New = group
httpapi.Write(ctx, rw, http.StatusCreated, convertGroup(group, nil))
}
func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
group = httpmw.GroupParam(r)
ctx = r.Context()
group = httpmw.GroupParam(r)
auditor = api.AGPL.Auditor.Load()
aReq, commitAudit = audit.InitRequest[database.Group](rw, &audit.RequestParams{
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionWrite,
})
)
defer commitAudit()
aReq.Old = group
if !api.Authorize(r, rbac.ActionUpdate, group) {
http.NotFound(rw, r)
@ -195,14 +214,25 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) {
return
}
aReq.New = group
httpapi.Write(ctx, rw, http.StatusOK, convertGroup(group, members))
}
func (api *API) deleteGroup(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
group = httpmw.GroupParam(r)
ctx = r.Context()
group = httpmw.GroupParam(r)
auditor = api.AGPL.Auditor.Load()
aReq, commitAudit = audit.InitRequest[database.Group](rw, &audit.RequestParams{
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionDelete,
})
)
defer commitAudit()
aReq.Old = group
if !api.Authorize(r, rbac.ActionDelete, group) {
httpapi.ResourceNotFound(rw)

View File

@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/require"
"k8s.io/utils/pointer"
"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
@ -39,6 +40,37 @@ func TestCreateGroup(t *testing.T) {
require.NotEqual(t, uuid.Nil.String(), group.ID.String())
})
t.Run("Audit", func(t *testing.T) {
t.Parallel()
auditor := audit.NewMock()
client := coderdenttest.New(t, &coderdenttest.Options{
AuditLogging: true,
Options: &coderdtest.Options{
IncludeProvisionerDaemon: true,
Auditor: auditor,
},
})
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
TemplateRBAC: true,
AuditLog: true,
})
ctx, _ := testutil.Context(t)
numLogs := len(auditor.AuditLogs)
group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "hi",
})
require.NoError(t, err)
numLogs++
require.Len(t, auditor.AuditLogs, numLogs)
require.Equal(t, database.AuditActionCreate, auditor.AuditLogs[numLogs-1].Action)
require.Equal(t, group.ID, auditor.AuditLogs[numLogs-1].ResourceID)
})
t.Run("Conflict", func(t *testing.T) {
t.Parallel()
@ -197,6 +229,43 @@ func TestPatchGroup(t *testing.T) {
require.Contains(t, group.Members, user4)
})
t.Run("Audit", func(t *testing.T) {
t.Parallel()
auditor := audit.NewMock()
client := coderdenttest.New(t, &coderdenttest.Options{
AuditLogging: true,
Options: &coderdtest.Options{
IncludeProvisionerDaemon: true,
Auditor: auditor,
},
})
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
TemplateRBAC: true,
AuditLog: true,
})
ctx, _ := testutil.Context(t)
group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "hi",
})
require.NoError(t, err)
numLogs := len(auditor.AuditLogs)
group, err = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
Name: "bye",
})
require.NoError(t, err)
numLogs++
require.Len(t, auditor.AuditLogs, numLogs)
require.Equal(t, database.AuditActionWrite, auditor.AuditLogs[numLogs-1].Action)
require.Equal(t, group.ID, auditor.AuditLogs[numLogs-1].ResourceID)
})
t.Run("NameConflict", func(t *testing.T) {
t.Parallel()
@ -547,6 +616,40 @@ func TestDeleteGroup(t *testing.T) {
require.Equal(t, http.StatusNotFound, cerr.StatusCode())
})
t.Run("Audit", func(t *testing.T) {
t.Parallel()
auditor := audit.NewMock()
client := coderdenttest.New(t, &coderdenttest.Options{
AuditLogging: true,
Options: &coderdtest.Options{
IncludeProvisionerDaemon: true,
Auditor: auditor,
},
})
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
TemplateRBAC: true,
AuditLog: true,
})
ctx, _ := testutil.Context(t)
group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "hi",
})
require.NoError(t, err)
numLogs := len(auditor.AuditLogs)
err = client.DeleteGroup(ctx, group.ID)
require.NoError(t, err)
numLogs++
require.Len(t, auditor.AuditLogs, numLogs)
require.Equal(t, database.AuditActionDelete, auditor.AuditLogs[numLogs-1].Action)
require.Equal(t, group.ID, auditor.AuditLogs[numLogs-1].ResourceID)
})
t.Run("allUsers", func(t *testing.T) {
t.Parallel()

View File

@ -10,6 +10,7 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
@ -18,8 +19,11 @@ import (
)
func (api *API) templateACL(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
template := httpmw.TemplateParam(r)
var (
ctx = r.Context()
template = httpmw.TemplateParam(r)
)
if !api.Authorize(r, rbac.ActionRead, template) {
httpapi.ResourceNotFound(rw)
return
@ -90,9 +94,18 @@ func (api *API) templateACL(rw http.ResponseWriter, r *http.Request) {
func (api *API) patchTemplateACL(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
template = httpmw.TemplateParam(r)
ctx = r.Context()
template = httpmw.TemplateParam(r)
auditor = api.AGPL.Auditor.Load()
aReq, commitAudit = audit.InitRequest[database.Template](rw, &audit.RequestParams{
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionWrite,
})
)
defer commitAudit()
aReq.Old = template
// Only users who are able to create templates (aka template admins)
// are able to control permissions.
@ -119,40 +132,43 @@ func (api *API) patchTemplateACL(rw http.ResponseWriter, r *http.Request) {
}
err := api.Database.InTx(func(tx database.Store) error {
var err error
template, err = tx.GetTemplateByID(ctx, template.ID)
if err != nil {
return xerrors.Errorf("get template by ID: %w", err)
}
if len(req.UserPerms) > 0 {
userACL := template.UserACL()
for id, role := range req.UserPerms {
// A user with an empty string implies
// deletion.
if role == "" {
delete(userACL, id)
delete(template.UserACL, id)
continue
}
userACL[id] = convertSDKTemplateRole(role)
}
err := tx.UpdateTemplateUserACLByID(r.Context(), template.ID, userACL)
if err != nil {
return xerrors.Errorf("update template user ACL: %w", err)
template.UserACL[id] = convertSDKTemplateRole(role)
}
}
if len(req.GroupPerms) > 0 {
groupACL := template.GroupACL()
for id, role := range req.GroupPerms {
// An id with an empty string implies
// deletion.
if role == "" {
delete(groupACL, id)
delete(template.GroupACL, id)
continue
}
groupACL[id] = convertSDKTemplateRole(role)
template.GroupACL[id] = convertSDKTemplateRole(role)
}
}
err := tx.UpdateTemplateGroupACLByID(ctx, template.ID, groupACL)
if err != nil {
return xerrors.Errorf("update template user ACL: %w", err)
}
template, err = tx.UpdateTemplateACLByID(ctx, database.UpdateTemplateACLByIDParams{
ID: template.ID,
UserACL: template.UserACL,
GroupACL: template.GroupACL,
})
if err != nil {
return xerrors.Errorf("update template ACL by ID: %w", err)
}
return nil
})
@ -161,6 +177,8 @@ func (api *API) patchTemplateACL(rw http.ResponseWriter, r *http.Request) {
return
}
aReq.New = template
httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{
Message: "Successfully updated template ACL list.",
})

View File

@ -8,7 +8,9 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/enterprise/coderd/coderdenttest"
"github.com/coder/coder/provisioner/echo"
@ -355,6 +357,46 @@ func TestUpdateTemplateACL(t *testing.T) {
require.Contains(t, acl.Users, templateUser3)
})
t.Run("Audit", func(t *testing.T) {
t.Parallel()
auditor := audit.NewMock()
client := coderdenttest.New(t, &coderdenttest.Options{
AuditLogging: true,
Options: &coderdtest.Options{
IncludeProvisionerDaemon: true,
Auditor: auditor,
},
})
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
TemplateRBAC: true,
AuditLog: true,
})
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
ctx, _ := testutil.Context(t)
numLogs := len(auditor.AuditLogs)
req := codersdk.UpdateTemplateACL{
GroupPerms: map[string]codersdk.TemplateRole{
user.OrganizationID.String(): codersdk.TemplateRoleDeleted,
},
}
err := client.UpdateTemplateACL(ctx, template.ID, req)
require.NoError(t, err)
numLogs++
require.Len(t, auditor.AuditLogs, numLogs)
require.Equal(t, database.AuditActionWrite, auditor.AuditLogs[numLogs-1].Action)
require.Equal(t, template.ID, auditor.AuditLogs[numLogs-1].ResourceID)
})
t.Run("DeleteUser", func(t *testing.T) {
t.Parallel()

2
go.mod
View File

@ -194,7 +194,7 @@ require (
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gin-gonic/gin v1.7.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/logr v1.2.3
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/locales v0.14.0 // indirect

View File

@ -940,6 +940,7 @@ export type ProvisionerType = "echo" | "terraform"
export type ResourceType =
| "api_key"
| "git_ssh_key"
| "group"
| "organization"
| "template"
| "template_version"