Skip to content

Commit dd161b1

Browse files
authored
feat: allow auditors to read template insights (#10860)
- Adds a template_insights pseudo-resource - Grants auditor and template admin roles read access on template_insights - Updates existing RBAC checks to check for read template_insights, falling back to template update permissions where necessary - Updates TemplateLayout to show Insights tab if can read template_insights or can update template
1 parent e73901c commit dd161b1

File tree

11 files changed

+186
-77
lines changed

11 files changed

+186
-77
lines changed

coderd/apidoc/docs.go

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbauthz/dbauthz.go

Lines changed: 92 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,26 +1294,31 @@ func (q *querier) GetTailnetTunnelPeerIDs(ctx context.Context, srcID uuid.UUID)
12941294
}
12951295

12961296
func (q *querier) GetTemplateAppInsights(ctx context.Context, arg database.GetTemplateAppInsightsParams) ([]database.GetTemplateAppInsightsRow, error) {
1297-
for _, templateID := range arg.TemplateIDs {
1298-
template, err := q.db.GetTemplateByID(ctx, templateID)
1299-
if err != nil {
1300-
return nil, err
1301-
}
1297+
// Used by TemplateAppInsights endpoint
1298+
// For auditors, check read template_insights, and fall back to update template.
1299+
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTemplateInsights); IsNotAuthorizedError(err) {
1300+
for _, templateID := range arg.TemplateIDs {
1301+
template, err := q.db.GetTemplateByID(ctx, templateID)
1302+
if err != nil {
1303+
return nil, err
1304+
}
13021305

1303-
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
1304-
return nil, err
1306+
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
1307+
return nil, err
1308+
}
13051309
}
1306-
}
1307-
if len(arg.TemplateIDs) == 0 {
1308-
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
1309-
return nil, err
1310+
if len(arg.TemplateIDs) == 0 {
1311+
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
1312+
return nil, err
1313+
}
13101314
}
13111315
}
13121316
return q.db.GetTemplateAppInsights(ctx, arg)
13131317
}
13141318

13151319
func (q *querier) GetTemplateAppInsightsByTemplate(ctx context.Context, arg database.GetTemplateAppInsightsByTemplateParams) ([]database.GetTemplateAppInsightsByTemplateRow, error) {
1316-
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
1320+
// Only used by prometheus metrics, so we don't strictly need to check update template perms.
1321+
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTemplateInsights); err != nil {
13171322
return nil, err
13181323
}
13191324
return q.db.GetTemplateAppInsightsByTemplate(ctx, arg)
@@ -1344,64 +1349,77 @@ func (q *querier) GetTemplateDAUs(ctx context.Context, arg database.GetTemplateD
13441349
}
13451350

13461351
func (q *querier) GetTemplateInsights(ctx context.Context, arg database.GetTemplateInsightsParams) (database.GetTemplateInsightsRow, error) {
1347-
for _, templateID := range arg.TemplateIDs {
1348-
template, err := q.db.GetTemplateByID(ctx, templateID)
1349-
if err != nil {
1350-
return database.GetTemplateInsightsRow{}, err
1351-
}
1352+
// Used by TemplateInsights endpoint
1353+
// For auditors, check read template_insights, and fall back to update template.
1354+
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTemplateInsights); IsNotAuthorizedError(err) {
1355+
for _, templateID := range arg.TemplateIDs {
1356+
template, err := q.db.GetTemplateByID(ctx, templateID)
1357+
if err != nil {
1358+
return database.GetTemplateInsightsRow{}, err
1359+
}
13521360

1353-
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
1354-
return database.GetTemplateInsightsRow{}, err
1361+
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
1362+
return database.GetTemplateInsightsRow{}, err
1363+
}
13551364
}
1356-
}
1357-
if len(arg.TemplateIDs) == 0 {
1358-
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
1359-
return database.GetTemplateInsightsRow{}, err
1365+
if len(arg.TemplateIDs) == 0 {
1366+
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
1367+
return database.GetTemplateInsightsRow{}, err
1368+
}
13601369
}
13611370
}
13621371
return q.db.GetTemplateInsights(ctx, arg)
13631372
}
13641373

13651374
func (q *querier) GetTemplateInsightsByInterval(ctx context.Context, arg database.GetTemplateInsightsByIntervalParams) ([]database.GetTemplateInsightsByIntervalRow, error) {
1366-
for _, templateID := range arg.TemplateIDs {
1367-
template, err := q.db.GetTemplateByID(ctx, templateID)
1368-
if err != nil {
1369-
return nil, err
1370-
}
1375+
// Used by TemplateInsights endpoint
1376+
// For auditors, check read template_insights, and fall back to update template.
1377+
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTemplateInsights); IsNotAuthorizedError(err) {
1378+
for _, templateID := range arg.TemplateIDs {
1379+
template, err := q.db.GetTemplateByID(ctx, templateID)
1380+
if err != nil {
1381+
return nil, err
1382+
}
13711383

1372-
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
1373-
return nil, err
1384+
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
1385+
return nil, err
1386+
}
13741387
}
1375-
}
1376-
if len(arg.TemplateIDs) == 0 {
1377-
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
1378-
return nil, err
1388+
if len(arg.TemplateIDs) == 0 {
1389+
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
1390+
return nil, err
1391+
}
13791392
}
13801393
}
13811394
return q.db.GetTemplateInsightsByInterval(ctx, arg)
13821395
}
13831396

13841397
func (q *querier) GetTemplateInsightsByTemplate(ctx context.Context, arg database.GetTemplateInsightsByTemplateParams) ([]database.GetTemplateInsightsByTemplateRow, error) {
1385-
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
1398+
// Only used by prometheus metrics collector. No need to check update template perms.
1399+
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTemplateInsights); err != nil {
13861400
return nil, err
13871401
}
13881402
return q.db.GetTemplateInsightsByTemplate(ctx, arg)
13891403
}
13901404

13911405
func (q *querier) GetTemplateParameterInsights(ctx context.Context, arg database.GetTemplateParameterInsightsParams) ([]database.GetTemplateParameterInsightsRow, error) {
1392-
for _, templateID := range arg.TemplateIDs {
1393-
template, err := q.db.GetTemplateByID(ctx, templateID)
1394-
if err != nil {
1395-
return nil, err
1396-
}
1406+
// Used by both insights endpoint and prometheus collector.
1407+
// For auditors, check read template_insights, and fall back to update template.
1408+
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTemplateInsights); IsNotAuthorizedError(err) {
1409+
for _, templateID := range arg.TemplateIDs {
1410+
template, err := q.db.GetTemplateByID(ctx, templateID)
1411+
if err != nil {
1412+
return nil, err
1413+
}
13971414

1398-
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
1399-
return nil, err
1415+
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
1416+
return nil, err
1417+
}
14001418
}
1401-
}
1402-
if len(arg.TemplateIDs) == 0 {
1403-
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
1404-
return nil, err
1419+
if len(arg.TemplateIDs) == 0 {
1420+
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
1421+
return nil, err
1422+
}
14051423
}
14061424
}
14071425
return q.db.GetTemplateParameterInsights(ctx, arg)
@@ -1559,19 +1577,22 @@ func (q *querier) GetUnexpiredLicenses(ctx context.Context) ([]database.License,
15591577
}
15601578

15611579
func (q *querier) GetUserActivityInsights(ctx context.Context, arg database.GetUserActivityInsightsParams) ([]database.GetUserActivityInsightsRow, error) {
1562-
for _, templateID := range arg.TemplateIDs {
1563-
template, err := q.db.GetTemplateByID(ctx, templateID)
1564-
if err != nil {
1565-
return nil, err
1566-
}
1580+
// Used by insights endpoints. Need to check both for auditors and for regular users with template acl perms.
1581+
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTemplateInsights); IsNotAuthorizedError(err) {
1582+
for _, templateID := range arg.TemplateIDs {
1583+
template, err := q.db.GetTemplateByID(ctx, templateID)
1584+
if err != nil {
1585+
return nil, err
1586+
}
15671587

1568-
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
1569-
return nil, err
1588+
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
1589+
return nil, err
1590+
}
15701591
}
1571-
}
1572-
if len(arg.TemplateIDs) == 0 {
1573-
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
1574-
return nil, err
1592+
if len(arg.TemplateIDs) == 0 {
1593+
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
1594+
return nil, err
1595+
}
15751596
}
15761597
}
15771598
return q.db.GetUserActivityInsights(ctx, arg)
@@ -1593,19 +1614,22 @@ func (q *querier) GetUserCount(ctx context.Context) (int64, error) {
15931614
}
15941615

15951616
func (q *querier) GetUserLatencyInsights(ctx context.Context, arg database.GetUserLatencyInsightsParams) ([]database.GetUserLatencyInsightsRow, error) {
1596-
for _, templateID := range arg.TemplateIDs {
1597-
template, err := q.db.GetTemplateByID(ctx, templateID)
1598-
if err != nil {
1599-
return nil, err
1600-
}
1617+
// Used by insights endpoints. Need to check both for auditors and for regular users with template acl perms.
1618+
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTemplateInsights); IsNotAuthorizedError(err) {
1619+
for _, templateID := range arg.TemplateIDs {
1620+
template, err := q.db.GetTemplateByID(ctx, templateID)
1621+
if err != nil {
1622+
return nil, err
1623+
}
16011624

1602-
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
1603-
return nil, err
1625+
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
1626+
return nil, err
1627+
}
16041628
}
1605-
}
1606-
if len(arg.TemplateIDs) == 0 {
1607-
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
1608-
return nil, err
1629+
if len(arg.TemplateIDs) == 0 {
1630+
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
1631+
return nil, err
1632+
}
16091633
}
16101634
}
16111635
return q.db.GetUserLatencyInsights(ctx, arg)

coderd/rbac/object.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,11 @@ var (
193193
ResourceTailnetCoordinator = Object{
194194
Type: "tailnet_coordinator",
195195
}
196+
197+
// ResourceTemplateInsights is a pseudo-resource for reading template insights data.
198+
ResourceTemplateInsights = Object{
199+
Type: "template_insights",
200+
}
196201
)
197202

198203
// ResourceUserObject is a helper function to create a user object for authz checks.

coderd/rbac/object_gen.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/rbac/roles.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,11 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
165165
Site: Permissions(map[string][]Action{
166166
// Should be able to read all template details, even in orgs they
167167
// are not in.
168-
ResourceTemplate.Type: {ActionRead},
169-
ResourceAuditLog.Type: {ActionRead},
170-
ResourceUser.Type: {ActionRead},
171-
ResourceGroup.Type: {ActionRead},
168+
ResourceTemplate.Type: {ActionRead},
169+
ResourceTemplateInsights.Type: {ActionRead},
170+
ResourceAuditLog.Type: {ActionRead},
171+
ResourceUser.Type: {ActionRead},
172+
ResourceGroup.Type: {ActionRead},
172173
// Allow auditors to query deployment stats and insights.
173174
ResourceDeploymentStats.Type: {ActionRead},
174175
ResourceDeploymentValues.Type: {ActionRead},
@@ -195,6 +196,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
195196
ResourceGroup.Type: {ActionRead},
196197
// Org roles are not really used yet, so grant the perm at the site level.
197198
ResourceOrganizationMember.Type: {ActionRead},
199+
// Template admins can read all template insights data
200+
ResourceTemplateInsights.Type: {ActionRead},
198201
}),
199202
Org: map[string][]Permission{},
200203
User: []Permission{},

codersdk/rbacresources.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const (
2525
ResourceReplicas RBACResource = "replicas"
2626
ResourceDebugInfo RBACResource = "debug_info"
2727
ResourceSystem RBACResource = "system"
28+
ResourceTemplateInsights RBACResource = "template_insights"
2829
)
2930

3031
const (
@@ -58,6 +59,7 @@ var (
5859
ResourceReplicas,
5960
ResourceDebugInfo,
6061
ResourceSystem,
62+
ResourceTemplateInsights,
6163
}
6264

6365
AllRBACActions = []string{

docs/api/schemas.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)