2022-02-21 20:36:29 +00:00
|
|
|
package coderd
|
|
|
|
|
|
|
|
import (
|
|
|
|
"database/sql"
|
2022-02-28 18:00:52 +00:00
|
|
|
"encoding/json"
|
2022-02-21 20:36:29 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
|
2022-03-28 19:31:03 +00:00
|
|
|
"github.com/coder/coder/coderd/awsidentity"
|
2022-04-19 13:48:13 +00:00
|
|
|
"github.com/coder/coder/coderd/azureidentity"
|
2022-03-25 21:07:45 +00:00
|
|
|
"github.com/coder/coder/coderd/database"
|
2023-02-14 14:27:06 +00:00
|
|
|
"github.com/coder/coder/coderd/database/dbauthz"
|
2022-03-25 21:07:45 +00:00
|
|
|
"github.com/coder/coder/coderd/httpapi"
|
2022-11-08 01:10:49 +00:00
|
|
|
"github.com/coder/coder/coderd/provisionerdserver"
|
2022-03-22 19:17:50 +00:00
|
|
|
"github.com/coder/coder/codersdk"
|
2023-01-29 21:47:24 +00:00
|
|
|
"github.com/coder/coder/codersdk/agentsdk"
|
2022-02-21 20:36:29 +00:00
|
|
|
|
|
|
|
"github.com/mitchellh/mapstructure"
|
|
|
|
)
|
|
|
|
|
2022-04-19 13:48:13 +00:00
|
|
|
// Azure supports instance identity verification:
|
|
|
|
// https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#tabgroup_14
|
2023-01-05 14:27:10 +00:00
|
|
|
//
|
|
|
|
// @Summary Authenticate agent on Azure instance
|
|
|
|
// @ID authenticate-agent-on-azure-instance
|
|
|
|
// @Security CoderSessionToken
|
2023-01-13 11:27:21 +00:00
|
|
|
// @Accept json
|
2023-01-05 14:27:10 +00:00
|
|
|
// @Produce json
|
|
|
|
// @Tags Agents
|
2023-01-29 21:47:24 +00:00
|
|
|
// @Param request body agentsdk.AzureInstanceIdentityToken true "Instance identity token"
|
|
|
|
// @Success 200 {object} agentsdk.AuthenticateResponse
|
2023-01-05 14:27:10 +00:00
|
|
|
// @Router /workspaceagents/azure-instance-identity [post]
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) postWorkspaceAuthAzureInstanceIdentity(rw http.ResponseWriter, r *http.Request) {
|
2022-09-21 22:07:00 +00:00
|
|
|
ctx := r.Context()
|
2023-01-29 21:47:24 +00:00
|
|
|
var req agentsdk.AzureInstanceIdentityToken
|
2022-09-21 22:07:00 +00:00
|
|
|
if !httpapi.Read(ctx, rw, r, &req) {
|
2022-04-19 13:48:13 +00:00
|
|
|
return
|
|
|
|
}
|
2023-03-09 01:32:10 +00:00
|
|
|
instanceID, err := azureidentity.Validate(r.Context(), req.Signature, azureidentity.Options{
|
|
|
|
VerifyOptions: api.AzureCertificates,
|
|
|
|
})
|
2022-04-19 13:48:13 +00:00
|
|
|
if err != nil {
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Invalid Azure identity.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-04-19 13:48:13 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
api.handleAuthInstanceID(rw, r, instanceID)
|
|
|
|
}
|
|
|
|
|
2023-01-13 11:27:21 +00:00
|
|
|
// AWS supports instance identity verification:
|
|
|
|
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
|
|
|
|
// Using this, we can exchange a signed instance payload for an agent token.
|
|
|
|
//
|
2023-01-05 14:27:10 +00:00
|
|
|
// @Summary Authenticate agent on AWS instance
|
|
|
|
// @ID authenticate-agent-on-aws-instance
|
|
|
|
// @Security CoderSessionToken
|
2023-01-13 11:27:21 +00:00
|
|
|
// @Accept json
|
2023-01-05 14:27:10 +00:00
|
|
|
// @Produce json
|
|
|
|
// @Tags Agents
|
2023-01-29 21:47:24 +00:00
|
|
|
// @Param request body agentsdk.AWSInstanceIdentityToken true "Instance identity token"
|
|
|
|
// @Success 200 {object} agentsdk.AuthenticateResponse
|
2023-01-05 14:27:10 +00:00
|
|
|
// @Router /workspaceagents/aws-instance-identity [post]
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) postWorkspaceAuthAWSInstanceIdentity(rw http.ResponseWriter, r *http.Request) {
|
2022-09-21 22:07:00 +00:00
|
|
|
ctx := r.Context()
|
2023-01-29 21:47:24 +00:00
|
|
|
var req agentsdk.AWSInstanceIdentityToken
|
2022-09-21 22:07:00 +00:00
|
|
|
if !httpapi.Read(ctx, rw, r, &req) {
|
2022-03-28 19:31:03 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
identity, err := awsidentity.Validate(req.Signature, req.Document, api.AWSCertificates)
|
|
|
|
if err != nil {
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Invalid AWS identity.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-03-28 19:31:03 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
api.handleAuthInstanceID(rw, r, identity.InstanceID)
|
|
|
|
}
|
|
|
|
|
2023-01-13 11:27:21 +00:00
|
|
|
// 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.
|
|
|
|
//
|
2023-01-05 14:27:10 +00:00
|
|
|
// @Summary Authenticate agent on Google Cloud instance
|
|
|
|
// @ID authenticate-agent-on-google-cloud-instance
|
|
|
|
// @Security CoderSessionToken
|
2023-01-13 11:27:21 +00:00
|
|
|
// @Accept json
|
2023-01-05 14:27:10 +00:00
|
|
|
// @Produce json
|
|
|
|
// @Tags Agents
|
2023-01-29 21:47:24 +00:00
|
|
|
// @Param request body agentsdk.GoogleInstanceIdentityToken true "Instance identity token"
|
|
|
|
// @Success 200 {object} agentsdk.AuthenticateResponse
|
2023-01-05 14:27:10 +00:00
|
|
|
// @Router /workspaceagents/google-instance-identity [post]
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) postWorkspaceAuthGoogleInstanceIdentity(rw http.ResponseWriter, r *http.Request) {
|
2022-09-21 22:07:00 +00:00
|
|
|
ctx := r.Context()
|
2023-01-29 21:47:24 +00:00
|
|
|
var req agentsdk.GoogleInstanceIdentityToken
|
2022-09-21 22:07:00 +00:00
|
|
|
if !httpapi.Read(ctx, rw, r, &req) {
|
2022-02-21 20:36:29 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// We leave the audience blank. It's not important we validate who made the token.
|
2022-09-21 22:07:00 +00:00
|
|
|
payload, err := api.GoogleTokenValidator.Validate(ctx, req.JSONWebToken, "")
|
2022-02-21 20:36:29 +00:00
|
|
|
if err != nil {
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Invalid GCP identity.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-02-21 20:36:29 +00:00
|
|
|
})
|
|
|
|
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 {
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Error decoding JWT claims.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-02-21 20:36:29 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-03-28 19:31:03 +00:00
|
|
|
api.handleAuthInstanceID(rw, r, claims.Google.ComputeEngine.InstanceID)
|
|
|
|
}
|
|
|
|
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) handleAuthInstanceID(rw http.ResponseWriter, r *http.Request, instanceID string) {
|
2022-09-21 22:07:00 +00:00
|
|
|
ctx := r.Context()
|
2023-02-14 14:27:06 +00:00
|
|
|
//nolint:gocritic // needed for auth instance id
|
2023-02-15 16:14:37 +00:00
|
|
|
agent, err := api.Database.GetWorkspaceAgentByInstanceID(dbauthz.AsSystemRestricted(ctx), instanceID)
|
2022-02-21 20:36:29 +00:00
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: fmt.Sprintf("Instance with id %q not found.", instanceID),
|
2022-02-21 20:36:29 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-02-28 18:00:52 +00:00
|
|
|
if err != nil {
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error fetching provisioner job agent.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-02-28 18:00:52 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2023-02-14 14:27:06 +00:00
|
|
|
//nolint:gocritic // needed for auth instance id
|
2023-02-15 16:14:37 +00:00
|
|
|
resource, err := api.Database.GetWorkspaceResourceByID(dbauthz.AsSystemRestricted(ctx), agent.ResourceID)
|
2022-02-28 18:00:52 +00:00
|
|
|
if err != nil {
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error fetching provisioner job resource.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-02-28 18:00:52 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2023-02-14 14:27:06 +00:00
|
|
|
//nolint:gocritic // needed for auth instance id
|
2023-02-15 16:14:37 +00:00
|
|
|
job, err := api.Database.GetProvisionerJobByID(dbauthz.AsSystemRestricted(ctx), resource.JobID)
|
2022-02-28 18:00:52 +00:00
|
|
|
if err != nil {
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error fetching provisioner job.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-02-28 18:00:52 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-03-07 17:40:54 +00:00
|
|
|
if job.Type != database.ProvisionerJobTypeWorkspaceBuild {
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: fmt.Sprintf("%q jobs cannot be authenticated.", job.Type),
|
2022-02-28 18:00:52 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-11-08 01:10:49 +00:00
|
|
|
var jobData provisionerdserver.WorkspaceProvisionJob
|
2022-02-28 18:00:52 +00:00
|
|
|
err = json.Unmarshal(job.Input, &jobData)
|
|
|
|
if err != nil {
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error extracting job data.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-02-28 18:00:52 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2023-02-14 14:27:06 +00:00
|
|
|
//nolint:gocritic // needed for auth instance id
|
2023-02-15 16:14:37 +00:00
|
|
|
resourceHistory, err := api.Database.GetWorkspaceBuildByID(dbauthz.AsSystemRestricted(ctx), jobData.WorkspaceBuildID)
|
2022-02-21 20:36:29 +00:00
|
|
|
if err != nil {
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error fetching workspace build.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-02-21 20:36:29 +00:00
|
|
|
})
|
|
|
|
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.
|
2023-02-14 14:27:06 +00:00
|
|
|
//nolint:gocritic // needed for auth instance id
|
2023-02-15 16:14:37 +00:00
|
|
|
latestHistory, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(dbauthz.AsSystemRestricted(ctx), resourceHistory.WorkspaceID)
|
2022-02-21 20:36:29 +00:00
|
|
|
if err != nil {
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: "Internal error fetching the latest workspace build.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-02-21 20:36:29 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-05-17 20:00:48 +00:00
|
|
|
if latestHistory.ID != resourceHistory.ID {
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: fmt.Sprintf("Resource found for id %q, but isn't registered on the latest history.", instanceID),
|
2022-02-21 20:36:29 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-04-12 15:17:33 +00:00
|
|
|
|
2023-01-29 21:47:24 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, agentsdk.AuthenticateResponse{
|
2022-02-28 18:00:52 +00:00
|
|
|
SessionToken: agent.AuthToken.String(),
|
2022-02-21 20:36:29 +00:00
|
|
|
})
|
|
|
|
}
|