Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .task/checksum/generate-ent-smart
Original file line number Diff line number Diff line change
@@ -1 +1 @@
72fa30a003302debf7d1483ca0dc91d9
61e2ab6dbc47a1e3535bd5d6128a8b14
2 changes: 1 addition & 1 deletion .task/checksum/generate-graphql-smart
Original file line number Diff line number Diff line change
@@ -1 +1 @@
80b2965e7574965c72276b404af7909
95641fc8f4f1b5a250da43b9dc00b5a5
2 changes: 1 addition & 1 deletion .task/checksum/generate-openapi-smart
Original file line number Diff line number Diff line change
@@ -1 +1 @@
a5ea6f046fb222aad2d3c0483d6da270
8b75c92d10e8d8e3150be9eb1d290e
25 changes: 25 additions & 0 deletions fga/model/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,31 @@ func OrganizationRoles() ([]modelparse.OrganizationRole, error) {
return roles, nil
}

// FilterOrganizationRoles ensures the assigned role is an organizational role and returns
// a filtered list of OrganizationRoles
func FilterOrganizationRoles(roles []modelparse.OrganizationRole, assigned []string) []modelparse.OrganizationRole {
filtered := make([]modelparse.OrganizationRole, 0, len(assigned))
for _, role := range roles {
if slices.Contains(assigned, role.ID) {
filtered = append(filtered, role)
}
}

return filtered
}

// GetOrganizationRoleStrings takes assigned roles and filters non organization roles and returns a string list of role names
func GetOrganizationRoleStrings(roles []modelparse.OrganizationRole, assigned []string) []string {
filtered := make([]string, 0, len(assigned))
for _, role := range roles {
if slices.Contains(assigned, role.ID) {
filtered = append(filtered, role.Name)
}
}

return filtered
}

func getRoleIDs() ([]string, error) {
roles, err := OrganizationRoles()
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/ent/checksum/.history_schema_checksum
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ab1a715fb86d24166a914417c34ed7ea5fa6e5e84c908dab42037e35f712e64c
92e46495f777060cab71bb459512b23cf85e73d2c70c3ea33b4407928758e396
2 changes: 1 addition & 1 deletion internal/ent/checksum/.schema_checksum
Original file line number Diff line number Diff line change
@@ -1 +1 @@
77569e5f492746f4a0dbb2afecb5c1a9e7566307cbfa805a10a197ca180faace
0a451c79267f3f3eae7b639646c5bb218c63d7a4271c535cc922c17ff82946e1
5 changes: 5 additions & 0 deletions internal/ent/generate/templates/ent/additional_fields.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,9 @@
// Base64 is the base64 representation of the file when using database storage
Base64 string `json:"base64,omitempty"`
{{- end }}
{{- if eq $.Name "OrgMembership" }}
// AdditionalRoles are the additional functional roles the user is assigned on top of their
// base organization role
AdditionalRoles []string `json:"additionalRoles,omitempty"`
{{- end }}
{{ end }}
4 changes: 4 additions & 0 deletions internal/ent/generated/orgmembership.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 60 additions & 1 deletion internal/ent/interceptors/orgmembers.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@

"entgo.io/ent"

"github.com/theopenlane/gqlgen-plugins/graphutils"
"github.com/theopenlane/iam/auth"
"github.com/theopenlane/iam/fgax"

fgamodel "github.com/theopenlane/core/fga/model"
"github.com/theopenlane/core/internal/ent/generated"
"github.com/theopenlane/core/internal/ent/generated/intercept"
"github.com/theopenlane/core/internal/ent/generated/orgmembership"
"github.com/theopenlane/core/internal/ent/generated/privacy"
"github.com/theopenlane/core/internal/ent/privacy/utils"
"github.com/theopenlane/core/pkg/logx"
)

Expand Down Expand Up @@ -55,7 +59,7 @@
}

// InterceptorOrgMember is middleware to change the OrgMember query result
func InterceptorOrgMember() ent.Interceptor {

Check failure on line 62 in internal/ent/interceptors/orgmembers.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 23 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=theopenlane_core&issues=AZ7RjiWK5Es7HYelSTjF&open=AZ7RjiWK5Es7HYelSTjF&pullRequest=2494
return ent.InterceptFunc(func(next ent.Querier) ent.Querier {
return intercept.OrgMembershipFunc(func(ctx context.Context, q *generated.OrgMembershipQuery) (generated.Value, error) {
// run the query
Expand All @@ -74,11 +78,66 @@
return v, nil
}

return dedupeOrgMembers(ctx, members)
res, err := dedupeOrgMembers(ctx, members)
if err != nil {
return nil, err
}

if !graphutils.CheckForRequestedField(ctx, "additionalRoles") {
return res, nil
}

// get additional roles per result
for i, r := range res {
res[i].AdditionalRoles, err = getFunctionalRoles(ctx, r.UserID)
if err != nil {
return nil, err
}
}

return res, nil
})
})
}

func getFunctionalRoles(ctx context.Context, userID string) ([]string, error) {
caller, ok := auth.CallerFromContext(ctx)
if !ok || caller == nil {
return []string{}, nil
}

roles, err := fgamodel.OrganizationRoles()
if err != nil {
return []string{}, err
}

ids := make([]string, 0, len(roles))
for _, role := range roles {
ids = append(ids, role.ID)
}

req := fgax.ListAccess{
SubjectType: auth.UserSubjectType,
SubjectID: userID,
ObjectID: caller.OrganizationID,
ObjectType: fgax.Kind(generated.TypeOrganization),
Relations: ids,
Context: utils.NewOrganizationContextKey(caller.SubjectEmail),
}

client := utils.AuthzClientFromContext(ctx)
if client == nil {
return []string{}, nil
}

assignedRoles, err := client.ListRelations(ctx, req)
if err != nil {
return []string{}, err
}

return fgamodel.GetOrganizationRoleStrings(roles, assignedRoles), nil
}

// dedupeOrgMembers removes duplicate org members from the list
func dedupeOrgMembers(ctx context.Context, members []*generated.OrgMembership) ([]*generated.OrgMembership, error) {
seen := map[string]*generated.OrgMembership{}
Expand Down
2 changes: 1 addition & 1 deletion internal/graphapi/checksum/.schema_checksum
Original file line number Diff line number Diff line change
@@ -1 +1 @@
a8bf587808eaf40c8d33c618b844d3ce84234f8a58300b750dfe1aaa27ba83c6
efaaa795e6ec05a60fe60bbee7308865268f3285a68ef9ea6bab6eeb1cb05108
2 changes: 1 addition & 1 deletion internal/graphapi/clientschema/checksum/.schema_checksum
Original file line number Diff line number Diff line change
@@ -1 +1 @@
cd298d4a4b78eb322e95c0e1c59fce8a99cc084ac2574450bc754afa2d5c4f97
6cf47179f1aae3893e66f2e20a0d85d5734ede4a24bd270f2cbbae791c9cd0c4
1 change: 1 addition & 0 deletions internal/graphapi/clientschema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -52503,6 +52503,7 @@ type OrgMembership implements Node {
"""
where: EventWhereInput
): EventConnection!
additionalRoles: [String!]
}
"""
Return response for createBulkOrgMembership mutation
Expand Down
25 changes: 25 additions & 0 deletions internal/graphapi/generated/ent.generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -108183,6 +108183,29 @@ func (ec *executionContext) fieldContext_OrgMembership_events(ctx context.Contex
return fc, nil
}

func (ec *executionContext) _OrgMembership_additionalRoles(ctx context.Context, field graphql.CollectedField, obj *generated.OrgMembership) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return ec.fieldContext_OrgMembership_additionalRoles(ctx, field)
},
func(ctx context.Context) (any, error) {
return obj.AdditionalRoles, nil
},
nil,
func(ctx context.Context, selections ast.SelectionSet, v []string) graphql.Marshaler {
return ec.marshalOString2ᚕstringᚄ(ctx, selections, v)
},
true,
false,
)
}
func (ec *executionContext) fieldContext_OrgMembership_additionalRoles(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
return graphql.NewScalarFieldContext("OrgMembership", field, false, false, errors.New("field of type String does not have child fields"))
}

func (ec *executionContext) _OrgMembershipConnection_edges(ctx context.Context, field graphql.CollectedField, obj *generated.OrgMembershipConnection) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
Expand Down Expand Up @@ -441683,6 +441706,8 @@ func (ec *executionContext) _OrgMembership(ctx context.Context, sel ast.Selectio
}

out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
case "additionalRoles":
out.Values[i] = ec._OrgMembership_additionalRoles(ctx, field, obj)
default:
panic("unknown field " + strconv.Quote(field.Name))
}
Expand Down
35 changes: 24 additions & 11 deletions internal/graphapi/generated/root_.generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 72 additions & 0 deletions internal/graphapi/orgmembers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,78 @@ func TestQueryOrgMembers(t *testing.T) {

assert.Assert(t, resp != nil)
assert.Check(t, is.Len(resp.OrgMemberships.Edges, tc.expectedLen))

// no org role set, so should return empty array
assert.Check(t, is.Len(resp.OrgMemberships.Edges[0].Node.AdditionalRoles, 0))
})
}

// delete created org
cleanupOrganizationDataWithContext(localTestOrg.owner.UserCtx, t)
}

func TestQueryOrgMembersWithAdditionalRoles(t *testing.T) {
t.Parallel()

localTestOrg := suite.seedFreshOrgUsers(t)
org1Member := localTestOrg.member

// add policy manager and trust center manager role
suite.addFunctionalRoleForUser(localTestOrg.owner.UserCtx, t, org1Member.ID, localTestOrg.owner.OrganizationID, []string{"policy_manager", "trust_center_manager"})
testCases := []struct {
name string
whereInput *testclient.OrgMembershipWhereInput
client *testclient.TestClient
ctx context.Context
expectErr bool
expectAdditionalRoles bool
}{
{
name: "happy path, get org member with additional roles",
whereInput: &testclient.OrgMembershipWhereInput{
UserID: &org1Member.ID,
},
client: suite.client.api,
ctx: localTestOrg.owner.UserCtx,
expectAdditionalRoles: true,
},
{
name: "happy path, get org auditor has no additional roles",
whereInput: &testclient.OrgMembershipWhereInput{
UserID: &localTestOrg.auditor.ID,
},
client: suite.client.api,
ctx: localTestOrg.owner.UserCtx,
expectAdditionalRoles: false,
},
}

for _, tc := range testCases {
t.Run("Get "+tc.name, func(t *testing.T) {
if tc.whereInput == nil {
tc.whereInput = &testclient.OrgMembershipWhereInput{}
}

resp, err := tc.client.GetOrgMembersByOrgID(tc.ctx, tc.whereInput)

if tc.expectErr {
assert.Assert(t, err != nil)
assert.Assert(t, is.Nil(resp))
return
}

assert.NilError(t, err)
assert.Assert(t, resp != nil)

assert.Assert(t, is.Len(resp.OrgMemberships.Edges, 1))

if tc.expectAdditionalRoles {
assert.Check(t, is.Len(resp.OrgMemberships.Edges[0].Node.AdditionalRoles, 2))
assert.Check(t, is.Contains(resp.OrgMemberships.Edges[0].Node.AdditionalRoles, "Policy Manager"))
assert.Check(t, is.Contains(resp.OrgMemberships.Edges[0].Node.AdditionalRoles, "Trust Center Manager"))
} else {
assert.Check(t, is.Len(resp.OrgMemberships.Edges[0].Node.AdditionalRoles, 0))
}
})
}

Expand Down
Loading