Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: add organization scope to shared ports#18277

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Draft
sreya wants to merge2 commits intomain
base:main
Choose a base branch
Loading
fromjon/orgports
Draft
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletionscoderd/database/dump.sql
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
-- Note: PostgreSQL does not support removing enum values directly.
-- This migration cannot be easily reversed without recreating the enum type.
-- In practice, this would require:
-- 1. Creating a new enum without 'organization'
-- 2. Updating all columns to use the new enum
-- 3. Dropping the old enum
-- 4. Renaming the new enum
-- For safety, we leave this as a no-op migration.
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
-- Add 'organization' value to app_sharing_level enum
ALTER TYPE app_sharing_level ADD VALUE 'organization';
3 changes: 3 additions & 0 deletionscoderd/database/models.go
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

90 changes: 90 additions & 0 deletionscoderd/workspaceagentportshare_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,3 +218,93 @@ func TestDeleteWorkspaceAgentPortShare(t *testing.T) {
})
require.Error(t, err)
}

func TestPostWorkspaceAgentPortShareOrganization(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
ownerClient, db := coderdtest.NewWithDatabase(t, nil)
owner := coderdtest.CreateFirstUser(t, ownerClient)
client, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)

tmpDir := t.TempDir()
r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
OrganizationID: owner.OrganizationID,
OwnerID: user.ID,
}).WithAgent(func(agents []*proto.Agent) []*proto.Agent {
agents[0].Directory = tmpDir
return agents
}).Do()
agents, err := db.GetWorkspaceAgentsInLatestBuildByWorkspaceID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(user, owner.OrganizationID)), r.Workspace.ID)
require.NoError(t, err)

// organization level should work
ps, err := client.UpsertWorkspaceAgentPortShare(ctx, r.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
AgentName: agents[0].Name,
Port: 8080,
ShareLevel: codersdk.WorkspaceAgentPortShareLevelOrganization,
Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP,
})
require.NoError(t, err)
require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelOrganization, ps.ShareLevel)

// update share level
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)
}

func TestWorkspaceAgentPortShareOrganizationBasic(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

// Create owner and organization
ownerClient, db := coderdtest.NewWithDatabase(t, nil)
owner := coderdtest.CreateFirstUser(t, ownerClient)

// Create a user in the same organization as the workspace
client, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)

// Create workspace in the organization
tmpDir := t.TempDir()
r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
OrganizationID: owner.OrganizationID,
OwnerID: user.ID,
}).WithAgent(func(agents []*proto.Agent) []*proto.Agent {
agents[0].Directory = tmpDir
return agents
}).Do()
agents, err := db.GetWorkspaceAgentsInLatestBuildByWorkspaceID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(user, owner.OrganizationID)), r.Workspace.ID)
require.NoError(t, err)

// Create organization-level port share
ps, err := client.UpsertWorkspaceAgentPortShare(ctx, r.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
AgentName: agents[0].Name,
Port: 8080,
ShareLevel: codersdk.WorkspaceAgentPortShareLevelOrganization,
Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP,
})
require.NoError(t, err)
require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelOrganization, ps.ShareLevel)

// Test that user in same organization can access the port share
shares, err := client.GetWorkspaceAgentPortShares(ctx, r.Workspace.ID)
require.NoError(t, err)
require.Len(t, shares.Shares, 1)
require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelOrganization, shares.Shares[0].ShareLevel)

// Test that owner can access the port share (owner is in same org)
shares, err = ownerClient.GetWorkspaceAgentPortShares(ctx, r.Workspace.ID)
require.NoError(t, err)
require.Len(t, shares.Shares, 1)
require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelOrganization, shares.Shares[0].ShareLevel)

// Verify that the organization level is properly stored and retrieved
require.Equal(t, user.OrganizationIDs[0], owner.OrganizationID)
}
46 changes: 46 additions & 0 deletionscoderd/workspaceapps/apptest/apptest.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -1312,6 +1312,52 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
assertWorkspaceLastUsedAtUpdated(t, appDetails)
})

t.Run("OrganizationOK", 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 organization users
_, err = appDetails.SDKClient.UpsertWorkspaceAgentPortShare(ctx, appDetails.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
AgentName: proxyTestAgentName,
Port: int32(port),
ShareLevel: codersdk.WorkspaceAgentPortShareLevelOrganization,
Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP,
})
require.NoError(t, err)

// User in the same organization should be able to access
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)
assertWorkspaceLastUsedAtUpdated(t, appDetails)

// User in a different organization should not be able to access
// Create a new organization and user
otherOrg, err := appDetails.SDKClient.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
Name: "other-org",
DisplayName: "Other Organization",
})
require.NoError(t, err)
otherUserClient, _ := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, otherOrg.ID, rbac.RoleMember())
otherUserAppClient := appDetails.AppClient(t)
otherUserAppClient.SetSessionToken(otherUserClient.SessionToken())

resp, err = requestWithRetries(ctx, t, otherUserAppClient, 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("HTTPS", func(t *testing.T) {
t.Parallel()

Expand Down
24 changes: 24 additions & 0 deletionscoderd/workspaceapps/db.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -354,6 +354,30 @@ func (p *DBTokenProvider) authorizeRequest(ctx context.Context, roles *rbac.Subj
if err == nil {
return true, []string{}, nil
}
case database.AppSharingLevelOrganization:
// Check if the user is in the same organization as the workspace's template.
// First check with the owned resource to ensure the API key has permissions
// to connect to the actor's own workspace. This enforces scopes.
err := p.Authorizer.Authorize(ctx, *roles, rbacAction, rbacResourceOwned)
if err != nil {
// If the user doesn't have permission to connect to their own workspace,
// they can't access organization-shared apps either.
break
}
// Get the template to check organization
template, err := p.Database.GetTemplateByID(ctx, dbReq.Workspace.TemplateID)
if err != nil {
// If we can't get the template, deny access
break
}
// Check if the user is in the same organization as the template
// We need to check if the user has access to the template's organization
// by checking if they can read workspaces in that organization
orgResource := rbac.ResourceWorkspace.InOrg(template.OrganizationID)
err = p.Authorizer.Authorize(ctx, *roles, policy.ActionRead, orgResource)
if err == nil {
return true, []string{}, nil
}
case database.AppSharingLevelPublic:
// We don't really care about scopes and stuff if it's public anyways.
// Someone with a restricted-scope API key could just not submit the API
Expand Down
7 changes: 5 additions & 2 deletionscodersdk/workspaceagentportshare.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ import (
const (
WorkspaceAgentPortShareLevelOwner WorkspaceAgentPortShareLevel = "owner"
WorkspaceAgentPortShareLevelAuthenticated WorkspaceAgentPortShareLevel = "authenticated"
WorkspaceAgentPortShareLevelOrganization WorkspaceAgentPortShareLevel = "organization"
WorkspaceAgentPortShareLevelPublic WorkspaceAgentPortShareLevel = "public"

WorkspaceAgentPortShareProtocolHTTP WorkspaceAgentPortShareProtocol = "http"
Expand All@@ -24,7 +25,7 @@ type (
UpsertWorkspaceAgentPortShareRequest struct {
AgentName string `json:"agent_name"`
Port int32 `json:"port"`
ShareLevel WorkspaceAgentPortShareLevel `json:"share_level" enums:"owner,authenticated,public"`
ShareLevel WorkspaceAgentPortShareLevel `json:"share_level" enums:"owner,authenticated,organization,public"`
Protocol WorkspaceAgentPortShareProtocol `json:"protocol" enums:"http,https"`
}
WorkspaceAgentPortShares struct {
Expand All@@ -34,7 +35,7 @@ type (
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"`
ShareLevel WorkspaceAgentPortShareLevel `json:"share_level" enums:"owner,authenticated,organization,public"`
Protocol WorkspaceAgentPortShareProtocol `json:"protocol" enums:"http,https"`
}
DeleteWorkspaceAgentPortShareRequest struct {
Expand All@@ -46,11 +47,13 @@ type (
func (l WorkspaceAgentPortShareLevel) ValidMaxLevel() bool {
return l == WorkspaceAgentPortShareLevelOwner ||
l == WorkspaceAgentPortShareLevelAuthenticated ||
l == WorkspaceAgentPortShareLevelOrganization ||
l == WorkspaceAgentPortShareLevelPublic
}

func (l WorkspaceAgentPortShareLevel) ValidPortShareLevel() bool {
return l == WorkspaceAgentPortShareLevelAuthenticated ||
l == WorkspaceAgentPortShareLevelOrganization ||
l == WorkspaceAgentPortShareLevelPublic
}

Expand Down
6 changes: 5 additions & 1 deletionenterprise/coderd/portsharing/portsharing.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,10 @@ func (EnterprisePortSharer) AuthorizedLevel(template database.Template, level co
if maxLevel != codersdk.WorkspaceAgentPortShareLevelPublic {
return xerrors.Errorf("port sharing level not allowed. Max level is '%s'", maxLevel)
}
case codersdk.WorkspaceAgentPortShareLevelOrganization:
if maxLevel == codersdk.WorkspaceAgentPortShareLevelOwner || maxLevel == codersdk.WorkspaceAgentPortShareLevelAuthenticated {
return xerrors.Errorf("port sharing level not allowed. Max level is '%s'", maxLevel)
}
case codersdk.WorkspaceAgentPortShareLevelAuthenticated:
if maxLevel == codersdk.WorkspaceAgentPortShareLevelOwner {
return xerrors.Errorf("port sharing level not allowed. Max level is '%s'", maxLevel)
Expand All@@ -33,7 +37,7 @@ func (EnterprisePortSharer) AuthorizedLevel(template database.Template, level co

func (EnterprisePortSharer) ValidateTemplateMaxLevel(level codersdk.WorkspaceAgentPortShareLevel) error {
if !level.ValidMaxLevel() {
return xerrors.New("invalid max port sharing level, value must be 'authenticated' or 'public'.")
return xerrors.New("invalid max port sharing level, value must be 'owner', 'authenticated', 'organization', or 'public'.")
}

return nil
Expand Down
73 changes: 73 additions & 0 deletionsenterprise/coderd/workspaceportshare_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,3 +60,76 @@ func TestWorkspacePortShare(t *testing.T) {
require.NoError(t, err)
require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelPublic, ps.ShareLevel)
}

func TestWorkspacePortShareOrganizationAuthorization(t *testing.T) {
t.Parallel()

ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
IncludeProvisionerDaemon: true,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureControlSharedPorts: 1,
codersdk.FeatureMultipleOrganizations: 1,
},
},
})

ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()

// Create a user in the same organization as the workspace
client, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())

// Create a second organization
org2 := coderdenttest.CreateOrganization(t, ownerClient, coderdenttest.CreateOrganizationOptions{})

// Create a user in the different organization
client2, user2 := coderdtest.CreateAnotherUser(t, ownerClient, org2.ID, rbac.RoleMember())

// Create workspace and agent in the first organization
r := setupWorkspaceAgent(t, client, codersdk.CreateFirstUserResponse{
UserID: user.ID,
OrganizationID: owner.OrganizationID,
}, 0)

// Update template to allow organization level port sharing
var maxLevel codersdk.WorkspaceAgentPortShareLevel = codersdk.WorkspaceAgentPortShareLevelOrganization
_, err := client.UpdateTemplateMeta(ctx, r.workspace.TemplateID, codersdk.UpdateTemplateMeta{
MaxPortShareLevel: &maxLevel,
})
require.NoError(t, err)

// Create organization-level port share
ps, err := client.UpsertWorkspaceAgentPortShare(ctx, r.workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
AgentName: r.sdkAgent.Name,
Port: 8080,
ShareLevel: codersdk.WorkspaceAgentPortShareLevelOrganization,
Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP,
})
require.NoError(t, err)
require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelOrganization, ps.ShareLevel)

// Test that user in same organization can access the port share
shares, err := client.GetWorkspaceAgentPortShares(ctx, r.workspace.ID)
require.NoError(t, err)
require.Len(t, shares.Shares, 1)
require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelOrganization, shares.Shares[0].ShareLevel)

// Test that user in different organization cannot access the workspace
// (they shouldn't even be able to see the workspace)
_, err = client2.GetWorkspaceAgentPortShares(ctx, r.workspace.ID)
require.Error(t, err)

// Test that owner can access the port share (owner is in same org)
shares, err = ownerClient.GetWorkspaceAgentPortShares(ctx, r.workspace.ID)
require.NoError(t, err)
require.Len(t, shares.Shares, 1)
require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelOrganization, shares.Shares[0].ShareLevel)

// Verify the users are in different organizations
require.NotEqual(t, user.OrganizationIDs[0], user2.OrganizationIDs[0])
require.Equal(t, user.OrganizationIDs[0], owner.OrganizationID)
require.Equal(t, user2.OrganizationIDs[0], org2.ID)
}
3 changes: 2 additions & 1 deletionsite/src/api/typesGenerated.ts
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

Loading

[8]ページ先頭

©2009-2025 Movatter.jp