@@ -1000,6 +1000,8 @@ func TestAuthorizeScope(t *testing.T) {
1000
1000
},
1001
1001
)
1002
1002
1003
+ createScopeWorkspaceID := uuid .New ()
1004
+
1003
1005
// This scope can only create workspaces
1004
1006
user = Subject {
1005
1007
ID :"me" ,
@@ -1012,15 +1014,15 @@ func TestAuthorizeScope(t *testing.T) {
1012
1014
Identifier :RoleIdentifier {Name :"create_workspace" },
1013
1015
DisplayName :"Create Workspace" ,
1014
1016
Site :Permissions (map [string ][]policy.Action {
1015
- // Onlyread access for workspaces.
1017
+ // Onlycreate access for workspaces.
1016
1018
ResourceWorkspace .Type : {policy .ActionCreate },
1017
1019
}),
1018
1020
Org :map [string ][]Permission {},
1019
1021
User : []Permission {},
1020
1022
},
1021
- //Empty string allow_list is allowed for actions like 'create'
1023
+ //Specific IDs still allow creation; reads require matching IDs.
1022
1024
AllowIDList : []AllowListElement {{
1023
- Type :ResourceWorkspace .Type ,ID :"" ,
1025
+ Type :ResourceWorkspace .Type ,ID :createScopeWorkspaceID . String () ,
1024
1026
}},
1025
1027
},
1026
1028
}
@@ -1190,8 +1192,7 @@ func TestScopeAllowList(t *testing.T) {
1190
1192
Site :allPermsExcept (ResourceUser ),
1191
1193
},
1192
1194
AllowIDList : []AllowListElement {
1193
- {Type :ResourceWorkspace .Type ,ID :wid .String ()},
1194
- {Type :ResourceWorkspace .Type ,ID :"" },// Allow to create
1195
+ {Type :ResourceWorkspace .Type ,ID :wid .String ()},// create bypass handled by policy
1195
1196
{Type :ResourceTemplate .Type ,ID :policy .WildcardSymbol },
1196
1197
{Type :ResourceGroup .Type ,ID :gid .String ()},
1197
1198
@@ -1219,6 +1220,7 @@ func TestScopeAllowList(t *testing.T) {
1219
1220
1220
1221
// Group
1221
1222
{resource :ResourceGroup .InOrg (defOrg ).WithID (gid ),actions : []policy.Action {policy .ActionRead }},
1223
+ {resource :ResourceGroup .InOrg (defOrg ),actions : []policy.Action {policy .ActionCreate }},
1222
1224
},
1223
1225
),
1224
1226
@@ -1233,13 +1235,40 @@ func TestScopeAllowList(t *testing.T) {
1233
1235
1234
1236
// `wid` matches on the uuid, but not the type
1235
1237
{resource :ResourceGroup .WithID (wid ),actions : []policy.Action {policy .ActionRead }},
1236
-
1237
- // no empty id for the create action
1238
- {resource :ResourceGroup .InOrg (defOrg ),actions : []policy.Action {policy .ActionCreate }},
1239
1238
},
1240
1239
),
1241
1240
)
1242
1241
1242
+ t .Run ("create requires matching type entry" ,func (t * testing.T ) {
1243
+ t .Parallel ()
1244
+
1245
+ subject := Subject {
1246
+ ID :"me" ,
1247
+ Roles :Roles {must (RoleByName (RoleOwner ()))},
1248
+ Scope :Scope {
1249
+ Role :Role {
1250
+ Identifier :RoleIdentifier {Name :"AllowListNoWorkspace" },
1251
+ Site :Permissions (map [string ][]policy.Action {
1252
+ ResourceWorkspace .Type : {policy .ActionCreate },
1253
+ }),
1254
+ },
1255
+ AllowIDList : []AllowListElement {{
1256
+ Type :ResourceTemplate .Type ,ID :uuid .NewString (),
1257
+ }},
1258
+ },
1259
+ }
1260
+
1261
+ testAuthorize (t ,"CreateRequiresMatchingType" ,subject ,
1262
+ []authTestCase {
1263
+ {
1264
+ resource :ResourceWorkspace .InOrg (defOrg ).WithOwner (subject .ID ),
1265
+ actions : []policy.Action {policy .ActionCreate },
1266
+ allow :false ,
1267
+ },
1268
+ },
1269
+ )
1270
+ })
1271
+
1243
1272
// Wildcard type
1244
1273
user = Subject {
1245
1274
ID :"me" ,
@@ -1271,6 +1300,7 @@ func TestScopeAllowList(t *testing.T) {
1271
1300
[]authTestCase {
1272
1301
// anything with the id is ok
1273
1302
{resource :ResourceWorkspace .InOrg (defOrg ).WithOwner (user .ID ).WithID (wid ),actions : []policy.Action {policy .ActionRead }},
1303
+ {resource :ResourceWorkspace .InOrg (defOrg ).WithOwner (user .ID ),actions : []policy.Action {policy .ActionCreate }},
1274
1304
{resource :ResourceGroup .InOrg (defOrg ).WithID (wid ),actions : []policy.Action {policy .ActionRead }},
1275
1305
{resource :ResourceTemplate .InOrg (defOrg ).WithID (wid ),actions : []policy.Action {policy .ActionRead }},
1276
1306
},
@@ -1283,13 +1313,144 @@ func TestScopeAllowList(t *testing.T) {
1283
1313
},
1284
1314
[]authTestCase {
1285
1315
// Anything without the id is not allowed
1286
- {resource :ResourceWorkspace .InOrg (defOrg ).WithOwner (user .ID ),actions : []policy.Action {policy .ActionCreate }},
1287
1316
{resource :ResourceWorkspace .InOrg (defOrg ).WithOwner (user .ID ).WithID (uuid .New ()),actions : []policy.Action {policy .ActionRead }},
1288
1317
},
1289
1318
),
1290
1319
)
1291
1320
}
1292
1321
1322
+ func TestScopeAllowListFilter (t * testing.T ) {
1323
+ t .Parallel ()
1324
+
1325
+ workspaceID := uuid .New ()
1326
+ otherWorkspace := uuid .New ()
1327
+ defOrg := uuid .New ()
1328
+
1329
+ subject := Subject {
1330
+ ID :"me" ,
1331
+ Roles :Roles {must (RoleByName (RoleOwner ()))},
1332
+ Scope :Scope {
1333
+ Role :Role {
1334
+ Identifier :RoleIdentifier {Name :"AllowListFilter" },
1335
+ Site :Permissions (map [string ][]policy.Action {
1336
+ ResourceWorkspace .Type : {policy .ActionRead },
1337
+ }),
1338
+ },
1339
+ AllowIDList : []AllowListElement {
1340
+ {Type :ResourceWorkspace .Type ,ID :workspaceID .String ()},
1341
+ },
1342
+ },
1343
+ }
1344
+
1345
+ allowed := ResourceWorkspace .WithID (workspaceID ).InOrg (defOrg ).WithOwner (subject .ID )
1346
+ denied := ResourceWorkspace .WithID (otherWorkspace ).InOrg (defOrg ).WithOwner (subject .ID )
1347
+
1348
+ ctx ,cancel := context .WithTimeout (context .Background (),testutil .WaitShort )
1349
+ defer cancel ()
1350
+
1351
+ auth := NewAuthorizer (prometheus .NewRegistry ())
1352
+ filtered ,err := Filter (ctx ,auth ,subject ,policy .ActionRead , []Object {allowed ,denied })
1353
+ require .NoError (t ,err )
1354
+ require .Len (t ,filtered ,1 )
1355
+ require .Equal (t ,workspaceID .String (),filtered [0 ].ID )
1356
+ }
1357
+
1358
+ func TestAuthorizeRequiresScope (t * testing.T ) {
1359
+ t .Parallel ()
1360
+
1361
+ subject := Subject {
1362
+ ID :"me" ,
1363
+ Roles :Roles {must (RoleByName (RoleOwner ()))},
1364
+ }
1365
+
1366
+ auth := NewAuthorizer (prometheus .NewRegistry ())
1367
+ ctx ,cancel := context .WithTimeout (context .Background (),testutil .WaitShort )
1368
+ defer cancel ()
1369
+
1370
+ err := auth .Authorize (ctx ,subject ,policy .ActionRead ,ResourceWorkspace .WithID (uuid .New ()))
1371
+ require .Error (t ,err )
1372
+ }
1373
+
1374
+ func TestScopeMetricsCounters (t * testing.T ) {
1375
+ t .Parallel ()
1376
+
1377
+ createSubject := func (allowList []AllowListElement )Subject {
1378
+ role := Role {
1379
+ Identifier :RoleIdentifier {Name :"metrics-role" },
1380
+ Site : []Permission {{
1381
+ ResourceType :ResourceWorkspace .Type ,
1382
+ Action :policy .ActionRead ,
1383
+ }},
1384
+ }
1385
+ scope := Scope {
1386
+ Role :Role {
1387
+ Identifier :RoleIdentifier {Name :"scope-metrics" },
1388
+ Site : []Permission {{
1389
+ ResourceType :ResourceWorkspace .Type ,
1390
+ Action :policy .ActionRead ,
1391
+ }},
1392
+ },
1393
+ AllowIDList :allowList ,
1394
+ }
1395
+ return Subject {
1396
+ ID :uuid .NewString (),
1397
+ Roles :Roles {role },
1398
+ Groups : []string {},
1399
+ Scope :scope ,
1400
+ }
1401
+ }
1402
+
1403
+ t .Run ("scope allow" ,func (t * testing.T ) {
1404
+ t .Parallel ()
1405
+
1406
+ reg := prometheus .NewRegistry ()
1407
+ auth := NewAuthorizer (reg )
1408
+
1409
+ subject := createSubject ([]AllowListElement {AllowListAll ()})
1410
+ obj := ResourceWorkspace .WithID (uuid .New ())
1411
+
1412
+ require .NoError (t ,auth .Authorize (context .Background (),subject ,policy .ActionRead ,obj ))
1413
+
1414
+ metrics ,err := reg .Gather ()
1415
+ require .NoError (t ,err )
1416
+
1417
+ require .True (t ,testutil .PromCounterHasValue (t ,metrics ,1 ,
1418
+ "coderd_authz_scope_enforcement_total" ,
1419
+ "scope_allow" ,"scope_metrics" ,ResourceWorkspace .Type ,"allow" ,
1420
+ ))
1421
+ require .False (t ,testutil .PromCounterGathered (t ,metrics ,
1422
+ "coderd_authz_scope_allowlist_miss_total" ,
1423
+ "scope_metrics" ,ResourceWorkspace .Type ,
1424
+ ))
1425
+ })
1426
+
1427
+ t .Run ("allow-list miss" ,func (t * testing.T ) {
1428
+ t .Parallel ()
1429
+
1430
+ reg := prometheus .NewRegistry ()
1431
+ auth := NewAuthorizer (reg )
1432
+
1433
+ allowedID := uuid .NewString ()
1434
+ subject := createSubject ([]AllowListElement {{Type :ResourceWorkspace .Type ,ID :allowedID }})
1435
+ obj := ResourceWorkspace .WithID (uuid .New ())
1436
+
1437
+ err := auth .Authorize (context .Background (),subject ,policy .ActionRead ,obj )
1438
+ require .Error (t ,err )
1439
+
1440
+ metrics ,gatherErr := reg .Gather ()
1441
+ require .NoError (t ,gatherErr )
1442
+
1443
+ require .True (t ,testutil .PromCounterHasValue (t ,metrics ,1 ,
1444
+ "coderd_authz_scope_enforcement_total" ,
1445
+ "allow_list_deny" ,"scope_metrics" ,ResourceWorkspace .Type ,"deny" ,
1446
+ ))
1447
+ require .True (t ,testutil .PromCounterHasValue (t ,metrics ,1 ,
1448
+ "coderd_authz_scope_allowlist_miss_total" ,
1449
+ "scope_metrics" ,ResourceWorkspace .Type ,
1450
+ ))
1451
+ })
1452
+ }
1453
+
1293
1454
// cases applies a given function to all test cases. This makes generalities easier to create.
1294
1455
func cases (opt func (c authTestCase )authTestCase ,cases []authTestCase ) []authTestCase {
1295
1456
if opt == nil {