mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: Allow admins to access member workspace terminals (#2114)
* allow workspace update permissions to access agents * do not show app links to users without workspace update access * address CR comments * initialize machine context in the hook * revert scoped connected status check
This commit is contained in:
@@ -292,6 +292,7 @@ func New(options *Options) *API {
|
|||||||
r.Use(
|
r.Use(
|
||||||
apiKeyMiddleware,
|
apiKeyMiddleware,
|
||||||
httpmw.ExtractWorkspaceAgentParam(options.Database),
|
httpmw.ExtractWorkspaceAgentParam(options.Database),
|
||||||
|
httpmw.ExtractWorkspaceParam(options.Database),
|
||||||
)
|
)
|
||||||
r.Get("/", api.workspaceAgent)
|
r.Get("/", api.workspaceAgent)
|
||||||
r.Get("/dial", api.workspaceAgentDial)
|
r.Get("/dial", api.workspaceAgentDial)
|
||||||
|
|||||||
+13
-3
@@ -153,10 +153,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
|
|||||||
"GET:/api/v2/workspaceagents/me/listen": {NoAuthorize: true},
|
"GET:/api/v2/workspaceagents/me/listen": {NoAuthorize: true},
|
||||||
"GET:/api/v2/workspaceagents/me/metadata": {NoAuthorize: true},
|
"GET:/api/v2/workspaceagents/me/metadata": {NoAuthorize: true},
|
||||||
"GET:/api/v2/workspaceagents/me/turn": {NoAuthorize: true},
|
"GET:/api/v2/workspaceagents/me/turn": {NoAuthorize: true},
|
||||||
"GET:/api/v2/workspaceagents/{workspaceagent}": {NoAuthorize: true},
|
|
||||||
"GET:/api/v2/workspaceagents/{workspaceagent}/dial": {NoAuthorize: true},
|
|
||||||
"GET:/api/v2/workspaceagents/{workspaceagent}/iceservers": {NoAuthorize: true},
|
"GET:/api/v2/workspaceagents/{workspaceagent}/iceservers": {NoAuthorize: true},
|
||||||
"GET:/api/v2/workspaceagents/{workspaceagent}/pty": {NoAuthorize: true},
|
|
||||||
"GET:/api/v2/workspaceagents/{workspaceagent}/turn": {NoAuthorize: true},
|
"GET:/api/v2/workspaceagents/{workspaceagent}/turn": {NoAuthorize: true},
|
||||||
|
|
||||||
// These endpoints have more assertions. This is good, add more endpoints to assert if you can!
|
// These endpoints have more assertions. This is good, add more endpoints to assert if you can!
|
||||||
@@ -210,6 +207,18 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
|
|||||||
AssertAction: rbac.ActionRead,
|
AssertAction: rbac.ActionRead,
|
||||||
AssertObject: workspaceRBACObj,
|
AssertObject: workspaceRBACObj,
|
||||||
},
|
},
|
||||||
|
"GET:/api/v2/workspaceagents/{workspaceagent}": {
|
||||||
|
AssertAction: rbac.ActionRead,
|
||||||
|
AssertObject: workspaceRBACObj,
|
||||||
|
},
|
||||||
|
"GET:/api/v2/workspaceagents/{workspaceagent}/dial": {
|
||||||
|
AssertAction: rbac.ActionUpdate,
|
||||||
|
AssertObject: workspaceRBACObj,
|
||||||
|
},
|
||||||
|
"GET:/api/v2/workspaceagents/{workspaceagent}/pty": {
|
||||||
|
AssertAction: rbac.ActionUpdate,
|
||||||
|
AssertObject: workspaceRBACObj,
|
||||||
|
},
|
||||||
"GET:/api/v2/workspaces/": {
|
"GET:/api/v2/workspaces/": {
|
||||||
StatusCode: http.StatusOK,
|
StatusCode: http.StatusOK,
|
||||||
AssertAction: rbac.ActionRead,
|
AssertAction: rbac.ActionRead,
|
||||||
@@ -378,6 +387,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
|
|||||||
route = strings.ReplaceAll(route, "{workspacebuild}", workspace.LatestBuild.ID.String())
|
route = strings.ReplaceAll(route, "{workspacebuild}", workspace.LatestBuild.ID.String())
|
||||||
route = strings.ReplaceAll(route, "{workspacename}", workspace.Name)
|
route = strings.ReplaceAll(route, "{workspacename}", workspace.Name)
|
||||||
route = strings.ReplaceAll(route, "{workspacebuildname}", workspace.LatestBuild.Name)
|
route = strings.ReplaceAll(route, "{workspacebuildname}", workspace.LatestBuild.Name)
|
||||||
|
route = strings.ReplaceAll(route, "{workspaceagent}", workspaceResources[0].Agents[0].ID.String())
|
||||||
route = strings.ReplaceAll(route, "{template}", template.ID.String())
|
route = strings.ReplaceAll(route, "{template}", template.ID.String())
|
||||||
route = strings.ReplaceAll(route, "{hash}", file.Hash)
|
route = strings.ReplaceAll(route, "{hash}", file.Hash)
|
||||||
route = strings.ReplaceAll(route, "{workspaceresource}", workspaceResources[0].ID.String())
|
route = strings.ReplaceAll(route, "{workspaceresource}", workspaceResources[0].ID.String())
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ type userRolesKey struct{}
|
|||||||
// AuthorizationUserRoles returns the roles used for authorization.
|
// AuthorizationUserRoles returns the roles used for authorization.
|
||||||
// Comes from the ExtractAPIKey handler.
|
// Comes from the ExtractAPIKey handler.
|
||||||
func AuthorizationUserRoles(r *http.Request) database.GetAuthorizationUserRolesRow {
|
func AuthorizationUserRoles(r *http.Request) database.GetAuthorizationUserRolesRow {
|
||||||
apiKey, ok := r.Context().Value(userRolesKey{}).(database.GetAuthorizationUserRolesRow)
|
userRoles, ok := r.Context().Value(userRolesKey{}).(database.GetAuthorizationUserRolesRow)
|
||||||
if !ok {
|
if !ok {
|
||||||
panic("developer error: user roles middleware not provided")
|
panic("developer error: user roles middleware not provided")
|
||||||
}
|
}
|
||||||
return apiKey
|
return userRoles
|
||||||
}
|
}
|
||||||
|
|
||||||
// OAuth2Configs is a collection of configurations for OAuth-based authentication.
|
// OAuth2Configs is a collection of configurations for OAuth-based authentication.
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
"github.com/coder/coder/coderd/database"
|
"github.com/coder/coder/coderd/database"
|
||||||
"github.com/coder/coder/coderd/httpapi"
|
"github.com/coder/coder/coderd/httpapi"
|
||||||
)
|
)
|
||||||
@@ -74,24 +76,9 @@ func ExtractWorkspaceAgentParam(db database.Store) func(http.Handler) http.Handl
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
workspace, err := db.GetWorkspaceByID(r.Context(), build.WorkspaceID)
|
|
||||||
if err != nil {
|
|
||||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
||||||
Message: "Internal error fetching workspace.",
|
|
||||||
Detail: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
apiKey := APIKey(r)
|
|
||||||
if apiKey.UserID != workspace.OwnerID {
|
|
||||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
|
||||||
Message: "Getting non-personal agents isn't supported.",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.WithValue(r.Context(), workspaceAgentParamContextKey{}, agent)
|
ctx := context.WithValue(r.Context(), workspaceAgentParamContextKey{}, agent)
|
||||||
|
chi.RouteContext(ctx).URLParams.Add("workspace", build.WorkspaceID.String())
|
||||||
next.ServeHTTP(rw, r.WithContext(ctx))
|
next.ServeHTTP(rw, r.WithContext(ctx))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/coder/coder/coderd/database"
|
"github.com/coder/coder/coderd/database"
|
||||||
"github.com/coder/coder/coderd/httpapi"
|
"github.com/coder/coder/coderd/httpapi"
|
||||||
"github.com/coder/coder/coderd/httpmw"
|
"github.com/coder/coder/coderd/httpmw"
|
||||||
|
"github.com/coder/coder/coderd/rbac"
|
||||||
"github.com/coder/coder/coderd/turnconn"
|
"github.com/coder/coder/coderd/turnconn"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
"github.com/coder/coder/peer"
|
"github.com/coder/coder/peer"
|
||||||
@@ -31,6 +32,10 @@ import (
|
|||||||
|
|
||||||
func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) {
|
func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) {
|
||||||
workspaceAgent := httpmw.WorkspaceAgentParam(r)
|
workspaceAgent := httpmw.WorkspaceAgentParam(r)
|
||||||
|
workspace := httpmw.WorkspaceParam(r)
|
||||||
|
if !api.Authorize(rw, r, rbac.ActionRead, workspace) {
|
||||||
|
return
|
||||||
|
}
|
||||||
dbApps, err := api.Database.GetWorkspaceAppsByAgentID(r.Context(), workspaceAgent.ID)
|
dbApps, err := api.Database.GetWorkspaceAppsByAgentID(r.Context(), workspaceAgent.ID)
|
||||||
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
|
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
|
||||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||||
@@ -58,6 +63,10 @@ func (api *API) workspaceAgentDial(rw http.ResponseWriter, r *http.Request) {
|
|||||||
defer api.websocketWaitGroup.Done()
|
defer api.websocketWaitGroup.Done()
|
||||||
|
|
||||||
workspaceAgent := httpmw.WorkspaceAgentParam(r)
|
workspaceAgent := httpmw.WorkspaceAgentParam(r)
|
||||||
|
workspace := httpmw.WorkspaceParam(r)
|
||||||
|
if !api.Authorize(rw, r, rbac.ActionUpdate, workspace) {
|
||||||
|
return
|
||||||
|
}
|
||||||
apiAgent, err := convertWorkspaceAgent(workspaceAgent, nil, api.AgentConnectionUpdateFrequency)
|
apiAgent, err := convertWorkspaceAgent(workspaceAgent, nil, api.AgentConnectionUpdateFrequency)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||||
@@ -369,6 +378,10 @@ func (api *API) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) {
|
|||||||
defer api.websocketWaitGroup.Done()
|
defer api.websocketWaitGroup.Done()
|
||||||
|
|
||||||
workspaceAgent := httpmw.WorkspaceAgentParam(r)
|
workspaceAgent := httpmw.WorkspaceAgentParam(r)
|
||||||
|
workspace := httpmw.WorkspaceParam(r)
|
||||||
|
if !api.Authorize(rw, r, rbac.ActionUpdate, workspace) {
|
||||||
|
return
|
||||||
|
}
|
||||||
apiAgent, err := convertWorkspaceAgent(workspaceAgent, nil, api.AgentConnectionUpdateFrequency)
|
apiAgent, err := convertWorkspaceAgent(workspaceAgent, nil, api.AgentConnectionUpdateFrequency)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||||
|
|||||||
@@ -63,9 +63,10 @@ interface ResourcesProps {
|
|||||||
resources?: WorkspaceResource[]
|
resources?: WorkspaceResource[]
|
||||||
getResourcesError?: Error
|
getResourcesError?: Error
|
||||||
workspace: Workspace
|
workspace: Workspace
|
||||||
|
canUpdateWorkspace: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Resources: FC<ResourcesProps> = ({ resources, getResourcesError, workspace }) => {
|
export const Resources: FC<ResourcesProps> = ({ resources, getResourcesError, workspace, canUpdateWorkspace }) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const theme: Theme = useTheme()
|
const theme: Theme = useTheme()
|
||||||
|
|
||||||
@@ -89,7 +90,7 @@ export const Resources: FC<ResourcesProps> = ({ resources, getResourcesError, wo
|
|||||||
<AgentHelpTooltip />
|
<AgentHelpTooltip />
|
||||||
</Stack>
|
</Stack>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{Language.accessLabel}</TableCell>
|
{canUpdateWorkspace && <TableCell>{Language.accessLabel}</TableCell>}
|
||||||
<TableCell>{Language.statusLabel}</TableCell>
|
<TableCell>{Language.statusLabel}</TableCell>
|
||||||
</TableHeaderRow>
|
</TableHeaderRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@@ -130,28 +131,30 @@ export const Resources: FC<ResourcesProps> = ({ resources, getResourcesError, wo
|
|||||||
{agent.name}
|
{agent.name}
|
||||||
<span className={styles.operatingSystem}>{agent.operating_system}</span>
|
<span className={styles.operatingSystem}>{agent.operating_system}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
{canUpdateWorkspace && (
|
||||||
<Stack>
|
<TableCell>
|
||||||
{agent.status === "connected" && (
|
<Stack>
|
||||||
<TerminalLink
|
{agent.status === "connected" && (
|
||||||
className={styles.accessLink}
|
<TerminalLink
|
||||||
workspaceName={workspace.name}
|
className={styles.accessLink}
|
||||||
agentName={agent.name}
|
|
||||||
userName={workspace.owner_name}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{agent.status === "connected" &&
|
|
||||||
agent.apps.map((app) => (
|
|
||||||
<AppLink
|
|
||||||
key={app.name}
|
|
||||||
appIcon={app.icon}
|
|
||||||
appName={app.name}
|
|
||||||
userName={workspace.owner_name}
|
|
||||||
workspaceName={workspace.name}
|
workspaceName={workspace.name}
|
||||||
|
agentName={agent.name}
|
||||||
|
userName={workspace.owner_name}
|
||||||
/>
|
/>
|
||||||
))}
|
)}
|
||||||
</Stack>
|
{agent.status === "connected" &&
|
||||||
</TableCell>
|
agent.apps.map((app) => (
|
||||||
|
<AppLink
|
||||||
|
key={app.name}
|
||||||
|
appIcon={app.icon}
|
||||||
|
appName={app.name}
|
||||||
|
userName={workspace.owner_name}
|
||||||
|
workspaceName={workspace.name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span style={{ color: getDisplayAgentStatus(theme, agent).color }}>
|
<span style={{ color: getDisplayAgentStatus(theme, agent).color }}>
|
||||||
{getDisplayAgentStatus(theme, agent).status}
|
{getDisplayAgentStatus(theme, agent).status}
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ Started.args = {
|
|||||||
handleStop: action("stop"),
|
handleStop: action("stop"),
|
||||||
resources: [Mocks.MockWorkspaceResource, Mocks.MockWorkspaceResource2],
|
resources: [Mocks.MockWorkspaceResource, Mocks.MockWorkspaceResource2],
|
||||||
builds: [Mocks.MockWorkspaceBuild],
|
builds: [Mocks.MockWorkspaceBuild],
|
||||||
|
canUpdateWorkspace: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithoutUpdateAccess = Template.bind({})
|
||||||
|
WithoutUpdateAccess.args = {
|
||||||
|
...Started.args,
|
||||||
|
canUpdateWorkspace: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Starting = Template.bind({})
|
export const Starting = Template.bind({})
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export interface WorkspaceProps {
|
|||||||
resources?: TypesGen.WorkspaceResource[]
|
resources?: TypesGen.WorkspaceResource[]
|
||||||
getResourcesError?: Error
|
getResourcesError?: Error
|
||||||
builds?: TypesGen.WorkspaceBuild[]
|
builds?: TypesGen.WorkspaceBuild[]
|
||||||
|
canUpdateWorkspace: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,6 +45,7 @@ export const Workspace: FC<WorkspaceProps> = ({
|
|||||||
resources,
|
resources,
|
||||||
getResourcesError,
|
getResourcesError,
|
||||||
builds,
|
builds,
|
||||||
|
canUpdateWorkspace,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -80,7 +82,12 @@ export const Workspace: FC<WorkspaceProps> = ({
|
|||||||
<WorkspaceStats workspace={workspace} />
|
<WorkspaceStats workspace={workspace} />
|
||||||
|
|
||||||
{!!resources && !!resources.length && (
|
{!!resources && !!resources.length && (
|
||||||
<Resources resources={resources} getResourcesError={getResourcesError} workspace={workspace} />
|
<Resources
|
||||||
|
resources={resources}
|
||||||
|
getResourcesError={getResourcesError}
|
||||||
|
workspace={workspace}
|
||||||
|
canUpdateWorkspace={canUpdateWorkspace}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<WorkspaceSection title="Timeline" contentsProps={{ className: styles.timelineContents }}>
|
<WorkspaceSection title="Timeline" contentsProps={{ className: styles.timelineContents }}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMachine } from "@xstate/react"
|
import { useMachine, useSelector } from "@xstate/react"
|
||||||
import React, { useEffect } from "react"
|
import React, { useContext, useEffect } from "react"
|
||||||
import { Helmet } from "react-helmet"
|
import { Helmet } from "react-helmet"
|
||||||
import { useParams } from "react-router-dom"
|
import { useParams } from "react-router-dom"
|
||||||
import { DeleteWorkspaceDialog } from "../../components/DeleteWorkspaceDialog/DeleteWorkspaceDialog"
|
import { DeleteWorkspaceDialog } from "../../components/DeleteWorkspaceDialog/DeleteWorkspaceDialog"
|
||||||
@@ -8,6 +8,8 @@ import { FullScreenLoader } from "../../components/Loader/FullScreenLoader"
|
|||||||
import { Workspace } from "../../components/Workspace/Workspace"
|
import { Workspace } from "../../components/Workspace/Workspace"
|
||||||
import { firstOrItem } from "../../util/array"
|
import { firstOrItem } from "../../util/array"
|
||||||
import { pageTitle } from "../../util/page"
|
import { pageTitle } from "../../util/page"
|
||||||
|
import { selectUser } from "../../xServices/auth/authSelectors"
|
||||||
|
import { XServiceContext } from "../../xServices/StateContext"
|
||||||
import { workspaceMachine } from "../../xServices/workspace/workspaceXService"
|
import { workspaceMachine } from "../../xServices/workspace/workspaceXService"
|
||||||
import { workspaceScheduleBannerMachine } from "../../xServices/workspaceSchedule/workspaceScheduleBannerXService"
|
import { workspaceScheduleBannerMachine } from "../../xServices/workspaceSchedule/workspaceScheduleBannerXService"
|
||||||
|
|
||||||
@@ -16,8 +18,17 @@ export const WorkspacePage: React.FC = () => {
|
|||||||
const username = firstOrItem(usernameQueryParam, null)
|
const username = firstOrItem(usernameQueryParam, null)
|
||||||
const workspaceName = firstOrItem(workspaceQueryParam, null)
|
const workspaceName = firstOrItem(workspaceQueryParam, null)
|
||||||
|
|
||||||
const [workspaceState, workspaceSend] = useMachine(workspaceMachine)
|
const xServices = useContext(XServiceContext)
|
||||||
const { workspace, resources, getWorkspaceError, getResourcesError, builds } = workspaceState.context
|
const me = useSelector(xServices.authXService, selectUser)
|
||||||
|
|
||||||
|
const [workspaceState, workspaceSend] = useMachine(workspaceMachine, {
|
||||||
|
context: {
|
||||||
|
userId: me?.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { workspace, resources, getWorkspaceError, getResourcesError, builds, permissions } = workspaceState.context
|
||||||
|
|
||||||
|
const canUpdateWorkspace = !!permissions?.updateWorkspace
|
||||||
|
|
||||||
const [bannerState, bannerSend] = useMachine(workspaceScheduleBannerMachine)
|
const [bannerState, bannerSend] = useMachine(workspaceScheduleBannerMachine)
|
||||||
|
|
||||||
@@ -56,6 +67,7 @@ export const WorkspacePage: React.FC = () => {
|
|||||||
resources={resources}
|
resources={resources}
|
||||||
getResourcesError={getResourcesError instanceof Error ? getResourcesError : undefined}
|
getResourcesError={getResourcesError instanceof Error ? getResourcesError : undefined}
|
||||||
builds={builds}
|
builds={builds}
|
||||||
|
canUpdateWorkspace={canUpdateWorkspace}
|
||||||
/>
|
/>
|
||||||
<DeleteWorkspaceDialog
|
<DeleteWorkspaceDialog
|
||||||
isOpen={workspaceState.matches({ ready: { build: "askingDelete" } })}
|
isOpen={workspaceState.matches({ ready: { build: "askingDelete" } })}
|
||||||
|
|||||||
@@ -10,3 +10,7 @@ export const selectOrgId = (state: AuthState): string | undefined => {
|
|||||||
export const selectPermissions = (state: AuthState): AuthContext["permissions"] => {
|
export const selectPermissions = (state: AuthState): AuthContext["permissions"] => {
|
||||||
return state.context.permissions
|
return state.context.permissions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const selectUser = (state: AuthState): AuthContext["me"] => {
|
||||||
|
return state.context.me
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ const Language = {
|
|||||||
buildError: "Workspace action failed.",
|
buildError: "Workspace action failed.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Permissions = Record<keyof ReturnType<typeof permissionsToCheck>, boolean>
|
||||||
|
|
||||||
export interface WorkspaceContext {
|
export interface WorkspaceContext {
|
||||||
workspace?: TypesGen.Workspace
|
workspace?: TypesGen.Workspace
|
||||||
template?: TypesGen.Template
|
template?: TypesGen.Template
|
||||||
@@ -34,6 +36,10 @@ export interface WorkspaceContext {
|
|||||||
getBuildsError?: Error | unknown
|
getBuildsError?: Error | unknown
|
||||||
loadMoreBuildsError?: Error | unknown
|
loadMoreBuildsError?: Error | unknown
|
||||||
cancellationMessage: string
|
cancellationMessage: string
|
||||||
|
// permissions
|
||||||
|
permissions?: Permissions
|
||||||
|
checkPermissionsError?: Error | unknown
|
||||||
|
userId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkspaceEvent =
|
export type WorkspaceEvent =
|
||||||
@@ -48,6 +54,30 @@ export type WorkspaceEvent =
|
|||||||
| { type: "LOAD_MORE_BUILDS" }
|
| { type: "LOAD_MORE_BUILDS" }
|
||||||
| { type: "REFRESH_TIMELINE" }
|
| { type: "REFRESH_TIMELINE" }
|
||||||
|
|
||||||
|
export const checks = {
|
||||||
|
readWorkspace: "readWorkspace",
|
||||||
|
updateWorkspace: "updateWorkspace",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const permissionsToCheck = (workspace: TypesGen.Workspace) => ({
|
||||||
|
[checks.readWorkspace]: {
|
||||||
|
object: {
|
||||||
|
resource_type: "workspace",
|
||||||
|
resource_id: workspace.id,
|
||||||
|
owner_id: workspace.owner_id,
|
||||||
|
},
|
||||||
|
action: "read",
|
||||||
|
},
|
||||||
|
[checks.updateWorkspace]: {
|
||||||
|
object: {
|
||||||
|
resource_type: "workspace",
|
||||||
|
resource_id: workspace.id,
|
||||||
|
owner_id: workspace.owner_id,
|
||||||
|
},
|
||||||
|
action: "update",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export const workspaceMachine = createMachine(
|
export const workspaceMachine = createMachine(
|
||||||
{
|
{
|
||||||
tsTypes: {} as import("./workspaceXService.typegen").Typegen0,
|
tsTypes: {} as import("./workspaceXService.typegen").Typegen0,
|
||||||
@@ -82,6 +112,9 @@ export const workspaceMachine = createMachine(
|
|||||||
loadMoreBuilds: {
|
loadMoreBuilds: {
|
||||||
data: TypesGen.WorkspaceBuild[]
|
data: TypesGen.WorkspaceBuild[]
|
||||||
}
|
}
|
||||||
|
checkPermissions: {
|
||||||
|
data: TypesGen.UserAuthorizationResponse
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
id: "workspaceState",
|
id: "workspaceState",
|
||||||
@@ -99,7 +132,7 @@ export const workspaceMachine = createMachine(
|
|||||||
src: "getWorkspace",
|
src: "getWorkspace",
|
||||||
id: "getWorkspace",
|
id: "getWorkspace",
|
||||||
onDone: {
|
onDone: {
|
||||||
target: "ready",
|
target: "gettingPermissions",
|
||||||
actions: ["assignWorkspace"],
|
actions: ["assignWorkspace"],
|
||||||
},
|
},
|
||||||
onError: {
|
onError: {
|
||||||
@@ -109,6 +142,25 @@ export const workspaceMachine = createMachine(
|
|||||||
},
|
},
|
||||||
tags: "loading",
|
tags: "loading",
|
||||||
},
|
},
|
||||||
|
gettingPermissions: {
|
||||||
|
entry: "clearGetPermissionsError",
|
||||||
|
invoke: {
|
||||||
|
src: "checkPermissions",
|
||||||
|
id: "checkPermissions",
|
||||||
|
onDone: [
|
||||||
|
{
|
||||||
|
actions: ["assignPermissions"],
|
||||||
|
target: "ready",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
actions: "assignGetPermissionsError",
|
||||||
|
target: "error",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
ready: {
|
ready: {
|
||||||
type: "parallel",
|
type: "parallel",
|
||||||
states: {
|
states: {
|
||||||
@@ -312,6 +364,7 @@ export const workspaceMachine = createMachine(
|
|||||||
workspace: undefined,
|
workspace: undefined,
|
||||||
template: undefined,
|
template: undefined,
|
||||||
build: undefined,
|
build: undefined,
|
||||||
|
permissions: undefined,
|
||||||
}),
|
}),
|
||||||
assignWorkspace: assign({
|
assignWorkspace: assign({
|
||||||
workspace: (_, event) => event.data,
|
workspace: (_, event) => event.data,
|
||||||
@@ -323,6 +376,17 @@ export const workspaceMachine = createMachine(
|
|||||||
assignTemplate: assign({
|
assignTemplate: assign({
|
||||||
template: (_, event) => event.data,
|
template: (_, event) => event.data,
|
||||||
}),
|
}),
|
||||||
|
assignPermissions: assign({
|
||||||
|
// Setting event.data as Permissions to be more stricted. So we know
|
||||||
|
// what permissions we asked for.
|
||||||
|
permissions: (_, event) => event.data as Permissions,
|
||||||
|
}),
|
||||||
|
assignGetPermissionsError: assign({
|
||||||
|
checkPermissionsError: (_, event) => event.data,
|
||||||
|
}),
|
||||||
|
clearGetPermissionsError: assign({
|
||||||
|
checkPermissionsError: (_) => undefined,
|
||||||
|
}),
|
||||||
assignBuild: (_, event) =>
|
assignBuild: (_, event) =>
|
||||||
assign({
|
assign({
|
||||||
build: event.data,
|
build: event.data,
|
||||||
@@ -489,14 +553,23 @@ export const workspaceMachine = createMachine(
|
|||||||
if (context.workspace) {
|
if (context.workspace) {
|
||||||
return await API.getWorkspaceBuilds(context.workspace.id)
|
return await API.getWorkspaceBuilds(context.workspace.id)
|
||||||
} else {
|
} else {
|
||||||
throw Error("Cannot refresh workspace without id")
|
throw Error("Cannot get builds without id")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
loadMoreBuilds: async (context) => {
|
loadMoreBuilds: async (context) => {
|
||||||
if (context.workspace) {
|
if (context.workspace) {
|
||||||
return await API.getWorkspaceBuilds(context.workspace.id)
|
return await API.getWorkspaceBuilds(context.workspace.id)
|
||||||
} else {
|
} else {
|
||||||
throw Error("Cannot refresh workspace without id")
|
throw Error("Cannot load more builds without id")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
checkPermissions: async (context) => {
|
||||||
|
if (context.workspace && context.userId) {
|
||||||
|
return await API.checkUserPermissions(context.userId, {
|
||||||
|
checks: permissionsToCheck(context.workspace),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
throw Error("Cannot check permissions without both workspace and user id")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user