feat: audit oauth2 app management (#12275)

* Audit oauth2 app management
* Use 201 for creating secrets
This commit is contained in:
Asher 2024-02-26 15:52:08 -08:00 committed by GitHub
parent 6b866b3f48
commit f74532ff50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 212 additions and 59 deletions

8
coderd/apidoc/docs.go generated
View File

@ -11504,7 +11504,9 @@ const docTemplate = `{
"convert_login",
"health_settings",
"workspace_proxy",
"organization"
"organization",
"oauth2_provider_app",
"oauth2_provider_app_secret"
],
"x-enum-varnames": [
"ResourceTypeTemplate",
@ -11519,7 +11521,9 @@ const docTemplate = `{
"ResourceTypeConvertLogin",
"ResourceTypeHealthSettings",
"ResourceTypeWorkspaceProxy",
"ResourceTypeOrganization"
"ResourceTypeOrganization",
"ResourceTypeOAuth2ProviderApp",
"ResourceTypeOAuth2ProviderAppSecret"
]
},
"codersdk.Response": {

View File

@ -10378,7 +10378,9 @@
"convert_login",
"health_settings",
"workspace_proxy",
"organization"
"organization",
"oauth2_provider_app",
"oauth2_provider_app_secret"
],
"x-enum-varnames": [
"ResourceTypeTemplate",
@ -10393,7 +10395,9 @@
"ResourceTypeConvertLogin",
"ResourceTypeHealthSettings",
"ResourceTypeWorkspaceProxy",
"ResourceTypeOrganization"
"ResourceTypeOrganization",
"ResourceTypeOAuth2ProviderApp",
"ResourceTypeOAuth2ProviderAppSecret"
]
},
"codersdk.Response": {

View File

@ -333,6 +333,22 @@ func (api *API) auditLogIsResourceDeleted(ctx context.Context, alog database.Get
api.Logger.Error(ctx, "unable to fetch workspace", slog.Error(err))
}
return workspace.Deleted
case database.ResourceTypeOauth2ProviderApp:
_, err := api.Database.GetOAuth2ProviderAppByID(ctx, alog.ResourceID)
if xerrors.Is(err, sql.ErrNoRows) {
return true
} else if err != nil {
api.Logger.Error(ctx, "unable to fetch oauth2 app", slog.Error(err))
}
return false
case database.ResourceTypeOauth2ProviderAppSecret:
_, err := api.Database.GetOAuth2ProviderAppSecretByID(ctx, alog.ResourceID)
if xerrors.Is(err, sql.ErrNoRows) {
return true
} else if err != nil {
api.Logger.Error(ctx, "unable to fetch oauth2 app secret", slog.Error(err))
}
return false
default:
return false
}
@ -379,6 +395,16 @@ func (api *API) auditLogResourceLink(ctx context.Context, alog database.GetAudit
return fmt.Sprintf("/@%s/%s/builds/%s",
workspaceOwner.Username, additionalFields.WorkspaceName, additionalFields.BuildNumber)
case database.ResourceTypeOauth2ProviderApp:
return fmt.Sprintf("/deployment/oauth2-provider/apps/%s", alog.ResourceID)
case database.ResourceTypeOauth2ProviderAppSecret:
secret, err := api.Database.GetOAuth2ProviderAppSecretByID(ctx, alog.ResourceID)
if err != nil {
return ""
}
return fmt.Sprintf("/deployment/oauth2-provider/apps/%s", secret.AppID)
default:
return ""
}

View File

@ -19,7 +19,9 @@ type Auditable interface {
database.License |
database.WorkspaceProxy |
database.AuditOAuthConvertState |
database.HealthSettings
database.HealthSettings |
database.OAuth2ProviderApp |
database.OAuth2ProviderAppSecret
}
// Map is a map of changed fields in an audited resource. It maps field names to

View File

@ -99,6 +99,10 @@ func ResourceTarget[T Auditable](tgt T) string {
return string(typed.ToLoginType)
case database.HealthSettings:
return "" // no target?
case database.OAuth2ProviderApp:
return typed.Name
case database.OAuth2ProviderAppSecret:
return typed.DisplaySecret
default:
panic(fmt.Sprintf("unknown resource %T for ResourceTarget", tgt))
}
@ -132,6 +136,10 @@ func ResourceID[T Auditable](tgt T) uuid.UUID {
case database.HealthSettings:
// Artificial ID for auditing purposes
return typed.ID
case database.OAuth2ProviderApp:
return typed.ID
case database.OAuth2ProviderAppSecret:
return typed.ID
default:
panic(fmt.Sprintf("unknown resource %T for ResourceID", tgt))
}
@ -163,6 +171,10 @@ func ResourceType[T Auditable](tgt T) database.ResourceType {
return database.ResourceTypeConvertLogin
case database.HealthSettings:
return database.ResourceTypeHealthSettings
case database.OAuth2ProviderApp:
return database.ResourceTypeOauth2ProviderApp
case database.OAuth2ProviderAppSecret:
return database.ResourceTypeOauth2ProviderAppSecret
default:
panic(fmt.Sprintf("unknown resource %T for ResourceType", typed))
}
@ -195,6 +207,10 @@ func ResourceRequiresOrgID[T Auditable]() bool {
case database.HealthSettings:
// Artificial ID for auditing purposes
return false
case database.OAuth2ProviderApp:
return false
case database.OAuth2ProviderAppSecret:
return false
default:
panic(fmt.Sprintf("unknown resource %T for ResourceRequiresOrgID", tgt))
}

View File

@ -135,7 +135,9 @@ CREATE TYPE resource_type AS ENUM (
'license',
'workspace_proxy',
'convert_login',
'health_settings'
'health_settings',
'oauth2_provider_app',
'oauth2_provider_app_secret'
);
CREATE TYPE startup_script_behavior AS ENUM (

View File

@ -0,0 +1,2 @@
-- It is not possible to drop enum values from enum types, so the UPs on
-- resource_type have "IF NOT EXISTS".

View File

@ -0,0 +1,2 @@
ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'oauth2_provider_app';
ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'oauth2_provider_app_secret';

View File

@ -1149,19 +1149,21 @@ func AllProvisionerTypeValues() []ProvisionerType {
type ResourceType string
const (
ResourceTypeOrganization ResourceType = "organization"
ResourceTypeTemplate ResourceType = "template"
ResourceTypeTemplateVersion ResourceType = "template_version"
ResourceTypeUser ResourceType = "user"
ResourceTypeWorkspace ResourceType = "workspace"
ResourceTypeGitSshKey ResourceType = "git_ssh_key"
ResourceTypeApiKey ResourceType = "api_key"
ResourceTypeGroup ResourceType = "group"
ResourceTypeWorkspaceBuild ResourceType = "workspace_build"
ResourceTypeLicense ResourceType = "license"
ResourceTypeWorkspaceProxy ResourceType = "workspace_proxy"
ResourceTypeConvertLogin ResourceType = "convert_login"
ResourceTypeHealthSettings ResourceType = "health_settings"
ResourceTypeOrganization ResourceType = "organization"
ResourceTypeTemplate ResourceType = "template"
ResourceTypeTemplateVersion ResourceType = "template_version"
ResourceTypeUser ResourceType = "user"
ResourceTypeWorkspace ResourceType = "workspace"
ResourceTypeGitSshKey ResourceType = "git_ssh_key"
ResourceTypeApiKey ResourceType = "api_key"
ResourceTypeGroup ResourceType = "group"
ResourceTypeWorkspaceBuild ResourceType = "workspace_build"
ResourceTypeLicense ResourceType = "license"
ResourceTypeWorkspaceProxy ResourceType = "workspace_proxy"
ResourceTypeConvertLogin ResourceType = "convert_login"
ResourceTypeHealthSettings ResourceType = "health_settings"
ResourceTypeOauth2ProviderApp ResourceType = "oauth2_provider_app"
ResourceTypeOauth2ProviderAppSecret ResourceType = "oauth2_provider_app_secret"
)
func (e *ResourceType) Scan(src interface{}) error {
@ -1213,7 +1215,9 @@ func (e ResourceType) Valid() bool {
ResourceTypeLicense,
ResourceTypeWorkspaceProxy,
ResourceTypeConvertLogin,
ResourceTypeHealthSettings:
ResourceTypeHealthSettings,
ResourceTypeOauth2ProviderApp,
ResourceTypeOauth2ProviderAppSecret:
return true
}
return false
@ -1234,6 +1238,8 @@ func AllResourceTypeValues() []ResourceType {
ResourceTypeWorkspaceProxy,
ResourceTypeConvertLogin,
ResourceTypeHealthSettings,
ResourceTypeOauth2ProviderApp,
ResourceTypeOauth2ProviderAppSecret,
}
}

View File

@ -14,19 +14,22 @@ import (
type ResourceType string
const (
ResourceTypeTemplate ResourceType = "template"
ResourceTypeTemplateVersion ResourceType = "template_version"
ResourceTypeUser ResourceType = "user"
ResourceTypeWorkspace ResourceType = "workspace"
ResourceTypeWorkspaceBuild ResourceType = "workspace_build"
ResourceTypeGitSSHKey ResourceType = "git_ssh_key"
ResourceTypeAPIKey ResourceType = "api_key"
ResourceTypeGroup ResourceType = "group"
ResourceTypeLicense ResourceType = "license"
ResourceTypeConvertLogin ResourceType = "convert_login"
ResourceTypeHealthSettings ResourceType = "health_settings"
ResourceTypeWorkspaceProxy ResourceType = "workspace_proxy"
ResourceTypeOrganization ResourceType = "organization"
ResourceTypeTemplate ResourceType = "template"
ResourceTypeTemplateVersion ResourceType = "template_version"
ResourceTypeUser ResourceType = "user"
ResourceTypeWorkspace ResourceType = "workspace"
ResourceTypeWorkspaceBuild ResourceType = "workspace_build"
ResourceTypeGitSSHKey ResourceType = "git_ssh_key"
ResourceTypeAPIKey ResourceType = "api_key"
ResourceTypeGroup ResourceType = "group"
ResourceTypeLicense ResourceType = "license"
ResourceTypeConvertLogin ResourceType = "convert_login"
ResourceTypeHealthSettings ResourceType = "health_settings"
ResourceTypeWorkspaceProxy ResourceType = "workspace_proxy"
ResourceTypeOrganization ResourceType = "organization"
ResourceTypeOAuth2ProviderApp ResourceType = "oauth2_provider_app"
// nolint:gosec // This is not a secret.
ResourceTypeOAuth2ProviderAppSecret ResourceType = "oauth2_provider_app_secret"
)
func (r ResourceType) FriendlyString() string {
@ -59,6 +62,10 @@ func (r ResourceType) FriendlyString() string {
return "organization"
case ResourceTypeHealthSettings:
return "health_settings"
case ResourceTypeOAuth2ProviderApp:
return "oauth2 app"
case ResourceTypeOAuth2ProviderAppSecret:
return "oauth2 app secret"
default:
return "unknown"
}

View File

@ -159,7 +159,7 @@ func (c *Client) PostOAuth2ProviderAppSecret(ctx context.Context, appID uuid.UUI
return OAuth2ProviderAppSecretFull{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
if res.StatusCode != http.StatusCreated {
return OAuth2ProviderAppSecretFull{}, ReadBodyAsError(res)
}
var resp OAuth2ProviderAppSecretFull

View File

@ -16,6 +16,8 @@ We track the following resources:
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| HealthSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>dismissed_healthchecks</td><td>true</td></tr><tr><td>id</td><td>false</td></tr></tbody></table> |
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
| OAuth2ProviderApp<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>callback_url</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| OAuth2ProviderAppSecret<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>app_id</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>display_secret</td><td>false</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>secret_prefix</td><td>false</td></tr></tbody></table> |
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>active_version_id</td><td>true</td></tr><tr><td>activity_bump</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deprecated</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_port_sharing_level</td><td>true</td></tr><tr><td>max_ttl</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>require_active_version</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>use_max_ttl</td><td>true</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>theme_preference</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |

32
docs/api/schemas.md generated
View File

@ -5367,21 +5367,23 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
#### Enumerated Values
| Value |
| ------------------ |
| `template` |
| `template_version` |
| `user` |
| `workspace` |
| `workspace_build` |
| `git_ssh_key` |
| `api_key` |
| `group` |
| `license` |
| `convert_login` |
| `health_settings` |
| `workspace_proxy` |
| `organization` |
| Value |
| ---------------------------- |
| `template` |
| `template_version` |
| `user` |
| `workspace` |
| `workspace_build` |
| `git_ssh_key` |
| `api_key` |
| `group` |
| `license` |
| `convert_login` |
| `health_settings` |
| `workspace_proxy` |
| `organization` |
| `oauth2_provider_app` |
| `oauth2_provider_app_secret` |
## codersdk.Response

View File

@ -219,6 +219,23 @@ var auditableResourcesTypes = map[any]map[string]Action{
"region_id": ActionTrack,
"version": ActionTrack,
},
&database.OAuth2ProviderApp{}: {
"id": ActionIgnore,
"created_at": ActionIgnore,
"updated_at": ActionIgnore,
"name": ActionTrack,
"icon": ActionTrack,
"callback_url": ActionTrack,
},
&database.OAuth2ProviderAppSecret{}: {
"id": ActionIgnore,
"created_at": ActionIgnore,
"last_used_at": ActionIgnore,
"hashed_secret": ActionIgnore,
"display_secret": ActionIgnore,
"app_id": ActionIgnore,
"secret_prefix": ActionIgnore,
},
}
// auditMap converts a map of struct pointers to a map of struct names as

View File

@ -7,6 +7,7 @@ import (
"github.com/google/uuid"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbtime"
@ -108,7 +109,17 @@ func (api *API) oAuth2ProviderApp(rw http.ResponseWriter, r *http.Request) {
// @Success 200 {object} codersdk.OAuth2ProviderApp
// @Router /oauth2-provider/apps [post]
func (api *API) postOAuth2ProviderApp(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var (
ctx = r.Context()
auditor = api.AGPL.Auditor.Load()
aReq, commitAudit = audit.InitRequest[database.OAuth2ProviderApp](rw, &audit.RequestParams{
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionCreate,
})
)
defer commitAudit()
var req codersdk.PostOAuth2ProviderAppRequest
if !httpapi.Read(ctx, rw, r, &req) {
return
@ -128,6 +139,7 @@ func (api *API) postOAuth2ProviderApp(rw http.ResponseWriter, r *http.Request) {
})
return
}
aReq.New = app
httpapi.Write(ctx, rw, http.StatusCreated, db2sdk.OAuth2ProviderApp(api.AccessURL, app))
}
@ -142,8 +154,19 @@ func (api *API) postOAuth2ProviderApp(rw http.ResponseWriter, r *http.Request) {
// @Success 200 {object} codersdk.OAuth2ProviderApp
// @Router /oauth2-provider/apps/{app} [put]
func (api *API) putOAuth2ProviderApp(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
app := httpmw.OAuth2ProviderApp(r)
var (
ctx = r.Context()
app = httpmw.OAuth2ProviderApp(r)
auditor = api.AGPL.Auditor.Load()
aReq, commitAudit = audit.InitRequest[database.OAuth2ProviderApp](rw, &audit.RequestParams{
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionWrite,
})
)
aReq.Old = app
defer commitAudit()
var req codersdk.PutOAuth2ProviderAppRequest
if !httpapi.Read(ctx, rw, r, &req) {
return
@ -162,6 +185,7 @@ func (api *API) putOAuth2ProviderApp(rw http.ResponseWriter, r *http.Request) {
})
return
}
aReq.New = app
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.OAuth2ProviderApp(api.AccessURL, app))
}
@ -173,8 +197,19 @@ func (api *API) putOAuth2ProviderApp(rw http.ResponseWriter, r *http.Request) {
// @Success 204
// @Router /oauth2-provider/apps/{app} [delete]
func (api *API) deleteOAuth2ProviderApp(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
app := httpmw.OAuth2ProviderApp(r)
var (
ctx = r.Context()
app = httpmw.OAuth2ProviderApp(r)
auditor = api.AGPL.Auditor.Load()
aReq, commitAudit = audit.InitRequest[database.OAuth2ProviderApp](rw, &audit.RequestParams{
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionDelete,
})
)
aReq.Old = app
defer commitAudit()
err := api.Database.DeleteOAuth2ProviderAppByID(ctx, app.ID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
@ -225,8 +260,18 @@ func (api *API) oAuth2ProviderAppSecrets(rw http.ResponseWriter, r *http.Request
// @Success 200 {array} codersdk.OAuth2ProviderAppSecretFull
// @Router /oauth2-provider/apps/{app}/secrets [post]
func (api *API) postOAuth2ProviderAppSecret(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
app := httpmw.OAuth2ProviderApp(r)
var (
ctx = r.Context()
app = httpmw.OAuth2ProviderApp(r)
auditor = api.AGPL.Auditor.Load()
aReq, commitAudit = audit.InitRequest[database.OAuth2ProviderAppSecret](rw, &audit.RequestParams{
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionCreate,
})
)
defer commitAudit()
secret, err := identityprovider.GenerateSecret()
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
@ -253,7 +298,8 @@ func (api *API) postOAuth2ProviderAppSecret(rw http.ResponseWriter, r *http.Requ
})
return
}
httpapi.Write(ctx, rw, http.StatusOK, codersdk.OAuth2ProviderAppSecretFull{
aReq.New = dbSecret
httpapi.Write(ctx, rw, http.StatusCreated, codersdk.OAuth2ProviderAppSecretFull{
ID: dbSecret.ID,
ClientSecretFull: secret.Formatted,
})
@ -268,8 +314,19 @@ func (api *API) postOAuth2ProviderAppSecret(rw http.ResponseWriter, r *http.Requ
// @Success 204
// @Router /oauth2-provider/apps/{app}/secrets/{secretID} [delete]
func (api *API) deleteOAuth2ProviderAppSecret(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
secret := httpmw.OAuth2ProviderAppSecret(r)
var (
ctx = r.Context()
secret = httpmw.OAuth2ProviderAppSecret(r)
auditor = api.AGPL.Auditor.Load()
aReq, commitAudit = audit.InitRequest[database.OAuth2ProviderAppSecret](rw, &audit.RequestParams{
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionDelete,
})
)
aReq.Old = secret
defer commitAudit()
err := api.Database.DeleteOAuth2ProviderAppSecretByID(ctx, secret.ID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{

View File

@ -2251,6 +2251,8 @@ export type ResourceType =
| "group"
| "health_settings"
| "license"
| "oauth2_provider_app"
| "oauth2_provider_app_secret"
| "organization"
| "template"
| "template_version"
@ -2265,6 +2267,8 @@ export const ResourceTypes: ResourceType[] = [
"group",
"health_settings",
"license",
"oauth2_provider_app",
"oauth2_provider_app_secret",
"organization",
"template",
"template_version",