mirror of
https://github.com/coder/coder.git
synced 2025-07-08 11:39:50 +00:00
feat: add organization scope for shared ports (#18314)
This commit is contained in:
@ -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
7
coderd/apidoc/docs.go
generated
@ -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"
|
||||
]
|
||||
},
|
||||
|
12
coderd/apidoc/swagger.json
generated
12
coderd/apidoc/swagger.json
generated
@ -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"
|
||||
]
|
||||
},
|
||||
|
1
coderd/database/dump.sql
generated
1
coderd/database/dump.sql
generated
@ -18,6 +18,7 @@ CREATE TYPE api_key_scope AS ENUM (
|
||||
CREATE TYPE app_sharing_level AS ENUM (
|
||||
'owner',
|
||||
'authenticated',
|
||||
'organization',
|
||||
'public'
|
||||
);
|
||||
|
||||
|
@ -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.';
|
@ -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.';
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user