Skip to content

Commit 06245a9

Browse files
Chief-Rishabravisuhag
authored andcommitted
feat(policy): introduce expression in additional appeal requirement trigger
1 parent 59a8047 commit 06245a9

File tree

4 files changed

+408
-6
lines changed

4 files changed

+408
-6
lines changed

api/handler/v1beta1/adapter.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ func (a *adapter) FromPolicyProto(p *guardianv1beta1.Policy) *domain.Policy {
265265
ResourceURN: r.GetOn().GetResourceUrn(),
266266
Role: r.GetOn().GetRole(),
267267
Conditions: conditions,
268+
Expression: r.GetOn().GetExpression(),
268269
}
269270
}
270271

@@ -394,6 +395,7 @@ func (a *adapter) ToPolicyProto(p *domain.Policy) (*guardianv1beta1.Policy, erro
394395
ResourceUrn: r.On.ResourceURN,
395396
Role: r.On.Role,
396397
Conditions: conditions,
398+
Expression: r.On.Expression,
397399
}
398400
}
399401

core/appeal/service_test.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,6 +1240,194 @@ func (s *ServiceTestSuite) TestCreateAppeal__WithExistingAppealAndWithAutoApprov
12401240
s.Equal(expectedResult, appeals)
12411241
}
12421242

1243+
func (s *ServiceTestSuite) TestCreateAppeal__WithAdditionalAppeals() {
1244+
s.setup()
1245+
providerType := "test-provider-type"
1246+
providerURN := "test-provider-urn"
1247+
resourceType := "test-resource-type"
1248+
targetResource := &domain.ResourceIdentifier{
1249+
ID: "test-resource-id-2",
1250+
}
1251+
targetRole := "test-role-1"
1252+
1253+
resources := []*domain.Resource{
1254+
{
1255+
ID: "test-resource-id-1",
1256+
URN: "test-resource-urn-1",
1257+
Type: resourceType,
1258+
ProviderType: providerType,
1259+
ProviderURN: providerURN,
1260+
},
1261+
{
1262+
ID: "test-resource-id-2",
1263+
URN: "test-resource-urn-2",
1264+
Type: resourceType,
1265+
ProviderType: providerType,
1266+
ProviderURN: providerURN,
1267+
},
1268+
}
1269+
policies := []*domain.Policy{
1270+
{
1271+
ID: "test-policy-id-1",
1272+
Version: 1,
1273+
Steps: []*domain.Step{
1274+
{
1275+
Name: "test-step-1",
1276+
Strategy: domain.ApprovalStepStrategyAuto,
1277+
ApproveIf: `true`,
1278+
},
1279+
},
1280+
Requirements: []*domain.Requirement{
1281+
{
1282+
On: &domain.RequirementTrigger{
1283+
Expression: `$appeal.resource.urn == "test-resource-urn-1"`,
1284+
},
1285+
Appeals: []*domain.AdditionalAppeal{
1286+
{
1287+
Resource: targetResource,
1288+
Role: targetRole,
1289+
},
1290+
},
1291+
},
1292+
},
1293+
},
1294+
}
1295+
providers := []*domain.Provider{
1296+
{
1297+
ID: "test-provider-id",
1298+
Type: providerType,
1299+
URN: providerURN,
1300+
Config: &domain.ProviderConfig{
1301+
Resources: []*domain.ResourceConfig{
1302+
{
1303+
Type: resourceType,
1304+
Policy: &domain.PolicyConfig{
1305+
ID: policies[0].ID,
1306+
Version: int(policies[0].Version),
1307+
},
1308+
Roles: []*domain.Role{
1309+
{
1310+
ID: "test-role-1",
1311+
Permissions: []interface{}{"test-permission-1"},
1312+
},
1313+
},
1314+
},
1315+
},
1316+
},
1317+
},
1318+
}
1319+
1320+
appealsPayload := []*domain.Appeal{
1321+
{
1322+
CreatedBy: "[email protected]",
1323+
AccountID: "[email protected]",
1324+
ResourceID: "test-resource-id-1",
1325+
Resource: &domain.Resource{
1326+
ID: "test-resource-id-1",
1327+
URN: "test-resource-urn-1",
1328+
Type: resourceType,
1329+
ProviderType: providerType,
1330+
ProviderURN: providerURN,
1331+
},
1332+
Role: "test-role-1",
1333+
},
1334+
}
1335+
1336+
// 1.a main appeal creation
1337+
expectedResourceFilters := domain.ListResourcesFilter{IDs: []string{appealsPayload[0].Resource.ID}}
1338+
s.mockResourceService.EXPECT().Find(mock.AnythingOfType("*context.emptyCtx"), expectedResourceFilters).Return([]*domain.Resource{resources[0]}, nil).Once()
1339+
s.mockProviderService.EXPECT().Find(mock.AnythingOfType("*context.emptyCtx")).Return(providers, nil).Once()
1340+
s.mockPolicyService.EXPECT().Find(mock.AnythingOfType("*context.emptyCtx")).Return(policies, nil).Once()
1341+
s.mockGrantService.EXPECT().List(mock.AnythingOfType("*context.emptyCtx"), mock.AnythingOfType("domain.ListGrantsFilter")).Return([]domain.Grant{}, nil).Once().Run(func(args mock.Arguments) {
1342+
filter := args.Get(1).(domain.ListGrantsFilter)
1343+
s.Equal([]string{appealsPayload[0].AccountID}, filter.AccountIDs)
1344+
s.Equal([]string{appealsPayload[0].Resource.ID}, filter.ResourceIDs)
1345+
s.Equal([]string{appealsPayload[0].Role}, filter.Roles)
1346+
})
1347+
s.mockRepository.EXPECT().Find(mock.AnythingOfType("*context.emptyCtx"), mock.AnythingOfType("*domain.ListAppealsFilter")).Return([]*domain.Appeal{}, nil).Once()
1348+
s.mockProviderService.EXPECT().ValidateAppeal(mock.AnythingOfType("*context.emptyCtx"), appealsPayload[0], providers[0], policies[0]).Return(nil).Once()
1349+
s.mockProviderService.EXPECT().GetPermissions(mock.AnythingOfType("*context.emptyCtx"), providers[0].Config, appealsPayload[0].Resource.Type, appealsPayload[0].Role).Return([]interface{}{"test-permission-1"}, nil).Once()
1350+
s.mockGrantService.EXPECT().List(mock.AnythingOfType("*context.emptyCtx"), mock.AnythingOfType("domain.ListGrantsFilter")).Return([]domain.Grant{}, nil).Once().Run(func(args mock.Arguments) {
1351+
filter := args.Get(1).(domain.ListGrantsFilter)
1352+
s.Equal([]string{appealsPayload[0].AccountID}, filter.AccountIDs)
1353+
s.Equal([]string{appealsPayload[0].Resource.ID}, filter.ResourceIDs)
1354+
})
1355+
expectedGrant := &domain.Grant{ID: "main-grant"}
1356+
s.mockGrantService.EXPECT().Prepare(mock.AnythingOfType("*context.emptyCtx"), mock.AnythingOfType("domain.Appeal")).Return(expectedGrant, nil).Once().Run(func(args mock.Arguments) {
1357+
appeal := args.Get(1).(domain.Appeal)
1358+
s.Equal(appealsPayload[0].AccountID, appeal.AccountID)
1359+
s.Equal(appealsPayload[0].Role, appeal.Role)
1360+
s.Equal(appealsPayload[0].ResourceID, appeal.ResourceID)
1361+
s.Equal(len(policies[0].Steps), len(appeal.Approvals))
1362+
})
1363+
s.mockPolicyService.EXPECT().GetOne(mock.AnythingOfType("*context.emptyCtx"), policies[0].ID, policies[0].Version).Return(policies[0], nil).Once()
1364+
1365+
// 2.a additional appeal creation
1366+
s.mockResourceService.EXPECT().Get(mock.AnythingOfType("*context.cancelCtx"), targetResource).Return(resources[1], nil).Once()
1367+
expectedResourceFilters = domain.ListResourcesFilter{IDs: []string{resources[1].ID}}
1368+
s.mockResourceService.EXPECT().Find(mock.AnythingOfType("*context.cancelCtx"), expectedResourceFilters).Return([]*domain.Resource{resources[1]}, nil).Once()
1369+
s.mockProviderService.EXPECT().Find(mock.AnythingOfType("*context.cancelCtx")).Return(providers, nil).Once()
1370+
s.mockPolicyService.EXPECT().Find(mock.AnythingOfType("*context.cancelCtx")).Return(policies, nil).Once()
1371+
s.mockGrantService.EXPECT().List(mock.AnythingOfType("*context.cancelCtx"), mock.AnythingOfType("domain.ListGrantsFilter")).Return([]domain.Grant{}, nil).Once().Run(func(args mock.Arguments) {
1372+
filter := args.Get(1).(domain.ListGrantsFilter)
1373+
s.Equal([]string{appealsPayload[0].AccountID}, filter.AccountIDs)
1374+
s.Equal([]string{targetResource.ID}, filter.ResourceIDs)
1375+
s.Equal([]string{targetRole}, filter.Roles)
1376+
})
1377+
s.mockRepository.EXPECT().Find(mock.AnythingOfType("*context.cancelCtx"), mock.AnythingOfType("*domain.ListAppealsFilter")).Return([]*domain.Appeal{}, nil).Once()
1378+
s.mockProviderService.EXPECT().ValidateAppeal(mock.AnythingOfType("*context.cancelCtx"), mock.AnythingOfType("*domain.Appeal"), providers[0], policies[0]).Return(nil).Once().Run(func(args mock.Arguments) {
1379+
appeal := args.Get(1).(*domain.Appeal)
1380+
s.Equal(appealsPayload[0].AccountID, appeal.AccountID)
1381+
s.Equal(targetRole, appeal.Role)
1382+
s.Equal(targetResource.ID, appeal.ResourceID)
1383+
})
1384+
s.mockProviderService.EXPECT().GetPermissions(mock.AnythingOfType("*context.cancelCtx"), providers[0].Config, resourceType, targetRole).Return([]interface{}{"test-permission-1"}, nil).Once()
1385+
s.mockGrantService.EXPECT().List(mock.AnythingOfType("*context.cancelCtx"), mock.AnythingOfType("domain.ListGrantsFilter")).Return([]domain.Grant{}, nil).Once().Run(func(args mock.Arguments) {
1386+
filter := args.Get(1).(domain.ListGrantsFilter)
1387+
s.Equal([]string{appealsPayload[0].AccountID}, filter.AccountIDs)
1388+
s.Equal([]string{targetResource.ID}, filter.ResourceIDs)
1389+
})
1390+
expectedAdditionalGrant := &domain.Grant{ID: "additional-grant"}
1391+
s.mockGrantService.EXPECT().Prepare(mock.AnythingOfType("*context.cancelCtx"), mock.AnythingOfType("domain.Appeal")).Return(expectedAdditionalGrant, nil).Once().Run(func(args mock.Arguments) {
1392+
appeal := args.Get(1).(domain.Appeal)
1393+
s.Equal(appealsPayload[0].AccountID, appeal.AccountID)
1394+
s.Equal(targetRole, appeal.Role)
1395+
s.Equal(targetResource.ID, appeal.ResourceID)
1396+
s.Equal(len(policies[0].Steps), len(appeal.Approvals))
1397+
})
1398+
s.mockPolicyService.EXPECT().GetOne(mock.AnythingOfType("*context.cancelCtx"), policies[0].ID, policies[0].Version).Return(policies[0], nil).Once()
1399+
1400+
// 2.b grant access for the additional appeal
1401+
s.mockProviderService.EXPECT().GrantAccess(mock.AnythingOfType("*context.cancelCtx"), mock.AnythingOfType("domain.Grant")).Return(nil).Once().Run(func(args mock.Arguments) {
1402+
grant := args.Get(1).(domain.Grant)
1403+
s.Equal(expectedAdditionalGrant.ID, grant.ID)
1404+
})
1405+
s.mockRepository.EXPECT().BulkUpsert(mock.AnythingOfType("*context.cancelCtx"), mock.AnythingOfType("[]*domain.Appeal")).Return(nil).Once().Run(func(args mock.Arguments) {
1406+
appeals := args.Get(1).([]*domain.Appeal)
1407+
appeal := appeals[0]
1408+
s.Equal(targetResource.ID, appeal.Resource.ID)
1409+
})
1410+
s.mockAuditLogger.EXPECT().Log(mock.AnythingOfType("*context.cancelCtx"), appeal.AuditKeyBulkInsert, mock.Anything).Return(nil).Once()
1411+
s.mockNotifier.EXPECT().Notify(mock.Anything).Return(nil).Once()
1412+
1413+
// 1.b grant access for the main appeal
1414+
s.mockProviderService.EXPECT().GrantAccess(mock.AnythingOfType("*context.emptyCtx"), mock.AnythingOfType("domain.Grant")).Return(nil).Once().Run(func(args mock.Arguments) {
1415+
grant := args.Get(1).(domain.Grant)
1416+
s.Equal(expectedGrant.ID, grant.ID)
1417+
})
1418+
s.mockRepository.EXPECT().BulkUpsert(mock.AnythingOfType("*context.emptyCtx"), mock.AnythingOfType("[]*domain.Appeal")).Return(nil).Once().Run(func(args mock.Arguments) {
1419+
appeals := args.Get(1).([]*domain.Appeal)
1420+
appeal := appeals[0]
1421+
s.Equal(appealsPayload[0].Resource.ID, appeal.Resource.ID)
1422+
})
1423+
s.mockAuditLogger.EXPECT().Log(mock.AnythingOfType("*context.emptyCtx"), appeal.AuditKeyBulkInsert, mock.Anything).Return(nil).Once()
1424+
s.mockNotifier.EXPECT().Notify(mock.Anything).Return(nil).Once()
1425+
1426+
err := s.service.Create(context.Background(), appealsPayload)
1427+
1428+
s.NoError(err)
1429+
}
1430+
12431431
func (s *ServiceTestSuite) TestUpdateApproval() {
12441432
timeNow := time.Now()
12451433
appeal.TimeNow = func() time.Time {

domain/policy.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,14 @@ func (s Step) ToApproval(a *Appeal, p *Policy, index int) (*Approval, error) {
181181
}
182182

183183
type RequirementTrigger struct {
184-
ProviderType string `json:"provider_type" yaml:"provider_type" validate:"required_without_all=ProviderURN ResourceType ResourceURN Role Conditions"`
185-
ProviderURN string `json:"provider_urn" yaml:"provider_urn" validate:"required_without_all=ProviderType ResourceType ResourceURN Role Conditions"`
186-
ResourceType string `json:"resource_type" yaml:"resource_type" validate:"required_without_all=ProviderType ProviderURN ResourceURN Role Conditions"`
187-
ResourceURN string `json:"resource_urn" yaml:"resource_urn" validate:"required_without_all=ProviderType ProviderURN ResourceType Role Conditions"`
188-
Role string `json:"role" yaml:"role" validate:"required_without_all=ProviderType ProviderURN ResourceType ResourceType Conditions"`
189-
Conditions []*Condition `json:"conditions" yaml:"conditions" validate:"required_without_all=ProviderType ProviderURN ResourceType ResourceType Role"`
184+
ProviderType string `json:"provider_type" yaml:"provider_type" validate:"required_without_all=ProviderURN ResourceType ResourceURN Role Conditions Expression"`
185+
ProviderURN string `json:"provider_urn" yaml:"provider_urn" validate:"required_without_all=ProviderType ResourceType ResourceURN Role Conditions Expression"`
186+
ResourceType string `json:"resource_type" yaml:"resource_type" validate:"required_without_all=ProviderType ProviderURN ResourceURN Role Conditions Expression"`
187+
ResourceURN string `json:"resource_urn" yaml:"resource_urn" validate:"required_without_all=ProviderType ProviderURN ResourceType Role Conditions Expression"`
188+
Role string `json:"role" yaml:"role" validate:"required_without_all=ProviderType ProviderURN ResourceType ResourceType Conditions Expression"`
189+
// Deprecated: use Expression instead
190+
Conditions []*Condition `json:"conditions" yaml:"conditions" validate:"required_without_all=ProviderType ProviderURN ResourceType ResourceType Role Expression"`
191+
Expression string `json:"expression" yaml:"expression" validate:"required_without_all=ProviderType ProviderURN ResourceType ResourceType Role Conditions"`
190192
}
191193

192194
func (r *RequirementTrigger) IsMatch(a *Appeal) (bool, error) {
@@ -234,6 +236,22 @@ func (r *RequirementTrigger) IsMatch(a *Appeal) (bool, error) {
234236
}
235237
}
236238
}
239+
if r.Expression != "" {
240+
appealMap, err := structToMap(a)
241+
if err != nil {
242+
return false, fmt.Errorf("parsing appeal to map: %w", err)
243+
}
244+
params := map[string]interface{}{"appeal": appealMap}
245+
v, err := evaluator.Expression(r.Expression).EvaluateWithVars(params)
246+
if err != nil {
247+
return false, fmt.Errorf("evaluating expression %q: %w", r.Expression, err)
248+
}
249+
if match, ok := v.(bool); !ok {
250+
return false, fmt.Errorf("expression %q did not evaluate to a boolean, evaluated value: %q", r.Expression, v)
251+
} else {
252+
return match, nil
253+
}
254+
}
237255

238256
return true, nil
239257
}

0 commit comments

Comments
 (0)