mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: secure and cross-domain subdomain-based proxying (#4136)
Co-authored-by: Kyle Carberry <kyle@carberry.com>
This commit is contained in:
co-authored by
Kyle Carberry
parent
80b45f1aa1
commit
6deef06ad2
+4
-8
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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({
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user