mirror of https://github.com/coder/coder.git
feat: add OAuth2 applications (#11197)
* Add database tables for OAuth2 applications These are applications that will be able to use OAuth2 to get an API key from Coder. * Add endpoints for managing OAuth2 applications These let you add, update, and remove OAuth2 applications. * Add frontend for managing OAuth2 applications
This commit is contained in:
parent
e044d3b752
commit
5cfa34b31e
|
@ -1304,6 +1304,282 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/oauth2-provider/apps": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Get OAuth2 applications.",
|
||||
"operationId": "get-oauth2-applications",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderApp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Create OAuth2 application.",
|
||||
"operationId": "create-oauth2-application",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "The OAuth2 application to create.",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.PostOAuth2ProviderAppRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderApp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/oauth2-provider/apps/{app}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Get OAuth2 application.",
|
||||
"operationId": "get-oauth2-application",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "App ID",
|
||||
"name": "app",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderApp"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Update OAuth2 application.",
|
||||
"operationId": "update-oauth2-application",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "App ID",
|
||||
"name": "app",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Update an OAuth2 application.",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.PutOAuth2ProviderAppRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderApp"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Delete OAuth2 application.",
|
||||
"operationId": "delete-oauth2-application",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "App ID",
|
||||
"name": "app",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/oauth2-provider/apps/{app}/secrets": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Get OAuth2 application secrets.",
|
||||
"operationId": "get-oauth2-application-secrets",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "App ID",
|
||||
"name": "app",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderAppSecret"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Create OAuth2 application secret.",
|
||||
"operationId": "create-oauth2-application-secret",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "App ID",
|
||||
"name": "app",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderAppSecretFull"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/oauth2-provider/apps/{app}/secrets/{secretID}": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Delete OAuth2 application secret.",
|
||||
"operationId": "delete-oauth2-application-secret",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "App ID",
|
||||
"name": "app",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Secret ID",
|
||||
"name": "secretID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/organizations": {
|
||||
"post": {
|
||||
"security": [
|
||||
|
@ -9413,6 +9689,51 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"codersdk.OAuth2ProviderApp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"callback_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.OAuth2ProviderAppSecret": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"client_secret_truncated": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"last_used_at": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.OAuth2ProviderAppSecretFull": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"client_secret_full": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.OAuthConversionResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -9653,6 +9974,24 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"codersdk.PostOAuth2ProviderAppRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"callback_url",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"callback_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.PprofConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -9932,6 +10271,24 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"codersdk.PutOAuth2ProviderAppRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"callback_url",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"callback_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.RBACResource": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
|
|
@ -1122,6 +1122,250 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/oauth2-provider/apps": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Get OAuth2 applications.",
|
||||
"operationId": "get-oauth2-applications",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderApp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"consumes": ["application/json"],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Create OAuth2 application.",
|
||||
"operationId": "create-oauth2-application",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "The OAuth2 application to create.",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.PostOAuth2ProviderAppRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderApp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/oauth2-provider/apps/{app}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Get OAuth2 application.",
|
||||
"operationId": "get-oauth2-application",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "App ID",
|
||||
"name": "app",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderApp"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"consumes": ["application/json"],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Update OAuth2 application.",
|
||||
"operationId": "update-oauth2-application",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "App ID",
|
||||
"name": "app",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Update an OAuth2 application.",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.PutOAuth2ProviderAppRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderApp"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Delete OAuth2 application.",
|
||||
"operationId": "delete-oauth2-application",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "App ID",
|
||||
"name": "app",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/oauth2-provider/apps/{app}/secrets": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Get OAuth2 application secrets.",
|
||||
"operationId": "get-oauth2-application-secrets",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "App ID",
|
||||
"name": "app",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderAppSecret"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Create OAuth2 application secret.",
|
||||
"operationId": "create-oauth2-application-secret",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "App ID",
|
||||
"name": "app",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderAppSecretFull"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/oauth2-provider/apps/{app}/secrets/{secretID}": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Delete OAuth2 application secret.",
|
||||
"operationId": "delete-oauth2-application-secret",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "App ID",
|
||||
"name": "app",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Secret ID",
|
||||
"name": "secretID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/organizations": {
|
||||
"post": {
|
||||
"security": [
|
||||
|
@ -8442,6 +8686,51 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"codersdk.OAuth2ProviderApp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"callback_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.OAuth2ProviderAppSecret": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"client_secret_truncated": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"last_used_at": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.OAuth2ProviderAppSecretFull": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"client_secret_full": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.OAuthConversionResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -8672,6 +8961,21 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"codersdk.PostOAuth2ProviderAppRequest": {
|
||||
"type": "object",
|
||||
"required": ["callback_url", "name"],
|
||||
"properties": {
|
||||
"callback_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.PprofConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -8928,6 +9232,21 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"codersdk.PutOAuth2ProviderAppRequest": {
|
||||
"type": "object",
|
||||
"required": ["callback_url", "name"],
|
||||
"properties": {
|
||||
"callback_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.RBACResource": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
|
|
@ -225,6 +225,23 @@ func templateVersionParameterOptions(rawOptions json.RawMessage) ([]codersdk.Tem
|
|||
return options, nil
|
||||
}
|
||||
|
||||
func OAuth2ProviderApp(dbApp database.OAuth2ProviderApp) codersdk.OAuth2ProviderApp {
|
||||
return codersdk.OAuth2ProviderApp{
|
||||
ID: dbApp.ID,
|
||||
Name: dbApp.Name,
|
||||
CallbackURL: dbApp.CallbackURL,
|
||||
Icon: dbApp.Icon,
|
||||
}
|
||||
}
|
||||
|
||||
func OAuth2ProviderApps(dbApps []database.OAuth2ProviderApp) []codersdk.OAuth2ProviderApp {
|
||||
apps := []codersdk.OAuth2ProviderApp{}
|
||||
for _, dbApp := range dbApps {
|
||||
apps = append(apps, OAuth2ProviderApp(dbApp))
|
||||
}
|
||||
return apps
|
||||
}
|
||||
|
||||
func convertDisplayApps(apps []database.DisplayApp) []codersdk.DisplayApp {
|
||||
dapps := make([]codersdk.DisplayApp, 0, len(apps))
|
||||
for _, app := range apps {
|
||||
|
|
|
@ -805,6 +805,20 @@ func (q *querier) DeleteLicense(ctx context.Context, id int32) (int32, error) {
|
|||
return id, nil
|
||||
}
|
||||
|
||||
func (q *querier) DeleteOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) error {
|
||||
if err := q.authorizeContext(ctx, rbac.ActionDelete, rbac.ResourceOAuth2ProviderApp); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.DeleteOAuth2ProviderAppByID(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) DeleteOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) error {
|
||||
if err := q.authorizeContext(ctx, rbac.ActionDelete, rbac.ResourceOAuth2ProviderAppSecret); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.DeleteOAuth2ProviderAppSecretByID(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) DeleteOldProvisionerDaemons(ctx context.Context) error {
|
||||
if err := q.authorizeContext(ctx, rbac.ActionDelete, rbac.ResourceSystem); err != nil {
|
||||
return err
|
||||
|
@ -1131,6 +1145,34 @@ func (q *querier) GetLogoURL(ctx context.Context) (string, error) {
|
|||
return q.db.GetLogoURL(ctx)
|
||||
}
|
||||
|
||||
func (q *querier) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) {
|
||||
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceOAuth2ProviderApp); err != nil {
|
||||
return database.OAuth2ProviderApp{}, err
|
||||
}
|
||||
return q.db.GetOAuth2ProviderAppByID(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) GetOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderAppSecret, error) {
|
||||
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceOAuth2ProviderAppSecret); err != nil {
|
||||
return database.OAuth2ProviderAppSecret{}, err
|
||||
}
|
||||
return q.db.GetOAuth2ProviderAppSecretByID(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) GetOAuth2ProviderAppSecretsByAppID(ctx context.Context, appID uuid.UUID) ([]database.OAuth2ProviderAppSecret, error) {
|
||||
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceOAuth2ProviderAppSecret); err != nil {
|
||||
return []database.OAuth2ProviderAppSecret{}, err
|
||||
}
|
||||
return q.db.GetOAuth2ProviderAppSecretsByAppID(ctx, appID)
|
||||
}
|
||||
|
||||
func (q *querier) GetOAuth2ProviderApps(ctx context.Context) ([]database.OAuth2ProviderApp, error) {
|
||||
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceOAuth2ProviderApp); err != nil {
|
||||
return []database.OAuth2ProviderApp{}, err
|
||||
}
|
||||
return q.db.GetOAuth2ProviderApps(ctx)
|
||||
}
|
||||
|
||||
func (q *querier) GetOAuthSigningKey(ctx context.Context) (string, error) {
|
||||
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceSystem); err != nil {
|
||||
return "", err
|
||||
|
@ -2145,6 +2187,20 @@ func (q *querier) InsertMissingGroups(ctx context.Context, arg database.InsertMi
|
|||
return q.db.InsertMissingGroups(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertOAuth2ProviderApp(ctx context.Context, arg database.InsertOAuth2ProviderAppParams) (database.OAuth2ProviderApp, error) {
|
||||
if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceOAuth2ProviderApp); err != nil {
|
||||
return database.OAuth2ProviderApp{}, err
|
||||
}
|
||||
return q.db.InsertOAuth2ProviderApp(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertOAuth2ProviderAppSecret(ctx context.Context, arg database.InsertOAuth2ProviderAppSecretParams) (database.OAuth2ProviderAppSecret, error) {
|
||||
if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceOAuth2ProviderAppSecret); err != nil {
|
||||
return database.OAuth2ProviderAppSecret{}, err
|
||||
}
|
||||
return q.db.InsertOAuth2ProviderAppSecret(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertOrganization(ctx context.Context, arg database.InsertOrganizationParams) (database.Organization, error) {
|
||||
return insert(q.log, q.auth, rbac.ResourceOrganization, q.db.InsertOrganization)(ctx, arg)
|
||||
}
|
||||
|
@ -2500,6 +2556,20 @@ func (q *querier) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemb
|
|||
return q.db.UpdateMemberRoles(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateOAuth2ProviderAppByID(ctx context.Context, arg database.UpdateOAuth2ProviderAppByIDParams) (database.OAuth2ProviderApp, error) {
|
||||
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceOAuth2ProviderApp); err != nil {
|
||||
return database.OAuth2ProviderApp{}, err
|
||||
}
|
||||
return q.db.UpdateOAuth2ProviderAppByID(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateOAuth2ProviderAppSecretByID(ctx context.Context, arg database.UpdateOAuth2ProviderAppSecretByIDParams) (database.OAuth2ProviderAppSecret, error) {
|
||||
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceOAuth2ProviderAppSecret); err != nil {
|
||||
return database.OAuth2ProviderAppSecret{}, err
|
||||
}
|
||||
return q.db.UpdateOAuth2ProviderAppSecretByID(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error {
|
||||
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceProvisionerDaemon); err != nil {
|
||||
return err
|
||||
|
|
|
@ -2200,3 +2200,86 @@ func (s *MethodTestSuite) TestSystemFunctions() {
|
|||
check.Args(uuid.New()).Asserts(rbac.ResourceSystem, rbac.ActionRead)
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *MethodTestSuite) TestOAuth2ProviderApps() {
|
||||
s.Run("GetOAuth2ProviderApps", s.Subtest(func(db database.Store, check *expects) {
|
||||
apps := []database.OAuth2ProviderApp{
|
||||
dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{Name: "first"}),
|
||||
dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{Name: "last"}),
|
||||
}
|
||||
check.Args().Asserts(rbac.ResourceOAuth2ProviderApp, rbac.ActionRead).Returns(apps)
|
||||
}))
|
||||
s.Run("GetOAuth2ProviderAppByID", s.Subtest(func(db database.Store, check *expects) {
|
||||
app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{})
|
||||
check.Args(app.ID).Asserts(rbac.ResourceOAuth2ProviderApp, rbac.ActionRead).Returns(app)
|
||||
}))
|
||||
s.Run("InsertOAuth2ProviderApp", s.Subtest(func(db database.Store, check *expects) {
|
||||
check.Args(database.InsertOAuth2ProviderAppParams{}).Asserts(rbac.ResourceOAuth2ProviderApp, rbac.ActionCreate)
|
||||
}))
|
||||
s.Run("UpdateOAuth2ProviderAppByID", s.Subtest(func(db database.Store, check *expects) {
|
||||
app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{})
|
||||
app.Name = "my-new-name"
|
||||
app.UpdatedAt = time.Now()
|
||||
check.Args(database.UpdateOAuth2ProviderAppByIDParams{
|
||||
ID: app.ID,
|
||||
Name: app.Name,
|
||||
CallbackURL: app.CallbackURL,
|
||||
UpdatedAt: app.UpdatedAt,
|
||||
}).Asserts(rbac.ResourceOAuth2ProviderApp, rbac.ActionUpdate).Returns(app)
|
||||
}))
|
||||
s.Run("DeleteOAuth2ProviderAppByID", s.Subtest(func(db database.Store, check *expects) {
|
||||
app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{})
|
||||
check.Args(app.ID).Asserts(rbac.ResourceOAuth2ProviderApp, rbac.ActionDelete)
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *MethodTestSuite) TestOAuth2ProviderAppSecrets() {
|
||||
s.Run("GetOAuth2ProviderAppSecretsByAppID", s.Subtest(func(db database.Store, check *expects) {
|
||||
app1 := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{})
|
||||
app2 := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{})
|
||||
secrets := []database.OAuth2ProviderAppSecret{
|
||||
dbgen.OAuth2ProviderAppSecret(s.T(), db, database.OAuth2ProviderAppSecret{
|
||||
AppID: app1.ID,
|
||||
CreatedAt: time.Now().Add(-time.Hour), // For ordering.
|
||||
}),
|
||||
dbgen.OAuth2ProviderAppSecret(s.T(), db, database.OAuth2ProviderAppSecret{
|
||||
AppID: app1.ID,
|
||||
}),
|
||||
}
|
||||
_ = dbgen.OAuth2ProviderAppSecret(s.T(), db, database.OAuth2ProviderAppSecret{
|
||||
AppID: app2.ID,
|
||||
})
|
||||
check.Args(app1.ID).Asserts(rbac.ResourceOAuth2ProviderAppSecret, rbac.ActionRead).Returns(secrets)
|
||||
}))
|
||||
s.Run("GetOAuth2ProviderAppSecretByID", s.Subtest(func(db database.Store, check *expects) {
|
||||
app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{})
|
||||
secret := dbgen.OAuth2ProviderAppSecret(s.T(), db, database.OAuth2ProviderAppSecret{
|
||||
AppID: app.ID,
|
||||
})
|
||||
check.Args(secret.ID).Asserts(rbac.ResourceOAuth2ProviderAppSecret, rbac.ActionRead).Returns(secret)
|
||||
}))
|
||||
s.Run("InsertOAuth2ProviderAppSecret", s.Subtest(func(db database.Store, check *expects) {
|
||||
app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{})
|
||||
check.Args(database.InsertOAuth2ProviderAppSecretParams{
|
||||
AppID: app.ID,
|
||||
}).Asserts(rbac.ResourceOAuth2ProviderAppSecret, rbac.ActionCreate)
|
||||
}))
|
||||
s.Run("UpdateOAuth2ProviderAppSecretByID", s.Subtest(func(db database.Store, check *expects) {
|
||||
app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{})
|
||||
secret := dbgen.OAuth2ProviderAppSecret(s.T(), db, database.OAuth2ProviderAppSecret{
|
||||
AppID: app.ID,
|
||||
})
|
||||
secret.LastUsedAt = sql.NullTime{Time: time.Now(), Valid: true}
|
||||
check.Args(database.UpdateOAuth2ProviderAppSecretByIDParams{
|
||||
ID: secret.ID,
|
||||
LastUsedAt: secret.LastUsedAt,
|
||||
}).Asserts(rbac.ResourceOAuth2ProviderAppSecret, rbac.ActionUpdate).Returns(secret)
|
||||
}))
|
||||
s.Run("DeleteOAuth2ProviderAppSecretByID", s.Subtest(func(db database.Store, check *expects) {
|
||||
app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{})
|
||||
secret := dbgen.OAuth2ProviderAppSecret(s.T(), db, database.OAuth2ProviderAppSecret{
|
||||
AppID: app.ID,
|
||||
})
|
||||
check.Args(secret.ID).Asserts(rbac.ResourceOAuth2ProviderAppSecret, rbac.ActionDelete)
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -676,6 +676,31 @@ func WorkspaceAgentStat(t testing.TB, db database.Store, orig database.Workspace
|
|||
return scheme
|
||||
}
|
||||
|
||||
func OAuth2ProviderApp(t testing.TB, db database.Store, seed database.OAuth2ProviderApp) database.OAuth2ProviderApp {
|
||||
app, err := db.InsertOAuth2ProviderApp(genCtx, database.InsertOAuth2ProviderAppParams{
|
||||
ID: takeFirst(seed.ID, uuid.New()),
|
||||
Name: takeFirst(seed.Name, namesgenerator.GetRandomName(1)),
|
||||
CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()),
|
||||
UpdatedAt: takeFirst(seed.UpdatedAt, dbtime.Now()),
|
||||
Icon: takeFirst(seed.Icon, ""),
|
||||
CallbackURL: takeFirst(seed.CallbackURL, "http://localhost"),
|
||||
})
|
||||
require.NoError(t, err, "insert oauth2 app")
|
||||
return app
|
||||
}
|
||||
|
||||
func OAuth2ProviderAppSecret(t testing.TB, db database.Store, seed database.OAuth2ProviderAppSecret) database.OAuth2ProviderAppSecret {
|
||||
app, err := db.InsertOAuth2ProviderAppSecret(genCtx, database.InsertOAuth2ProviderAppSecretParams{
|
||||
ID: takeFirst(seed.ID, uuid.New()),
|
||||
CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()),
|
||||
HashedSecret: takeFirstSlice(seed.HashedSecret, []byte("hashed-secret")),
|
||||
DisplaySecret: takeFirst(seed.DisplaySecret, "secret"),
|
||||
AppID: takeFirst(seed.AppID, uuid.New()),
|
||||
})
|
||||
require.NoError(t, err, "insert oauth2 app secret")
|
||||
return app
|
||||
}
|
||||
|
||||
func must[V any](v V, err error) V {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
|
|
@ -130,6 +130,8 @@ type data struct {
|
|||
groupMembers []database.GroupMember
|
||||
groups []database.Group
|
||||
licenses []database.License
|
||||
oauth2ProviderApps []database.OAuth2ProviderApp
|
||||
oauth2ProviderAppSecrets []database.OAuth2ProviderAppSecret
|
||||
parameterSchemas []database.ParameterSchema
|
||||
provisionerDaemons []database.ProvisionerDaemon
|
||||
provisionerJobLogs []database.ProvisionerJobLog
|
||||
|
@ -1144,6 +1146,43 @@ func (q *FakeQuerier) DeleteLicense(_ context.Context, id int32) (int32, error)
|
|||
return 0, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) DeleteOAuth2ProviderAppByID(_ context.Context, id uuid.UUID) error {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for index, app := range q.oauth2ProviderApps {
|
||||
if app.ID == id {
|
||||
q.oauth2ProviderApps[index] = q.oauth2ProviderApps[len(q.oauth2ProviderApps)-1]
|
||||
q.oauth2ProviderApps = q.oauth2ProviderApps[:len(q.oauth2ProviderApps)-1]
|
||||
|
||||
secrets := []database.OAuth2ProviderAppSecret{}
|
||||
for _, secret := range q.oauth2ProviderAppSecrets {
|
||||
if secret.AppID != id {
|
||||
secrets = append(secrets, secret)
|
||||
}
|
||||
}
|
||||
q.oauth2ProviderAppSecrets = secrets
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) DeleteOAuth2ProviderAppSecretByID(_ context.Context, id uuid.UUID) error {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for index, secret := range q.oauth2ProviderAppSecrets {
|
||||
if secret.ID == id {
|
||||
q.oauth2ProviderAppSecrets[index] = q.oauth2ProviderAppSecrets[len(q.oauth2ProviderAppSecrets)-1]
|
||||
q.oauth2ProviderAppSecrets = q.oauth2ProviderAppSecrets[:len(q.oauth2ProviderAppSecrets)-1]
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) DeleteOldProvisionerDaemons(_ context.Context) error {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
@ -2004,6 +2043,68 @@ func (q *FakeQuerier) GetLogoURL(_ context.Context) (string, error) {
|
|||
return q.logoURL, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetOAuth2ProviderAppByID(_ context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for _, app := range q.oauth2ProviderApps {
|
||||
if app.ID == id {
|
||||
return app, nil
|
||||
}
|
||||
}
|
||||
return database.OAuth2ProviderApp{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetOAuth2ProviderAppSecretByID(_ context.Context, id uuid.UUID) (database.OAuth2ProviderAppSecret, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for _, secret := range q.oauth2ProviderAppSecrets {
|
||||
if secret.ID == id {
|
||||
return secret, nil
|
||||
}
|
||||
}
|
||||
return database.OAuth2ProviderAppSecret{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetOAuth2ProviderAppSecretsByAppID(_ context.Context, appID uuid.UUID) ([]database.OAuth2ProviderAppSecret, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for _, app := range q.oauth2ProviderApps {
|
||||
if app.ID == appID {
|
||||
secrets := []database.OAuth2ProviderAppSecret{}
|
||||
for _, secret := range q.oauth2ProviderAppSecrets {
|
||||
if secret.AppID == appID {
|
||||
secrets = append(secrets, secret)
|
||||
}
|
||||
}
|
||||
|
||||
slices.SortFunc(secrets, func(a, b database.OAuth2ProviderAppSecret) int {
|
||||
if a.CreatedAt.Before(b.CreatedAt) {
|
||||
return -1
|
||||
} else if a.CreatedAt.Equal(b.CreatedAt) {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
})
|
||||
return secrets, nil
|
||||
}
|
||||
}
|
||||
|
||||
return []database.OAuth2ProviderAppSecret{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetOAuth2ProviderApps(_ context.Context) ([]database.OAuth2ProviderApp, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
slices.SortFunc(q.oauth2ProviderApps, func(a, b database.OAuth2ProviderApp) int {
|
||||
return slice.Ascending(a.Name, b.Name)
|
||||
})
|
||||
return q.oauth2ProviderApps, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetOAuthSigningKey(_ context.Context) (string, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
@ -4946,6 +5047,61 @@ func (q *FakeQuerier) InsertMissingGroups(_ context.Context, arg database.Insert
|
|||
return newGroups, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) InsertOAuth2ProviderApp(_ context.Context, arg database.InsertOAuth2ProviderAppParams) (database.OAuth2ProviderApp, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return database.OAuth2ProviderApp{}, err
|
||||
}
|
||||
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for _, app := range q.oauth2ProviderApps {
|
||||
if app.Name == arg.Name {
|
||||
return database.OAuth2ProviderApp{}, errDuplicateKey
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gosimple // Go wants database.OAuth2ProviderApp(arg), but we cannot be sure the structs will remain identical.
|
||||
app := database.OAuth2ProviderApp{
|
||||
ID: arg.ID,
|
||||
CreatedAt: arg.CreatedAt,
|
||||
UpdatedAt: arg.UpdatedAt,
|
||||
Name: arg.Name,
|
||||
Icon: arg.Icon,
|
||||
CallbackURL: arg.CallbackURL,
|
||||
}
|
||||
q.oauth2ProviderApps = append(q.oauth2ProviderApps, app)
|
||||
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) InsertOAuth2ProviderAppSecret(_ context.Context, arg database.InsertOAuth2ProviderAppSecretParams) (database.OAuth2ProviderAppSecret, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return database.OAuth2ProviderAppSecret{}, err
|
||||
}
|
||||
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for _, app := range q.oauth2ProviderApps {
|
||||
if app.ID == arg.AppID {
|
||||
secret := database.OAuth2ProviderAppSecret{
|
||||
ID: arg.ID,
|
||||
CreatedAt: arg.CreatedAt,
|
||||
HashedSecret: arg.HashedSecret,
|
||||
DisplaySecret: arg.DisplaySecret,
|
||||
AppID: arg.AppID,
|
||||
}
|
||||
q.oauth2ProviderAppSecrets = append(q.oauth2ProviderAppSecrets, secret)
|
||||
return secret, nil
|
||||
}
|
||||
}
|
||||
|
||||
return database.OAuth2ProviderAppSecret{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) InsertOrganization(_ context.Context, arg database.InsertOrganizationParams) (database.Organization, error) {
|
||||
if err := validateDatabaseType(arg); err != nil {
|
||||
return database.Organization{}, err
|
||||
|
@ -5947,6 +6103,64 @@ func (q *FakeQuerier) UpdateMemberRoles(_ context.Context, arg database.UpdateMe
|
|||
return database.OrganizationMember{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateOAuth2ProviderAppByID(_ context.Context, arg database.UpdateOAuth2ProviderAppByIDParams) (database.OAuth2ProviderApp, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return database.OAuth2ProviderApp{}, err
|
||||
}
|
||||
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for _, app := range q.oauth2ProviderApps {
|
||||
if app.Name == arg.Name && app.ID != arg.ID {
|
||||
return database.OAuth2ProviderApp{}, errDuplicateKey
|
||||
}
|
||||
}
|
||||
|
||||
for index, app := range q.oauth2ProviderApps {
|
||||
if app.ID == arg.ID {
|
||||
newApp := database.OAuth2ProviderApp{
|
||||
ID: arg.ID,
|
||||
CreatedAt: app.CreatedAt,
|
||||
UpdatedAt: arg.UpdatedAt,
|
||||
Name: arg.Name,
|
||||
Icon: arg.Icon,
|
||||
CallbackURL: arg.CallbackURL,
|
||||
}
|
||||
q.oauth2ProviderApps[index] = newApp
|
||||
return newApp, nil
|
||||
}
|
||||
}
|
||||
return database.OAuth2ProviderApp{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateOAuth2ProviderAppSecretByID(_ context.Context, arg database.UpdateOAuth2ProviderAppSecretByIDParams) (database.OAuth2ProviderAppSecret, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return database.OAuth2ProviderAppSecret{}, err
|
||||
}
|
||||
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for index, secret := range q.oauth2ProviderAppSecrets {
|
||||
if secret.ID == arg.ID {
|
||||
newSecret := database.OAuth2ProviderAppSecret{
|
||||
ID: arg.ID,
|
||||
CreatedAt: secret.CreatedAt,
|
||||
HashedSecret: secret.HashedSecret,
|
||||
DisplaySecret: secret.DisplaySecret,
|
||||
AppID: secret.AppID,
|
||||
LastUsedAt: arg.LastUsedAt,
|
||||
}
|
||||
q.oauth2ProviderAppSecrets[index] = newSecret
|
||||
return newSecret, nil
|
||||
}
|
||||
}
|
||||
return database.OAuth2ProviderAppSecret{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateProvisionerDaemonLastSeenAt(_ context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
|
|
|
@ -218,6 +218,20 @@ func (m metricsStore) DeleteLicense(ctx context.Context, id int32) (int32, error
|
|||
return licenseID, err
|
||||
}
|
||||
|
||||
func (m metricsStore) DeleteOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.DeleteOAuth2ProviderAppByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("DeleteOAuth2ProviderAppByID").Observe(time.Since(start).Seconds())
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m metricsStore) DeleteOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.DeleteOAuth2ProviderAppSecretByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("DeleteOAuth2ProviderAppSecretByID").Observe(time.Since(start).Seconds())
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m metricsStore) DeleteOldProvisionerDaemons(ctx context.Context) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.DeleteOldProvisionerDaemons(ctx)
|
||||
|
@ -566,6 +580,34 @@ func (m metricsStore) GetLogoURL(ctx context.Context) (string, error) {
|
|||
return url, err
|
||||
}
|
||||
|
||||
func (m metricsStore) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetOAuth2ProviderAppByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("GetOAuth2ProviderAppByID").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) GetOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderAppSecret, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetOAuth2ProviderAppSecretByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("GetOAuth2ProviderAppSecretByID").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) GetOAuth2ProviderAppSecretsByAppID(ctx context.Context, appID uuid.UUID) ([]database.OAuth2ProviderAppSecret, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetOAuth2ProviderAppSecretsByAppID(ctx, appID)
|
||||
m.queryLatencies.WithLabelValues("GetOAuth2ProviderAppSecretsByAppID").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) GetOAuth2ProviderApps(ctx context.Context) ([]database.OAuth2ProviderApp, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetOAuth2ProviderApps(ctx)
|
||||
m.queryLatencies.WithLabelValues("GetOAuth2ProviderApps").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) GetOAuthSigningKey(ctx context.Context) (string, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetOAuthSigningKey(ctx)
|
||||
|
@ -1334,6 +1376,20 @@ func (m metricsStore) InsertMissingGroups(ctx context.Context, arg database.Inse
|
|||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) InsertOAuth2ProviderApp(ctx context.Context, arg database.InsertOAuth2ProviderAppParams) (database.OAuth2ProviderApp, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.InsertOAuth2ProviderApp(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("InsertOAuth2ProviderApp").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) InsertOAuth2ProviderAppSecret(ctx context.Context, arg database.InsertOAuth2ProviderAppSecretParams) (database.OAuth2ProviderAppSecret, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.InsertOAuth2ProviderAppSecret(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("InsertOAuth2ProviderAppSecret").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) InsertOrganization(ctx context.Context, arg database.InsertOrganizationParams) (database.Organization, error) {
|
||||
start := time.Now()
|
||||
organization, err := m.s.InsertOrganization(ctx, arg)
|
||||
|
@ -1593,6 +1649,20 @@ func (m metricsStore) UpdateMemberRoles(ctx context.Context, arg database.Update
|
|||
return member, err
|
||||
}
|
||||
|
||||
func (m metricsStore) UpdateOAuth2ProviderAppByID(ctx context.Context, arg database.UpdateOAuth2ProviderAppByIDParams) (database.OAuth2ProviderApp, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateOAuth2ProviderAppByID(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateOAuth2ProviderAppByID").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) UpdateOAuth2ProviderAppSecretByID(ctx context.Context, arg database.UpdateOAuth2ProviderAppSecretByIDParams) (database.OAuth2ProviderAppSecret, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateOAuth2ProviderAppSecretByID(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateOAuth2ProviderAppSecretByID").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpdateProvisionerDaemonLastSeenAt(ctx, arg)
|
||||
|
|
|
@ -323,6 +323,34 @@ func (mr *MockStoreMockRecorder) DeleteLicense(arg0, arg1 interface{}) *gomock.C
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLicense", reflect.TypeOf((*MockStore)(nil).DeleteLicense), arg0, arg1)
|
||||
}
|
||||
|
||||
// DeleteOAuth2ProviderAppByID mocks base method.
|
||||
func (m *MockStore) DeleteOAuth2ProviderAppByID(arg0 context.Context, arg1 uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteOAuth2ProviderAppByID", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeleteOAuth2ProviderAppByID indicates an expected call of DeleteOAuth2ProviderAppByID.
|
||||
func (mr *MockStoreMockRecorder) DeleteOAuth2ProviderAppByID(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOAuth2ProviderAppByID", reflect.TypeOf((*MockStore)(nil).DeleteOAuth2ProviderAppByID), arg0, arg1)
|
||||
}
|
||||
|
||||
// DeleteOAuth2ProviderAppSecretByID mocks base method.
|
||||
func (m *MockStore) DeleteOAuth2ProviderAppSecretByID(arg0 context.Context, arg1 uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteOAuth2ProviderAppSecretByID", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeleteOAuth2ProviderAppSecretByID indicates an expected call of DeleteOAuth2ProviderAppSecretByID.
|
||||
func (mr *MockStoreMockRecorder) DeleteOAuth2ProviderAppSecretByID(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOAuth2ProviderAppSecretByID", reflect.TypeOf((*MockStore)(nil).DeleteOAuth2ProviderAppSecretByID), arg0, arg1)
|
||||
}
|
||||
|
||||
// DeleteOldProvisionerDaemons mocks base method.
|
||||
func (m *MockStore) DeleteOldProvisionerDaemons(arg0 context.Context) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -1113,6 +1141,66 @@ func (mr *MockStoreMockRecorder) GetLogoURL(arg0 interface{}) *gomock.Call {
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogoURL", reflect.TypeOf((*MockStore)(nil).GetLogoURL), arg0)
|
||||
}
|
||||
|
||||
// GetOAuth2ProviderAppByID mocks base method.
|
||||
func (m *MockStore) GetOAuth2ProviderAppByID(arg0 context.Context, arg1 uuid.UUID) (database.OAuth2ProviderApp, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetOAuth2ProviderAppByID", arg0, arg1)
|
||||
ret0, _ := ret[0].(database.OAuth2ProviderApp)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetOAuth2ProviderAppByID indicates an expected call of GetOAuth2ProviderAppByID.
|
||||
func (mr *MockStoreMockRecorder) GetOAuth2ProviderAppByID(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOAuth2ProviderAppByID", reflect.TypeOf((*MockStore)(nil).GetOAuth2ProviderAppByID), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetOAuth2ProviderAppSecretByID mocks base method.
|
||||
func (m *MockStore) GetOAuth2ProviderAppSecretByID(arg0 context.Context, arg1 uuid.UUID) (database.OAuth2ProviderAppSecret, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetOAuth2ProviderAppSecretByID", arg0, arg1)
|
||||
ret0, _ := ret[0].(database.OAuth2ProviderAppSecret)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetOAuth2ProviderAppSecretByID indicates an expected call of GetOAuth2ProviderAppSecretByID.
|
||||
func (mr *MockStoreMockRecorder) GetOAuth2ProviderAppSecretByID(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOAuth2ProviderAppSecretByID", reflect.TypeOf((*MockStore)(nil).GetOAuth2ProviderAppSecretByID), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetOAuth2ProviderAppSecretsByAppID mocks base method.
|
||||
func (m *MockStore) GetOAuth2ProviderAppSecretsByAppID(arg0 context.Context, arg1 uuid.UUID) ([]database.OAuth2ProviderAppSecret, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetOAuth2ProviderAppSecretsByAppID", arg0, arg1)
|
||||
ret0, _ := ret[0].([]database.OAuth2ProviderAppSecret)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetOAuth2ProviderAppSecretsByAppID indicates an expected call of GetOAuth2ProviderAppSecretsByAppID.
|
||||
func (mr *MockStoreMockRecorder) GetOAuth2ProviderAppSecretsByAppID(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOAuth2ProviderAppSecretsByAppID", reflect.TypeOf((*MockStore)(nil).GetOAuth2ProviderAppSecretsByAppID), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetOAuth2ProviderApps mocks base method.
|
||||
func (m *MockStore) GetOAuth2ProviderApps(arg0 context.Context) ([]database.OAuth2ProviderApp, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetOAuth2ProviderApps", arg0)
|
||||
ret0, _ := ret[0].([]database.OAuth2ProviderApp)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetOAuth2ProviderApps indicates an expected call of GetOAuth2ProviderApps.
|
||||
func (mr *MockStoreMockRecorder) GetOAuth2ProviderApps(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOAuth2ProviderApps", reflect.TypeOf((*MockStore)(nil).GetOAuth2ProviderApps), arg0)
|
||||
}
|
||||
|
||||
// GetOAuthSigningKey mocks base method.
|
||||
func (m *MockStore) GetOAuthSigningKey(arg0 context.Context) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -2803,6 +2891,36 @@ func (mr *MockStoreMockRecorder) InsertMissingGroups(arg0, arg1 interface{}) *go
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertMissingGroups", reflect.TypeOf((*MockStore)(nil).InsertMissingGroups), arg0, arg1)
|
||||
}
|
||||
|
||||
// InsertOAuth2ProviderApp mocks base method.
|
||||
func (m *MockStore) InsertOAuth2ProviderApp(arg0 context.Context, arg1 database.InsertOAuth2ProviderAppParams) (database.OAuth2ProviderApp, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "InsertOAuth2ProviderApp", arg0, arg1)
|
||||
ret0, _ := ret[0].(database.OAuth2ProviderApp)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// InsertOAuth2ProviderApp indicates an expected call of InsertOAuth2ProviderApp.
|
||||
func (mr *MockStoreMockRecorder) InsertOAuth2ProviderApp(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertOAuth2ProviderApp", reflect.TypeOf((*MockStore)(nil).InsertOAuth2ProviderApp), arg0, arg1)
|
||||
}
|
||||
|
||||
// InsertOAuth2ProviderAppSecret mocks base method.
|
||||
func (m *MockStore) InsertOAuth2ProviderAppSecret(arg0 context.Context, arg1 database.InsertOAuth2ProviderAppSecretParams) (database.OAuth2ProviderAppSecret, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "InsertOAuth2ProviderAppSecret", arg0, arg1)
|
||||
ret0, _ := ret[0].(database.OAuth2ProviderAppSecret)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// InsertOAuth2ProviderAppSecret indicates an expected call of InsertOAuth2ProviderAppSecret.
|
||||
func (mr *MockStoreMockRecorder) InsertOAuth2ProviderAppSecret(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertOAuth2ProviderAppSecret", reflect.TypeOf((*MockStore)(nil).InsertOAuth2ProviderAppSecret), arg0, arg1)
|
||||
}
|
||||
|
||||
// InsertOrganization mocks base method.
|
||||
func (m *MockStore) InsertOrganization(arg0 context.Context, arg1 database.InsertOrganizationParams) (database.Organization, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -3362,6 +3480,36 @@ func (mr *MockStoreMockRecorder) UpdateMemberRoles(arg0, arg1 interface{}) *gomo
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMemberRoles", reflect.TypeOf((*MockStore)(nil).UpdateMemberRoles), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateOAuth2ProviderAppByID mocks base method.
|
||||
func (m *MockStore) UpdateOAuth2ProviderAppByID(arg0 context.Context, arg1 database.UpdateOAuth2ProviderAppByIDParams) (database.OAuth2ProviderApp, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateOAuth2ProviderAppByID", arg0, arg1)
|
||||
ret0, _ := ret[0].(database.OAuth2ProviderApp)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateOAuth2ProviderAppByID indicates an expected call of UpdateOAuth2ProviderAppByID.
|
||||
func (mr *MockStoreMockRecorder) UpdateOAuth2ProviderAppByID(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOAuth2ProviderAppByID", reflect.TypeOf((*MockStore)(nil).UpdateOAuth2ProviderAppByID), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateOAuth2ProviderAppSecretByID mocks base method.
|
||||
func (m *MockStore) UpdateOAuth2ProviderAppSecretByID(arg0 context.Context, arg1 database.UpdateOAuth2ProviderAppSecretByIDParams) (database.OAuth2ProviderAppSecret, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateOAuth2ProviderAppSecretByID", arg0, arg1)
|
||||
ret0, _ := ret[0].(database.OAuth2ProviderAppSecret)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateOAuth2ProviderAppSecretByID indicates an expected call of UpdateOAuth2ProviderAppSecretByID.
|
||||
func (mr *MockStoreMockRecorder) UpdateOAuth2ProviderAppSecretByID(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOAuth2ProviderAppSecretByID", reflect.TypeOf((*MockStore)(nil).UpdateOAuth2ProviderAppSecretByID), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateProvisionerDaemonLastSeenAt mocks base method.
|
||||
func (m *MockStore) UpdateProvisionerDaemonLastSeenAt(arg0 context.Context, arg1 database.UpdateProvisionerDaemonLastSeenAtParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
|
@ -458,6 +458,28 @@ CREATE SEQUENCE licenses_id_seq
|
|||
|
||||
ALTER SEQUENCE licenses_id_seq OWNED BY licenses.id;
|
||||
|
||||
CREATE TABLE oauth2_provider_app_secrets (
|
||||
id uuid NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
last_used_at timestamp with time zone,
|
||||
hashed_secret bytea NOT NULL,
|
||||
display_secret text NOT NULL,
|
||||
app_id uuid NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN oauth2_provider_app_secrets.display_secret IS 'The tail end of the original secret so secrets can be differentiated.';
|
||||
|
||||
CREATE TABLE oauth2_provider_apps (
|
||||
id uuid NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
name character varying(64) NOT NULL,
|
||||
icon character varying(256) NOT NULL,
|
||||
callback_url text NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON TABLE oauth2_provider_apps IS 'A table used to configure apps that can use Coder as an OAuth2 provider, the reverse of what we are calling external authentication.';
|
||||
|
||||
CREATE TABLE organization_members (
|
||||
user_id uuid NOT NULL,
|
||||
organization_id uuid NOT NULL,
|
||||
|
@ -1270,6 +1292,18 @@ ALTER TABLE ONLY licenses
|
|||
ALTER TABLE ONLY licenses
|
||||
ADD CONSTRAINT licenses_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY oauth2_provider_app_secrets
|
||||
ADD CONSTRAINT oauth2_provider_app_secrets_app_id_hashed_secret_key UNIQUE (app_id, hashed_secret);
|
||||
|
||||
ALTER TABLE ONLY oauth2_provider_app_secrets
|
||||
ADD CONSTRAINT oauth2_provider_app_secrets_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY oauth2_provider_apps
|
||||
ADD CONSTRAINT oauth2_provider_apps_name_key UNIQUE (name);
|
||||
|
||||
ALTER TABLE ONLY oauth2_provider_apps
|
||||
ADD CONSTRAINT oauth2_provider_apps_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY organization_members
|
||||
ADD CONSTRAINT organization_members_pkey PRIMARY KEY (organization_id, user_id);
|
||||
|
||||
|
@ -1496,6 +1530,9 @@ ALTER TABLE ONLY group_members
|
|||
ALTER TABLE ONLY groups
|
||||
ADD CONSTRAINT groups_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY oauth2_provider_app_secrets
|
||||
ADD CONSTRAINT oauth2_provider_app_secrets_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY organization_members
|
||||
ADD CONSTRAINT organization_members_organization_id_uuid_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ const (
|
|||
ForeignKeyGroupMembersGroupID ForeignKeyConstraint = "group_members_group_id_fkey" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE;
|
||||
ForeignKeyGroupMembersUserID ForeignKeyConstraint = "group_members_user_id_fkey" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ForeignKeyGroupsOrganizationID ForeignKeyConstraint = "groups_organization_id_fkey" // ALTER TABLE ONLY groups ADD CONSTRAINT groups_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
ForeignKeyOauth2ProviderAppSecretsAppID ForeignKeyConstraint = "oauth2_provider_app_secrets_app_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_secrets ADD CONSTRAINT oauth2_provider_app_secrets_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE;
|
||||
ForeignKeyOrganizationMembersOrganizationIDUUID ForeignKeyConstraint = "organization_members_organization_id_uuid_fkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_organization_id_uuid_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
ForeignKeyOrganizationMembersUserIDUUID ForeignKeyConstraint = "organization_members_user_id_uuid_fkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ForeignKeyParameterSchemasJobID ForeignKeyConstraint = "parameter_schemas_job_id_fkey" // ALTER TABLE ONLY parameter_schemas ADD CONSTRAINT parameter_schemas_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE;
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
DROP TABLE oauth2_provider_app_secrets;
|
||||
DROP TABLE oauth2_provider_apps;
|
|
@ -0,0 +1,25 @@
|
|||
CREATE TABLE oauth2_provider_apps (
|
||||
id uuid NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
name varchar(64) NOT NULL,
|
||||
icon varchar(256) NOT NULL,
|
||||
callback_url text NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE(name)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE oauth2_provider_apps IS 'A table used to configure apps that can use Coder as an OAuth2 provider, the reverse of what we are calling external authentication.';
|
||||
|
||||
CREATE TABLE oauth2_provider_app_secrets (
|
||||
id uuid NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
last_used_at timestamp with time zone NULL,
|
||||
hashed_secret bytea NOT NULL,
|
||||
display_secret text NOT NULL,
|
||||
app_id uuid NOT NULL REFERENCES oauth2_provider_apps (id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE(app_id, hashed_secret)
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN oauth2_provider_app_secrets.display_secret IS 'The tail end of the original secret so secrets can be differentiated.';
|
|
@ -0,0 +1,21 @@
|
|||
INSERT INTO oauth2_provider_apps
|
||||
(id, created_at, updated_at, name, icon, callback_url)
|
||||
VALUES (
|
||||
'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',
|
||||
'2023-06-15 10:23:54+00',
|
||||
'2023-06-15 10:23:54+00',
|
||||
'oauth2-app',
|
||||
'/some/icon.svg',
|
||||
'http://coder.com/oauth2/callback'
|
||||
);
|
||||
|
||||
INSERT INTO oauth2_provider_app_secrets
|
||||
(id, created_at, last_used_at, hashed_secret, display_secret, app_id)
|
||||
VALUES (
|
||||
'b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',
|
||||
'2023-06-15 10:25:33+00',
|
||||
'2023-12-15 11:40:20+00',
|
||||
CAST('abcdefg' AS bytea),
|
||||
'fg',
|
||||
'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'
|
||||
);
|
|
@ -1788,6 +1788,26 @@ type License struct {
|
|||
UUID uuid.UUID `db:"uuid" json:"uuid"`
|
||||
}
|
||||
|
||||
// A table used to configure apps that can use Coder as an OAuth2 provider, the reverse of what we are calling external authentication.
|
||||
type OAuth2ProviderApp struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Icon string `db:"icon" json:"icon"`
|
||||
CallbackURL string `db:"callback_url" json:"callback_url"`
|
||||
}
|
||||
|
||||
type OAuth2ProviderAppSecret struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
LastUsedAt sql.NullTime `db:"last_used_at" json:"last_used_at"`
|
||||
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
|
||||
// The tail end of the original secret so secrets can be differentiated.
|
||||
DisplaySecret string `db:"display_secret" json:"display_secret"`
|
||||
AppID uuid.UUID `db:"app_id" json:"app_id"`
|
||||
}
|
||||
|
||||
type Organization struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
|
|
|
@ -57,6 +57,8 @@ type sqlcQuerier interface {
|
|||
DeleteGroupMemberFromGroup(ctx context.Context, arg DeleteGroupMemberFromGroupParams) error
|
||||
DeleteGroupMembersByOrgAndUser(ctx context.Context, arg DeleteGroupMembersByOrgAndUserParams) error
|
||||
DeleteLicense(ctx context.Context, id int32) (int32, error)
|
||||
DeleteOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) error
|
||||
DeleteOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) error
|
||||
// Delete provisioner daemons that have been created at least a week ago
|
||||
// and have not connected to coderd since a week.
|
||||
// A provisioner daemon with "zeroed" last_seen_at column indicates possible
|
||||
|
@ -122,6 +124,10 @@ type sqlcQuerier interface {
|
|||
GetLicenseByID(ctx context.Context, id int32) (License, error)
|
||||
GetLicenses(ctx context.Context) ([]License, error)
|
||||
GetLogoURL(ctx context.Context) (string, error)
|
||||
GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderApp, error)
|
||||
GetOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppSecret, error)
|
||||
GetOAuth2ProviderAppSecretsByAppID(ctx context.Context, appID uuid.UUID) ([]OAuth2ProviderAppSecret, error)
|
||||
GetOAuth2ProviderApps(ctx context.Context) ([]OAuth2ProviderApp, error)
|
||||
GetOAuthSigningKey(ctx context.Context) (string, error)
|
||||
GetOrganizationByID(ctx context.Context, id uuid.UUID) (Organization, error)
|
||||
GetOrganizationByName(ctx context.Context, name string) (Organization, error)
|
||||
|
@ -275,6 +281,8 @@ type sqlcQuerier interface {
|
|||
// values for avatar, display name, and quota allowance (all zero values).
|
||||
// If the name conflicts, do nothing.
|
||||
InsertMissingGroups(ctx context.Context, arg InsertMissingGroupsParams) ([]Group, error)
|
||||
InsertOAuth2ProviderApp(ctx context.Context, arg InsertOAuth2ProviderAppParams) (OAuth2ProviderApp, error)
|
||||
InsertOAuth2ProviderAppSecret(ctx context.Context, arg InsertOAuth2ProviderAppSecretParams) (OAuth2ProviderAppSecret, error)
|
||||
InsertOrganization(ctx context.Context, arg InsertOrganizationParams) (Organization, error)
|
||||
InsertOrganizationMember(ctx context.Context, arg InsertOrganizationMemberParams) (OrganizationMember, error)
|
||||
InsertProvisionerJob(ctx context.Context, arg InsertProvisionerJobParams) (ProvisionerJob, error)
|
||||
|
@ -318,6 +326,8 @@ type sqlcQuerier interface {
|
|||
UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error)
|
||||
UpdateInactiveUsersToDormant(ctx context.Context, arg UpdateInactiveUsersToDormantParams) ([]UpdateInactiveUsersToDormantRow, error)
|
||||
UpdateMemberRoles(ctx context.Context, arg UpdateMemberRolesParams) (OrganizationMember, error)
|
||||
UpdateOAuth2ProviderAppByID(ctx context.Context, arg UpdateOAuth2ProviderAppByIDParams) (OAuth2ProviderApp, error)
|
||||
UpdateOAuth2ProviderAppSecretByID(ctx context.Context, arg UpdateOAuth2ProviderAppSecretByIDParams) (OAuth2ProviderAppSecret, error)
|
||||
UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg UpdateProvisionerDaemonLastSeenAtParams) error
|
||||
UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error
|
||||
UpdateProvisionerJobWithCancelByID(ctx context.Context, arg UpdateProvisionerJobWithCancelByIDParams) error
|
||||
|
|
|
@ -2610,6 +2610,282 @@ func (q *sqlQuerier) TryAcquireLock(ctx context.Context, pgTryAdvisoryXactLock i
|
|||
return pg_try_advisory_xact_lock, err
|
||||
}
|
||||
|
||||
const deleteOAuth2ProviderAppByID = `-- name: DeleteOAuth2ProviderAppByID :exec
|
||||
DELETE FROM oauth2_provider_apps WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) DeleteOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteOAuth2ProviderAppByID, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteOAuth2ProviderAppSecretByID = `-- name: DeleteOAuth2ProviderAppSecretByID :exec
|
||||
DELETE FROM oauth2_provider_app_secrets WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) DeleteOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteOAuth2ProviderAppSecretByID, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getOAuth2ProviderAppByID = `-- name: GetOAuth2ProviderAppByID :one
|
||||
SELECT id, created_at, updated_at, name, icon, callback_url FROM oauth2_provider_apps WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderApp, error) {
|
||||
row := q.db.QueryRowContext(ctx, getOAuth2ProviderAppByID, id)
|
||||
var i OAuth2ProviderApp
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Name,
|
||||
&i.Icon,
|
||||
&i.CallbackURL,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getOAuth2ProviderAppSecretByID = `-- name: GetOAuth2ProviderAppSecretByID :one
|
||||
SELECT id, created_at, last_used_at, hashed_secret, display_secret, app_id FROM oauth2_provider_app_secrets WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppSecret, error) {
|
||||
row := q.db.QueryRowContext(ctx, getOAuth2ProviderAppSecretByID, id)
|
||||
var i OAuth2ProviderAppSecret
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.LastUsedAt,
|
||||
&i.HashedSecret,
|
||||
&i.DisplaySecret,
|
||||
&i.AppID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getOAuth2ProviderAppSecretsByAppID = `-- name: GetOAuth2ProviderAppSecretsByAppID :many
|
||||
SELECT id, created_at, last_used_at, hashed_secret, display_secret, app_id FROM oauth2_provider_app_secrets WHERE app_id = $1 ORDER BY (created_at, id) ASC
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetOAuth2ProviderAppSecretsByAppID(ctx context.Context, appID uuid.UUID) ([]OAuth2ProviderAppSecret, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getOAuth2ProviderAppSecretsByAppID, appID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []OAuth2ProviderAppSecret
|
||||
for rows.Next() {
|
||||
var i OAuth2ProviderAppSecret
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.LastUsedAt,
|
||||
&i.HashedSecret,
|
||||
&i.DisplaySecret,
|
||||
&i.AppID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getOAuth2ProviderApps = `-- name: GetOAuth2ProviderApps :many
|
||||
SELECT id, created_at, updated_at, name, icon, callback_url FROM oauth2_provider_apps ORDER BY (name, id) ASC
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetOAuth2ProviderApps(ctx context.Context) ([]OAuth2ProviderApp, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getOAuth2ProviderApps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []OAuth2ProviderApp
|
||||
for rows.Next() {
|
||||
var i OAuth2ProviderApp
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Name,
|
||||
&i.Icon,
|
||||
&i.CallbackURL,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const insertOAuth2ProviderApp = `-- name: InsertOAuth2ProviderApp :one
|
||||
INSERT INTO oauth2_provider_apps (
|
||||
id,
|
||||
created_at,
|
||||
updated_at,
|
||||
name,
|
||||
icon,
|
||||
callback_url
|
||||
) VALUES(
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6
|
||||
) RETURNING id, created_at, updated_at, name, icon, callback_url
|
||||
`
|
||||
|
||||
type InsertOAuth2ProviderAppParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Icon string `db:"icon" json:"icon"`
|
||||
CallbackURL string `db:"callback_url" json:"callback_url"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertOAuth2ProviderApp(ctx context.Context, arg InsertOAuth2ProviderAppParams) (OAuth2ProviderApp, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertOAuth2ProviderApp,
|
||||
arg.ID,
|
||||
arg.CreatedAt,
|
||||
arg.UpdatedAt,
|
||||
arg.Name,
|
||||
arg.Icon,
|
||||
arg.CallbackURL,
|
||||
)
|
||||
var i OAuth2ProviderApp
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Name,
|
||||
&i.Icon,
|
||||
&i.CallbackURL,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const insertOAuth2ProviderAppSecret = `-- name: InsertOAuth2ProviderAppSecret :one
|
||||
INSERT INTO oauth2_provider_app_secrets (
|
||||
id,
|
||||
created_at,
|
||||
hashed_secret,
|
||||
display_secret,
|
||||
app_id
|
||||
) VALUES(
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5
|
||||
) RETURNING id, created_at, last_used_at, hashed_secret, display_secret, app_id
|
||||
`
|
||||
|
||||
type InsertOAuth2ProviderAppSecretParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
|
||||
DisplaySecret string `db:"display_secret" json:"display_secret"`
|
||||
AppID uuid.UUID `db:"app_id" json:"app_id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertOAuth2ProviderAppSecret(ctx context.Context, arg InsertOAuth2ProviderAppSecretParams) (OAuth2ProviderAppSecret, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertOAuth2ProviderAppSecret,
|
||||
arg.ID,
|
||||
arg.CreatedAt,
|
||||
arg.HashedSecret,
|
||||
arg.DisplaySecret,
|
||||
arg.AppID,
|
||||
)
|
||||
var i OAuth2ProviderAppSecret
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.LastUsedAt,
|
||||
&i.HashedSecret,
|
||||
&i.DisplaySecret,
|
||||
&i.AppID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateOAuth2ProviderAppByID = `-- name: UpdateOAuth2ProviderAppByID :one
|
||||
UPDATE oauth2_provider_apps SET
|
||||
updated_at = $2,
|
||||
name = $3,
|
||||
icon = $4,
|
||||
callback_url = $5
|
||||
WHERE id = $1 RETURNING id, created_at, updated_at, name, icon, callback_url
|
||||
`
|
||||
|
||||
type UpdateOAuth2ProviderAppByIDParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Icon string `db:"icon" json:"icon"`
|
||||
CallbackURL string `db:"callback_url" json:"callback_url"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateOAuth2ProviderAppByID(ctx context.Context, arg UpdateOAuth2ProviderAppByIDParams) (OAuth2ProviderApp, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateOAuth2ProviderAppByID,
|
||||
arg.ID,
|
||||
arg.UpdatedAt,
|
||||
arg.Name,
|
||||
arg.Icon,
|
||||
arg.CallbackURL,
|
||||
)
|
||||
var i OAuth2ProviderApp
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Name,
|
||||
&i.Icon,
|
||||
&i.CallbackURL,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateOAuth2ProviderAppSecretByID = `-- name: UpdateOAuth2ProviderAppSecretByID :one
|
||||
UPDATE oauth2_provider_app_secrets SET
|
||||
last_used_at = $2
|
||||
WHERE id = $1 RETURNING id, created_at, last_used_at, hashed_secret, display_secret, app_id
|
||||
`
|
||||
|
||||
type UpdateOAuth2ProviderAppSecretByIDParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
LastUsedAt sql.NullTime `db:"last_used_at" json:"last_used_at"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateOAuth2ProviderAppSecretByID(ctx context.Context, arg UpdateOAuth2ProviderAppSecretByIDParams) (OAuth2ProviderAppSecret, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateOAuth2ProviderAppSecretByID, arg.ID, arg.LastUsedAt)
|
||||
var i OAuth2ProviderAppSecret
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.LastUsedAt,
|
||||
&i.HashedSecret,
|
||||
&i.DisplaySecret,
|
||||
&i.AppID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getOrganizationIDsByMemberIDs = `-- name: GetOrganizationIDsByMemberIDs :many
|
||||
SELECT
|
||||
user_id, array_agg(organization_id) :: uuid [ ] AS "organization_IDs"
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
-- name: GetOAuth2ProviderApps :many
|
||||
SELECT * FROM oauth2_provider_apps ORDER BY (name, id) ASC;
|
||||
|
||||
-- name: GetOAuth2ProviderAppByID :one
|
||||
SELECT * FROM oauth2_provider_apps WHERE id = $1;
|
||||
|
||||
-- name: InsertOAuth2ProviderApp :one
|
||||
INSERT INTO oauth2_provider_apps (
|
||||
id,
|
||||
created_at,
|
||||
updated_at,
|
||||
name,
|
||||
icon,
|
||||
callback_url
|
||||
) VALUES(
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6
|
||||
) RETURNING *;
|
||||
|
||||
-- name: UpdateOAuth2ProviderAppByID :one
|
||||
UPDATE oauth2_provider_apps SET
|
||||
updated_at = $2,
|
||||
name = $3,
|
||||
icon = $4,
|
||||
callback_url = $5
|
||||
WHERE id = $1 RETURNING *;
|
||||
|
||||
-- name: DeleteOAuth2ProviderAppByID :exec
|
||||
DELETE FROM oauth2_provider_apps WHERE id = $1;
|
||||
|
||||
-- name: GetOAuth2ProviderAppSecretByID :one
|
||||
SELECT * FROM oauth2_provider_app_secrets WHERE id = $1;
|
||||
|
||||
-- name: GetOAuth2ProviderAppSecretsByAppID :many
|
||||
SELECT * FROM oauth2_provider_app_secrets WHERE app_id = $1 ORDER BY (created_at, id) ASC;
|
||||
|
||||
-- name: InsertOAuth2ProviderAppSecret :one
|
||||
INSERT INTO oauth2_provider_app_secrets (
|
||||
id,
|
||||
created_at,
|
||||
hashed_secret,
|
||||
display_secret,
|
||||
app_id
|
||||
) VALUES(
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5
|
||||
) RETURNING *;
|
||||
|
||||
-- name: UpdateOAuth2ProviderAppSecretByID :one
|
||||
UPDATE oauth2_provider_app_secrets SET
|
||||
last_used_at = $2
|
||||
WHERE id = $1 RETURNING *;
|
||||
|
||||
-- name: DeleteOAuth2ProviderAppSecretByID :exec
|
||||
DELETE FROM oauth2_provider_app_secrets WHERE id = $1;
|
|
@ -80,6 +80,9 @@ overrides:
|
|||
template_ids: TemplateIDs
|
||||
active_user_ids: ActiveUserIDs
|
||||
display_app_ssh_helper: DisplayAppSSHHelper
|
||||
oauth2_provider_app: OAuth2ProviderApp
|
||||
oauth2_provider_app_secret: OAuth2ProviderAppSecret
|
||||
callback_url: CallbackURL
|
||||
|
||||
sql:
|
||||
- schema: "./dump.sql"
|
||||
|
|
|
@ -21,6 +21,10 @@ const (
|
|||
UniqueGroupsPkey UniqueConstraint = "groups_pkey" // ALTER TABLE ONLY groups ADD CONSTRAINT groups_pkey PRIMARY KEY (id);
|
||||
UniqueLicensesJWTKey UniqueConstraint = "licenses_jwt_key" // ALTER TABLE ONLY licenses ADD CONSTRAINT licenses_jwt_key UNIQUE (jwt);
|
||||
UniqueLicensesPkey UniqueConstraint = "licenses_pkey" // ALTER TABLE ONLY licenses ADD CONSTRAINT licenses_pkey PRIMARY KEY (id);
|
||||
UniqueOauth2ProviderAppSecretsAppIDHashedSecretKey UniqueConstraint = "oauth2_provider_app_secrets_app_id_hashed_secret_key" // ALTER TABLE ONLY oauth2_provider_app_secrets ADD CONSTRAINT oauth2_provider_app_secrets_app_id_hashed_secret_key UNIQUE (app_id, hashed_secret);
|
||||
UniqueOauth2ProviderAppSecretsPkey UniqueConstraint = "oauth2_provider_app_secrets_pkey" // ALTER TABLE ONLY oauth2_provider_app_secrets ADD CONSTRAINT oauth2_provider_app_secrets_pkey PRIMARY KEY (id);
|
||||
UniqueOauth2ProviderAppsNameKey UniqueConstraint = "oauth2_provider_apps_name_key" // ALTER TABLE ONLY oauth2_provider_apps ADD CONSTRAINT oauth2_provider_apps_name_key UNIQUE (name);
|
||||
UniqueOauth2ProviderAppsPkey UniqueConstraint = "oauth2_provider_apps_pkey" // ALTER TABLE ONLY oauth2_provider_apps ADD CONSTRAINT oauth2_provider_apps_pkey PRIMARY KEY (id);
|
||||
UniqueOrganizationMembersPkey UniqueConstraint = "organization_members_pkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_pkey PRIMARY KEY (organization_id, user_id);
|
||||
UniqueOrganizationsPkey UniqueConstraint = "organizations_pkey" // ALTER TABLE ONLY organizations ADD CONSTRAINT organizations_pkey PRIMARY KEY (id);
|
||||
UniqueParameterSchemasJobIDNameKey UniqueConstraint = "parameter_schemas_job_id_name_key" // ALTER TABLE ONLY parameter_schemas ADD CONSTRAINT parameter_schemas_job_id_name_key UNIQUE (job_id, name);
|
||||
|
|
|
@ -46,7 +46,7 @@ func init() {
|
|||
valid := NameValid(str)
|
||||
return valid == nil
|
||||
}
|
||||
for _, tag := range []string{"username", "template_name", "workspace_name"} {
|
||||
for _, tag := range []string{"username", "template_name", "workspace_name", "oauth2_app_name"} {
|
||||
err := Validate.RegisterValidation(tag, nameValidator)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/cryptorand"
|
||||
|
@ -178,3 +179,96 @@ func ExtractOAuth2(config OAuth2Config, client *http.Client, authURLOpts map[str
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
type (
|
||||
oauth2ProviderAppParamContextKey struct{}
|
||||
oauth2ProviderAppSecretParamContextKey struct{}
|
||||
)
|
||||
|
||||
// OAuth2ProviderApp returns the OAuth2 app from the ExtractOAuth2ProviderAppParam handler.
|
||||
func OAuth2ProviderApp(r *http.Request) database.OAuth2ProviderApp {
|
||||
app, ok := r.Context().Value(oauth2ProviderAppParamContextKey{}).(database.OAuth2ProviderApp)
|
||||
if !ok {
|
||||
panic("developer error: oauth2 app param middleware not provided")
|
||||
}
|
||||
return app
|
||||
}
|
||||
|
||||
// ExtractOAuth2ProviderApp grabs an OAuth2 app from the "app" URL parameter. This
|
||||
// middleware requires the API key middleware higher in the call stack for
|
||||
// authentication.
|
||||
func ExtractOAuth2ProviderApp(db database.Store) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
appID, ok := ParseUUIDParam(rw, r, "app")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
app, err := db.GetOAuth2ProviderAppByID(ctx, appID)
|
||||
if httpapi.Is404Error(err) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching OAuth2 app.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
ctx = context.WithValue(ctx, oauth2ProviderAppParamContextKey{}, app)
|
||||
next.ServeHTTP(rw, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// OAuth2ProviderAppSecret returns the OAuth2 app secret from the
|
||||
// ExtractOAuth2ProviderAppSecretParam handler.
|
||||
func OAuth2ProviderAppSecret(r *http.Request) database.OAuth2ProviderAppSecret {
|
||||
app, ok := r.Context().Value(oauth2ProviderAppSecretParamContextKey{}).(database.OAuth2ProviderAppSecret)
|
||||
if !ok {
|
||||
panic("developer error: oauth2 app secret param middleware not provided")
|
||||
}
|
||||
return app
|
||||
}
|
||||
|
||||
// ExtractOAuth2ProviderAppSecret grabs an OAuth2 app secret from the "app" and
|
||||
// "secret" URL parameters. This middleware requires the ExtractOAuth2ProviderApp
|
||||
// middleware higher in the stack
|
||||
func ExtractOAuth2ProviderAppSecret(db database.Store) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
secretID, ok := ParseUUIDParam(rw, r, "secretID")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
app := OAuth2ProviderApp(r)
|
||||
secret, err := db.GetOAuth2ProviderAppSecretByID(ctx, secretID)
|
||||
if httpapi.Is404Error(err) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching OAuth2 app secret.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
// If the user can read the secret they can probably also read the app it
|
||||
// belongs to and they can read this app as well, so it seems safe to give
|
||||
// them a more helpful message than a 404 on mismatches.
|
||||
if app.ID != secret.AppID {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "App ID does not match secret app ID.",
|
||||
})
|
||||
return
|
||||
}
|
||||
ctx = context.WithValue(ctx, oauth2ProviderAppSecretParamContextKey{}, secret)
|
||||
next.ServeHTTP(rw, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -198,6 +198,22 @@ var (
|
|||
ResourceTemplateInsights = Object{
|
||||
Type: "template_insights",
|
||||
}
|
||||
|
||||
// ResourceOAuth2ProviderApp CRUD.
|
||||
// create/delete = Make or delete an OAuth2 app.
|
||||
// update = Update the properties of the OAuth2 app.
|
||||
// read = Read OAuth2 apps.
|
||||
ResourceOAuth2ProviderApp = Object{
|
||||
Type: "oauth2_app",
|
||||
}
|
||||
|
||||
// ResourceOAuth2ProviderAppSecrets CRUD.
|
||||
// create/delete = Make or delete an OAuth2 app secret.
|
||||
// update = Update last used date.
|
||||
// read = Read OAuth2 app hashed or truncated secret.
|
||||
ResourceOAuth2ProviderAppSecret = Object{
|
||||
Type: "oauth2_app_secrets",
|
||||
}
|
||||
)
|
||||
|
||||
// ResourceUserObject is a helper function to create a user object for authz checks.
|
||||
|
|
|
@ -11,6 +11,8 @@ func AllResources() []Object {
|
|||
ResourceFile,
|
||||
ResourceGroup,
|
||||
ResourceLicense,
|
||||
ResourceOAuth2ProviderApp,
|
||||
ResourceOAuth2ProviderAppSecret,
|
||||
ResourceOrgRoleAssignment,
|
||||
ResourceOrganization,
|
||||
ResourceOrganizationMember,
|
||||
|
|
|
@ -50,6 +50,7 @@ const (
|
|||
FeatureExternalTokenEncryption FeatureName = "external_token_encryption"
|
||||
FeatureWorkspaceBatchActions FeatureName = "workspace_batch_actions"
|
||||
FeatureAccessControl FeatureName = "access_control"
|
||||
FeatureOAuth2Provider FeatureName = "oauth2_provider"
|
||||
)
|
||||
|
||||
// FeatureNames must be kept in-sync with the Feature enum above.
|
||||
|
@ -69,6 +70,7 @@ var FeatureNames = []FeatureName{
|
|||
FeatureExternalTokenEncryption,
|
||||
FeatureWorkspaceBatchActions,
|
||||
FeatureAccessControl,
|
||||
FeatureOAuth2Provider,
|
||||
}
|
||||
|
||||
// Humanize returns the feature name in a human-readable format.
|
||||
|
@ -78,6 +80,8 @@ func (n FeatureName) Humanize() string {
|
|||
return "Template RBAC"
|
||||
case FeatureSCIM:
|
||||
return "SCIM"
|
||||
case FeatureOAuth2Provider:
|
||||
return "OAuth Provider"
|
||||
default:
|
||||
return strings.Title(strings.ReplaceAll(string(n), "_", " "))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
package codersdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type OAuth2ProviderApp struct {
|
||||
ID uuid.UUID `json:"id" format:"uuid"`
|
||||
Name string `json:"name"`
|
||||
CallbackURL string `json:"callback_url"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
|
||||
// OAuth2ProviderApps returns the applications configured to authenticate using
|
||||
// Coder as an OAuth2 provider.
|
||||
func (c *Client) OAuth2ProviderApps(ctx context.Context) ([]OAuth2ProviderApp, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, "/api/v2/oauth2-provider/apps", nil)
|
||||
if err != nil {
|
||||
return []OAuth2ProviderApp{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return []OAuth2ProviderApp{}, ReadBodyAsError(res)
|
||||
}
|
||||
var apps []OAuth2ProviderApp
|
||||
return apps, json.NewDecoder(res.Body).Decode(&apps)
|
||||
}
|
||||
|
||||
// OAuth2ProviderApp returns an application configured to authenticate using
|
||||
// Coder as an OAuth2 provider.
|
||||
func (c *Client) OAuth2ProviderApp(ctx context.Context, id uuid.UUID) (OAuth2ProviderApp, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/oauth2-provider/apps/%s", id), nil)
|
||||
if err != nil {
|
||||
return OAuth2ProviderApp{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return OAuth2ProviderApp{}, ReadBodyAsError(res)
|
||||
}
|
||||
var apps OAuth2ProviderApp
|
||||
return apps, json.NewDecoder(res.Body).Decode(&apps)
|
||||
}
|
||||
|
||||
type PostOAuth2ProviderAppRequest struct {
|
||||
Name string `json:"name" validate:"required,oauth2_app_name"`
|
||||
CallbackURL string `json:"callback_url" validate:"required,http_url"`
|
||||
Icon string `json:"icon" validate:"omitempty"`
|
||||
}
|
||||
|
||||
// PostOAuth2ProviderApp adds an application that can authenticate using Coder
|
||||
// as an OAuth2 provider.
|
||||
func (c *Client) PostOAuth2ProviderApp(ctx context.Context, app PostOAuth2ProviderAppRequest) (OAuth2ProviderApp, error) {
|
||||
res, err := c.Request(ctx, http.MethodPost, "/api/v2/oauth2-provider/apps", app)
|
||||
if err != nil {
|
||||
return OAuth2ProviderApp{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return OAuth2ProviderApp{}, ReadBodyAsError(res)
|
||||
}
|
||||
var resp OAuth2ProviderApp
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
||||
type PutOAuth2ProviderAppRequest struct {
|
||||
Name string `json:"name" validate:"required,oauth2_app_name"`
|
||||
CallbackURL string `json:"callback_url" validate:"required,http_url"`
|
||||
Icon string `json:"icon" validate:"omitempty"`
|
||||
}
|
||||
|
||||
// PutOAuth2ProviderApp updates an application that can authenticate using Coder
|
||||
// as an OAuth2 provider.
|
||||
func (c *Client) PutOAuth2ProviderApp(ctx context.Context, id uuid.UUID, app PutOAuth2ProviderAppRequest) (OAuth2ProviderApp, error) {
|
||||
res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/oauth2-provider/apps/%s", id), app)
|
||||
if err != nil {
|
||||
return OAuth2ProviderApp{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return OAuth2ProviderApp{}, ReadBodyAsError(res)
|
||||
}
|
||||
var resp OAuth2ProviderApp
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
||||
// DeleteOAuth2ProviderApp deletes an application, also invalidating any tokens
|
||||
// that were generated from it.
|
||||
func (c *Client) DeleteOAuth2ProviderApp(ctx context.Context, id uuid.UUID) error {
|
||||
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/oauth2-provider/apps/%s", id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusNoContent {
|
||||
return ReadBodyAsError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type OAuth2ProviderAppSecretFull struct {
|
||||
ID uuid.UUID `json:"id" format:"uuid"`
|
||||
ClientSecretFull string `json:"client_secret_full"`
|
||||
}
|
||||
|
||||
type OAuth2ProviderAppSecret struct {
|
||||
ID uuid.UUID `json:"id" format:"uuid"`
|
||||
LastUsedAt NullTime `json:"last_used_at"`
|
||||
ClientSecretTruncated string `json:"client_secret_truncated"`
|
||||
}
|
||||
|
||||
// OAuth2ProviderAppSecrets returns the truncated secrets for an OAuth2
|
||||
// application.
|
||||
func (c *Client) OAuth2ProviderAppSecrets(ctx context.Context, appID uuid.UUID) ([]OAuth2ProviderAppSecret, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/oauth2-provider/apps/%s/secrets", appID), nil)
|
||||
if err != nil {
|
||||
return []OAuth2ProviderAppSecret{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return []OAuth2ProviderAppSecret{}, ReadBodyAsError(res)
|
||||
}
|
||||
var resp []OAuth2ProviderAppSecret
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
||||
// PostOAuth2ProviderAppSecret creates a new secret for an OAuth2 application.
|
||||
// This is the only time the full secret will be revealed.
|
||||
func (c *Client) PostOAuth2ProviderAppSecret(ctx context.Context, appID uuid.UUID) (OAuth2ProviderAppSecretFull, error) {
|
||||
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/oauth2-provider/apps/%s/secrets", appID), nil)
|
||||
if err != nil {
|
||||
return OAuth2ProviderAppSecretFull{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return OAuth2ProviderAppSecretFull{}, ReadBodyAsError(res)
|
||||
}
|
||||
var resp OAuth2ProviderAppSecretFull
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
||||
// DeleteOAuth2ProviderAppSecret deletes a secret from an OAuth2 application,
|
||||
// also invalidating any tokens that generated from it.
|
||||
func (c *Client) DeleteOAuth2ProviderAppSecret(ctx context.Context, appID uuid.UUID, secretID uuid.UUID) error {
|
||||
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/oauth2-provider/apps/%s/secrets/%s", appID, secretID), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusNoContent {
|
||||
return ReadBodyAsError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -430,6 +430,352 @@ curl -X DELETE http://coder-server:8080/api/v2/licenses/{id} \
|
|||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Get OAuth2 applications.
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X GET http://coder-server:8080/api/v2/oauth2-provider/apps \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`GET /oauth2-provider/apps`
|
||||
|
||||
### Example responses
|
||||
|
||||
> 200 Response
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"callback_url": "string",
|
||||
"icon": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"name": "string"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
| ------ | ------------------------------------------------------- | ----------- | --------------------------------------------------------------------------- |
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.OAuth2ProviderApp](schemas.md#codersdkoauth2providerapp) |
|
||||
|
||||
<h3 id="get-oauth2-applications.-responseschema">Response Schema</h3>
|
||||
|
||||
Status Code **200**
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ---------------- | ------------ | -------- | ------------ | ----------- |
|
||||
| `[array item]` | array | false | | |
|
||||
| `» callback_url` | string | false | | |
|
||||
| `» icon` | string | false | | |
|
||||
| `» id` | string(uuid) | false | | |
|
||||
| `» name` | string | false | | |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Create OAuth2 application.
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X POST http://coder-server:8080/api/v2/oauth2-provider/apps \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`POST /oauth2-provider/apps`
|
||||
|
||||
> Body parameter
|
||||
|
||||
```json
|
||||
{
|
||||
"callback_url": "string",
|
||||
"icon": "string",
|
||||
"name": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
| ------ | ---- | ---------------------------------------------------------------------------------------- | -------- | --------------------------------- |
|
||||
| `body` | body | [codersdk.PostOAuth2ProviderAppRequest](schemas.md#codersdkpostoauth2providerapprequest) | true | The OAuth2 application to create. |
|
||||
|
||||
### Example responses
|
||||
|
||||
> 200 Response
|
||||
|
||||
```json
|
||||
{
|
||||
"callback_url": "string",
|
||||
"icon": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"name": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------------ |
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.OAuth2ProviderApp](schemas.md#codersdkoauth2providerapp) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Get OAuth2 application.
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X GET http://coder-server:8080/api/v2/oauth2-provider/apps/{app} \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`GET /oauth2-provider/apps/{app}`
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
| ----- | ---- | ------ | -------- | ----------- |
|
||||
| `app` | path | string | true | App ID |
|
||||
|
||||
### Example responses
|
||||
|
||||
> 200 Response
|
||||
|
||||
```json
|
||||
{
|
||||
"callback_url": "string",
|
||||
"icon": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"name": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------------ |
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.OAuth2ProviderApp](schemas.md#codersdkoauth2providerapp) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Update OAuth2 application.
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X PUT http://coder-server:8080/api/v2/oauth2-provider/apps/{app} \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`PUT /oauth2-provider/apps/{app}`
|
||||
|
||||
> Body parameter
|
||||
|
||||
```json
|
||||
{
|
||||
"callback_url": "string",
|
||||
"icon": "string",
|
||||
"name": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
| ------ | ---- | -------------------------------------------------------------------------------------- | -------- | ----------------------------- |
|
||||
| `app` | path | string | true | App ID |
|
||||
| `body` | body | [codersdk.PutOAuth2ProviderAppRequest](schemas.md#codersdkputoauth2providerapprequest) | true | Update an OAuth2 application. |
|
||||
|
||||
### Example responses
|
||||
|
||||
> 200 Response
|
||||
|
||||
```json
|
||||
{
|
||||
"callback_url": "string",
|
||||
"icon": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"name": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------------ |
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.OAuth2ProviderApp](schemas.md#codersdkoauth2providerapp) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Delete OAuth2 application.
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X DELETE http://coder-server:8080/api/v2/oauth2-provider/apps/{app} \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`DELETE /oauth2-provider/apps/{app}`
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
| ----- | ---- | ------ | -------- | ----------- |
|
||||
| `app` | path | string | true | App ID |
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
| ------ | --------------------------------------------------------------- | ----------- | ------ |
|
||||
| 204 | [No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5) | No Content | |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Get OAuth2 application secrets.
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X GET http://coder-server:8080/api/v2/oauth2-provider/apps/{app}/secrets \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`GET /oauth2-provider/apps/{app}/secrets`
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
| ----- | ---- | ------ | -------- | ----------- |
|
||||
| `app` | path | string | true | App ID |
|
||||
|
||||
### Example responses
|
||||
|
||||
> 200 Response
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"client_secret_truncated": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_used_at": "string"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
| ------ | ------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------- |
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.OAuth2ProviderAppSecret](schemas.md#codersdkoauth2providerappsecret) |
|
||||
|
||||
<h3 id="get-oauth2-application-secrets.-responseschema">Response Schema</h3>
|
||||
|
||||
Status Code **200**
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| --------------------------- | ------------ | -------- | ------------ | ----------- |
|
||||
| `[array item]` | array | false | | |
|
||||
| `» client_secret_truncated` | string | false | | |
|
||||
| `» id` | string(uuid) | false | | |
|
||||
| `» last_used_at` | string | false | | |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Create OAuth2 application secret.
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X POST http://coder-server:8080/api/v2/oauth2-provider/apps/{app}/secrets \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`POST /oauth2-provider/apps/{app}/secrets`
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
| ----- | ---- | ------ | -------- | ----------- |
|
||||
| `app` | path | string | true | App ID |
|
||||
|
||||
### Example responses
|
||||
|
||||
> 200 Response
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"client_secret_full": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
| ------ | ------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------- |
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.OAuth2ProviderAppSecretFull](schemas.md#codersdkoauth2providerappsecretfull) |
|
||||
|
||||
<h3 id="create-oauth2-application-secret.-responseschema">Response Schema</h3>
|
||||
|
||||
Status Code **200**
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ---------------------- | ------------ | -------- | ------------ | ----------- |
|
||||
| `[array item]` | array | false | | |
|
||||
| `» client_secret_full` | string | false | | |
|
||||
| `» id` | string(uuid) | false | | |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Delete OAuth2 application secret.
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X DELETE http://coder-server:8080/api/v2/oauth2-provider/apps/{app}/secrets/{secretID} \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`DELETE /oauth2-provider/apps/{app}/secrets/{secretID}`
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
| ---------- | ---- | ------ | -------- | ----------- |
|
||||
| `app` | path | string | true | App ID |
|
||||
| `secretID` | path | string | true | Secret ID |
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
| ------ | --------------------------------------------------------------- | ----------- | ------ |
|
||||
| 204 | [No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5) | No Content | |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Get groups by organization
|
||||
|
||||
### Code samples
|
||||
|
|
|
@ -3539,6 +3539,60 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
| `client_secret` | string | false | | |
|
||||
| `enterprise_base_url` | string | false | | |
|
||||
|
||||
## codersdk.OAuth2ProviderApp
|
||||
|
||||
```json
|
||||
{
|
||||
"callback_url": "string",
|
||||
"icon": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"name": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| -------------- | ------ | -------- | ------------ | ----------- |
|
||||
| `callback_url` | string | false | | |
|
||||
| `icon` | string | false | | |
|
||||
| `id` | string | false | | |
|
||||
| `name` | string | false | | |
|
||||
|
||||
## codersdk.OAuth2ProviderAppSecret
|
||||
|
||||
```json
|
||||
{
|
||||
"client_secret_truncated": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_used_at": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ------------------------- | ------ | -------- | ------------ | ----------- |
|
||||
| `client_secret_truncated` | string | false | | |
|
||||
| `id` | string | false | | |
|
||||
| `last_used_at` | string | false | | |
|
||||
|
||||
## codersdk.OAuth2ProviderAppSecretFull
|
||||
|
||||
```json
|
||||
{
|
||||
"client_secret_full": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| -------------------- | ------ | -------- | ------------ | ----------- |
|
||||
| `client_secret_full` | string | false | | |
|
||||
| `id` | string | false | | |
|
||||
|
||||
## codersdk.OAuthConversionResponse
|
||||
|
||||
```json
|
||||
|
@ -3756,6 +3810,24 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
| `name` | string | true | | |
|
||||
| `regenerate_token` | boolean | false | | |
|
||||
|
||||
## codersdk.PostOAuth2ProviderAppRequest
|
||||
|
||||
```json
|
||||
{
|
||||
"callback_url": "string",
|
||||
"icon": "string",
|
||||
"name": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| -------------- | ------ | -------- | ------------ | ----------- |
|
||||
| `callback_url` | string | true | | |
|
||||
| `icon` | string | false | | |
|
||||
| `name` | string | true | | |
|
||||
|
||||
## codersdk.PprofConfig
|
||||
|
||||
```json
|
||||
|
@ -4035,6 +4107,24 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
| ---------- | ------ | -------- | ------------ | ----------- |
|
||||
| `deadline` | string | true | | |
|
||||
|
||||
## codersdk.PutOAuth2ProviderAppRequest
|
||||
|
||||
```json
|
||||
{
|
||||
"callback_url": "string",
|
||||
"icon": "string",
|
||||
"name": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| -------------- | ------ | -------- | ------------ | ----------- |
|
||||
| `callback_url` | string | true | | |
|
||||
| `icon` | string | false | | |
|
||||
| `name` | string | true | | |
|
||||
|
||||
## codersdk.RBACResource
|
||||
|
||||
```json
|
||||
|
|
|
@ -311,6 +311,33 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
|||
r.Get("/", api.userQuietHoursSchedule)
|
||||
r.Put("/", api.putUserQuietHoursSchedule)
|
||||
})
|
||||
r.Route("/oauth2-provider", func(r chi.Router) {
|
||||
r.Use(
|
||||
apiKeyMiddleware,
|
||||
api.oAuth2ProviderMiddleware,
|
||||
)
|
||||
r.Route("/apps", func(r chi.Router) {
|
||||
r.Get("/", api.oAuth2ProviderApps)
|
||||
r.Post("/", api.postOAuth2ProviderApp)
|
||||
|
||||
r.Route("/{app}", func(r chi.Router) {
|
||||
r.Use(httpmw.ExtractOAuth2ProviderApp(options.Database))
|
||||
r.Get("/", api.oAuth2ProviderApp)
|
||||
r.Put("/", api.putOAuth2ProviderApp)
|
||||
r.Delete("/", api.deleteOAuth2ProviderApp)
|
||||
|
||||
r.Route("/secrets", func(r chi.Router) {
|
||||
r.Get("/", api.oAuth2ProviderAppSecrets)
|
||||
r.Post("/", api.postOAuth2ProviderAppSecret)
|
||||
|
||||
r.Route("/{secretID}", func(r chi.Router) {
|
||||
r.Use(httpmw.ExtractOAuth2ProviderAppSecret(options.Database))
|
||||
r.Delete("/", api.deleteOAuth2ProviderAppSecret)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
if len(options.SCIMAPIKey) != 0 {
|
||||
|
@ -487,6 +514,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
|
|||
codersdk.FeatureBrowserOnly: api.BrowserOnly,
|
||||
codersdk.FeatureSCIM: len(api.SCIMAPIKey) != 0,
|
||||
codersdk.FeatureMultipleExternalAuth: len(api.ExternalAuthConfigs) > 1,
|
||||
codersdk.FeatureOAuth2Provider: true,
|
||||
codersdk.FeatureTemplateRBAC: api.RBAC,
|
||||
codersdk.FeatureExternalTokenEncryption: len(api.ExternalTokenEncryption) > 0,
|
||||
codersdk.FeatureExternalProvisionerDaemons: true,
|
||||
|
|
|
@ -0,0 +1,255 @@
|
|||
package coderd
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/v2/buildinfo"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/cryptorand"
|
||||
)
|
||||
|
||||
func (api *API) oAuth2ProviderMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
if !buildinfo.IsDev() {
|
||||
httpapi.Write(r.Context(), rw, http.StatusForbidden, codersdk.Response{
|
||||
Message: "OAuth2 provider is under development.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
api.entitlementsMu.RLock()
|
||||
entitled := api.entitlements.Features[codersdk.FeatureOAuth2Provider].Entitlement != codersdk.EntitlementNotEntitled
|
||||
api.entitlementsMu.RUnlock()
|
||||
|
||||
if !entitled {
|
||||
httpapi.Write(r.Context(), rw, http.StatusForbidden, codersdk.Response{
|
||||
Message: "OAuth2 provider is an Enterprise feature. Contact sales!",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(rw, r)
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Get OAuth2 applications.
|
||||
// @ID get-oauth2-applications
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Enterprise
|
||||
// @Success 200 {array} codersdk.OAuth2ProviderApp
|
||||
// @Router /oauth2-provider/apps [get]
|
||||
func (api *API) oAuth2ProviderApps(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
dbApps, err := api.Database.GetOAuth2ProviderApps(ctx)
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.OAuth2ProviderApps(dbApps))
|
||||
}
|
||||
|
||||
// @Summary Get OAuth2 application.
|
||||
// @ID get-oauth2-application
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Enterprise
|
||||
// @Param app path string true "App ID"
|
||||
// @Success 200 {object} codersdk.OAuth2ProviderApp
|
||||
// @Router /oauth2-provider/apps/{app} [get]
|
||||
func (*API) oAuth2ProviderApp(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
app := httpmw.OAuth2ProviderApp(r)
|
||||
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.OAuth2ProviderApp(app))
|
||||
}
|
||||
|
||||
// @Summary Create OAuth2 application.
|
||||
// @ID create-oauth2-application
|
||||
// @Security CoderSessionToken
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Tags Enterprise
|
||||
// @Param request body codersdk.PostOAuth2ProviderAppRequest true "The OAuth2 application to create."
|
||||
// @Success 200 {object} codersdk.OAuth2ProviderApp
|
||||
// @Router /oauth2-provider/apps [post]
|
||||
func (api *API) postOAuth2ProviderApp(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var req codersdk.PostOAuth2ProviderAppRequest
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
}
|
||||
app, err := api.Database.InsertOAuth2ProviderApp(ctx, database.InsertOAuth2ProviderAppParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: dbtime.Now(),
|
||||
UpdatedAt: dbtime.Now(),
|
||||
Name: req.Name,
|
||||
Icon: req.Icon,
|
||||
CallbackURL: req.CallbackURL,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error creating OAuth2 application.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusCreated, db2sdk.OAuth2ProviderApp(app))
|
||||
}
|
||||
|
||||
// @Summary Update OAuth2 application.
|
||||
// @ID update-oauth2-application
|
||||
// @Security CoderSessionToken
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Tags Enterprise
|
||||
// @Param app path string true "App ID"
|
||||
// @Param request body codersdk.PutOAuth2ProviderAppRequest true "Update an OAuth2 application."
|
||||
// @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 req codersdk.PutOAuth2ProviderAppRequest
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
}
|
||||
app, err := api.Database.UpdateOAuth2ProviderAppByID(ctx, database.UpdateOAuth2ProviderAppByIDParams{
|
||||
ID: app.ID,
|
||||
UpdatedAt: dbtime.Now(),
|
||||
Name: req.Name,
|
||||
Icon: req.Icon,
|
||||
CallbackURL: req.CallbackURL,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error creating OAuth2 application.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.OAuth2ProviderApp(app))
|
||||
}
|
||||
|
||||
// @Summary Delete OAuth2 application.
|
||||
// @ID delete-oauth2-application
|
||||
// @Security CoderSessionToken
|
||||
// @Tags Enterprise
|
||||
// @Param app path string true "App ID"
|
||||
// @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)
|
||||
err := api.Database.DeleteOAuth2ProviderAppByID(ctx, app.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error deleting OAuth2 application.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
// @Summary Get OAuth2 application secrets.
|
||||
// @ID get-oauth2-application-secrets
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Enterprise
|
||||
// @Param app path string true "App ID"
|
||||
// @Success 200 {array} codersdk.OAuth2ProviderAppSecret
|
||||
// @Router /oauth2-provider/apps/{app}/secrets [get]
|
||||
func (api *API) oAuth2ProviderAppSecrets(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
app := httpmw.OAuth2ProviderApp(r)
|
||||
dbSecrets, err := api.Database.GetOAuth2ProviderAppSecretsByAppID(ctx, app.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error getting OAuth2 client secrets.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
secrets := []codersdk.OAuth2ProviderAppSecret{}
|
||||
for _, secret := range dbSecrets {
|
||||
secrets = append(secrets, codersdk.OAuth2ProviderAppSecret{
|
||||
ID: secret.ID,
|
||||
LastUsedAt: codersdk.NullTime{NullTime: secret.LastUsedAt},
|
||||
ClientSecretTruncated: secret.DisplaySecret,
|
||||
})
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusOK, secrets)
|
||||
}
|
||||
|
||||
// @Summary Create OAuth2 application secret.
|
||||
// @ID create-oauth2-application-secret
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Enterprise
|
||||
// @Param app path string true "App ID"
|
||||
// @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)
|
||||
// 40 characters matches the length of GitHub's client secrets.
|
||||
rawSecret, err := cryptorand.String(40)
|
||||
if err != nil {
|
||||
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to generate OAuth2 client secret.",
|
||||
})
|
||||
return
|
||||
}
|
||||
hashed := sha256.Sum256([]byte(rawSecret))
|
||||
secret, err := api.Database.InsertOAuth2ProviderAppSecret(ctx, database.InsertOAuth2ProviderAppSecretParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: dbtime.Now(),
|
||||
HashedSecret: hashed[:],
|
||||
// DisplaySecret is the last six characters of the original unhashed secret.
|
||||
// This is done so they can be differentiated and it matches how GitHub
|
||||
// displays their client secrets.
|
||||
DisplaySecret: rawSecret[len(rawSecret)-6:],
|
||||
AppID: app.ID,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error creating OAuth2 client secret.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.OAuth2ProviderAppSecretFull{
|
||||
ID: secret.ID,
|
||||
ClientSecretFull: rawSecret,
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Delete OAuth2 application secret.
|
||||
// @ID delete-oauth2-application-secret
|
||||
// @Security CoderSessionToken
|
||||
// @Tags Enterprise
|
||||
// @Param app path string true "App ID"
|
||||
// @Param secretID path string true "Secret ID"
|
||||
// @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)
|
||||
err := api.Database.DeleteOAuth2ProviderAppSecretByID(ctx, secret.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error deleting OAuth2 client secret.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusNoContent, nil)
|
||||
}
|
|
@ -0,0 +1,367 @@
|
|||
package coderd_test
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/license"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestOAuthApps(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Validation", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureOAuth2Provider: 1,
|
||||
},
|
||||
}})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req codersdk.PostOAuth2ProviderAppRequest
|
||||
}{
|
||||
{
|
||||
name: "NameMissing",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
CallbackURL: "http://localhost:3000",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NameSpaces",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo bar",
|
||||
CallbackURL: "http://localhost:3000",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NameTooLong",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "too loooooooooooooooooooooooooong",
|
||||
CallbackURL: "http://localhost:3000",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NameTaken",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "taken",
|
||||
CallbackURL: "http://localhost:3000",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLMissing",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLLocalhostNoScheme",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "localhost:3000",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLNoScheme",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "coder.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLNoColon",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "http//coder",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLJustBar",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "bar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLPathOnly",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "/bar/baz/qux",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLJustHttp",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "http",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLNoHost",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "http://",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLSpaces",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "bar baz qux",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Generate an application for testing name conflicts.
|
||||
req := codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "taken",
|
||||
CallbackURL: "http://coder.com",
|
||||
}
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
_, err := client.PostOAuth2ProviderApp(ctx, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate an application for testing PUTs.
|
||||
req = codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "quark",
|
||||
CallbackURL: "http://coder.com",
|
||||
}
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
existingApp, err := client.PostOAuth2ProviderApp(ctx, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
_, err := client.PostOAuth2ProviderApp(ctx, test.req)
|
||||
require.Error(t, err)
|
||||
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
_, err = client.PutOAuth2ProviderApp(ctx, existingApp.ID, codersdk.PutOAuth2ProviderAppRequest{
|
||||
Name: test.req.Name,
|
||||
CallbackURL: test.req.CallbackURL,
|
||||
})
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DeleteNonExisting", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureOAuth2Provider: 1,
|
||||
},
|
||||
}})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
_, err := client.OAuth2ProviderApp(ctx, uuid.New())
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureOAuth2Provider: 1,
|
||||
},
|
||||
}})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// No apps yet.
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
apps, err := client.OAuth2ProviderApps(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps, 0)
|
||||
|
||||
// Should be able to add apps.
|
||||
expected := []codersdk.OAuth2ProviderApp{}
|
||||
for i := 0; i < 5; i++ {
|
||||
postReq := codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo-" + strconv.Itoa(i),
|
||||
CallbackURL: "http://" + strconv.Itoa(i) + ".localhost:3000",
|
||||
}
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
app, err := client.PostOAuth2ProviderApp(ctx, postReq)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, postReq.Name, app.Name)
|
||||
require.Equal(t, postReq.CallbackURL, app.CallbackURL)
|
||||
expected = append(expected, app)
|
||||
}
|
||||
|
||||
// Should get all the apps now.
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
apps, err = client.OAuth2ProviderApps(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps, 5)
|
||||
require.Equal(t, expected, apps)
|
||||
|
||||
// Should be able to keep the same name when updating.
|
||||
req := codersdk.PutOAuth2ProviderAppRequest{
|
||||
Name: expected[0].Name,
|
||||
CallbackURL: "http://coder.com",
|
||||
Icon: "test",
|
||||
}
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
newApp, err := client.PutOAuth2ProviderApp(ctx, expected[0].ID, req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, req.Name, newApp.Name)
|
||||
require.Equal(t, req.CallbackURL, newApp.CallbackURL)
|
||||
require.Equal(t, req.Icon, newApp.Icon)
|
||||
require.Equal(t, expected[0].ID, newApp.ID)
|
||||
|
||||
// Should be able to update name.
|
||||
req = codersdk.PutOAuth2ProviderAppRequest{
|
||||
Name: "new-foo",
|
||||
CallbackURL: "http://coder.com",
|
||||
Icon: "test",
|
||||
}
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
newApp, err = client.PutOAuth2ProviderApp(ctx, expected[0].ID, req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, req.Name, newApp.Name)
|
||||
require.Equal(t, req.CallbackURL, newApp.CallbackURL)
|
||||
require.Equal(t, req.Icon, newApp.Icon)
|
||||
require.Equal(t, expected[0].ID, newApp.ID)
|
||||
|
||||
// Should be able to get a single app.
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
got, err := client.OAuth2ProviderApp(ctx, expected[0].ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newApp, got)
|
||||
|
||||
// Should be able to delete an app.
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
err = client.DeleteOAuth2ProviderApp(ctx, expected[0].ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should show the new count.
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
newApps, err := client.OAuth2ProviderApps(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, newApps, 4)
|
||||
require.Equal(t, expected[1:], newApps)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOAuthAppSecrets(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureOAuth2Provider: 1,
|
||||
},
|
||||
}})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// Make some apps.
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
app1, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "razzle-dazzle",
|
||||
CallbackURL: "http://localhost",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
app2, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "razzle-dazzle-the-sequel",
|
||||
CallbackURL: "http://localhost",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("DeleteNonExisting", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Should not be able to create secrets for a non-existent app.
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
_, err = client.OAuth2ProviderAppSecrets(ctx, uuid.New())
|
||||
require.Error(t, err)
|
||||
|
||||
// Should not be able to delete non-existing secrets when there is no app.
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
err = client.DeleteOAuth2ProviderAppSecret(ctx, uuid.New(), uuid.New())
|
||||
require.Error(t, err)
|
||||
|
||||
// Should not be able to delete non-existing secrets when the app exists.
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
err = client.DeleteOAuth2ProviderAppSecret(ctx, app1.ID, uuid.New())
|
||||
require.Error(t, err)
|
||||
|
||||
// Should not be able to delete an existing secret with the wrong app ID.
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
secret, err := client.PostOAuth2ProviderAppSecret(ctx, app2.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
err = client.DeleteOAuth2ProviderAppSecret(ctx, app1.ID, secret.ID)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// No secrets yet.
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
secrets, err := client.OAuth2ProviderAppSecrets(ctx, app1.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, secrets, 0)
|
||||
|
||||
// Should be able to create secrets.
|
||||
for i := 0; i < 5; i++ {
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
secret, err := client.PostOAuth2ProviderAppSecret(ctx, app1.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, secret.ClientSecretFull)
|
||||
require.True(t, len(secret.ClientSecretFull) > 6)
|
||||
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
_, err = client.PostOAuth2ProviderAppSecret(ctx, app2.ID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Should get secrets now, but only for the one app.
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
secrets, err = client.OAuth2ProviderAppSecrets(ctx, app1.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, secrets, 5)
|
||||
for _, secret := range secrets {
|
||||
require.Len(t, secret.ClientSecretTruncated, 6)
|
||||
}
|
||||
|
||||
// Should be able to delete a secret.
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
err = client.DeleteOAuth2ProviderAppSecret(ctx, app1.ID, secrets[0].ID)
|
||||
require.NoError(t, err)
|
||||
secrets, err = client.OAuth2ProviderAppSecrets(ctx, app1.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, secrets, 4)
|
||||
|
||||
// No secrets once the app is deleted.
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
err = client.DeleteOAuth2ProviderApp(ctx, app1.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
_, err = client.OAuth2ProviderAppSecrets(ctx, app1.ID)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
|
@ -120,6 +120,24 @@ const ExternalAuthSettingsPage = lazy(
|
|||
"./pages/DeploySettingsPage/ExternalAuthSettingsPage/ExternalAuthSettingsPage"
|
||||
),
|
||||
);
|
||||
const OAuth2AppsSettingsPage = lazy(
|
||||
() =>
|
||||
import(
|
||||
"./pages/DeploySettingsPage/OAuth2AppsSettingsPage/OAuth2AppsSettingsPage"
|
||||
),
|
||||
);
|
||||
const EditOAuth2AppPage = lazy(
|
||||
() =>
|
||||
import(
|
||||
"./pages/DeploySettingsPage/OAuth2AppsSettingsPage/EditOAuth2AppPage"
|
||||
),
|
||||
);
|
||||
const CreateOAuth2AppPage = lazy(
|
||||
() =>
|
||||
import(
|
||||
"./pages/DeploySettingsPage/OAuth2AppsSettingsPage/CreateOAuth2AppPage"
|
||||
),
|
||||
);
|
||||
const NetworkSettingsPage = lazy(
|
||||
() =>
|
||||
import(
|
||||
|
@ -315,6 +333,16 @@ export const AppRouter: FC = () => {
|
|||
path="external-auth"
|
||||
element={<ExternalAuthSettingsPage />}
|
||||
/>
|
||||
|
||||
<Route path="oauth2-provider">
|
||||
<Route index element={<NotFoundPage />} />
|
||||
<Route path="apps">
|
||||
<Route index element={<OAuth2AppsSettingsPage />} />
|
||||
<Route path="add" element={<CreateOAuth2AppPage />} />
|
||||
<Route path=":appId" element={<EditOAuth2AppPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path="workspace-proxies"
|
||||
element={<WorkspaceProxyPage />}
|
||||
|
|
|
@ -960,6 +960,61 @@ export const unlinkExternalAuthProvider = async (
|
|||
return resp.data;
|
||||
};
|
||||
|
||||
export const getOAuth2ProviderApps = async (): Promise<
|
||||
TypesGen.OAuth2ProviderApp[]
|
||||
> => {
|
||||
const resp = await axios.get(`/api/v2/oauth2-provider/apps`);
|
||||
return resp.data;
|
||||
};
|
||||
|
||||
export const getOAuth2ProviderApp = async (
|
||||
id: string,
|
||||
): Promise<TypesGen.OAuth2ProviderApp> => {
|
||||
const resp = await axios.get(`/api/v2/oauth2-provider/apps/${id}`);
|
||||
return resp.data;
|
||||
};
|
||||
|
||||
export const postOAuth2ProviderApp = async (
|
||||
data: TypesGen.PostOAuth2ProviderAppRequest,
|
||||
): Promise<TypesGen.OAuth2ProviderApp> => {
|
||||
const response = await axios.post(`/api/v2/oauth2-provider/apps`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const putOAuth2ProviderApp = async (
|
||||
id: string,
|
||||
data: TypesGen.PutOAuth2ProviderAppRequest,
|
||||
): Promise<TypesGen.OAuth2ProviderApp> => {
|
||||
const response = await axios.put(`/api/v2/oauth2-provider/apps/${id}`, data);
|
||||
return response.data;
|
||||
};
|
||||
export const deleteOAuth2ProviderApp = async (id: string): Promise<void> => {
|
||||
await axios.delete(`/api/v2/oauth2-provider/apps/${id}`);
|
||||
};
|
||||
|
||||
export const getOAuth2ProviderAppSecrets = async (
|
||||
id: string,
|
||||
): Promise<TypesGen.OAuth2ProviderAppSecret[]> => {
|
||||
const resp = await axios.get(`/api/v2/oauth2-provider/apps/${id}/secrets`);
|
||||
return resp.data;
|
||||
};
|
||||
|
||||
export const postOAuth2ProviderAppSecret = async (
|
||||
id: string,
|
||||
): Promise<TypesGen.OAuth2ProviderAppSecretFull> => {
|
||||
const resp = await axios.post(`/api/v2/oauth2-provider/apps/${id}/secrets`);
|
||||
return resp.data;
|
||||
};
|
||||
|
||||
export const deleteOAuth2ProviderAppSecret = async (
|
||||
appId: string,
|
||||
secretId: string,
|
||||
): Promise<void> => {
|
||||
await axios.delete(
|
||||
`/api/v2/oauth2-provider/apps/${appId}/secrets/${secretId}`,
|
||||
);
|
||||
};
|
||||
|
||||
export const getAuditLogs = async (
|
||||
options: TypesGen.AuditLogsRequest,
|
||||
): Promise<TypesGen.AuditLogResponse> => {
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
import type { QueryClient } from "react-query";
|
||||
import * as API from "api/api";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
|
||||
const appsKey = ["oauth2-provider", "apps"];
|
||||
const appKey = (id: string) => appsKey.concat(id);
|
||||
const appSecretsKey = (id: string) => appKey(id).concat("secrets");
|
||||
|
||||
export const getApps = () => {
|
||||
return {
|
||||
queryKey: appsKey,
|
||||
queryFn: () => API.getOAuth2ProviderApps(),
|
||||
};
|
||||
};
|
||||
|
||||
export const getApp = (id: string) => {
|
||||
return {
|
||||
queryKey: appKey(id),
|
||||
queryFn: () => API.getOAuth2ProviderApp(id),
|
||||
};
|
||||
};
|
||||
|
||||
export const postApp = (queryClient: QueryClient) => {
|
||||
return {
|
||||
mutationFn: API.postOAuth2ProviderApp,
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: appsKey,
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const putApp = (queryClient: QueryClient) => {
|
||||
return {
|
||||
mutationFn: ({
|
||||
id,
|
||||
req,
|
||||
}: {
|
||||
id: string;
|
||||
req: TypesGen.PutOAuth2ProviderAppRequest;
|
||||
}) => API.putOAuth2ProviderApp(id, req),
|
||||
onSuccess: async (app: TypesGen.OAuth2ProviderApp) => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: appKey(app.id),
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const deleteApp = (queryClient: QueryClient) => {
|
||||
return {
|
||||
mutationFn: API.deleteOAuth2ProviderApp,
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: appsKey,
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getAppSecrets = (id: string) => {
|
||||
return {
|
||||
queryKey: appSecretsKey(id),
|
||||
queryFn: () => API.getOAuth2ProviderAppSecrets(id),
|
||||
};
|
||||
};
|
||||
|
||||
export const postAppSecret = (queryClient: QueryClient) => {
|
||||
return {
|
||||
mutationFn: API.postOAuth2ProviderAppSecret,
|
||||
onSuccess: async (
|
||||
_: TypesGen.OAuth2ProviderAppSecretFull,
|
||||
appId: string,
|
||||
) => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: appSecretsKey(appId),
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const deleteAppSecret = (queryClient: QueryClient) => {
|
||||
return {
|
||||
mutationFn: ({ appId, secretId }: { appId: string; secretId: string }) =>
|
||||
API.deleteOAuth2ProviderAppSecret(appId, secretId),
|
||||
onSuccess: async (_: void, { appId }: { appId: string }) => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: appSecretsKey(appId),
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
|
@ -660,6 +660,27 @@ export interface OAuth2GithubConfig {
|
|||
readonly enterprise_base_url: string;
|
||||
}
|
||||
|
||||
// From codersdk/oauth2.go
|
||||
export interface OAuth2ProviderApp {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly callback_url: string;
|
||||
readonly icon: string;
|
||||
}
|
||||
|
||||
// From codersdk/oauth2.go
|
||||
export interface OAuth2ProviderAppSecret {
|
||||
readonly id: string;
|
||||
readonly last_used_at?: string;
|
||||
readonly client_secret_truncated: string;
|
||||
}
|
||||
|
||||
// From codersdk/oauth2.go
|
||||
export interface OAuth2ProviderAppSecretFull {
|
||||
readonly id: string;
|
||||
readonly client_secret_full: string;
|
||||
}
|
||||
|
||||
// From codersdk/users.go
|
||||
export interface OAuthConversionResponse {
|
||||
readonly state_string: string;
|
||||
|
@ -750,6 +771,13 @@ export interface PatchWorkspaceProxy {
|
|||
readonly regenerate_token: boolean;
|
||||
}
|
||||
|
||||
// From codersdk/oauth2.go
|
||||
export interface PostOAuth2ProviderAppRequest {
|
||||
readonly name: string;
|
||||
readonly callback_url: string;
|
||||
readonly icon: string;
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export interface PprofConfig {
|
||||
readonly enable: boolean;
|
||||
|
@ -823,6 +851,13 @@ export interface PutExtendWorkspaceRequest {
|
|||
readonly deadline: string;
|
||||
}
|
||||
|
||||
// From codersdk/oauth2.go
|
||||
export interface PutOAuth2ProviderAppRequest {
|
||||
readonly name: string;
|
||||
readonly callback_url: string;
|
||||
readonly icon: string;
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export interface RateLimitConfig {
|
||||
readonly disable_all: boolean;
|
||||
|
@ -1799,6 +1834,7 @@ export type FeatureName =
|
|||
| "external_token_encryption"
|
||||
| "high_availability"
|
||||
| "multiple_external_auth"
|
||||
| "oauth2_provider"
|
||||
| "scim"
|
||||
| "template_rbac"
|
||||
| "user_limit"
|
||||
|
@ -1815,6 +1851,7 @@ export const FeatureNames: FeatureName[] = [
|
|||
"external_token_encryption",
|
||||
"high_availability",
|
||||
"multiple_external_auth",
|
||||
"oauth2_provider",
|
||||
"scim",
|
||||
"template_rbac",
|
||||
"user_limit",
|
||||
|
|
|
@ -7,6 +7,7 @@ import Globe from "@mui/icons-material/PublicOutlined";
|
|||
import HubOutlinedIcon from "@mui/icons-material/HubOutlined";
|
||||
import VpnKeyOutlined from "@mui/icons-material/VpnKeyOutlined";
|
||||
import MonitorHeartOutlined from "@mui/icons-material/MonitorHeartOutlined";
|
||||
// import Token from "@mui/icons-material/Token";
|
||||
import { type FC } from "react";
|
||||
import { useDashboard } from "components/Dashboard/DashboardProvider";
|
||||
import { GitIcon } from "components/Icons/GitIcon";
|
||||
|
@ -35,6 +36,10 @@ export const Sidebar: FC = () => {
|
|||
<SidebarNavItem href="external-auth" icon={GitIcon}>
|
||||
External Authentication
|
||||
</SidebarNavItem>
|
||||
{/* Not exposing this yet since token exchange is not finished yet.
|
||||
<SidebarNavItem href="oauth2-provider/apps" icon={Token}>
|
||||
OAuth2 Applications
|
||||
</SidebarNavItem>*/}
|
||||
<SidebarNavItem href="network" icon={Globe}>
|
||||
Network
|
||||
</SidebarNavItem>
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import { useMutation, useQueryClient } from "react-query";
|
||||
import { postApp } from "api/queries/oauth2";
|
||||
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
|
||||
import { FC } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { CreateOAuth2AppPageView } from "./CreateOAuth2AppPageView";
|
||||
import { pageTitle } from "utils/page";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
|
||||
const CreateOAuth2AppPage: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const postAppMutation = useMutation(postApp(queryClient));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{pageTitle("New OAuth2 Application")}</title>
|
||||
</Helmet>
|
||||
|
||||
<CreateOAuth2AppPageView
|
||||
isUpdating={postAppMutation.isLoading}
|
||||
error={postAppMutation.error}
|
||||
createApp={async (req) => {
|
||||
try {
|
||||
const app = await postAppMutation.mutateAsync(req);
|
||||
displaySuccess(
|
||||
`Successfully added the OAuth2 application "${app.name}".`,
|
||||
);
|
||||
navigate(`/deployment/oauth2-provider/apps/${app.id}?created=true`);
|
||||
} catch (ignore) {
|
||||
displayError("Failed to create OAuth2 application");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateOAuth2AppPage;
|
|
@ -0,0 +1,45 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { mockApiError } from "testHelpers/entities";
|
||||
import { CreateOAuth2AppPageView } from "./CreateOAuth2AppPageView";
|
||||
|
||||
const meta: Meta = {
|
||||
title: "pages/DeploySettingsPage/CreateOAuth2AppPageView",
|
||||
component: CreateOAuth2AppPageView,
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof CreateOAuth2AppPageView>;
|
||||
|
||||
export const Updating: Story = {
|
||||
args: {
|
||||
isUpdating: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Error: Story = {
|
||||
args: {
|
||||
error: mockApiError({
|
||||
message: "Validation failed",
|
||||
validations: [
|
||||
{
|
||||
field: "name",
|
||||
detail: "name error",
|
||||
},
|
||||
{
|
||||
field: "callback_url",
|
||||
detail: "url error",
|
||||
},
|
||||
{
|
||||
field: "icon",
|
||||
detail: "icon error",
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
// Nothing.
|
||||
},
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
import Button from "@mui/material/Button";
|
||||
import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft";
|
||||
import { type FC } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Header } from "components/DeploySettingsLayout/Header";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { OAuth2AppForm } from "./OAuth2AppForm";
|
||||
|
||||
type CreateOAuth2AppProps = {
|
||||
isUpdating: boolean;
|
||||
createApp: (req: TypesGen.PostOAuth2ProviderAppRequest) => void;
|
||||
error?: unknown;
|
||||
};
|
||||
|
||||
export const CreateOAuth2AppPageView: FC<CreateOAuth2AppProps> = ({
|
||||
isUpdating,
|
||||
createApp,
|
||||
error,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
alignItems="baseline"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Header
|
||||
title="Add an OAuth2 application"
|
||||
description="Configure an application to use Coder as an OAuth2 provider."
|
||||
/>
|
||||
<Button
|
||||
component={Link}
|
||||
startIcon={<KeyboardArrowLeft />}
|
||||
to="/deployment/oauth2-provider/apps"
|
||||
>
|
||||
All OAuth2 Applications
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<Stack>
|
||||
{error ? <ErrorAlert error={error} /> : undefined}
|
||||
<OAuth2AppForm
|
||||
onSubmit={createApp}
|
||||
isUpdating={isUpdating}
|
||||
error={error}
|
||||
/>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,105 @@
|
|||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import * as oauth2 from "api/queries/oauth2";
|
||||
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
|
||||
import { FC, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { EditOAuth2AppPageView } from "./EditOAuth2AppPageView";
|
||||
import { pageTitle } from "utils/page";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
|
||||
const EditOAuth2AppPage: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { appId } = useParams() as { appId: string };
|
||||
|
||||
// When a new secret is created it is returned with the full secret. This is
|
||||
// the only time it will be visible. The secret list only returns a truncated
|
||||
// version of the secret (for differentiation purposes). Once the user
|
||||
// acknowledges the secret we will clear it from the state.
|
||||
const [fullNewSecret, setFullNewSecret] =
|
||||
useState<TypesGen.OAuth2ProviderAppSecretFull>();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const appQuery = useQuery(oauth2.getApp(appId));
|
||||
const putAppMutation = useMutation(oauth2.putApp(queryClient));
|
||||
const deleteAppMutation = useMutation(oauth2.deleteApp(queryClient));
|
||||
const secretsQuery = useQuery(oauth2.getAppSecrets(appId));
|
||||
const postSecretMutation = useMutation(oauth2.postAppSecret(queryClient));
|
||||
const deleteSecretMutation = useMutation(oauth2.deleteAppSecret(queryClient));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{pageTitle("Edit OAuth2 Application")}</title>
|
||||
</Helmet>
|
||||
|
||||
<EditOAuth2AppPageView
|
||||
app={appQuery.data}
|
||||
secrets={secretsQuery.data}
|
||||
isLoadingApp={appQuery.isLoading}
|
||||
isLoadingSecrets={secretsQuery.isLoading}
|
||||
mutatingResource={{
|
||||
updateApp: putAppMutation.isLoading,
|
||||
deleteApp: deleteAppMutation.isLoading,
|
||||
createSecret: postSecretMutation.isLoading,
|
||||
deleteSecret: deleteSecretMutation.isLoading,
|
||||
}}
|
||||
fullNewSecret={fullNewSecret}
|
||||
ackFullNewSecret={() => setFullNewSecret(undefined)}
|
||||
error={
|
||||
appQuery.error ||
|
||||
putAppMutation.error ||
|
||||
deleteAppMutation.error ||
|
||||
secretsQuery.error ||
|
||||
postSecretMutation.error ||
|
||||
deleteSecretMutation.error
|
||||
}
|
||||
updateApp={async (req) => {
|
||||
try {
|
||||
await putAppMutation.mutateAsync({ id: appId, req });
|
||||
// REVIEW: Maybe it is better to stay on the same page?
|
||||
displaySuccess(
|
||||
`Successfully updated the OAuth2 application "${req.name}".`,
|
||||
);
|
||||
navigate("/deployment/oauth2-provider/apps?updated=true");
|
||||
} catch (ignore) {
|
||||
displayError("Failed to update OAuth2 application");
|
||||
}
|
||||
}}
|
||||
deleteApp={async (name) => {
|
||||
try {
|
||||
await deleteAppMutation.mutateAsync(appId);
|
||||
displaySuccess(
|
||||
`You have successfully deleted the OAuth2 application "${name}"`,
|
||||
);
|
||||
navigate("/deployment/oauth2-provider/apps?deleted=true");
|
||||
} catch (error) {
|
||||
displayError("Failed to delete OAuth2 application");
|
||||
}
|
||||
}}
|
||||
generateAppSecret={async () => {
|
||||
try {
|
||||
const secret = await postSecretMutation.mutateAsync(appId);
|
||||
displaySuccess("Successfully generated OAuth2 client secret");
|
||||
setFullNewSecret(secret);
|
||||
} catch (ignore) {
|
||||
displayError("Failed to generate OAuth2 client secret");
|
||||
}
|
||||
}}
|
||||
deleteAppSecret={async (secretId: string) => {
|
||||
try {
|
||||
await deleteSecretMutation.mutateAsync({ appId, secretId });
|
||||
displaySuccess("Successfully deleted an OAuth2 client secret");
|
||||
if (fullNewSecret?.id === secretId) {
|
||||
setFullNewSecret(undefined);
|
||||
}
|
||||
} catch (ignore) {
|
||||
displayError("Failed to delete OAuth2 client secret");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditOAuth2AppPage;
|
|
@ -0,0 +1,83 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import {
|
||||
MockOAuth2ProviderApps,
|
||||
MockOAuth2ProviderAppSecrets,
|
||||
mockApiError,
|
||||
} from "testHelpers/entities";
|
||||
import { EditOAuth2AppPageView } from "./EditOAuth2AppPageView";
|
||||
|
||||
const meta: Meta = {
|
||||
title: "pages/DeploySettingsPage/EditOAuth2AppPageView",
|
||||
component: EditOAuth2AppPageView,
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof EditOAuth2AppPageView>;
|
||||
|
||||
export const LoadingApp: Story = {
|
||||
args: {
|
||||
isLoadingApp: true,
|
||||
mutatingResource: {
|
||||
updateApp: false,
|
||||
deleteApp: false,
|
||||
createSecret: false,
|
||||
deleteSecret: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const LoadingSecrets: Story = {
|
||||
args: {
|
||||
app: MockOAuth2ProviderApps[0],
|
||||
isLoadingSecrets: true,
|
||||
mutatingResource: {
|
||||
updateApp: false,
|
||||
deleteApp: false,
|
||||
createSecret: false,
|
||||
deleteSecret: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Error: Story = {
|
||||
args: {
|
||||
app: MockOAuth2ProviderApps[0],
|
||||
secrets: MockOAuth2ProviderAppSecrets,
|
||||
mutatingResource: {
|
||||
updateApp: false,
|
||||
deleteApp: false,
|
||||
createSecret: false,
|
||||
deleteSecret: false,
|
||||
},
|
||||
error: mockApiError({
|
||||
message: "Validation failed",
|
||||
validations: [
|
||||
{
|
||||
field: "name",
|
||||
detail: "name error",
|
||||
},
|
||||
{
|
||||
field: "callback_url",
|
||||
detail: "url error",
|
||||
},
|
||||
{
|
||||
field: "icon",
|
||||
detail: "icon error",
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
app: MockOAuth2ProviderApps[0],
|
||||
secrets: MockOAuth2ProviderAppSecrets,
|
||||
mutatingResource: {
|
||||
updateApp: false,
|
||||
deleteApp: false,
|
||||
createSecret: false,
|
||||
deleteSecret: false,
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,305 @@
|
|||
import { useTheme } from "@emotion/react";
|
||||
import CopyIcon from "@mui/icons-material/FileCopyOutlined";
|
||||
import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import Button from "@mui/material/Button";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import TableHead from "@mui/material/TableHead";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import { type FC, useState } from "react";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import { Alert } from "components/Alert/Alert";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { CodeExample } from "components/CodeExample/CodeExample";
|
||||
import { CopyableValue } from "components/CopyableValue/CopyableValue";
|
||||
import { Header } from "components/DeploySettingsLayout/Header";
|
||||
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { TableLoader } from "components/TableLoader/TableLoader";
|
||||
import { createDayString } from "utils/createDayString";
|
||||
import { OAuth2AppForm } from "./OAuth2AppForm";
|
||||
|
||||
export type MutatingResource = {
|
||||
updateApp: boolean;
|
||||
createSecret: boolean;
|
||||
deleteApp: boolean;
|
||||
deleteSecret: boolean;
|
||||
};
|
||||
|
||||
type EditOAuth2AppProps = {
|
||||
app?: TypesGen.OAuth2ProviderApp;
|
||||
isLoadingApp: boolean;
|
||||
isLoadingSecrets: boolean;
|
||||
// mutatingResource indicates which resources, if any, are currently being
|
||||
// mutated.
|
||||
mutatingResource: MutatingResource;
|
||||
updateApp: (req: TypesGen.PutOAuth2ProviderAppRequest) => void;
|
||||
deleteApp: (name: string) => void;
|
||||
generateAppSecret: () => void;
|
||||
deleteAppSecret: (id: string) => void;
|
||||
secrets?: readonly TypesGen.OAuth2ProviderAppSecret[];
|
||||
fullNewSecret?: TypesGen.OAuth2ProviderAppSecretFull;
|
||||
ackFullNewSecret: () => void;
|
||||
error?: unknown;
|
||||
};
|
||||
|
||||
export const EditOAuth2AppPageView: FC<EditOAuth2AppProps> = ({
|
||||
app,
|
||||
isLoadingApp,
|
||||
isLoadingSecrets,
|
||||
mutatingResource,
|
||||
updateApp,
|
||||
deleteApp,
|
||||
generateAppSecret,
|
||||
deleteAppSecret,
|
||||
secrets,
|
||||
fullNewSecret,
|
||||
ackFullNewSecret,
|
||||
error,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [showDelete, setShowDelete] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
alignItems="baseline"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Header
|
||||
title="Edit OAuth2 application"
|
||||
description="Configure an application to use Coder as an OAuth2 provider."
|
||||
/>
|
||||
<Button
|
||||
component={Link}
|
||||
startIcon={<KeyboardArrowLeft />}
|
||||
to="/deployment/oauth2-provider/apps"
|
||||
>
|
||||
All OAuth2 Applications
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
{fullNewSecret && (
|
||||
<ConfirmDialog
|
||||
hideCancel
|
||||
open={Boolean(fullNewSecret)}
|
||||
onConfirm={ackFullNewSecret}
|
||||
onClose={ackFullNewSecret}
|
||||
title="OAuth2 client secret"
|
||||
confirmText="OK"
|
||||
description={
|
||||
<>
|
||||
<p>
|
||||
Your new client secret is displayed below. Make sure to copy it
|
||||
now; you will not be able to see it again.
|
||||
</p>
|
||||
<CodeExample
|
||||
code={fullNewSecret.client_secret_full}
|
||||
css={{
|
||||
minHeight: "auto",
|
||||
userSelect: "all",
|
||||
width: "100%",
|
||||
marginTop: 24,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Stack>
|
||||
{searchParams.has("created") && (
|
||||
<Alert severity="info" dismissible>
|
||||
Your OAuth2 application has been created. Generate a client secret
|
||||
below to start using your application.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{error ? <ErrorAlert error={error} /> : undefined}
|
||||
|
||||
{isLoadingApp && <Loader />}
|
||||
|
||||
{!isLoadingApp && app && (
|
||||
<>
|
||||
<DeleteDialog
|
||||
isOpen={showDelete}
|
||||
confirmLoading={mutatingResource.deleteApp}
|
||||
name={app.name}
|
||||
entity="OAuth2 application"
|
||||
onConfirm={() => deleteApp(app.name)}
|
||||
onCancel={() => setShowDelete(false)}
|
||||
/>
|
||||
|
||||
<h2 css={{ marginBottom: 0 }}>Client ID</h2>
|
||||
<CopyableValue value={app.id}>
|
||||
{app.id}{" "}
|
||||
<CopyIcon
|
||||
css={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
}}
|
||||
/>
|
||||
</CopyableValue>
|
||||
|
||||
<Divider css={{ borderColor: theme.palette.divider }} />
|
||||
|
||||
<OAuth2AppForm
|
||||
app={app}
|
||||
onSubmit={updateApp}
|
||||
isUpdating={mutatingResource.updateApp}
|
||||
error={error}
|
||||
actions={
|
||||
<Button
|
||||
variant="outlined"
|
||||
type="button"
|
||||
color="error"
|
||||
onClick={() => setShowDelete(true)}
|
||||
>
|
||||
Delete…
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Divider css={{ borderColor: theme.palette.divider }} />
|
||||
|
||||
<OAuth2AppSecretsTable
|
||||
secrets={secrets}
|
||||
generateAppSecret={generateAppSecret}
|
||||
deleteAppSecret={deleteAppSecret}
|
||||
isLoadingSecrets={isLoadingSecrets}
|
||||
mutatingResource={mutatingResource}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type OAuth2AppSecretsTableProps = {
|
||||
secrets?: readonly TypesGen.OAuth2ProviderAppSecret[];
|
||||
generateAppSecret: () => void;
|
||||
isLoadingSecrets: boolean;
|
||||
mutatingResource: MutatingResource;
|
||||
deleteAppSecret: (id: string) => void;
|
||||
};
|
||||
|
||||
const OAuth2AppSecretsTable: FC<OAuth2AppSecretsTableProps> = ({
|
||||
secrets,
|
||||
generateAppSecret,
|
||||
isLoadingSecrets,
|
||||
mutatingResource,
|
||||
deleteAppSecret,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
alignItems="baseline"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<h2>Client secrets</h2>
|
||||
<LoadingButton
|
||||
loading={mutatingResource.createSecret}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
onClick={generateAppSecret}
|
||||
>
|
||||
Generate secret
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell width="80%">Secret</TableCell>
|
||||
<TableCell width="20%">Last Used</TableCell>
|
||||
<TableCell width="1%" />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{isLoadingSecrets && <TableLoader />}
|
||||
{!isLoadingSecrets && (!secrets || secrets.length === 0) && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<div css={{ textAlign: "center" }}>
|
||||
No client secrets have been generated.
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{!isLoadingSecrets &&
|
||||
secrets?.map((secret) => (
|
||||
<OAuth2SecretRow
|
||||
key={secret.id}
|
||||
secret={secret}
|
||||
mutatingResource={mutatingResource}
|
||||
deleteAppSecret={deleteAppSecret}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type OAuth2SecretRowProps = {
|
||||
secret: TypesGen.OAuth2ProviderAppSecret;
|
||||
deleteAppSecret: (id: string) => void;
|
||||
mutatingResource: MutatingResource;
|
||||
};
|
||||
|
||||
const OAuth2SecretRow: FC<OAuth2SecretRowProps> = ({
|
||||
secret,
|
||||
deleteAppSecret,
|
||||
mutatingResource,
|
||||
}) => {
|
||||
const [showDelete, setShowDelete] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<TableRow key={secret.id} data-testid={`secret-${secret.id}`}>
|
||||
<TableCell>*****{secret.client_secret_truncated}</TableCell>
|
||||
<TableCell data-chromatic="ignore">
|
||||
{secret.last_used_at ? createDayString(secret.last_used_at) : "never"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ConfirmDialog
|
||||
type="delete"
|
||||
hideCancel={false}
|
||||
open={showDelete}
|
||||
onConfirm={() => deleteAppSecret(secret.id)}
|
||||
onClose={() => setShowDelete(false)}
|
||||
title="Delete OAuth2 client secret"
|
||||
confirmLoading={mutatingResource.deleteSecret}
|
||||
confirmText="Delete"
|
||||
description={
|
||||
<>
|
||||
Deleting <strong>*****{secret.client_secret_truncated}</strong> is
|
||||
irreversible and will revoke all the tokens generated by it. Are
|
||||
you sure you want to proceed?
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
type="button"
|
||||
color="error"
|
||||
onClick={() => setShowDelete(true)}
|
||||
>
|
||||
Delete…
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,87 @@
|
|||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { type FC, type ReactNode } from "react";
|
||||
import { isApiValidationError, mapApiErrorToFieldErrors } from "api/errors";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
|
||||
type OAuth2AppFormProps = {
|
||||
app?: TypesGen.OAuth2ProviderApp;
|
||||
onSubmit: (data: {
|
||||
name: string;
|
||||
callback_url: string;
|
||||
icon: string;
|
||||
}) => void;
|
||||
error?: unknown;
|
||||
isUpdating: boolean;
|
||||
actions?: ReactNode;
|
||||
};
|
||||
|
||||
export const OAuth2AppForm: FC<OAuth2AppFormProps> = ({
|
||||
app,
|
||||
onSubmit,
|
||||
error,
|
||||
isUpdating,
|
||||
actions,
|
||||
}) => {
|
||||
const apiValidationErrors = isApiValidationError(error)
|
||||
? mapApiErrorToFieldErrors(error.response.data)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<form
|
||||
css={{ marginTop: 10 }}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(event.target as HTMLFormElement);
|
||||
onSubmit({
|
||||
name: formData.get("name") as string,
|
||||
callback_url: formData.get("callback_url") as string,
|
||||
icon: formData.get("icon") as string,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2.5}>
|
||||
<TextField
|
||||
name="name"
|
||||
label="Application name"
|
||||
defaultValue={app?.name}
|
||||
error={Boolean(apiValidationErrors?.name)}
|
||||
helperText={
|
||||
apiValidationErrors?.name || "The name of your Coder app."
|
||||
}
|
||||
autoFocus
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
name="callback_url"
|
||||
label="Callback URL"
|
||||
defaultValue={app?.callback_url}
|
||||
error={Boolean(apiValidationErrors?.callback_url)}
|
||||
helperText={
|
||||
apiValidationErrors?.callback_url ||
|
||||
"The full URL to redirect to after a user authorizes an installation."
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
name="icon"
|
||||
label="Application icon"
|
||||
defaultValue={app?.icon}
|
||||
error={Boolean(apiValidationErrors?.icon)}
|
||||
helperText={
|
||||
apiValidationErrors?.icon || "A full or relative URL to an icon."
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<Stack direction="row">
|
||||
<LoadingButton loading={isUpdating} type="submit" variant="contained">
|
||||
{app ? "Update application" : "Create application"}
|
||||
</LoadingButton>
|
||||
{actions}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
import { useQuery } from "react-query";
|
||||
import { getApps } from "api/queries/oauth2";
|
||||
import { useDashboard } from "components/Dashboard/DashboardProvider";
|
||||
import { FC } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { pageTitle } from "utils/page";
|
||||
import OAuth2AppsSettingsPageView from "./OAuth2AppsSettingsPageView";
|
||||
|
||||
const OAuth2AppsSettingsPage: FC = () => {
|
||||
const { entitlements } = useDashboard();
|
||||
const appsQuery = useQuery(getApps());
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{pageTitle("OAuth2 Applications")}</title>
|
||||
</Helmet>
|
||||
<OAuth2AppsSettingsPageView
|
||||
apps={appsQuery.data}
|
||||
isLoading={appsQuery.isLoading}
|
||||
isEntitled={
|
||||
entitlements.features.oauth2_provider.entitlement !== "not_entitled"
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2AppsSettingsPage;
|
|
@ -0,0 +1,38 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { MockOAuth2ProviderApps } from "testHelpers/entities";
|
||||
import OAuth2AppsSettingsPageView from "./OAuth2AppsSettingsPageView";
|
||||
|
||||
const meta: Meta = {
|
||||
title: "pages/DeploySettingsPage/OAuth2AppsSettingsPageView",
|
||||
component: OAuth2AppsSettingsPageView,
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof OAuth2AppsSettingsPageView>;
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
isLoading: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Unentitled: Story = {
|
||||
args: {
|
||||
isLoading: false,
|
||||
apps: MockOAuth2ProviderApps,
|
||||
},
|
||||
};
|
||||
|
||||
export const Entitled: Story = {
|
||||
args: {
|
||||
isLoading: false,
|
||||
apps: MockOAuth2ProviderApps,
|
||||
isEntitled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
isLoading: false,
|
||||
},
|
||||
};
|
|
@ -0,0 +1,132 @@
|
|||
import { useTheme } from "@emotion/react";
|
||||
import AddIcon from "@mui/icons-material/AddOutlined";
|
||||
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
|
||||
import Button from "@mui/material/Button";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import TableHead from "@mui/material/TableHead";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import { type FC } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import { AvatarData } from "components/AvatarData/AvatarData";
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
import {
|
||||
Badges,
|
||||
DisabledBadge,
|
||||
EnterpriseBadge,
|
||||
EntitledBadge,
|
||||
} from "components/Badges/Badges";
|
||||
import { Header } from "components/DeploySettingsLayout/Header";
|
||||
import { TableLoader } from "components/TableLoader/TableLoader";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { useClickableTableRow } from "hooks/useClickableTableRow";
|
||||
|
||||
type OAuth2AppsSettingsProps = {
|
||||
apps?: TypesGen.OAuth2ProviderApp[];
|
||||
isEntitled: boolean;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
const OAuth2AppsSettingsPageView: FC<OAuth2AppsSettingsProps> = ({
|
||||
apps,
|
||||
isEntitled,
|
||||
isLoading,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
alignItems="baseline"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<div>
|
||||
<Header
|
||||
title="OAuth2 Applications"
|
||||
description="Configure applications to use Coder as an OAuth2 provider."
|
||||
/>
|
||||
<Badges>
|
||||
{isEntitled ? <EntitledBadge /> : <DisabledBadge />}
|
||||
<EnterpriseBadge />
|
||||
</Badges>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
component={Link}
|
||||
to="/deployment/oauth2-provider/apps/add"
|
||||
startIcon={<AddIcon />}
|
||||
>
|
||||
Add application
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<TableContainer css={{ marginTop: 32 }}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell width="100%">Name</TableCell>
|
||||
<TableCell width="1%" />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{isLoading && <TableLoader />}
|
||||
{!isLoading &&
|
||||
apps?.map((app) => <OAuth2AppRow key={app.id} app={app} />)}
|
||||
{!isLoading && (!apps || apps?.length === 0) && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<div css={{ textAlign: "center" }}>
|
||||
No OAuth2 applications have been configured.
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type OAuth2AppRowProps = {
|
||||
app: TypesGen.OAuth2ProviderApp;
|
||||
};
|
||||
|
||||
const OAuth2AppRow: FC<OAuth2AppRowProps> = ({ app }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const clickableProps = useClickableTableRow({
|
||||
onClick: () => navigate(`/deployment/oauth2-provider/apps/${app.id}`),
|
||||
});
|
||||
|
||||
return (
|
||||
<TableRow key={app.id} data-testid={`app-${app.id}`} {...clickableProps}>
|
||||
<TableCell>
|
||||
<AvatarData
|
||||
title={app.name}
|
||||
avatar={
|
||||
Boolean(app.icon) && (
|
||||
<Avatar src={app.icon} variant="square" fitImage />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<div css={{ display: "flex", paddingLeft: 16 }}>
|
||||
<KeyboardArrowRight
|
||||
css={{
|
||||
color: theme.palette.text.secondary,
|
||||
width: 20,
|
||||
height: 20,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2AppsSettingsPageView;
|
|
@ -3214,3 +3214,25 @@ export const MockGithubAuthLink: TypesGen.ExternalAuthLink = {
|
|||
authenticated: true,
|
||||
validate_error: "",
|
||||
};
|
||||
|
||||
export const MockOAuth2ProviderApps: TypesGen.OAuth2ProviderApp[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "foo",
|
||||
callback_url: "http://localhost:3001",
|
||||
icon: "/icon/github.svg",
|
||||
},
|
||||
];
|
||||
|
||||
export const MockOAuth2ProviderAppSecrets: TypesGen.OAuth2ProviderAppSecret[] =
|
||||
[
|
||||
{
|
||||
id: "1",
|
||||
client_secret_truncated: "foo",
|
||||
},
|
||||
{
|
||||
id: "1",
|
||||
last_used_at: "2022-12-16T20:10:45.637452Z",
|
||||
client_secret_truncated: "foo",
|
||||
},
|
||||
];
|
||||
|
|
Loading…
Reference in New Issue