mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
- As part of merging support for Template RBAC and user groups a permission check on reading files was relaxed. With the addition of admin roles on individual templates, regular users are now able to push template versions if they have inherited the 'admin' role for a template. In order to do so they need to be able to create and read their own files. Since collisions on hash in the past were ignored, this means that a regular user who pushes a template version with a file hash that collides with an existing hash will not be able to read the file (since it belongs to another user). This commit fixes the underlying problem which was that the files table had a primary key on the 'hash' column. This was not a problem at the time because only template admins and other users with similar elevated roles were able to read all files regardless of ownership. To fix this a new column and primary key 'id' has been introduced to the files table. The unique constraint has been updated to be hash+created_by. Tables (provisioner_jobs) that referenced files.hash have been updated to reference files.id. Relevant API endpoints have also been updated.
165 lines
4.6 KiB
Go
165 lines
4.6 KiB
Go
package codersdk
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/cookiejar"
|
|
"net/url"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/xerrors"
|
|
"nhooyr.io/websocket"
|
|
)
|
|
|
|
type LogSource string
|
|
|
|
const (
|
|
LogSourceProvisionerDaemon LogSource = "provisioner_daemon"
|
|
LogSourceProvisioner LogSource = "provisioner"
|
|
)
|
|
|
|
type LogLevel string
|
|
|
|
const (
|
|
LogLevelTrace LogLevel = "trace"
|
|
LogLevelDebug LogLevel = "debug"
|
|
LogLevelInfo LogLevel = "info"
|
|
LogLevelWarn LogLevel = "warn"
|
|
LogLevelError LogLevel = "error"
|
|
)
|
|
|
|
type ProvisionerDaemon struct {
|
|
ID uuid.UUID `json:"id"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt sql.NullTime `json:"updated_at"`
|
|
Name string `json:"name"`
|
|
Provisioners []ProvisionerType `json:"provisioners"`
|
|
}
|
|
|
|
// ProvisionerJobStatus represents the at-time state of a job.
|
|
type ProvisionerJobStatus string
|
|
|
|
// Active returns whether the job is still active or not.
|
|
// It returns true if canceling as well, since the job isn't
|
|
// in an entirely inactive state yet.
|
|
func (p ProvisionerJobStatus) Active() bool {
|
|
return p == ProvisionerJobPending ||
|
|
p == ProvisionerJobRunning ||
|
|
p == ProvisionerJobCanceling
|
|
}
|
|
|
|
const (
|
|
ProvisionerJobPending ProvisionerJobStatus = "pending"
|
|
ProvisionerJobRunning ProvisionerJobStatus = "running"
|
|
ProvisionerJobSucceeded ProvisionerJobStatus = "succeeded"
|
|
ProvisionerJobCanceling ProvisionerJobStatus = "canceling"
|
|
ProvisionerJobCanceled ProvisionerJobStatus = "canceled"
|
|
ProvisionerJobFailed ProvisionerJobStatus = "failed"
|
|
)
|
|
|
|
type ProvisionerJob struct {
|
|
ID uuid.UUID `json:"id"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
StartedAt *time.Time `json:"started_at,omitempty"`
|
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
|
CanceledAt *time.Time `json:"canceled_at,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
Status ProvisionerJobStatus `json:"status"`
|
|
WorkerID *uuid.UUID `json:"worker_id,omitempty"`
|
|
FileID uuid.UUID `json:"file_id"`
|
|
}
|
|
|
|
type ProvisionerJobLog struct {
|
|
ID uuid.UUID `json:"id"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
Source LogSource `json:"log_source"`
|
|
Level LogLevel `json:"log_level"`
|
|
Stage string `json:"stage"`
|
|
Output string `json:"output"`
|
|
}
|
|
|
|
// provisionerJobLogsBefore provides log output that occurred before a time.
|
|
// This is abstracted from a specific job type to provide consistency between
|
|
// APIs. Logs is the only shared route between jobs.
|
|
func (c *Client) provisionerJobLogsBefore(ctx context.Context, path string, before time.Time) ([]ProvisionerJobLog, error) {
|
|
values := url.Values{}
|
|
if !before.IsZero() {
|
|
values["before"] = []string{strconv.FormatInt(before.UTC().UnixMilli(), 10)}
|
|
}
|
|
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("%s?%s", path, values.Encode()), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if res.StatusCode != http.StatusOK {
|
|
defer res.Body.Close()
|
|
return nil, readBodyAsError(res)
|
|
}
|
|
|
|
var logs []ProvisionerJobLog
|
|
return logs, json.NewDecoder(res.Body).Decode(&logs)
|
|
}
|
|
|
|
// provisionerJobLogsAfter streams logs that occurred after a specific time.
|
|
func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after time.Time) (<-chan ProvisionerJobLog, io.Closer, error) {
|
|
afterQuery := ""
|
|
if !after.IsZero() {
|
|
afterQuery = fmt.Sprintf("&after=%d", after.UTC().UnixMilli())
|
|
}
|
|
followURL, err := c.URL.Parse(fmt.Sprintf("%s?follow%s", path, afterQuery))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
jar, err := cookiejar.New(nil)
|
|
if err != nil {
|
|
return nil, nil, xerrors.Errorf("create cookie jar: %w", err)
|
|
}
|
|
jar.SetCookies(followURL, []*http.Cookie{{
|
|
Name: SessionTokenKey,
|
|
Value: c.SessionToken,
|
|
}})
|
|
httpClient := &http.Client{
|
|
Jar: jar,
|
|
}
|
|
conn, res, err := websocket.Dial(ctx, followURL.String(), &websocket.DialOptions{
|
|
HTTPClient: httpClient,
|
|
CompressionMode: websocket.CompressionDisabled,
|
|
})
|
|
if err != nil {
|
|
if res == nil {
|
|
return nil, nil, err
|
|
}
|
|
return nil, nil, readBodyAsError(res)
|
|
}
|
|
logs := make(chan ProvisionerJobLog)
|
|
decoder := json.NewDecoder(websocket.NetConn(ctx, conn, websocket.MessageText))
|
|
closed := make(chan struct{})
|
|
go func() {
|
|
defer close(closed)
|
|
defer close(logs)
|
|
defer conn.Close(websocket.StatusGoingAway, "")
|
|
var log ProvisionerJobLog
|
|
for {
|
|
err = decoder.Decode(&log)
|
|
if err != nil {
|
|
return
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case logs <- log:
|
|
}
|
|
}
|
|
}()
|
|
return logs, closeFunc(func() error {
|
|
_ = conn.Close(websocket.StatusNormalClosure, "")
|
|
<-closed
|
|
return nil
|
|
}), nil
|
|
}
|