diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 6daf5f481a..79e24f32d0 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -12619,8 +12619,28 @@ const docTemplate = `{ "port": { "type": "integer" }, + "protocol": { + "enum": [ + "http", + "https" + ], + "allOf": [ + { + "$ref": "#/definitions/codersdk.WorkspaceAgentPortShareProtocol" + } + ] + }, "share_level": { - "$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel" + "enum": [ + "owner", + "authenticated", + "public" + ], + "allOf": [ + { + "$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel" + } + ] } } }, @@ -13366,8 +13386,28 @@ const docTemplate = `{ "port": { "type": "integer" }, + "protocol": { + "enum": [ + "http", + "https" + ], + "allOf": [ + { + "$ref": "#/definitions/codersdk.WorkspaceAgentPortShareProtocol" + } + ] + }, "share_level": { - "$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel" + "enum": [ + "owner", + "authenticated", + "public" + ], + "allOf": [ + { + "$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel" + } + ] }, "workspace_id": { "type": "string", @@ -13388,6 +13428,17 @@ const docTemplate = `{ "WorkspaceAgentPortShareLevelPublic" ] }, + "codersdk.WorkspaceAgentPortShareProtocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "x-enum-varnames": [ + "WorkspaceAgentPortShareProtocolHTTP", + "WorkspaceAgentPortShareProtocolHTTPS" + ] + }, "codersdk.WorkspaceAgentPortShares": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index d911ecac28..20934d817b 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -11442,8 +11442,21 @@ "port": { "type": "integer" }, + "protocol": { + "enum": ["http", "https"], + "allOf": [ + { + "$ref": "#/definitions/codersdk.WorkspaceAgentPortShareProtocol" + } + ] + }, "share_level": { - "$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel" + "enum": ["owner", "authenticated", "public"], + "allOf": [ + { + "$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel" + } + ] } } }, @@ -12164,8 +12177,21 @@ "port": { "type": "integer" }, + "protocol": { + "enum": ["http", "https"], + "allOf": [ + { + "$ref": "#/definitions/codersdk.WorkspaceAgentPortShareProtocol" + } + ] + }, "share_level": { - "$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel" + "enum": ["owner", "authenticated", "public"], + "allOf": [ + { + "$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel" + } + ] }, "workspace_id": { "type": "string", @@ -12182,6 +12208,14 @@ "WorkspaceAgentPortShareLevelPublic" ] }, + "codersdk.WorkspaceAgentPortShareProtocol": { + "type": "string", + "enum": ["http", "https"], + "x-enum-varnames": [ + "WorkspaceAgentPortShareProtocolHTTP", + "WorkspaceAgentPortShareProtocolHTTPS" + ] + }, "codersdk.WorkspaceAgentPortShares": { "type": "object", "properties": { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index f457edde8e..65b18b01fb 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -1608,6 +1608,7 @@ func (s *MethodTestSuite) TestWorkspacePortSharing() { AgentName: ps.AgentName, Port: ps.Port, ShareLevel: ps.ShareLevel, + Protocol: ps.Protocol, }).Asserts(ws, rbac.ActionUpdate).Returns(ps) })) s.Run("GetWorkspaceAgentPortShare", s.Subtest(func(db database.Store, check *expects) { diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index c24f4cb826..59dbbb9715 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -140,6 +140,7 @@ func WorkspaceAgentPortShare(t testing.TB, db database.Store, orig database.Work AgentName: takeFirst(orig.AgentName, namesgenerator.GetRandomName(1)), Port: takeFirst(orig.Port, 8080), ShareLevel: takeFirst(orig.ShareLevel, database.AppSharingLevelPublic), + Protocol: takeFirst(orig.Protocol, database.PortShareProtocolHttp), }) require.NoError(t, err, "insert workspace agent") return ps diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 1753362c30..71950aefd9 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -86,7 +86,7 @@ func New() database.Store { UpdatedAt: dbtime.Now(), }) if err != nil { - panic(fmt.Errorf("failed to create default organization: %w", err)) + panic(xerrors.Errorf("failed to create default organization: %w", err)) } q.defaultProxyDisplayName = "Default" q.defaultProxyIconURL = "/emojis/1f3e1.png" @@ -7933,6 +7933,7 @@ func (q *FakeQuerier) UpsertWorkspaceAgentPortShare(_ context.Context, arg datab for i, share := range q.workspaceAgentPortShares { if share.WorkspaceID == arg.WorkspaceID && share.Port == arg.Port && share.AgentName == arg.AgentName { share.ShareLevel = arg.ShareLevel + share.Protocol = arg.Protocol q.workspaceAgentPortShares[i] = share return share, nil } @@ -7944,6 +7945,7 @@ func (q *FakeQuerier) UpsertWorkspaceAgentPortShare(_ context.Context, arg datab AgentName: arg.AgentName, Port: arg.Port, ShareLevel: arg.ShareLevel, + Protocol: arg.Protocol, } q.workspaceAgentPortShares = append(q.workspaceAgentPortShares, psl) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index b9f002d251..81a6b77df8 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -95,6 +95,11 @@ CREATE TYPE parameter_type_system AS ENUM ( 'hcl' ); +CREATE TYPE port_share_protocol AS ENUM ( + 'http', + 'https' +); + CREATE TYPE provisioner_job_status AS ENUM ( 'pending', 'running', @@ -1027,7 +1032,8 @@ CREATE TABLE workspace_agent_port_share ( workspace_id uuid NOT NULL, agent_name text NOT NULL, port integer NOT NULL, - share_level app_sharing_level NOT NULL + share_level app_sharing_level NOT NULL, + protocol port_share_protocol DEFAULT 'http'::port_share_protocol NOT NULL ); CREATE TABLE workspace_agent_scripts ( diff --git a/coderd/database/migrations/000199_port_share_protocol.down.sql b/coderd/database/migrations/000199_port_share_protocol.down.sql new file mode 100644 index 0000000000..79643075d8 --- /dev/null +++ b/coderd/database/migrations/000199_port_share_protocol.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE workspace_agent_port_share DROP COLUMN protocol; + +DROP TYPE port_share_protocol; diff --git a/coderd/database/migrations/000199_port_share_protocol.up.sql b/coderd/database/migrations/000199_port_share_protocol.up.sql new file mode 100644 index 0000000000..fde5001737 --- /dev/null +++ b/coderd/database/migrations/000199_port_share_protocol.up.sql @@ -0,0 +1,4 @@ +CREATE TYPE port_share_protocol AS ENUM ('http', 'https'); + +ALTER TABLE workspace_agent_port_share + ADD COLUMN protocol port_share_protocol NOT NULL DEFAULT 'http'::port_share_protocol; diff --git a/coderd/database/models.go b/coderd/database/models.go index 677d258105..6047ea619a 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -898,6 +898,64 @@ func AllParameterTypeSystemValues() []ParameterTypeSystem { } } +type PortShareProtocol string + +const ( + PortShareProtocolHttp PortShareProtocol = "http" + PortShareProtocolHttps PortShareProtocol = "https" +) + +func (e *PortShareProtocol) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = PortShareProtocol(s) + case string: + *e = PortShareProtocol(s) + default: + return fmt.Errorf("unsupported scan type for PortShareProtocol: %T", src) + } + return nil +} + +type NullPortShareProtocol struct { + PortShareProtocol PortShareProtocol `json:"port_share_protocol"` + Valid bool `json:"valid"` // Valid is true if PortShareProtocol is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullPortShareProtocol) Scan(value interface{}) error { + if value == nil { + ns.PortShareProtocol, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.PortShareProtocol.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullPortShareProtocol) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.PortShareProtocol), nil +} + +func (e PortShareProtocol) Valid() bool { + switch e { + case PortShareProtocolHttp, + PortShareProtocolHttps: + return true + } + return false +} + +func AllPortShareProtocolValues() []PortShareProtocol { + return []PortShareProtocol{ + PortShareProtocolHttp, + PortShareProtocolHttps, + } +} + // Computed status of a provisioner job. Jobs could be stuck in a hung state, these states do not guarantee any transition to another state. type ProvisionerJobStatus string @@ -2312,10 +2370,11 @@ type WorkspaceAgentMetadatum struct { } type WorkspaceAgentPortShare struct { - WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` - AgentName string `db:"agent_name" json:"agent_name"` - Port int32 `db:"port" json:"port"` - ShareLevel AppSharingLevel `db:"share_level" json:"share_level"` + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + AgentName string `db:"agent_name" json:"agent_name"` + Port int32 `db:"port" json:"port"` + ShareLevel AppSharingLevel `db:"share_level" json:"share_level"` + Protocol PortShareProtocol `db:"protocol" json:"protocol"` } type WorkspaceAgentScript struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index fe2393a137..e3123cb5f2 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -8469,7 +8469,12 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP } const deleteWorkspaceAgentPortShare = `-- name: DeleteWorkspaceAgentPortShare :exec -DELETE FROM workspace_agent_port_share WHERE workspace_id = $1 AND agent_name = $2 AND port = $3 +DELETE FROM + workspace_agent_port_share +WHERE + workspace_id = $1 + AND agent_name = $2 + AND port = $3 ` type DeleteWorkspaceAgentPortShareParams struct { @@ -8484,7 +8489,17 @@ func (q *sqlQuerier) DeleteWorkspaceAgentPortShare(ctx context.Context, arg Dele } const deleteWorkspaceAgentPortSharesByTemplate = `-- name: DeleteWorkspaceAgentPortSharesByTemplate :exec -DELETE FROM workspace_agent_port_share WHERE workspace_id IN (SELECT id FROM workspaces WHERE template_id = $1) +DELETE FROM + workspace_agent_port_share +WHERE + workspace_id IN ( + SELECT + id + FROM + workspaces + WHERE + template_id = $1 + ) ` func (q *sqlQuerier) DeleteWorkspaceAgentPortSharesByTemplate(ctx context.Context, templateID uuid.UUID) error { @@ -8493,7 +8508,14 @@ func (q *sqlQuerier) DeleteWorkspaceAgentPortSharesByTemplate(ctx context.Contex } const getWorkspaceAgentPortShare = `-- name: GetWorkspaceAgentPortShare :one -SELECT workspace_id, agent_name, port, share_level FROM workspace_agent_port_share WHERE workspace_id = $1 AND agent_name = $2 AND port = $3 +SELECT + workspace_id, agent_name, port, share_level, protocol +FROM + workspace_agent_port_share +WHERE + workspace_id = $1 + AND agent_name = $2 + AND port = $3 ` type GetWorkspaceAgentPortShareParams struct { @@ -8510,12 +8532,18 @@ func (q *sqlQuerier) GetWorkspaceAgentPortShare(ctx context.Context, arg GetWork &i.AgentName, &i.Port, &i.ShareLevel, + &i.Protocol, ) return i, err } const listWorkspaceAgentPortShares = `-- name: ListWorkspaceAgentPortShares :many -SELECT workspace_id, agent_name, port, share_level FROM workspace_agent_port_share WHERE workspace_id = $1 +SELECT + workspace_id, agent_name, port, share_level, protocol +FROM + workspace_agent_port_share +WHERE + workspace_id = $1 ` func (q *sqlQuerier) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgentPortShare, error) { @@ -8532,6 +8560,7 @@ func (q *sqlQuerier) ListWorkspaceAgentPortShares(ctx context.Context, workspace &i.AgentName, &i.Port, &i.ShareLevel, + &i.Protocol, ); err != nil { return nil, err } @@ -8547,7 +8576,20 @@ func (q *sqlQuerier) ListWorkspaceAgentPortShares(ctx context.Context, workspace } const reduceWorkspaceAgentShareLevelToAuthenticatedByTemplate = `-- name: ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate :exec -UPDATE workspace_agent_port_share SET share_level = 'authenticated' WHERE share_level = 'public' AND workspace_id IN (SELECT id FROM workspaces WHERE template_id = $1) +UPDATE + workspace_agent_port_share +SET + share_level = 'authenticated' +WHERE + share_level = 'public' + AND workspace_id IN ( + SELECT + id + FROM + workspaces + WHERE + template_id = $1 + ) ` func (q *sqlQuerier) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error { @@ -8556,16 +8598,38 @@ func (q *sqlQuerier) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx } const upsertWorkspaceAgentPortShare = `-- name: UpsertWorkspaceAgentPortShare :one -INSERT INTO workspace_agent_port_share (workspace_id, agent_name, port, share_level) -VALUES ($1, $2, $3, $4) -ON CONFLICT (workspace_id, agent_name, port) DO UPDATE SET share_level = $4 RETURNING workspace_id, agent_name, port, share_level +INSERT INTO + workspace_agent_port_share ( + workspace_id, + agent_name, + port, + share_level, + protocol + ) +VALUES ( + $1, + $2, + $3, + $4, + $5 +) +ON CONFLICT ( + workspace_id, + agent_name, + port +) +DO UPDATE SET + share_level = $4, + protocol = $5 +RETURNING workspace_id, agent_name, port, share_level, protocol ` type UpsertWorkspaceAgentPortShareParams struct { - WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` - AgentName string `db:"agent_name" json:"agent_name"` - Port int32 `db:"port" json:"port"` - ShareLevel AppSharingLevel `db:"share_level" json:"share_level"` + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + AgentName string `db:"agent_name" json:"agent_name"` + Port int32 `db:"port" json:"port"` + ShareLevel AppSharingLevel `db:"share_level" json:"share_level"` + Protocol PortShareProtocol `db:"protocol" json:"protocol"` } func (q *sqlQuerier) UpsertWorkspaceAgentPortShare(ctx context.Context, arg UpsertWorkspaceAgentPortShareParams) (WorkspaceAgentPortShare, error) { @@ -8574,6 +8638,7 @@ func (q *sqlQuerier) UpsertWorkspaceAgentPortShare(ctx context.Context, arg Upse arg.AgentName, arg.Port, arg.ShareLevel, + arg.Protocol, ) var i WorkspaceAgentPortShare err := row.Scan( @@ -8581,6 +8646,7 @@ func (q *sqlQuerier) UpsertWorkspaceAgentPortShare(ctx context.Context, arg Upse &i.AgentName, &i.Port, &i.ShareLevel, + &i.Protocol, ) return i, err } diff --git a/coderd/database/queries/workspaceagentportshare.sql b/coderd/database/queries/workspaceagentportshare.sql index 021089348f..d2e5c3a5ff 100644 --- a/coderd/database/queries/workspaceagentportshare.sql +++ b/coderd/database/queries/workspaceagentportshare.sql @@ -1,19 +1,80 @@ -- name: GetWorkspaceAgentPortShare :one -SELECT * FROM workspace_agent_port_share WHERE workspace_id = $1 AND agent_name = $2 AND port = $3; +SELECT + * +FROM + workspace_agent_port_share +WHERE + workspace_id = $1 + AND agent_name = $2 + AND port = $3; -- name: ListWorkspaceAgentPortShares :many -SELECT * FROM workspace_agent_port_share WHERE workspace_id = $1; +SELECT + * +FROM + workspace_agent_port_share +WHERE + workspace_id = $1; -- name: DeleteWorkspaceAgentPortShare :exec -DELETE FROM workspace_agent_port_share WHERE workspace_id = $1 AND agent_name = $2 AND port = $3; +DELETE FROM + workspace_agent_port_share +WHERE + workspace_id = $1 + AND agent_name = $2 + AND port = $3; -- name: UpsertWorkspaceAgentPortShare :one -INSERT INTO workspace_agent_port_share (workspace_id, agent_name, port, share_level) -VALUES ($1, $2, $3, $4) -ON CONFLICT (workspace_id, agent_name, port) DO UPDATE SET share_level = $4 RETURNING *; +INSERT INTO + workspace_agent_port_share ( + workspace_id, + agent_name, + port, + share_level, + protocol + ) +VALUES ( + $1, + $2, + $3, + $4, + $5 +) +ON CONFLICT ( + workspace_id, + agent_name, + port +) +DO UPDATE SET + share_level = $4, + protocol = $5 +RETURNING *; -- name: ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate :exec -UPDATE workspace_agent_port_share SET share_level = 'authenticated' WHERE share_level = 'public' AND workspace_id IN (SELECT id FROM workspaces WHERE template_id = $1); +UPDATE + workspace_agent_port_share +SET + share_level = 'authenticated' +WHERE + share_level = 'public' + AND workspace_id IN ( + SELECT + id + FROM + workspaces + WHERE + template_id = $1 + ); -- name: DeleteWorkspaceAgentPortSharesByTemplate :exec -DELETE FROM workspace_agent_port_share WHERE workspace_id IN (SELECT id FROM workspaces WHERE template_id = $1); +DELETE FROM + workspace_agent_port_share +WHERE + workspace_id IN ( + SELECT + id + FROM + workspaces + WHERE + template_id = $1 + ); diff --git a/coderd/workspaceagentportshare.go b/coderd/workspaceagentportshare.go index 273439d8a0..1252054804 100644 --- a/coderd/workspaceagentportshare.go +++ b/coderd/workspaceagentportshare.go @@ -4,6 +4,7 @@ import ( "database/sql" "errors" "net/http" + "slices" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/httpapi" @@ -55,6 +56,12 @@ func (api *API) postWorkspaceAgentPortShare(rw http.ResponseWriter, r *http.Requ }) return } + if !req.Protocol.ValidPortProtocol() { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Port protocol not allowed.", + }) + return + } template, err := api.Database.GetTemplateByID(ctx, workspace.TemplateID) if err != nil { @@ -95,6 +102,7 @@ func (api *API) postWorkspaceAgentPortShare(rw http.ResponseWriter, r *http.Requ AgentName: req.AgentName, Port: req.Port, ShareLevel: database.AppSharingLevel(req.ShareLevel), + Protocol: database.PortShareProtocol(req.Protocol), }) if err != nil { httpapi.InternalServerError(rw, err) @@ -179,6 +187,9 @@ func convertPortShares(shares []database.WorkspaceAgentPortShare) []codersdk.Wor for _, share := range shares { converted = append(converted, convertPortShare(share)) } + slices.SortFunc(converted, func(i, j codersdk.WorkspaceAgentPortShare) int { + return (int)(i.Port - j.Port) + }) return converted } @@ -188,5 +199,6 @@ func convertPortShare(share database.WorkspaceAgentPortShare) codersdk.Workspace AgentName: share.AgentName, Port: share.Port, ShareLevel: codersdk.WorkspaceAgentPortShareLevel(share.ShareLevel), + Protocol: codersdk.WorkspaceAgentPortShareProtocol(share.Protocol), } } diff --git a/coderd/workspaceagentportshare_test.go b/coderd/workspaceagentportshare_test.go index ad019fa955..ae8b9fb96d 100644 --- a/coderd/workspaceagentportshare_test.go +++ b/coderd/workspaceagentportshare_test.go @@ -43,6 +43,7 @@ func TestPostWorkspaceAgentPortShare(t *testing.T) { AgentName: agents[0].Name, Port: 8080, ShareLevel: codersdk.WorkspaceAgentPortShareLevel("owner"), + Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP, }) require.Error(t, err) @@ -51,6 +52,16 @@ func TestPostWorkspaceAgentPortShare(t *testing.T) { AgentName: agents[0].Name, Port: 8080, ShareLevel: codersdk.WorkspaceAgentPortShareLevel("invalid"), + Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP, + }) + require.Error(t, err) + + // invalid protocol should fail + _, err = client.UpsertWorkspaceAgentPortShare(ctx, r.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{ + AgentName: agents[0].Name, + Port: 8080, + ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic, + Protocol: codersdk.WorkspaceAgentPortShareProtocol("invalid"), }) require.Error(t, err) @@ -59,6 +70,7 @@ func TestPostWorkspaceAgentPortShare(t *testing.T) { AgentName: agents[0].Name, Port: 0, ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic, + Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP, }) require.Error(t, err) _, err = client.UpsertWorkspaceAgentPortShare(ctx, r.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{ @@ -73,18 +85,54 @@ func TestPostWorkspaceAgentPortShare(t *testing.T) { AgentName: agents[0].Name, Port: 8080, ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic, + Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTPS, }) require.NoError(t, err) require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelPublic, ps.ShareLevel) + require.EqualValues(t, codersdk.WorkspaceAgentPortShareProtocolHTTPS, ps.Protocol) - // update share level + // list + list, err := client.GetWorkspaceAgentPortShares(ctx, r.Workspace.ID) + require.NoError(t, err) + require.Len(t, list.Shares, 1) + require.EqualValues(t, agents[0].Name, list.Shares[0].AgentName) + require.EqualValues(t, 8080, list.Shares[0].Port) + require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelPublic, list.Shares[0].ShareLevel) + require.EqualValues(t, codersdk.WorkspaceAgentPortShareProtocolHTTPS, list.Shares[0].Protocol) + + // update share level and protocol ps, err = client.UpsertWorkspaceAgentPortShare(ctx, r.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{ AgentName: agents[0].Name, Port: 8080, ShareLevel: codersdk.WorkspaceAgentPortShareLevelAuthenticated, + Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP, }) require.NoError(t, err) require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelAuthenticated, ps.ShareLevel) + require.EqualValues(t, codersdk.WorkspaceAgentPortShareProtocolHTTP, ps.Protocol) + + // list + list, err = client.GetWorkspaceAgentPortShares(ctx, r.Workspace.ID) + require.NoError(t, err) + require.Len(t, list.Shares, 1) + require.EqualValues(t, agents[0].Name, list.Shares[0].AgentName) + require.EqualValues(t, 8080, list.Shares[0].Port) + require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelAuthenticated, list.Shares[0].ShareLevel) + require.EqualValues(t, codersdk.WorkspaceAgentPortShareProtocolHTTP, list.Shares[0].Protocol) + + // list 2 ordered by port + ps, err = client.UpsertWorkspaceAgentPortShare(ctx, r.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{ + AgentName: agents[0].Name, + Port: 8081, + ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic, + Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTPS, + }) + require.NoError(t, err) + list, err = client.GetWorkspaceAgentPortShares(ctx, r.Workspace.ID) + require.NoError(t, err) + require.Len(t, list.Shares, 2) + require.EqualValues(t, 8080, list.Shares[0].Port) + require.EqualValues(t, 8081, list.Shares[1].Port) } func TestGetWorkspaceAgentPortShares(t *testing.T) { @@ -115,6 +163,7 @@ func TestGetWorkspaceAgentPortShares(t *testing.T) { AgentName: agents[0].Name, Port: 8080, ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic, + Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP, }) require.NoError(t, err) @@ -155,6 +204,7 @@ func TestDeleteWorkspaceAgentPortShare(t *testing.T) { AgentName: agents[0].Name, Port: 8080, ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic, + Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP, }) require.NoError(t, err) require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelPublic, ps.ShareLevel) diff --git a/coderd/workspaceapps/apptest/apptest.go b/coderd/workspaceapps/apptest/apptest.go index d72cf522d2..6a17df402f 100644 --- a/coderd/workspaceapps/apptest/apptest.go +++ b/coderd/workspaceapps/apptest/apptest.go @@ -909,79 +909,6 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { require.Equal(t, http.StatusOK, resp.StatusCode) }) - t.Run("PortSharingNoShare", func(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - userClient, _ := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, appDetails.FirstUser.OrganizationID, rbac.RoleMember()) - userAppClient := appDetails.AppClient(t) - userAppClient.SetSessionToken(userClient.SessionToken()) - - resp, err := requestWithRetries(ctx, t, userAppClient, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.Port).String(), nil) - require.NoError(t, err) - defer resp.Body.Close() - require.Equal(t, http.StatusNotFound, resp.StatusCode) - }) - - t.Run("PortSharingAuthenticatedOK", func(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - // we are shadowing the parent since we are changing the state - appDetails := setupProxyTest(t, nil) - - port, err := strconv.ParseInt(appDetails.Apps.Port.AppSlugOrPort, 10, 32) - require.NoError(t, err) - // set the port we have to be shared with authenticated users - _, err = appDetails.SDKClient.UpsertWorkspaceAgentPortShare(ctx, appDetails.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{ - AgentName: proxyTestAgentName, - Port: int32(port), - ShareLevel: codersdk.WorkspaceAgentPortShareLevelAuthenticated, - }) - require.NoError(t, err) - - userClient, _ := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, appDetails.FirstUser.OrganizationID, rbac.RoleMember()) - userAppClient := appDetails.AppClient(t) - userAppClient.SetSessionToken(userClient.SessionToken()) - - resp, err := requestWithRetries(ctx, t, userAppClient, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.Port).String(), nil) - require.NoError(t, err) - defer resp.Body.Close() - require.Equal(t, http.StatusOK, resp.StatusCode) - }) - - t.Run("PortSharingPublicOK", func(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - // we are shadowing the parent since we are changing the state - appDetails := setupProxyTest(t, nil) - - port, err := strconv.ParseInt(appDetails.Apps.Port.AppSlugOrPort, 10, 32) - require.NoError(t, err) - // set the port we have to be shared with public - _, err = appDetails.SDKClient.UpsertWorkspaceAgentPortShare(ctx, appDetails.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{ - AgentName: proxyTestAgentName, - Port: int32(port), - ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic, - }) - require.NoError(t, err) - - publicAppClient := appDetails.AppClient(t) - publicAppClient.SetSessionToken("") - - resp, err := requestWithRetries(ctx, t, publicAppClient, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.Port).String(), nil) - require.NoError(t, err) - defer resp.Body.Close() - require.Equal(t, http.StatusOK, resp.StatusCode) - }) - t.Run("ProxyError", func(t *testing.T) { t.Parallel() @@ -1119,6 +1046,108 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { }) }) + t.Run("PortSharing", func(t *testing.T) { + t.Run("NoShare", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + appDetails := setupProxyTest(t, nil) + userClient, _ := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, appDetails.FirstUser.OrganizationID, rbac.RoleMember()) + userAppClient := appDetails.AppClient(t) + userAppClient.SetSessionToken(userClient.SessionToken()) + + resp, err := requestWithRetries(ctx, t, userAppClient, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.Port).String(), nil) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusNotFound, resp.StatusCode) + }) + + t.Run("AuthenticatedOK", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + appDetails := setupProxyTest(t, nil) + port, err := strconv.ParseInt(appDetails.Apps.Port.AppSlugOrPort, 10, 32) + require.NoError(t, err) + // set the port we have to be shared with authenticated users + _, err = appDetails.SDKClient.UpsertWorkspaceAgentPortShare(ctx, appDetails.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{ + AgentName: proxyTestAgentName, + Port: int32(port), + ShareLevel: codersdk.WorkspaceAgentPortShareLevelAuthenticated, + Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP, + }) + require.NoError(t, err) + + userClient, _ := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, appDetails.FirstUser.OrganizationID, rbac.RoleMember()) + userAppClient := appDetails.AppClient(t) + userAppClient.SetSessionToken(userClient.SessionToken()) + + resp, err := requestWithRetries(ctx, t, userAppClient, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.Port).String(), nil) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode) + }) + + t.Run("PublicOK", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + appDetails := setupProxyTest(t, nil) + port, err := strconv.ParseInt(appDetails.Apps.Port.AppSlugOrPort, 10, 32) + require.NoError(t, err) + // set the port we have to be shared with public + _, err = appDetails.SDKClient.UpsertWorkspaceAgentPortShare(ctx, appDetails.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{ + AgentName: proxyTestAgentName, + Port: int32(port), + ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic, + Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP, + }) + require.NoError(t, err) + + publicAppClient := appDetails.AppClient(t) + publicAppClient.SetSessionToken("") + + resp, err := requestWithRetries(ctx, t, publicAppClient, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.Port).String(), nil) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode) + }) + + t.Run("HTTPS", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + appDetails := setupProxyTest(t, &DeploymentOptions{ + ServeHTTPS: true, + }) + port, err := strconv.ParseInt(appDetails.Apps.Port.AppSlugOrPort, 10, 32) + require.NoError(t, err) + _, err = appDetails.SDKClient.UpsertWorkspaceAgentPortShare(ctx, appDetails.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{ + AgentName: proxyTestAgentName, + Port: int32(port), + ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic, + Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTPS, + }) + require.NoError(t, err) + + publicAppClient := appDetails.AppClient(t) + publicAppClient.SetSessionToken("") + + resp, err := requestWithRetries(ctx, t, publicAppClient, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.Port).String(), nil) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode) + }) + }) + t.Run("AppSharing", func(t *testing.T) { t.Parallel() diff --git a/coderd/workspaceapps/request.go b/coderd/workspaceapps/request.go index 2c10ac3687..0f3eddf6cb 100644 --- a/coderd/workspaceapps/request.go +++ b/coderd/workspaceapps/request.go @@ -342,6 +342,10 @@ func (r Request) getDatabase(ctx context.Context, db database.Store) (*databaseR } // No port share found, so we keep default to owner. } else { + if ps.Protocol == database.PortShareProtocolHttps { + // Apply HTTPS protocol if specified. + appURL = fmt.Sprintf("https://127.0.0.1:%d", portUint) + } appSharingLevel = ps.ShareLevel } } else { diff --git a/coderd/workspaceapps/request_test.go b/coderd/workspaceapps/request_test.go index eebda105f0..7240937a06 100644 --- a/coderd/workspaceapps/request_test.go +++ b/coderd/workspaceapps/request_test.go @@ -269,6 +269,3 @@ func Test_RequestValidate(t *testing.T) { }) } } - -// getDatabase is tested heavily in auth_test.go, so we don't have specific -// tests for it here. diff --git a/codersdk/workspaceagentportshare.go b/codersdk/workspaceagentportshare.go index f5acf276ea..46b31fcd1e 100644 --- a/codersdk/workspaceagentportshare.go +++ b/codersdk/workspaceagentportshare.go @@ -13,23 +13,29 @@ const ( WorkspaceAgentPortShareLevelOwner WorkspaceAgentPortShareLevel = "owner" WorkspaceAgentPortShareLevelAuthenticated WorkspaceAgentPortShareLevel = "authenticated" WorkspaceAgentPortShareLevelPublic WorkspaceAgentPortShareLevel = "public" + + WorkspaceAgentPortShareProtocolHTTP WorkspaceAgentPortShareProtocol = "http" + WorkspaceAgentPortShareProtocolHTTPS WorkspaceAgentPortShareProtocol = "https" ) type ( WorkspaceAgentPortShareLevel string + WorkspaceAgentPortShareProtocol string UpsertWorkspaceAgentPortShareRequest struct { - AgentName string `json:"agent_name"` - Port int32 `json:"port"` - ShareLevel WorkspaceAgentPortShareLevel `json:"share_level"` + AgentName string `json:"agent_name"` + Port int32 `json:"port"` + ShareLevel WorkspaceAgentPortShareLevel `json:"share_level" enums:"owner,authenticated,public"` + Protocol WorkspaceAgentPortShareProtocol `json:"protocol" enums:"http,https"` } WorkspaceAgentPortShares struct { Shares []WorkspaceAgentPortShare `json:"shares"` } WorkspaceAgentPortShare struct { - WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"` - AgentName string `json:"agent_name"` - Port int32 `json:"port"` - ShareLevel WorkspaceAgentPortShareLevel `json:"share_level"` + WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"` + AgentName string `json:"agent_name"` + Port int32 `json:"port"` + ShareLevel WorkspaceAgentPortShareLevel `json:"share_level" enums:"owner,authenticated,public"` + Protocol WorkspaceAgentPortShareProtocol `json:"protocol" enums:"http,https"` } DeleteWorkspaceAgentPortShareRequest struct { AgentName string `json:"agent_name"` @@ -48,6 +54,11 @@ func (l WorkspaceAgentPortShareLevel) ValidPortShareLevel() bool { l == WorkspaceAgentPortShareLevelPublic } +func (p WorkspaceAgentPortShareProtocol) ValidPortProtocol() bool { + return p == WorkspaceAgentPortShareProtocolHTTP || + p == WorkspaceAgentPortShareProtocolHTTPS +} + func (c *Client) GetWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) (WorkspaceAgentPortShares, error) { var shares WorkspaceAgentPortShares res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/port-share", workspaceID), nil) diff --git a/docs/api/portsharing.md b/docs/api/portsharing.md index 5b2de111c2..179ab63f31 100644 --- a/docs/api/portsharing.md +++ b/docs/api/portsharing.md @@ -57,6 +57,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/port-share \ { "agent_name": "string", "port": 0, + "protocol": "http", "share_level": "owner" } ``` @@ -76,6 +77,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/port-share \ { "agent_name": "string", "port": 0, + "protocol": "http", "share_level": "owner", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" } diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 406eb9202d..8dbc25d371 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -6613,17 +6613,29 @@ If the schedule is empty, the user will be updated to use the default schedule.| { "agent_name": "string", "port": 0, + "protocol": "http", "share_level": "owner" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| ------------- | ------------------------------------------------------------------------------ | -------- | ------------ | ----------- | -| `agent_name` | string | false | | | -| `port` | integer | false | | | -| `share_level` | [codersdk.WorkspaceAgentPortShareLevel](#codersdkworkspaceagentportsharelevel) | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------- | ------------------------------------------------------------------------------------ | -------- | ------------ | ----------- | +| `agent_name` | string | false | | | +| `port` | integer | false | | | +| `protocol` | [codersdk.WorkspaceAgentPortShareProtocol](#codersdkworkspaceagentportshareprotocol) | false | | | +| `share_level` | [codersdk.WorkspaceAgentPortShareLevel](#codersdkworkspaceagentportsharelevel) | false | | | + +#### Enumerated Values + +| Property | Value | +| ------------- | --------------- | +| `protocol` | `http` | +| `protocol` | `https` | +| `share_level` | `owner` | +| `share_level` | `authenticated` | +| `share_level` | `public` | ## codersdk.User @@ -7579,6 +7591,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| { "agent_name": "string", "port": 0, + "protocol": "http", "share_level": "owner", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" } @@ -7586,12 +7599,23 @@ If the schedule is empty, the user will be updated to use the default schedule.| ### Properties -| Name | Type | Required | Restrictions | Description | -| -------------- | ------------------------------------------------------------------------------ | -------- | ------------ | ----------- | -| `agent_name` | string | false | | | -| `port` | integer | false | | | -| `share_level` | [codersdk.WorkspaceAgentPortShareLevel](#codersdkworkspaceagentportsharelevel) | false | | | -| `workspace_id` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| -------------- | ------------------------------------------------------------------------------------ | -------- | ------------ | ----------- | +| `agent_name` | string | false | | | +| `port` | integer | false | | | +| `protocol` | [codersdk.WorkspaceAgentPortShareProtocol](#codersdkworkspaceagentportshareprotocol) | false | | | +| `share_level` | [codersdk.WorkspaceAgentPortShareLevel](#codersdkworkspaceagentportsharelevel) | false | | | +| `workspace_id` | string | false | | | + +#### Enumerated Values + +| Property | Value | +| ------------- | --------------- | +| `protocol` | `http` | +| `protocol` | `https` | +| `share_level` | `owner` | +| `share_level` | `authenticated` | +| `share_level` | `public` | ## codersdk.WorkspaceAgentPortShareLevel @@ -7609,6 +7633,21 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `authenticated` | | `public` | +## codersdk.WorkspaceAgentPortShareProtocol + +```json +"http" +``` + +### Properties + +#### Enumerated Values + +| Value | +| ------- | +| `http` | +| `https` | + ## codersdk.WorkspaceAgentPortShares ```json @@ -7617,6 +7656,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| { "agent_name": "string", "port": 0, + "protocol": "http", "share_level": "owner", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" } diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go index 4e2c91f891..a6514867ae 100644 --- a/enterprise/coderd/templates_test.go +++ b/enterprise/coderd/templates_test.go @@ -219,6 +219,7 @@ func TestTemplates(t *testing.T) { AgentName: ws.LatestBuild.Resources[0].Agents[0].Name, Port: 8080, ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic, + Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP, }) require.NoError(t, err) diff --git a/enterprise/coderd/workspaceportshare_test.go b/enterprise/coderd/workspaceportshare_test.go index 1a8543db68..c5dd072261 100644 --- a/enterprise/coderd/workspaceportshare_test.go +++ b/enterprise/coderd/workspaceportshare_test.go @@ -43,6 +43,7 @@ func TestWorkspacePortShare(t *testing.T) { AgentName: r.sdkAgent.Name, Port: 8080, ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic, + Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP, }) require.Error(t, err, "Port sharing level not allowed") @@ -57,6 +58,7 @@ func TestWorkspacePortShare(t *testing.T) { AgentName: r.sdkAgent.Name, Port: 8080, ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic, + Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP, }) require.NoError(t, err) require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelPublic, ps.ShareLevel) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 6ac25d9b7c..d5ce9dd5ec 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1504,6 +1504,7 @@ export interface UpsertWorkspaceAgentPortShareRequest { readonly agent_name: string; readonly port: number; readonly share_level: WorkspaceAgentPortShareLevel; + readonly protocol: WorkspaceAgentPortShareProtocol; } // From codersdk/users.go @@ -1762,6 +1763,7 @@ export interface WorkspaceAgentPortShare { readonly agent_name: string; readonly port: number; readonly share_level: WorkspaceAgentPortShareLevel; + readonly protocol: WorkspaceAgentPortShareProtocol; } // From codersdk/workspaceagentportshare.go @@ -2351,6 +2353,11 @@ export const WorkspaceAgentPortShareLevels: WorkspaceAgentPortShareLevel[] = [ "public", ]; +// From codersdk/workspaceagentportshare.go +export type WorkspaceAgentPortShareProtocol = "http" | "https"; +export const WorkspaceAgentPortShareProtocols: WorkspaceAgentPortShareProtocol[] = + ["http", "https"]; + // From codersdk/workspaceagents.go export type WorkspaceAgentStartupScriptBehavior = "blocking" | "non-blocking"; export const WorkspaceAgentStartupScriptBehaviors: WorkspaceAgentStartupScriptBehavior[] = diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx index 87cb088624..e08ebcb3eb 100644 --- a/site/src/modules/resources/PortForwardButton.tsx +++ b/site/src/modules/resources/PortForwardButton.tsx @@ -26,10 +26,11 @@ import { } from "api/queries/workspaceportsharing"; import { type Template, - type UpsertWorkspaceAgentPortShareRequest, type WorkspaceAgent, type WorkspaceAgentListeningPort, type WorkspaceAgentPortShareLevel, + type UpsertWorkspaceAgentPortShareRequest, + type WorkspaceAgentPortShareProtocol, WorkspaceAppSharingLevels, } from "api/typesGenerated"; import { @@ -162,6 +163,7 @@ export const PortForwardPopoverView: FC = ({ initialValues: { agent_name: agent.name, port: undefined, + protocol: "http", share_level: "authenticated", }, validationSchema, @@ -325,6 +327,7 @@ export const PortForwardPopoverView: FC = ({ await upsertSharedPortMutation.mutateAsync({ agent_name: agent.name, port: port.port, + protocol: "http", share_level: "authenticated", }); await sharedPortsQuery.refetch(); @@ -384,8 +387,31 @@ export const PortForwardPopoverView: FC = ({ )} {label} + + + + - +