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
+4 -8
View File
@@ -108,14 +108,10 @@ export const getAuthMethods = async (): Promise<TypesGen.AuthMethods> => {
return response.data
}
export const checkUserPermissions = async (
userId: string,
params: TypesGen.UserAuthorizationRequest,
): Promise<TypesGen.UserAuthorizationResponse> => {
const response = await axios.post<TypesGen.UserAuthorizationResponse>(
`/api/v2/users/${userId}/authorization`,
params,
)
export const checkAuthorization = async (
params: TypesGen.AuthorizationRequest,
): Promise<TypesGen.AuthorizationResponse> => {
const response = await axios.post<TypesGen.AuthorizationResponse>(`/api/v2/authcheck`, params)
return response.data
}
+27 -22
View File
@@ -103,6 +103,28 @@ export interface AuthMethods {
readonly oidc: boolean
}
// From codersdk/authorization.go
export interface AuthorizationCheck {
readonly object: AuthorizationObject
readonly action: string
}
// From codersdk/authorization.go
export interface AuthorizationObject {
readonly resource_type: string
readonly owner_id?: string
readonly organization_id?: string
readonly resource_id?: string
}
// From codersdk/authorization.go
export interface AuthorizationRequest {
readonly checks: Record<string, AuthorizationCheck>
}
// From codersdk/authorization.go
export type AuthorizationResponse = Record<string, boolean>
// From codersdk/workspaceagents.go
export interface AzureInstanceIdentityToken {
readonly signature: string
@@ -242,6 +264,11 @@ export interface GenerateAPIKeyResponse {
readonly key: string
}
// From codersdk/workspaces.go
export interface GetAppHostResponse {
readonly host: string
}
// From codersdk/gitsshkey.go
export interface GitSSHKey {
readonly user_id: string
@@ -497,28 +524,6 @@ export interface User {
readonly avatar_url: string
}
// From codersdk/users.go
export interface UserAuthorization {
readonly object: UserAuthorizationObject
readonly action: string
}
// From codersdk/users.go
export interface UserAuthorizationObject {
readonly resource_type: string
readonly owner_id?: string
readonly organization_id?: string
readonly resource_id?: string
}
// From codersdk/users.go
export interface UserAuthorizationRequest {
readonly checks: Record<string, UserAuthorization>
}
// From codersdk/users.go
export type UserAuthorizationResponse = Record<string, boolean>
// From codersdk/users.go
export interface UserRoles {
readonly roles: string[]
+2 -6
View File
@@ -2,11 +2,7 @@ import { render, screen, waitFor } from "@testing-library/react"
import { App } from "app"
import { Language } from "components/NavbarView/NavbarView"
import { rest } from "msw"
import {
MockEntitlementsWithAuditLog,
MockMemberPermissions,
MockUser,
} from "testHelpers/renderHelpers"
import { MockEntitlementsWithAuditLog, MockMemberPermissions } from "testHelpers/renderHelpers"
import { server } from "testHelpers/server"
/**
@@ -47,7 +43,7 @@ describe("Navbar", () => {
it("does not show Audit Log link when not permitted via role", async () => {
// set permissions to Member (can't audit)
server.use(
rest.post(`/api/v2/users/${MockUser.id}/authorization`, async (req, res, ctx) => {
rest.post("/api/v2/authcheck", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(MockMemberPermissions))
}),
)
@@ -7,7 +7,6 @@ import {
MockMemberPermissions,
MockTemplate,
MockTemplateVersion,
MockUser,
MockWorkspaceResource,
renderWithAuth,
} from "../../testHelpers/renderHelpers"
@@ -47,7 +46,7 @@ describe("TemplatePage", () => {
it("does not allow a member to delete a template", () => {
// get member-level permissions
server.use(
rest.post(`/api/v2/users/${MockUser.id}/authorization`, async (req, res, ctx) => {
rest.post("/api/v2/authcheck", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(MockMemberPermissions))
}),
)
@@ -21,7 +21,7 @@ describe("TemplatesPage", () => {
rest.get("/api/v2/organizations/:organizationId/templates", (req, res, ctx) => {
return res(ctx.status(200), ctx.json([]))
}),
rest.post("/api/v2/users/:userId/authorization", async (req, res, ctx) => {
rest.post("/api/v2/authcheck", async (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
@@ -51,7 +51,7 @@ describe("TemplatesPage", () => {
rest.get("/api/v2/organizations/:organizationId/templates", (req, res, ctx) => {
return res(ctx.status(200), ctx.json([]))
}),
rest.post("/api/v2/users/:userId/authorization", async (req, res, ctx) => {
rest.post("/api/v2/authcheck", async (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
+1 -1
View File
@@ -184,7 +184,7 @@ describe("UsersPage", () => {
it("does not show 'Create user' button to unauthorized user", async () => {
server.use(
rest.post("/api/v2/users/:userId/authorization", async (req, res, ctx) => {
rest.post("/api/v2/authcheck", async (req, res, ctx) => {
const permissions = Object.keys(permissionsToCheck)
const response = permissions.reduce((obj, permission) => {
return {
@@ -16,7 +16,6 @@ import { firstOrItem } from "../../util/array"
import { pageTitle } from "../../util/page"
import { canExtendDeadline, canReduceDeadline, maxDeadline, minDeadline } from "../../util/schedule"
import { getFaviconByStatus } from "../../util/workspace"
import { selectUser } from "../../xServices/auth/authSelectors"
import { XServiceContext } from "../../xServices/StateContext"
import { workspaceMachine } from "../../xServices/workspace/workspaceXService"
import { workspaceScheduleBannerMachine } from "../../xServices/workspaceSchedule/workspaceScheduleBannerXService"
@@ -27,18 +26,11 @@ export const WorkspacePage: FC = () => {
const { username: usernameQueryParam, workspace: workspaceQueryParam } = useParams()
const username = firstOrItem(usernameQueryParam, null)
const workspaceName = firstOrItem(workspaceQueryParam, null)
const { t } = useTranslation("workspacePage")
const xServices = useContext(XServiceContext)
const me = useSelector(xServices.authXService, selectUser)
const featureVisibility = useSelector(xServices.entitlementsXService, selectFeatureVisibility)
const [workspaceState, workspaceSend] = useMachine(workspaceMachine, {
context: {
userId: me?.id,
},
})
const [workspaceState, workspaceSend] = useMachine(workspaceMachine)
const {
workspace,
getWorkspaceError,
@@ -1,15 +1,13 @@
import { useMachine, useSelector } from "@xstate/react"
import { useMachine } from "@xstate/react"
import { scheduleToAutoStart } from "pages/WorkspaceSchedulePage/schedule"
import { ttlMsToAutoStop } from "pages/WorkspaceSchedulePage/ttl"
import React, { useContext, useEffect, useState } from "react"
import React, { useEffect, useState } from "react"
import { Navigate, useNavigate, useParams } from "react-router-dom"
import * as TypesGen from "../../api/typesGenerated"
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
import { FullScreenLoader } from "../../components/Loader/FullScreenLoader"
import { WorkspaceScheduleForm } from "../../components/WorkspaceScheduleForm/WorkspaceScheduleForm"
import { firstOrItem } from "../../util/array"
import { selectUser } from "../../xServices/auth/authSelectors"
import { XServiceContext } from "../../xServices/StateContext"
import { workspaceSchedule } from "../../xServices/workspaceSchedule/workspaceScheduleXService"
import { formValuesToAutoStartRequest, formValuesToTTLRequest } from "./formToRequest"
@@ -24,15 +22,7 @@ export const WorkspaceSchedulePage: React.FC = () => {
const navigate = useNavigate()
const username = firstOrItem(usernameQueryParam, null)
const workspaceName = firstOrItem(workspaceQueryParam, null)
const xServices = useContext(XServiceContext)
const me = useSelector(xServices.authXService, selectUser)
const [scheduleState, scheduleSend] = useMachine(workspaceSchedule, {
context: {
userId: me?.id,
},
})
const [scheduleState, scheduleSend] = useMachine(workspaceSchedule)
const { checkPermissionsError, submitScheduleError, getWorkspaceError, permissions, workspace } =
scheduleState.context
+1 -1
View File
@@ -79,7 +79,7 @@ export const handlers = [
rest.get("/api/v2/users/roles", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockSiteRoles))
}),
rest.post("/api/v2/users/:userId/authorization", async (req, res, ctx) => {
rest.post("/api/v2/authcheck", async (req, res, ctx) => {
const permissions = Object.keys(permissionsToCheck)
const response = permissions.reduce((obj, permission) => {
return {
+3 -7
View File
@@ -114,7 +114,7 @@ export const authMachine =
data: undefined
}
checkPermissions: {
data: TypesGen.UserAuthorizationResponse
data: TypesGen.AuthorizationResponse
}
getSSHKey: {
data: TypesGen.GitSSHKey
@@ -438,12 +438,8 @@ export const authMachine =
return API.updateUserPassword(context.me.id, event.data)
},
checkPermissions: async (context) => {
if (!context.me) {
throw new Error("No current user found")
}
return API.checkUserPermissions(context.me.id, {
checkPermissions: async () => {
return API.checkAuthorization({
checks: permissionsToCheck,
})
},
@@ -39,7 +39,6 @@ export interface WorkspaceContext {
// permissions
permissions?: Permissions
checkPermissionsError?: Error | unknown
userId?: string
}
export type WorkspaceEvent =
@@ -117,7 +116,7 @@ export const workspaceMachine = createMachine(
data: TypesGen.WorkspaceBuild[]
}
checkPermissions: {
data: TypesGen.UserAuthorizationResponse
data: TypesGen.AuthorizationResponse
}
},
},
@@ -604,12 +603,12 @@ export const workspaceMachine = createMachine(
}
},
checkPermissions: async (context) => {
if (context.workspace && context.userId) {
return await API.checkUserPermissions(context.userId, {
if (context.workspace) {
return await API.checkAuthorization({
checks: permissionsToCheck(context.workspace),
})
} else {
throw Error("Cannot check permissions without both workspace and user id")
throw Error("Cannot check permissions workspace id")
}
},
},
@@ -21,8 +21,6 @@ export interface WorkspaceScheduleContext {
* machine is partially influenced by workspaceXService.
*/
workspace?: TypesGen.Workspace
// permissions
userId?: string
permissions?: Permissions
checkPermissionsError?: Error | unknown
submitScheduleError?: Error | unknown
@@ -178,8 +176,8 @@ export const workspaceSchedule = createMachine(
return await API.getWorkspaceByOwnerAndName(event.username, event.workspaceName)
},
checkPermissions: async (context) => {
if (context.workspace && context.userId) {
return await API.checkUserPermissions(context.userId, {
if (context.workspace) {
return await API.checkAuthorization({
checks: permissionsToCheck(context.workspace),
})
} else {