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
This commit is contained in:
Cian Johnston
2023-11-24 17:21:32 +00:00
committed by GitHub
parent e73901cf56
commit dd161b172e
11 changed files with 186 additions and 77 deletions

6
coderd/apidoc/docs.go generated
View File

@ -9673,7 +9673,8 @@ const docTemplate = `{
"deployment_stats",
"replicas",
"debug_info",
"system"
"system",
"template_insights"
],
"x-enum-varnames": [
"ResourceWorkspace",
@ -9697,7 +9698,8 @@ const docTemplate = `{
"ResourceDeploymentStats",
"ResourceReplicas",
"ResourceDebugInfo",
"ResourceSystem"
"ResourceSystem",
"ResourceTemplateInsights"
]
},
"codersdk.RateLimitConfig": {

View File

@ -8701,7 +8701,8 @@
"deployment_stats",
"replicas",
"debug_info",
"system"
"system",
"template_insights"
],
"x-enum-varnames": [
"ResourceWorkspace",
@ -8725,7 +8726,8 @@
"ResourceDeploymentStats",
"ResourceReplicas",
"ResourceDebugInfo",
"ResourceSystem"
"ResourceSystem",
"ResourceTemplateInsights"
]
},
"codersdk.RateLimitConfig": {

View File

@ -1294,26 +1294,31 @@ func (q *querier) GetTailnetTunnelPeerIDs(ctx context.Context, srcID uuid.UUID)
}
func (q *querier) GetTemplateAppInsights(ctx context.Context, arg database.GetTemplateAppInsightsParams) ([]database.GetTemplateAppInsightsRow, error) {
for _, templateID := range arg.TemplateIDs {
template, err := q.db.GetTemplateByID(ctx, templateID)
if err != nil {
return nil, err
}
// Used by TemplateAppInsights endpoint
// For auditors, check read template_insights, and fall back to update template.
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTemplateInsights); IsNotAuthorizedError(err) {
for _, templateID := range arg.TemplateIDs {
template, err := q.db.GetTemplateByID(ctx, templateID)
if err != nil {
return nil, err
}
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
return nil, err
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
return nil, err
}
}
}
if len(arg.TemplateIDs) == 0 {
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
return nil, err
if len(arg.TemplateIDs) == 0 {
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
return nil, err
}
}
}
return q.db.GetTemplateAppInsights(ctx, arg)
}
func (q *querier) GetTemplateAppInsightsByTemplate(ctx context.Context, arg database.GetTemplateAppInsightsByTemplateParams) ([]database.GetTemplateAppInsightsByTemplateRow, error) {
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
// Only used by prometheus metrics, so we don't strictly need to check update template perms.
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTemplateInsights); err != nil {
return nil, err
}
return q.db.GetTemplateAppInsightsByTemplate(ctx, arg)
@ -1344,64 +1349,77 @@ func (q *querier) GetTemplateDAUs(ctx context.Context, arg database.GetTemplateD
}
func (q *querier) GetTemplateInsights(ctx context.Context, arg database.GetTemplateInsightsParams) (database.GetTemplateInsightsRow, error) {
for _, templateID := range arg.TemplateIDs {
template, err := q.db.GetTemplateByID(ctx, templateID)
if err != nil {
return database.GetTemplateInsightsRow{}, err
}
// Used by TemplateInsights endpoint
// For auditors, check read template_insights, and fall back to update template.
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTemplateInsights); IsNotAuthorizedError(err) {
for _, templateID := range arg.TemplateIDs {
template, err := q.db.GetTemplateByID(ctx, templateID)
if err != nil {
return database.GetTemplateInsightsRow{}, err
}
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
return database.GetTemplateInsightsRow{}, err
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
return database.GetTemplateInsightsRow{}, err
}
}
}
if len(arg.TemplateIDs) == 0 {
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
return database.GetTemplateInsightsRow{}, err
if len(arg.TemplateIDs) == 0 {
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
return database.GetTemplateInsightsRow{}, err
}
}
}
return q.db.GetTemplateInsights(ctx, arg)
}
func (q *querier) GetTemplateInsightsByInterval(ctx context.Context, arg database.GetTemplateInsightsByIntervalParams) ([]database.GetTemplateInsightsByIntervalRow, error) {
for _, templateID := range arg.TemplateIDs {
template, err := q.db.GetTemplateByID(ctx, templateID)
if err != nil {
return nil, err
}
// Used by TemplateInsights endpoint
// For auditors, check read template_insights, and fall back to update template.
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTemplateInsights); IsNotAuthorizedError(err) {
for _, templateID := range arg.TemplateIDs {
template, err := q.db.GetTemplateByID(ctx, templateID)
if err != nil {
return nil, err
}
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
return nil, err
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
return nil, err
}
}
}
if len(arg.TemplateIDs) == 0 {
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
return nil, err
if len(arg.TemplateIDs) == 0 {
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
return nil, err
}
}
}
return q.db.GetTemplateInsightsByInterval(ctx, arg)
}
func (q *querier) GetTemplateInsightsByTemplate(ctx context.Context, arg database.GetTemplateInsightsByTemplateParams) ([]database.GetTemplateInsightsByTemplateRow, error) {
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
// Only used by prometheus metrics collector. No need to check update template perms.
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTemplateInsights); err != nil {
return nil, err
}
return q.db.GetTemplateInsightsByTemplate(ctx, arg)
}
func (q *querier) GetTemplateParameterInsights(ctx context.Context, arg database.GetTemplateParameterInsightsParams) ([]database.GetTemplateParameterInsightsRow, error) {
for _, templateID := range arg.TemplateIDs {
template, err := q.db.GetTemplateByID(ctx, templateID)
if err != nil {
return nil, err
}
// Used by both insights endpoint and prometheus collector.
// For auditors, check read template_insights, and fall back to update template.
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTemplateInsights); IsNotAuthorizedError(err) {
for _, templateID := range arg.TemplateIDs {
template, err := q.db.GetTemplateByID(ctx, templateID)
if err != nil {
return nil, err
}
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
return nil, err
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
return nil, err
}
}
}
if len(arg.TemplateIDs) == 0 {
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
return nil, err
if len(arg.TemplateIDs) == 0 {
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
return nil, err
}
}
}
return q.db.GetTemplateParameterInsights(ctx, arg)
@ -1559,19 +1577,22 @@ func (q *querier) GetUnexpiredLicenses(ctx context.Context) ([]database.License,
}
func (q *querier) GetUserActivityInsights(ctx context.Context, arg database.GetUserActivityInsightsParams) ([]database.GetUserActivityInsightsRow, error) {
for _, templateID := range arg.TemplateIDs {
template, err := q.db.GetTemplateByID(ctx, templateID)
if err != nil {
return nil, err
}
// Used by insights endpoints. Need to check both for auditors and for regular users with template acl perms.
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTemplateInsights); IsNotAuthorizedError(err) {
for _, templateID := range arg.TemplateIDs {
template, err := q.db.GetTemplateByID(ctx, templateID)
if err != nil {
return nil, err
}
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
return nil, err
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
return nil, err
}
}
}
if len(arg.TemplateIDs) == 0 {
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
return nil, err
if len(arg.TemplateIDs) == 0 {
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
return nil, err
}
}
}
return q.db.GetUserActivityInsights(ctx, arg)
@ -1593,19 +1614,22 @@ func (q *querier) GetUserCount(ctx context.Context) (int64, error) {
}
func (q *querier) GetUserLatencyInsights(ctx context.Context, arg database.GetUserLatencyInsightsParams) ([]database.GetUserLatencyInsightsRow, error) {
for _, templateID := range arg.TemplateIDs {
template, err := q.db.GetTemplateByID(ctx, templateID)
if err != nil {
return nil, err
}
// Used by insights endpoints. Need to check both for auditors and for regular users with template acl perms.
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTemplateInsights); IsNotAuthorizedError(err) {
for _, templateID := range arg.TemplateIDs {
template, err := q.db.GetTemplateByID(ctx, templateID)
if err != nil {
return nil, err
}
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
return nil, err
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
return nil, err
}
}
}
if len(arg.TemplateIDs) == 0 {
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
return nil, err
if len(arg.TemplateIDs) == 0 {
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
return nil, err
}
}
}
return q.db.GetUserLatencyInsights(ctx, arg)

View File

@ -193,6 +193,11 @@ var (
ResourceTailnetCoordinator = Object{
Type: "tailnet_coordinator",
}
// ResourceTemplateInsights is a pseudo-resource for reading template insights data.
ResourceTemplateInsights = Object{
Type: "template_insights",
}
)
// ResourceUserObject is a helper function to create a user object for authz checks.

View File

@ -20,6 +20,7 @@ func AllResources() []Object {
ResourceSystem,
ResourceTailnetCoordinator,
ResourceTemplate,
ResourceTemplateInsights,
ResourceUser,
ResourceUserData,
ResourceWildcard,

View File

@ -165,10 +165,11 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
Site: Permissions(map[string][]Action{
// Should be able to read all template details, even in orgs they
// are not in.
ResourceTemplate.Type: {ActionRead},
ResourceAuditLog.Type: {ActionRead},
ResourceUser.Type: {ActionRead},
ResourceGroup.Type: {ActionRead},
ResourceTemplate.Type: {ActionRead},
ResourceTemplateInsights.Type: {ActionRead},
ResourceAuditLog.Type: {ActionRead},
ResourceUser.Type: {ActionRead},
ResourceGroup.Type: {ActionRead},
// Allow auditors to query deployment stats and insights.
ResourceDeploymentStats.Type: {ActionRead},
ResourceDeploymentValues.Type: {ActionRead},
@ -195,6 +196,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
ResourceGroup.Type: {ActionRead},
// Org roles are not really used yet, so grant the perm at the site level.
ResourceOrganizationMember.Type: {ActionRead},
// Template admins can read all template insights data
ResourceTemplateInsights.Type: {ActionRead},
}),
Org: map[string][]Permission{},
User: []Permission{},