mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
* Add templates
* Move API structs to codersdk
* Back to green tests!
* It all works, but now with tea! 🧋
* It works!
* Add cancellation to provisionerd
* Tests pass!
* Add deletion of workspaces and projects
* Fix agent lock
* Add clog
* Fix linting errors
* Remove unused CLI tests
* Rename daemon to start
* Fix leaking command
* Fix promptui test
* Update agent connection frequency
* Skip login tests on Windows
* Increase tunnel connect timeout
* Fix templater
* Lower test requirements
* Fix embed
* Disable promptui tests for Windows
* Fix write newline
* Fix PTY write newline
* Fix CloseReader
* Fix compilation on Windows
* Fix linting error
* Remove bubbletea
* Cleanup readwriter
* Use embedded templates instead of serving over API
* Move templates to examples
* Improve workspace create flow
* Fix Windows build
* Fix tests
* Fix linting errors
* Fix untar with extracting max size
* Fix newline char
119 lines
3.9 KiB
Go
119 lines
3.9 KiB
Go
package coderd
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/render"
|
|
|
|
"github.com/coder/coder/codersdk"
|
|
"github.com/coder/coder/database"
|
|
"github.com/coder/coder/httpapi"
|
|
|
|
"github.com/mitchellh/mapstructure"
|
|
)
|
|
|
|
// Google Compute Engine supports instance identity verification:
|
|
// https://cloud.google.com/compute/docs/instances/verifying-instance-identity
|
|
// Using this, we can exchange a signed instance payload for an agent token.
|
|
func (api *api) postWorkspaceAuthGoogleInstanceIdentity(rw http.ResponseWriter, r *http.Request) {
|
|
var req codersdk.GoogleInstanceIdentityToken
|
|
if !httpapi.Read(rw, r, &req) {
|
|
return
|
|
}
|
|
|
|
// We leave the audience blank. It's not important we validate who made the token.
|
|
payload, err := api.GoogleTokenValidator.Validate(r.Context(), req.JSONWebToken, "")
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
|
Message: fmt.Sprintf("validate: %s", err),
|
|
})
|
|
return
|
|
}
|
|
claims := struct {
|
|
Google struct {
|
|
ComputeEngine struct {
|
|
InstanceID string `mapstructure:"instance_id"`
|
|
} `mapstructure:"compute_engine"`
|
|
} `mapstructure:"google"`
|
|
}{}
|
|
err = mapstructure.Decode(payload.Claims, &claims)
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
|
Message: fmt.Sprintf("decode jwt claims: %s", err),
|
|
})
|
|
return
|
|
}
|
|
agent, err := api.Database.GetWorkspaceAgentByInstanceID(r.Context(), claims.Google.ComputeEngine.InstanceID)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
|
Message: fmt.Sprintf("instance with id %q not found", claims.Google.ComputeEngine.InstanceID),
|
|
})
|
|
return
|
|
}
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("get provisioner job agent: %s", err),
|
|
})
|
|
return
|
|
}
|
|
resource, err := api.Database.GetWorkspaceResourceByID(r.Context(), agent.ResourceID)
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("get provisioner job resource: %s", err),
|
|
})
|
|
return
|
|
}
|
|
job, err := api.Database.GetProvisionerJobByID(r.Context(), resource.JobID)
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("get provisioner job: %s", err),
|
|
})
|
|
return
|
|
}
|
|
if job.Type != database.ProvisionerJobTypeWorkspaceBuild {
|
|
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
|
Message: fmt.Sprintf("%q jobs cannot be authenticated", job.Type),
|
|
})
|
|
return
|
|
}
|
|
var jobData workspaceProvisionJob
|
|
err = json.Unmarshal(job.Input, &jobData)
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("extract job data: %s", err),
|
|
})
|
|
return
|
|
}
|
|
resourceHistory, err := api.Database.GetWorkspaceBuildByID(r.Context(), jobData.WorkspaceBuildID)
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("get workspace build: %s", err),
|
|
})
|
|
return
|
|
}
|
|
// This token should only be exchanged if the instance ID is valid
|
|
// for the latest history. If an instance ID is recycled by a cloud,
|
|
// we'd hate to leak access to a user's workspace.
|
|
latestHistory, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), resourceHistory.WorkspaceID)
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("get latest workspace build: %s", err),
|
|
})
|
|
return
|
|
}
|
|
if latestHistory.ID.String() != resourceHistory.ID.String() {
|
|
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
|
Message: fmt.Sprintf("resource found for id %q, but isn't registered on the latest history", claims.Google.ComputeEngine.InstanceID),
|
|
})
|
|
return
|
|
}
|
|
render.Status(r, http.StatusOK)
|
|
render.JSON(rw, r, codersdk.WorkspaceAgentAuthenticateResponse{
|
|
SessionToken: agent.AuthToken.String(),
|
|
})
|
|
}
|