feat: add organization scope for shared ports (#18314)

This commit is contained in:
ケイラ
2025-06-16 16:15:59 -06:00
committed by GitHub
parent eff2174198
commit 5df70a613d
30 changed files with 1245 additions and 811 deletions

View File

@ -5,6 +5,7 @@ import (
"database/sql"
"errors"
"fmt"
"strings"
"github.com/google/uuid"
"github.com/sqlc-dev/pqtype"
@ -140,20 +141,15 @@ func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.Create
health = database.WorkspaceAppHealthInitializing
}
var sharingLevel database.AppSharingLevel
switch app.GetShare() {
case agentproto.CreateSubAgentRequest_App_OWNER:
sharingLevel = database.AppSharingLevelOwner
case agentproto.CreateSubAgentRequest_App_AUTHENTICATED:
sharingLevel = database.AppSharingLevelAuthenticated
case agentproto.CreateSubAgentRequest_App_PUBLIC:
sharingLevel = database.AppSharingLevelPublic
default:
share := app.GetShare()
protoSharingLevel, ok := agentproto.CreateSubAgentRequest_App_SharingLevel_name[int32(share)]
if !ok {
return codersdk.ValidationError{
Field: "share",
Detail: fmt.Sprintf("%q is not a valid app sharing level", app.GetShare()),
Detail: fmt.Sprintf("%q is not a valid app sharing level", share.String()),
}
}
sharingLevel := database.AppSharingLevel(strings.ToLower(protoSharingLevel))
var openIn database.WorkspaceAppOpenIn
switch app.GetOpenIn() {

7
coderd/apidoc/docs.go generated
View File

@ -16833,6 +16833,7 @@ const docTemplate = `{
"enum": [
"owner",
"authenticated",
"organization",
"public"
],
"allOf": [
@ -17747,6 +17748,7 @@ const docTemplate = `{
"enum": [
"owner",
"authenticated",
"organization",
"public"
],
"allOf": [
@ -17766,11 +17768,13 @@ const docTemplate = `{
"enum": [
"owner",
"authenticated",
"organization",
"public"
],
"x-enum-varnames": [
"WorkspaceAgentPortShareLevelOwner",
"WorkspaceAgentPortShareLevelAuthenticated",
"WorkspaceAgentPortShareLevelOrganization",
"WorkspaceAgentPortShareLevelPublic"
]
},
@ -17905,6 +17909,7 @@ const docTemplate = `{
"enum": [
"owner",
"authenticated",
"organization",
"public"
],
"allOf": [
@ -17969,11 +17974,13 @@ const docTemplate = `{
"enum": [
"owner",
"authenticated",
"organization",
"public"
],
"x-enum-varnames": [
"WorkspaceAppSharingLevelOwner",
"WorkspaceAppSharingLevelAuthenticated",
"WorkspaceAppSharingLevelOrganization",
"WorkspaceAppSharingLevelPublic"
]
},

View File

@ -15353,7 +15353,7 @@
]
},
"share_level": {
"enum": ["owner", "authenticated", "public"],
"enum": ["owner", "authenticated", "organization", "public"],
"allOf": [
{
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel"
@ -16227,7 +16227,7 @@
]
},
"share_level": {
"enum": ["owner", "authenticated", "public"],
"enum": ["owner", "authenticated", "organization", "public"],
"allOf": [
{
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel"
@ -16242,10 +16242,11 @@
},
"codersdk.WorkspaceAgentPortShareLevel": {
"type": "string",
"enum": ["owner", "authenticated", "public"],
"enum": ["owner", "authenticated", "organization", "public"],
"x-enum-varnames": [
"WorkspaceAgentPortShareLevelOwner",
"WorkspaceAgentPortShareLevelAuthenticated",
"WorkspaceAgentPortShareLevelOrganization",
"WorkspaceAgentPortShareLevelPublic"
]
},
@ -16366,7 +16367,7 @@
"$ref": "#/definitions/codersdk.WorkspaceAppOpenIn"
},
"sharing_level": {
"enum": ["owner", "authenticated", "public"],
"enum": ["owner", "authenticated", "organization", "public"],
"allOf": [
{
"$ref": "#/definitions/codersdk.WorkspaceAppSharingLevel"
@ -16418,10 +16419,11 @@
},
"codersdk.WorkspaceAppSharingLevel": {
"type": "string",
"enum": ["owner", "authenticated", "public"],
"enum": ["owner", "authenticated", "organization", "public"],
"x-enum-varnames": [
"WorkspaceAppSharingLevelOwner",
"WorkspaceAppSharingLevelAuthenticated",
"WorkspaceAppSharingLevelOrganization",
"WorkspaceAppSharingLevelPublic"
]
},

View File

@ -18,6 +18,7 @@ CREATE TYPE api_key_scope AS ENUM (
CREATE TYPE app_sharing_level AS ENUM (
'owner',
'authenticated',
'organization',
'public'
);

View File

@ -0,0 +1,92 @@
-- Drop the view that depends on the templates table
DROP VIEW template_with_names;
-- Remove 'organization' from the app_sharing_level enum
CREATE TYPE new_app_sharing_level AS ENUM (
'owner',
'authenticated',
'public'
);
-- Update workspace_agent_port_share table to use old enum
-- Convert any 'organization' values to 'authenticated' during downgrade
ALTER TABLE workspace_agent_port_share
ALTER COLUMN share_level TYPE new_app_sharing_level USING (
CASE
WHEN share_level = 'organization' THEN 'authenticated'::new_app_sharing_level
ELSE share_level::text::new_app_sharing_level
END
);
-- Update workspace_apps table to use old enum
-- Convert any 'organization' values to 'authenticated' during downgrade
ALTER TABLE workspace_apps
ALTER COLUMN sharing_level DROP DEFAULT,
ALTER COLUMN sharing_level TYPE new_app_sharing_level USING (
CASE
WHEN sharing_level = 'organization' THEN 'authenticated'::new_app_sharing_level
ELSE sharing_level::text::new_app_sharing_level
END
),
ALTER COLUMN sharing_level SET DEFAULT 'owner'::new_app_sharing_level;
-- Update templates table to use old enum
-- Convert any 'organization' values to 'authenticated' during downgrade
ALTER TABLE templates
ALTER COLUMN max_port_sharing_level DROP DEFAULT,
ALTER COLUMN max_port_sharing_level TYPE new_app_sharing_level USING (
CASE
WHEN max_port_sharing_level = 'organization' THEN 'owner'::new_app_sharing_level
ELSE max_port_sharing_level::text::new_app_sharing_level
END
),
ALTER COLUMN max_port_sharing_level SET DEFAULT 'owner'::new_app_sharing_level;
-- Drop old enum and rename new one
DROP TYPE app_sharing_level;
ALTER TYPE new_app_sharing_level RENAME TO app_sharing_level;
-- Recreate the template_with_names view
CREATE VIEW template_with_names AS
SELECT templates.id,
templates.created_at,
templates.updated_at,
templates.organization_id,
templates.deleted,
templates.name,
templates.provisioner,
templates.active_version_id,
templates.description,
templates.default_ttl,
templates.created_by,
templates.icon,
templates.user_acl,
templates.group_acl,
templates.display_name,
templates.allow_user_cancel_workspace_jobs,
templates.allow_user_autostart,
templates.allow_user_autostop,
templates.failure_ttl,
templates.time_til_dormant,
templates.time_til_dormant_autodelete,
templates.autostop_requirement_days_of_week,
templates.autostop_requirement_weeks,
templates.autostart_block_days_of_week,
templates.require_active_version,
templates.deprecated,
templates.activity_bump,
templates.max_port_sharing_level,
templates.use_classic_parameter_flow,
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
COALESCE(visible_users.username, ''::text) AS created_by_username,
COALESCE(visible_users.name, ''::text) AS created_by_name,
COALESCE(organizations.name, ''::text) AS organization_name,
COALESCE(organizations.display_name, ''::text) AS organization_display_name,
COALESCE(organizations.icon, ''::text) AS organization_icon
FROM ((templates
LEFT JOIN visible_users ON ((templates.created_by = visible_users.id)))
LEFT JOIN organizations ON ((templates.organization_id = organizations.id)));
COMMENT ON VIEW template_with_names IS 'Joins in the display name information such as username, avatar, and organization name.';

View File

@ -0,0 +1,73 @@
-- Drop the view that depends on the templates table
DROP VIEW template_with_names;
-- Add 'organization' to the app_sharing_level enum
CREATE TYPE new_app_sharing_level AS ENUM (
'owner',
'authenticated',
'organization',
'public'
);
-- Update workspace_agent_port_share table to use new enum
ALTER TABLE workspace_agent_port_share
ALTER COLUMN share_level TYPE new_app_sharing_level USING (share_level::text::new_app_sharing_level);
-- Update workspace_apps table to use new enum
ALTER TABLE workspace_apps
ALTER COLUMN sharing_level DROP DEFAULT,
ALTER COLUMN sharing_level TYPE new_app_sharing_level USING (sharing_level::text::new_app_sharing_level),
ALTER COLUMN sharing_level SET DEFAULT 'owner'::new_app_sharing_level;
-- Update templates table to use new enum
ALTER TABLE templates
ALTER COLUMN max_port_sharing_level DROP DEFAULT,
ALTER COLUMN max_port_sharing_level TYPE new_app_sharing_level USING (max_port_sharing_level::text::new_app_sharing_level),
ALTER COLUMN max_port_sharing_level SET DEFAULT 'owner'::new_app_sharing_level;
-- Drop old enum and rename new one
DROP TYPE app_sharing_level;
ALTER TYPE new_app_sharing_level RENAME TO app_sharing_level;
-- Recreate the template_with_names view
CREATE VIEW template_with_names AS
SELECT templates.id,
templates.created_at,
templates.updated_at,
templates.organization_id,
templates.deleted,
templates.name,
templates.provisioner,
templates.active_version_id,
templates.description,
templates.default_ttl,
templates.created_by,
templates.icon,
templates.user_acl,
templates.group_acl,
templates.display_name,
templates.allow_user_cancel_workspace_jobs,
templates.allow_user_autostart,
templates.allow_user_autostop,
templates.failure_ttl,
templates.time_til_dormant,
templates.time_til_dormant_autodelete,
templates.autostop_requirement_days_of_week,
templates.autostop_requirement_weeks,
templates.autostart_block_days_of_week,
templates.require_active_version,
templates.deprecated,
templates.activity_bump,
templates.max_port_sharing_level,
templates.use_classic_parameter_flow,
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
COALESCE(visible_users.username, ''::text) AS created_by_username,
COALESCE(visible_users.name, ''::text) AS created_by_name,
COALESCE(organizations.name, ''::text) AS organization_name,
COALESCE(organizations.display_name, ''::text) AS organization_display_name,
COALESCE(organizations.icon, ''::text) AS organization_icon
FROM ((templates
LEFT JOIN visible_users ON ((templates.created_by = visible_users.id)))
LEFT JOIN organizations ON ((templates.organization_id = organizations.id)));
COMMENT ON VIEW template_with_names IS 'Joins in the display name information such as username, avatar, and organization name.';

View File

@ -137,6 +137,7 @@ type AppSharingLevel string
const (
AppSharingLevelOwner AppSharingLevel = "owner"
AppSharingLevelAuthenticated AppSharingLevel = "authenticated"
AppSharingLevelOrganization AppSharingLevel = "organization"
AppSharingLevelPublic AppSharingLevel = "public"
)
@ -179,6 +180,7 @@ func (e AppSharingLevel) Valid() bool {
switch e {
case AppSharingLevelOwner,
AppSharingLevelAuthenticated,
AppSharingLevelOrganization,
AppSharingLevelPublic:
return true
}
@ -189,6 +191,7 @@ func AllAppSharingLevelValues() []AppSharingLevel {
return []AppSharingLevel{
AppSharingLevelOwner,
AppSharingLevelAuthenticated,
AppSharingLevelOrganization,
AppSharingLevelPublic,
}
}

View File

@ -258,7 +258,7 @@ func (p *DBTokenProvider) Issue(ctx context.Context, rw http.ResponseWriter, r *
return &token, tokenStr, true
}
// authorizeRequest returns true/false if the request is authorized. The returned []string
// authorizeRequest returns true if the request is authorized. The returned []string
// are warnings that aid in debugging. These messages do not prevent authorization,
// but may indicate that the request is not configured correctly.
// If an error is returned, the request should be aborted with a 500 error.
@ -310,7 +310,7 @@ func (p *DBTokenProvider) authorizeRequest(ctx context.Context, roles *rbac.Subj
// This is not ideal to check for the 'owner' role, but we are only checking
// to determine whether to show a warning for debugging reasons. This does
// not do any authz checks, so it is ok.
if roles != nil && slices.Contains(roles.Roles.Names(), rbac.RoleOwner()) {
if slices.Contains(roles.Roles.Names(), rbac.RoleOwner()) {
warnings = append(warnings, "path-based apps with \"owner\" share level are only accessible by the workspace owner (see --dangerous-allow-path-app-site-owner-access)")
}
return false, warnings, nil
@ -354,6 +354,27 @@ 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 a member of the same organization as the workspace
// First check if they have permission to connect to their own workspace (enforces scopes)
err := p.Authorizer.Authorize(ctx, *roles, rbacAction, rbacResourceOwned)
if err != nil {
return false, warnings, nil
}
// Check if the user is a member of the workspace's organization
workspaceOrgID := dbReq.Workspace.OrganizationID
expandedRoles, err := roles.Roles.Expand()
if err != nil {
return false, warnings, xerrors.Errorf("expand roles: %w", err)
}
for _, role := range expandedRoles {
if _, ok := role.Org[workspaceOrgID.String()]; ok {
return true, []string{}, nil
}
}
// User is not a member of the workspace's organization
return false, warnings, 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