feat: secure and cross-domain subdomain-based proxying (#4136)

Co-authored-by: Kyle Carberry <kyle@carberry.com>
This commit is contained in:
Dean Sheather
2022-09-22 22:30:32 +00:00
committed by GitHub
co-authored by Kyle Carberry
parent 80b45f1aa1
commit 6deef06ad2
51 changed files with 1655 additions and 594 deletions
+70
View File
@@ -0,0 +1,70 @@
package codersdk
import (
"context"
"encoding/json"
"net/http"
)
type AuthorizationResponse map[string]bool
// AuthorizationRequest is a structure instead of a map because
// go-playground/validate can only validate structs. If you attempt to pass
// a map into 'httpapi.Read', you will get an invalid type error.
type AuthorizationRequest struct {
// Checks is a map keyed with an arbitrary string to a permission check.
// The key can be any string that is helpful to the caller, and allows
// multiple permission checks to be run in a single request.
// The key ensures that each permission check has the same key in the
// response.
Checks map[string]AuthorizationCheck `json:"checks"`
}
// AuthorizationCheck is used to check if the currently authenticated user (or
// the specified user) can do a given action to a given set of objects.
type AuthorizationCheck struct {
// Object can represent a "set" of objects, such as:
// - All workspaces in an organization
// - All workspaces owned by me
// - All workspaces across the entire product
// When defining an object, use the most specific language when possible to
// produce the smallest set. Meaning to set as many fields on 'Object' as
// you can. Example, if you want to check if you can update all workspaces
// owned by 'me', try to also add an 'OrganizationID' to the settings.
// Omitting the 'OrganizationID' could produce the incorrect value, as
// workspaces have both `user` and `organization` owners.
Object AuthorizationObject `json:"object"`
// Action can be 'create', 'read', 'update', or 'delete'
Action string `json:"action"`
}
type AuthorizationObject struct {
// ResourceType is the name of the resource.
// './coderd/rbac/object.go' has the list of valid resource types.
ResourceType string `json:"resource_type"`
// OwnerID (optional) is a user_id. It adds the set constraint to all resources owned
// by a given user.
OwnerID string `json:"owner_id,omitempty"`
// OrganizationID (optional) is an organization_id. It adds the set constraint to
// all resources owned by a given organization.
OrganizationID string `json:"organization_id,omitempty"`
// ResourceID (optional) reduces the set to a singular resource. This assigns
// a resource ID to the resource type, eg: a single workspace.
// The rbac library will not fetch the resource from the database, so if you
// are using this option, you should also set the 'OwnerID' and 'OrganizationID'
// if possible. Be as specific as possible using all the fields relevant.
ResourceID string `json:"resource_id,omitempty"`
}
func (c *Client) CheckAuthorization(ctx context.Context, req AuthorizationRequest) (AuthorizationResponse, error) {
res, err := c.Request(ctx, http.MethodPost, "/api/v2/authcheck", req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return AuthorizationResponse{}, readBodyAsError(res)
}
var resp AuthorizationResponse
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
+12 -2
View File
@@ -42,11 +42,21 @@ type Client struct {
URL *url.URL
}
type requestOption func(*http.Request)
type RequestOption func(*http.Request)
func WithQueryParams(params map[string]string) RequestOption {
return func(r *http.Request) {
q := r.URL.Query()
for k, v := range params {
q.Add(k, v)
}
r.URL.RawQuery = q.Encode()
}
}
// Request performs an HTTP request with the body provided.
// The caller is responsible for closing the response body.
func (c *Client) Request(ctx context.Context, method, path string, body interface{}, opts ...requestOption) (*http.Response, error) {
func (c *Client) Request(ctx context.Context, method, path string, body interface{}, opts ...RequestOption) (*http.Response, error) {
serverURL, err := c.URL.Parse(path)
if err != nil {
return nil, xerrors.Errorf("parse url: %w", err)
+1 -1
View File
@@ -28,7 +28,7 @@ type Pagination struct {
// asRequestOption returns a function that can be used in (*Client).Request.
// It modifies the request query parameters.
func (p Pagination) asRequestOption() requestOption {
func (p Pagination) asRequestOption() RequestOption {
return func(r *http.Request) {
q := r.URL.Query()
if p.AfterID != uuid.Nil {
-13
View File
@@ -46,16 +46,3 @@ func (c *Client) ListOrganizationRoles(ctx context.Context, org uuid.UUID) ([]As
var roles []AssignableRoles
return roles, json.NewDecoder(res.Body).Decode(&roles)
}
func (c *Client) CheckPermissions(ctx context.Context, checks UserAuthorizationRequest) (UserAuthorizationResponse, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/authorization", Me), checks)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var roles UserAuthorizationResponse
return roles, json.NewDecoder(res.Body).Decode(&roles)
}
+2 -51
View File
@@ -54,7 +54,8 @@ type User struct {
}
type APIKey struct {
ID string `json:"id" validate:"required"`
ID string `json:"id" validate:"required"`
// NOTE: do not ever return the HashedSecret
UserID uuid.UUID `json:"user_id" validate:"required"`
LastUsed time.Time `json:"last_used" validate:"required"`
ExpiresAt time.Time `json:"expires_at" validate:"required"`
@@ -102,56 +103,6 @@ type UserRoles struct {
OrganizationRoles map[uuid.UUID][]string `json:"organization_roles"`
}
type UserAuthorizationResponse map[string]bool
// UserAuthorizationRequest is a structure instead of a map because
// go-playground/validate can only validate structs. If you attempt to pass
// a map into 'httpapi.Read', you will get an invalid type error.
type UserAuthorizationRequest struct {
// Checks is a map keyed with an arbitrary string to a permission check.
// The key can be any string that is helpful to the caller, and allows
// multiple permission checks to be run in a single request.
// The key ensures that each permission check has the same key in the
// response.
Checks map[string]UserAuthorization `json:"checks"`
}
// UserAuthorization is used to check if a user can do a given action
// to a given set of objects.
type UserAuthorization struct {
// Object can represent a "set" of objects, such as:
// - All workspaces in an organization
// - All workspaces owned by me
// - All workspaces across the entire product
// When defining an object, use the most specific language when possible to
// produce the smallest set. Meaning to set as many fields on 'Object' as
// you can. Example, if you want to check if you can update all workspaces
// owned by 'me', try to also add an 'OrganizationID' to the settings.
// Omitting the 'OrganizationID' could produce the incorrect value, as
// workspaces have both `user` and `organization` owners.
Object UserAuthorizationObject `json:"object"`
// Action can be 'create', 'read', 'update', or 'delete'
Action string `json:"action"`
}
type UserAuthorizationObject struct {
// ResourceType is the name of the resource.
// './coderd/rbac/object.go' has the list of valid resource types.
ResourceType string `json:"resource_type"`
// OwnerID (optional) is a user_id. It adds the set constraint to all resources owned
// by a given user.
OwnerID string `json:"owner_id,omitempty"`
// OrganizationID (optional) is an organization_id. It adds the set constraint to
// all resources owned by a given organization.
OrganizationID string `json:"organization_id,omitempty"`
// ResourceID (optional) reduces the set to a singular resource. This assigns
// a resource ID to the resource type, eg: a single workspace.
// The rbac library will not fetch the resource from the database, so if you
// are using this option, you should also set the 'OwnerID' and 'OrganizationID'
// if possible. Be as specific as possible using all the fields relevant.
ResourceID string `json:"resource_id,omitempty"`
}
// LoginWithPasswordRequest enables callers to authenticate with email and password.
type LoginWithPasswordRequest struct {
Email string `json:"email" validate:"required,email"`
+28 -3
View File
@@ -51,7 +51,7 @@ type WorkspaceOptions struct {
// asRequestOption returns a function that can be used in (*Client).Request.
// It modifies the request query parameters.
func (o WorkspaceOptions) asRequestOption() requestOption {
func (o WorkspaceOptions) asRequestOption() RequestOption {
return func(r *http.Request) {
q := r.URL.Query()
if o.IncludeDeleted {
@@ -74,7 +74,7 @@ func (c *Client) DeletedWorkspace(ctx context.Context, id uuid.UUID) (Workspace,
return c.getWorkspace(ctx, id, o.asRequestOption())
}
func (c *Client) getWorkspace(ctx context.Context, id uuid.UUID, opts ...requestOption) (Workspace, error) {
func (c *Client) getWorkspace(ctx context.Context, id uuid.UUID, opts ...RequestOption) (Workspace, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s", id), nil, opts...)
if err != nil {
return Workspace{}, err
@@ -254,7 +254,7 @@ type WorkspaceFilter struct {
// asRequestOption returns a function that can be used in (*Client).Request.
// It modifies the request query parameters.
func (f WorkspaceFilter) asRequestOption() requestOption {
func (f WorkspaceFilter) asRequestOption() RequestOption {
return func(r *http.Request) {
var params []string
// Make sure all user input is quoted to ensure it's parsed as a single
@@ -314,3 +314,28 @@ func (c *Client) WorkspaceByOwnerAndName(ctx context.Context, owner string, name
var workspace Workspace
return workspace, json.NewDecoder(res.Body).Decode(&workspace)
}
type GetAppHostResponse struct {
Host string `json:"host"`
}
// GetAppHost returns the site-wide application wildcard hostname without the
// leading "*.", e.g. "apps.coder.com". Apps are accessible at:
// "<app-name>--<agent-name>--<workspace-name>--<username>.<app-host>", e.g.
// "my-app--agent--workspace--username.apps.coder.com".
//
// If the app host is not set, the response will contain an empty string.
func (c *Client) GetAppHost(ctx context.Context) (GetAppHostResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/applications/host", nil)
if err != nil {
return GetAppHostResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return GetAppHostResponse{}, readBodyAsError(res)
}
var host GetAppHostResponse
return host, json.NewDecoder(res.Body).Decode(&host)
}