diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index bf44240fd0..6daf5f481a 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -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": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index edae5dee2f..d911ecac28 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -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": { diff --git a/coderd/audit.go b/coderd/audit.go index d78fba6116..782c977afc 100644 --- a/coderd/audit.go +++ b/coderd/audit.go @@ -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 "" } diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go index bdaef00bb0..a6835014d4 100644 --- a/coderd/audit/diff.go +++ b/coderd/audit/diff.go @@ -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 diff --git a/coderd/audit/request.go b/coderd/audit/request.go index 130ce9dd56..e6d9d01fbf 100644 --- a/coderd/audit/request.go +++ b/coderd/audit/request.go @@ -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)) } diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 07b4da8ae5..b9f002d251 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -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 ( diff --git a/coderd/database/migrations/000197_oauth2_provider_app_audit.down.sql b/coderd/database/migrations/000197_oauth2_provider_app_audit.down.sql new file mode 100644 index 0000000000..8761aff760 --- /dev/null +++ b/coderd/database/migrations/000197_oauth2_provider_app_audit.down.sql @@ -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". diff --git a/coderd/database/migrations/000197_oauth2_provider_app_audit.up.sql b/coderd/database/migrations/000197_oauth2_provider_app_audit.up.sql new file mode 100644 index 0000000000..694e34b810 --- /dev/null +++ b/coderd/database/migrations/000197_oauth2_provider_app_audit.up.sql @@ -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'; diff --git a/coderd/database/models.go b/coderd/database/models.go index 7336dea1f3..677d258105 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -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, } } diff --git a/codersdk/audit.go b/codersdk/audit.go index c1ea077ec0..553bd9cc2d 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -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" } diff --git a/codersdk/oauth2.go b/codersdk/oauth2.go index 74f12bd303..726a50907e 100644 --- a/codersdk/oauth2.go +++ b/codersdk/oauth2.go @@ -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 diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md index 8d7f638dca..666b8d3d3f 100644 --- a/docs/admin/audit-logs.md +++ b/docs/admin/audit-logs.md @@ -16,6 +16,8 @@ We track the following resources: | GitSSHKey
create |
FieldTracked
created_atfalse
private_keytrue
public_keytrue
updated_atfalse
user_idtrue
| | HealthSettings
|
FieldTracked
dismissed_healthcheckstrue
idfalse
| | License
create, delete |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| +| OAuth2ProviderApp
|
FieldTracked
callback_urltrue
created_atfalse
icontrue
idfalse
nametrue
updated_atfalse
| +| OAuth2ProviderAppSecret
|
FieldTracked
app_idfalse
created_atfalse
display_secretfalse
hashed_secretfalse
idfalse
last_used_atfalse
secret_prefixfalse
| | Template
write, delete |
FieldTracked
active_version_idtrue
activity_bumptrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
autostart_block_days_of_weektrue
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
default_ttltrue
deletedfalse
deprecatedtrue
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_port_sharing_leveltrue
max_ttltrue
nametrue
organization_idfalse
provisionertrue
require_active_versiontrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
use_max_ttltrue
user_acltrue
| | TemplateVersion
create, write |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
external_auth_providersfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
template_idtrue
updated_atfalse
| | User
create, write, delete |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
hashed_passwordtrue
idtrue
last_seen_atfalse
login_typetrue
nametrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
theme_preferencefalse
updated_atfalse
usernametrue
| diff --git a/docs/api/schemas.md b/docs/api/schemas.md index b383a707c6..406eb9202d 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -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 diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index 821fcd3878..f557ded214 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -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 diff --git a/enterprise/coderd/oauth2.go b/enterprise/coderd/oauth2.go index 9c75b0b7a6..26b9555bf3 100644 --- a/enterprise/coderd/oauth2.go +++ b/enterprise/coderd/oauth2.go @@ -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{ diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 1e27e3c980..7b0f21a3fa 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -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",