Skip to content

Commit a6e111c

Browse files
Add limited collaborator role (#9299)
This PR adds a new role, of a "limited collaborator". The role essentially has restrictions on actions on networking resources while allowing normal collaborator permissions on compute resources. The `limited collaborator` role can `read` and `list_children`, but can _not_ `create_children`, or `modify` (or delete) the following resources: - VPC - Subnet - Firewall Rule - Custom Router - Route - Internet Gateway (including attach/detach IP pools and IP addresses) Limited Collaborators will still be allowed full create / modify / delete permissions on these resources: - Floating IP - Instance Network Interfaces - compute resources --------- Co-authored-by: David Pacheco <[email protected]>
1 parent c787f64 commit a6e111c

File tree

22 files changed

+2827
-1386
lines changed

22 files changed

+2827
-1386
lines changed

docs/adding-an-endpoint.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ this document should act as a jumping-off point.
3232
* Add helper functions to `LookupPath` to make it possible to fetch the resource by either UUID or name (https://github.com/oxidecomputer/omicron/blob/1dfe47c1b3122bc4f32a9c517cb31b1600581ea2/nexus/src/db/lookup.rs#L225-L237[Example])
3333
** These are often named `pub fn <my_resource>_name`, or `pub fn <my_resource>_id`
3434
* Use the https://github.com/oxidecomputer/omicron/blob/main/nexus/authz-macros/src/lib.rs[`authz_resource!` macro] to define a new `authz::...` structure, which is returned from the **Lookup** functions (https://github.com/oxidecomputer/omicron/blob/1dfe47c1b3122bc4f32a9c517cb31b1600581ea2/nexus/src/authz/api_resources.rs#L758-L764[Example])
35-
** If you define `polar_snippet = InProject` (for developer resources) or `polar_snippet = FleetChild` (for operator resources), most of the polar policy is automatically defined for you
35+
** If you define `polar_snippet = InProjectLimited` or `polar_snippet = InProjectFull` (for developer resources) or `polar_snippet = FleetChild` (for operator resources), most of the polar policy is automatically defined for you
3636
** If you define `polar_snippet = Custom`, you should edit the omicron.polar file to describe the authorization policy for your object (https://github.com/oxidecomputer/omicron/blob/1dfe47c1b3122bc4f32a9c517cb31b1600581ea2/nexus/src/authz/omicron.polar#L376-L393[Example])
3737
* Either way, you should add reference the new resource when https://github.com/oxidecomputer/omicron/blob/1dfe47c1b3122bc4f32a9c517cb31b1600581ea2/nexus/src/authz/oso_generic.rs#L119-L148[constructing the Oso structure]
3838

nexus/auth/src/authz/api_resources.rs

Lines changed: 91 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,55 @@ impl AuthorizedResource for SiloUserTokenList {
931931
}
932932
}
933933

934+
/// Synthetic resource describing the list of VPCs in a Project
935+
#[derive(Clone, Debug, Eq, PartialEq)]
936+
pub struct VpcList(Project);
937+
938+
impl VpcList {
939+
pub fn new(project: Project) -> VpcList {
940+
VpcList(project)
941+
}
942+
943+
pub fn project(&self) -> &Project {
944+
&self.0
945+
}
946+
}
947+
948+
impl oso::PolarClass for VpcList {
949+
fn get_polar_class_builder() -> oso::ClassBuilder<Self> {
950+
oso::Class::builder()
951+
.with_equality_check()
952+
.add_attribute_getter("project", |list: &VpcList| list.0.clone())
953+
}
954+
}
955+
956+
impl AuthorizedResource for VpcList {
957+
fn load_roles<'fut>(
958+
&'fut self,
959+
opctx: &'fut OpContext,
960+
authn: &'fut authn::Context,
961+
roleset: &'fut mut RoleSet,
962+
) -> futures::future::BoxFuture<'fut, Result<(), Error>> {
963+
// There are no roles on this resource, but we still need to load the
964+
// Project-related roles.
965+
self.project().load_roles(opctx, authn, roleset)
966+
}
967+
968+
fn on_unauthorized(
969+
&self,
970+
_: &Authz,
971+
error: Error,
972+
_: AnyActor,
973+
_: Action,
974+
) -> Error {
975+
error
976+
}
977+
978+
fn polar_class(&self) -> oso::Class {
979+
Self::get_polar_class()
980+
}
981+
}
982+
934983
#[derive(Clone, Copy, Debug)]
935984
pub struct UpdateTrustRootList;
936985

@@ -1130,124 +1179,150 @@ impl ApiResourceWithRolesType for Project {
11301179
type AllowedRoles = ProjectRole;
11311180
}
11321181

1182+
// ============================================================================
1183+
// Project Resources - Non-Networking (InProjectLimited)
1184+
//
1185+
// These resources can be created and modified by users with the
1186+
// "limited-collaborator" role and above. These are "regular" project resources
1187+
// that users can work with without needing permission to reconfigure network
1188+
// infrastructure.
1189+
//
1190+
// Examples: Instances, Disks, Snapshots, Images, Floating IPs
1191+
// ============================================================================
1192+
11331193
authz_resource! {
11341194
name = "Disk",
11351195
parent = "Project",
11361196
primary_key = Uuid,
11371197
roles_allowed = false,
1138-
polar_snippet = InProject,
1198+
polar_snippet = InProjectLimited,
11391199
}
11401200

11411201
authz_resource! {
11421202
name = "ProjectImage",
11431203
parent = "Project",
11441204
primary_key = Uuid,
11451205
roles_allowed = false,
1146-
polar_snippet = InProject,
1206+
polar_snippet = InProjectLimited,
11471207
}
11481208

11491209
authz_resource! {
11501210
name = "Snapshot",
11511211
parent = "Project",
11521212
primary_key = Uuid,
11531213
roles_allowed = false,
1154-
polar_snippet = InProject,
1214+
polar_snippet = InProjectLimited,
11551215
}
11561216

11571217
authz_resource! {
11581218
name = "Instance",
11591219
parent = "Project",
11601220
primary_key = Uuid,
11611221
roles_allowed = false,
1162-
polar_snippet = InProject,
1222+
polar_snippet = InProjectLimited,
11631223
}
11641224

11651225
authz_resource! {
11661226
name = "AffinityGroup",
11671227
parent = "Project",
11681228
primary_key = Uuid,
11691229
roles_allowed = false,
1170-
polar_snippet = InProject,
1230+
polar_snippet = InProjectLimited,
11711231
}
11721232

11731233
authz_resource! {
11741234
name = "AntiAffinityGroup",
11751235
parent = "Project",
11761236
primary_key = Uuid,
11771237
roles_allowed = false,
1178-
polar_snippet = InProject,
1238+
polar_snippet = InProjectLimited,
11791239
}
11801240

11811241
authz_resource! {
11821242
name = "InstanceNetworkInterface",
11831243
parent = "Instance",
11841244
primary_key = Uuid,
11851245
roles_allowed = false,
1186-
polar_snippet = InProject,
1187-
}
1246+
polar_snippet = InProjectLimited,
1247+
}
1248+
1249+
// ============================================================================
1250+
// Project Resources - Networking Infrastructure (InProjectFull)
1251+
//
1252+
// These resources require the full "collaborator" role to create or modify.
1253+
// Users with only the "limited-collaborator" role can *read* these resources
1254+
// (via viewer inheritance) but cannot create or modify them.
1255+
//
1256+
// This distinction allows organizations to restrict who can reconfigure the
1257+
// network topology while still allowing those users to work with compute
1258+
// resources (instances, disks, etc.) within the existing network.
1259+
//
1260+
// Resources in this category: VPCs, Subnets, Routers, Router Routes,
1261+
// Internet Gateways, and their child resources (IP pools, IP addresses)
1262+
// ============================================================================
11881263

11891264
authz_resource! {
11901265
name = "Vpc",
11911266
parent = "Project",
11921267
primary_key = Uuid,
11931268
roles_allowed = false,
1194-
polar_snippet = InProject,
1269+
polar_snippet = InProjectFull,
11951270
}
11961271

11971272
authz_resource! {
11981273
name = "VpcRouter",
11991274
parent = "Vpc",
12001275
primary_key = Uuid,
12011276
roles_allowed = false,
1202-
polar_snippet = InProject,
1277+
polar_snippet = InProjectFull,
12031278
}
12041279

12051280
authz_resource! {
12061281
name = "RouterRoute",
12071282
parent = "VpcRouter",
12081283
primary_key = Uuid,
12091284
roles_allowed = false,
1210-
polar_snippet = InProject,
1285+
polar_snippet = InProjectFull,
12111286
}
12121287

12131288
authz_resource! {
12141289
name = "VpcSubnet",
12151290
parent = "Vpc",
12161291
primary_key = Uuid,
12171292
roles_allowed = false,
1218-
polar_snippet = InProject,
1293+
polar_snippet = InProjectFull,
12191294
}
12201295

12211296
authz_resource! {
12221297
name = "InternetGateway",
12231298
parent = "Vpc",
12241299
primary_key = Uuid,
12251300
roles_allowed = false,
1226-
polar_snippet = InProject,
1301+
polar_snippet = InProjectFull,
12271302
}
12281303

12291304
authz_resource! {
12301305
name = "InternetGatewayIpPool",
12311306
parent = "InternetGateway",
12321307
primary_key = Uuid,
12331308
roles_allowed = false,
1234-
polar_snippet = InProject,
1309+
polar_snippet = InProjectFull,
12351310
}
12361311

12371312
authz_resource! {
12381313
name = "InternetGatewayIpAddress",
12391314
parent = "InternetGateway",
12401315
primary_key = Uuid,
12411316
roles_allowed = false,
1242-
polar_snippet = InProject,
1317+
polar_snippet = InProjectFull,
12431318
}
12441319

12451320
authz_resource! {
12461321
name = "FloatingIp",
12471322
parent = "Project",
12481323
primary_key = Uuid,
12491324
roles_allowed = false,
1250-
polar_snippet = InProject,
1325+
polar_snippet = InProjectLimited,
12511326
}
12521327

12531328
// Customer network integration resources nested below "Fleet"

nexus/auth/src/authz/omicron.polar

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,15 @@ has_role(actor: AuthenticatedActor, role: String, resource: Resource)
6363
# - fleet.collaborator (can manage Silos)
6464
# - fleet.viewer (can read most non-siloed resources in the system)
6565
# - silo.admin (superuser for the silo)
66-
# - silo.collaborator (can create and own Organizations)
67-
# - silo.viewer (can read most resources within the Silo)
66+
# - silo.collaborator (can create and own Organizations; grants project.admin on all projects)
67+
# - silo.limited-collaborator (grants project.limited-collaborator on all projects)
68+
# - silo.viewer (can read most resources within the Silo; grants project.viewer)
6869
# - organization.admin (complete control over an organization)
6970
# - organization.collaborator (can manage Projects)
7071
# - organization.viewer (can read most resources within the Organization)
7172
# - project.admin (complete control over a Project)
72-
# - project.collaborator (can manage all resources within the Project)
73+
# - project.collaborator (can manage all resources within the Project, including networking)
74+
# - project.limited-collaborator (can manage compute resources, but not networking resources)
7375
# - project.viewer (can read most resources within the Project)
7476
#
7577
# Outside the Silo/Organization/Project hierarchy, we (currently) treat most
@@ -125,10 +127,11 @@ resource Silo {
125127
"read",
126128
"create_child",
127129
];
128-
roles = [ "admin", "collaborator", "viewer" ];
130+
roles = [ "admin", "collaborator", "limited-collaborator", "viewer" ];
129131

130132
# Roles implied by other roles on this resource
131-
"viewer" if "collaborator";
133+
"viewer" if "limited-collaborator";
134+
"limited-collaborator" if "collaborator";
132135
"collaborator" if "admin";
133136

134137
# Permissions granted directly by roles on this resource
@@ -184,21 +187,29 @@ resource Project {
184187
"read",
185188
"create_child",
186189
];
187-
roles = [ "admin", "collaborator", "viewer" ];
190+
roles = [ "admin", "collaborator", "limited-collaborator", "viewer" ];
188191

189192
# Roles implied by other roles on this resource
190-
"viewer" if "collaborator";
193+
# Role hierarchy: admin > collaborator > limited-collaborator > viewer
194+
#
195+
# The "limited-collaborator" role can create/modify non-networking
196+
# resources (instances, disks, etc.) but cannot create/modify networking
197+
# infrastructure (VPCs, subnets, routers, internet gateways).
198+
# See nexus/authz-macros for InProjectLimited vs InProjectFull.
199+
"viewer" if "limited-collaborator";
200+
"limited-collaborator" if "collaborator";
191201
"collaborator" if "admin";
192202

193203
# Permissions granted directly by roles on this resource
194204
"list_children" if "viewer";
195205
"read" if "viewer";
196-
"create_child" if "collaborator";
206+
"create_child" if "limited-collaborator";
197207
"modify" if "admin";
198208

199209
# Roles implied by roles on this resource's parent (Silo)
200210
relations = { parent_silo: Silo };
201211
"admin" if "collaborator" on "parent_silo";
212+
"limited-collaborator" if "limited-collaborator" on "parent_silo";
202213
"viewer" if "viewer" on "parent_silo";
203214
}
204215
has_relation(silo: Silo, "parent_silo", project: Project)
@@ -797,3 +808,20 @@ has_relation(silo: Silo, "parent_silo", scim_client_bearer_token_list: ScimClien
797808
if scim_client_bearer_token_list.silo = silo;
798809
has_relation(fleet: Fleet, "parent_fleet", collection: ScimClientBearerTokenList)
799810
if collection.silo.fleet = fleet;
811+
812+
# VpcList is a synthetic resource for controlling VPC creation.
813+
# Unlike other project resources, VPC creation requires the full "collaborator"
814+
# role rather than "limited-collaborator", enforcing the networking restriction.
815+
# This allows organizations to restrict who can reconfigure the network topology
816+
# while still allowing users with limited-collaborator to work with compute
817+
# resources (instances, disks, etc.) within the existing network.
818+
resource VpcList {
819+
permissions = [ "list_children", "create_child" ];
820+
821+
relations = { containing_project: Project };
822+
823+
"list_children" if "read" on "containing_project";
824+
"create_child" if "collaborator" on "containing_project";
825+
}
826+
has_relation(project: Project, "containing_project", collection: VpcList)
827+
if collection.project = project;

nexus/auth/src/authz/oso_generic.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ pub fn make_omicron_oso(log: &slog::Logger) -> Result<OsoInit, anyhow::Error> {
110110
Fleet::get_polar_class(),
111111
Inventory::get_polar_class(),
112112
IpPoolList::get_polar_class(),
113+
VpcList::get_polar_class(),
113114
ConsoleSessionList::get_polar_class(),
114115
DeviceAuthRequestList::get_polar_class(),
115116
QuiesceState::get_polar_class(),

nexus/authz-macros/outputs/instance.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ impl Instance {
4141
pub(super) fn init() -> Init {
4242
use oso::PolarClass;
4343
Init {
44-
polar_snippet: "\n resource Instance {\n permissions = [\n \"list_children\",\n \"modify\",\n \"read\",\n \"create_child\",\n ];\n\n relations = { containing_project: Project };\n \"list_children\" if \"viewer\" on \"containing_project\";\n \"read\" if \"viewer\" on \"containing_project\";\n \"modify\" if \"collaborator\" on \"containing_project\";\n \"create_child\" if \"collaborator\" on \"containing_project\";\n }\n\n has_relation(parent: Project, \"containing_project\", child: Instance)\n if child.project = parent;\n ",
44+
polar_snippet: "\n resource Instance {\n permissions = [\n \"list_children\",\n \"modify\",\n \"read\",\n \"create_child\",\n ];\n\n relations = { containing_project: Project };\n \"list_children\" if \"viewer\" on \"containing_project\";\n \"read\" if \"viewer\" on \"containing_project\";\n \"modify\" if \"limited-collaborator\" on \"containing_project\";\n \"create_child\" if \"limited-collaborator\" on \"containing_project\";\n }\n\n has_relation(parent: Project, \"containing_project\", child: Instance)\n if child.project = parent;\n ",
4545
polar_class: Instance::get_polar_class(),
4646
}
4747
}

0 commit comments

Comments
 (0)