Skip to content

Commit

Permalink
user org role expressed as relationship to org instead of user node p…
Browse files Browse the repository at this point in the history
…roperty

Signed-off-by: Daniel Brauer <[email protected]>
  • Loading branch information
danbrauer committed Dec 13, 2024
1 parent e68522e commit 8af1a58
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 20 deletions.
5 changes: 4 additions & 1 deletion cartography/intel/github/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,13 @@ def transform_users(user_data: List[Dict], owners_data: List[Dict], org_data: Di

users_dict = {}
for user in user_data:
# all members get the 'MEMBER_OF' relationship
processed_user = deepcopy(user['node'])
processed_user['role'] = user['role']
processed_user['hasTwoFactorEnabled'] = user['hasTwoFactorEnabled']
processed_user['MEMBER_OF'] = org_data['url']
# admins get a second relationship expressing them as such
if user['role'] == 'ADMIN':
processed_user['ADMIN_OF'] = org_data['url']
users_dict[processed_user['url']] = processed_user

owners_dict = {}
Expand Down
2 changes: 1 addition & 1 deletion cartography/models/github/orgs.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
This schema does not handle the org's relationships. Those are handled by other schemas, for example:
* GitHubTeamSchema defines (GitHubOrganization)-[RESOURCE]->(GitHubTeam)
* GitHubUserSchema defines (GitHubUser)-[MEMBER_OF|UNAFFILIATED]->(GitHubOrganization)
* GitHubUserSchema defines (GitHubUser)-[MEMBER_OF|ADMIN_OF|UNAFFILIATED]->(GitHubOrganization)
(There may be others, these are just two examples.)
"""
from dataclasses import dataclass
Expand Down
21 changes: 15 additions & 6 deletions cartography/models/github/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,12 @@
RE: GitHubOrganizationUserSchema vs GitHubUnaffiliatedUserSchema
As noted above, there are implicitly two types of users, those that are part of, or affiliated, to a target
GitHubOrganization, and those thare are not part, or unaffiliated. Both are represented as GitHubUser nodes,
but there are two schemas below to allow for some differences between them, e.g., unaffiliated lack these properties:
* the 'role' property, because unaffiliated have no 'role' in the target org
GitHubOrganization, and those that are not part, or unaffiliated. Both are represented as GitHubUser nodes,
but there are two schemas below to allow for a difference between them: unaffiliated nodes lack this property:
* the 'has_2fa_enabled' property, because the GitHub api does not return it, for these users
The main importance of having two schemas is to allow the two sets of users to be loaded separately. If we are loading
an unaffiliated user, but the user already exists in the graph (perhaps they are members of another GitHub orgs for
example), then loading the unaffiliated user will not blank out the 'role' and 'has_2fa_enabled' properties.
example), then loading the unaffiliated user will not blank out the 'has_2fa_enabled' property.
"""
from dataclasses import dataclass

Expand Down Expand Up @@ -58,8 +57,6 @@ class BaseGitHubUserNodeProperties(CartographyNodeProperties):
class GitHubOrganizationUserNodeProperties(BaseGitHubUserNodeProperties):
# specified for affiliated users only. The GitHub api does not return this property for unaffiliated users.
has_2fa_enabled: PropertyRef = PropertyRef('hasTwoFactorEnabled')
# specified for affiliated uers only. Unaffiliated users do not have a 'role' in the target organization.
role: PropertyRef = PropertyRef('role')


@dataclass(frozen=True)
Expand All @@ -84,6 +81,17 @@ class GitHubUserMemberOfOrganizationRel(CartographyRelSchema):
properties: GitHubUserToOrganizationRelProperties = GitHubUserToOrganizationRelProperties()


@dataclass(frozen=True)
class GitHubUserAdminOfOrganizationRel(CartographyRelSchema):
target_node_label: str = 'GitHubOrganization'
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
{'id': PropertyRef('ADMIN_OF')},
)
direction: LinkDirection = LinkDirection.OUTWARD
rel_label: str = "ADMIN_OF"
properties: GitHubUserToOrganizationRelProperties = GitHubUserToOrganizationRelProperties()


@dataclass(frozen=True)
class GitHubUserUnaffiliatedOrganizationRel(CartographyRelSchema):
target_node_label: str = 'GitHubOrganization'
Expand All @@ -102,6 +110,7 @@ class GitHubOrganizationUserSchema(CartographyNodeSchema):
other_relationships: OtherRelationships = OtherRelationships(
[
GitHubUserMemberOfOrganizationRel(),
GitHubUserAdminOfOrganizationRel(),
],
)
sub_resource_relationship = None
Expand Down
31 changes: 26 additions & 5 deletions docs/root/modules/github/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,21 @@ Representation of a single GitHubOrganization [organization object](https://deve
(GitHubOrganization)-[RESOURCE]->(GitHubTeam)
```
- GitHubUsers are members of an organization. In some cases there may be a user who is "unaffiliated" with an org, for example if the user is an enterprise owner, but not member of, the org. [Enterprise owners](https://docs.github.com/en/enterprise-cloud@latest/admin/managing-accounts-and-repositories/managing-users-in-your-enterprise/roles-in-an-enterprise#enterprise-owners) have complete control over the enterprise (i.e. they can manage all enterprise settings, members, and policies) yet may not show up on member lists of the GitHub org.
- GitHubUsers relate to GitHubOrganizations in a few ways:
- Most typically, they are members of an organization.
- They may also be org admins (aka org owners), with broad permissions over repo and team settings. In these cases, they will be graphed with two relationships between GitHubUser and GitHubOrganization, both `MEMBER_OF` and `ADMIN_OF`.
- In some cases there may be a user who is "unaffiliated" with an org, for example if the user is an enterprise owner, but not member of, the org. [Enterprise owners](https://docs.github.com/en/enterprise-cloud@latest/admin/managing-accounts-and-repositories/managing-users-in-your-enterprise/roles-in-an-enterprise#enterprise-owners) have complete control over the enterprise (i.e. they can manage all enterprise settings, members, and policies) yet may not show up on member lists of the GitHub org.
```
(GitHubUser)-[MEMBER_OF|UNAFFILIATED]->(GitHubOrganization)
# a typical member
(GitHubUser)-[MEMBER_OF]->(GitHubOrganization)
# an admin member has two relationships to the org
(GitHubUser)-[MEMBER_OF]->(GitHubOrganization)
(GitHubUser)-[ADMIN_OF]->(GitHubOrganization)
# an unaffiliated user (e.g. an enterprise owner)
(GitHubUser)-[UNAFFILIATED]->(GitHubOrganization)
```
Expand Down Expand Up @@ -148,7 +159,6 @@ Representation of a single GitHubUser [user object](https://developer.github.com
| username | Name of the user |
| fullname | The full name |
| has_2fa_enabled | Whether the user has 2-factor authentication enabled |
| role | Either 'ADMIN' (denoting that the user is an owner of a Github organization) or 'MEMBER' |
| is_site_admin | Whether the user is a site admin |
| is_enterprise_owner | Whether the user is an [enterprise owner](https://docs.github.com/en/enterprise-cloud@latest/admin/managing-accounts-and-repositories/managing-users-in-your-enterprise/roles-in-an-enterprise#enterprise-owners) |
| permission | Only present if the user is an [outside collaborator](https://docs.github.com/en/graphql/reference/objects#repositorycollaboratorconnection) of this repo. `permission` is either ADMIN, MAINTAIN, READ, TRIAGE, or WRITE ([ref](https://docs.github.com/en/graphql/reference/enums#repositorypermission)). |
Expand Down Expand Up @@ -178,10 +188,21 @@ WRITE, MAINTAIN, TRIAGE, and READ ([Reference](https://docs.github.com/en/graphq
(GitHubUser)-[:DIRECT_COLLAB_{ACTION}]->(GitHubRepository)
```
- GitHubUsers are members of an organization. In some cases there may be a user who is "unaffiliated" with an org, for example if the user is an enterprise owner, but not member of, the org. [Enterprise owners](https://docs.github.com/en/enterprise-cloud@latest/admin/managing-accounts-and-repositories/managing-users-in-your-enterprise/roles-in-an-enterprise#enterprise-owners) have complete control over the enterprise (i.e. they can manage all enterprise settings, members, and policies) yet may not show up on member lists of the GitHub org.
- GitHubUsers relate to GitHubOrganizations in a few ways:
- Most typically, they are members of an organization.
- They may also be org admins (aka org owners), with broad permissions over repo and team settings. In these cases, they will be graphed with two relationships between GitHubUser and GitHubOrganization, both `MEMBER_OF` and `ADMIN_OF`.
- In some cases there may be a user who is "unaffiliated" with an org, for example if the user is an enterprise owner, but not member of, the org. [Enterprise owners](https://docs.github.com/en/enterprise-cloud@latest/admin/managing-accounts-and-repositories/managing-users-in-your-enterprise/roles-in-an-enterprise#enterprise-owners) have complete control over the enterprise (i.e. they can manage all enterprise settings, members, and policies) yet may not show up on member lists of the GitHub org.
```
(GitHubUser)-[MEMBER_OF|UNAFFILIATED]->(GitHubOrganization)
# a typical member
(GitHubUser)-[MEMBER_OF]->(GitHubOrganization)
# an admin member has two relationships to the org
(GitHubUser)-[MEMBER_OF]->(GitHubOrganization)
(GitHubUser)-[ADMIN_OF]->(GitHubOrganization)
# an unaffiliated user (e.g. an enterprise owner)
(GitHubUser)-[UNAFFILIATED]->(GitHubOrganization)
```
- GitHubUsers may be ['immediate'](https://docs.github.com/en/graphql/reference/enums#teammembershiptype) members of a team (as opposed to being members via membership in a child team), with their membership [role](https://docs.github.com/en/graphql/reference/enums#teammemberrole) being MEMBER or MAINTAINER.
Expand Down
17 changes: 10 additions & 7 deletions tests/integration/cartography/intel/github/test_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,21 @@ def test_sync(mock_owners, mock_users, neo4j_session):

# Assert

# Ensure users got loaded
# Ensure the expected users are there
nodes = neo4j_session.run(
"""
MATCH (g:GitHubUser) RETURN g.id, g.role;
MATCH (g:GitHubUser) RETURN g.id;
""",
)
expected_nodes = {
("https://example.com/hjsimpson", 'MEMBER'),
("https://example.com/lmsimpson", 'MEMBER'),
("https://example.com/mbsimpson", 'ADMIN'),
("https://example.com/kbroflovski", None),
("https://example.com/hjsimpson",),
("https://example.com/lmsimpson",),
("https://example.com/mbsimpson",),
("https://example.com/kbroflovski",),
}
actual_nodes = {
(
n['g.id'],
n['g.role'],
) for n in nodes
}
assert actual_nodes == expected_nodes
Expand Down Expand Up @@ -95,6 +94,10 @@ def test_sync(mock_owners, mock_users, neo4j_session):
'https://example.com/mbsimpson',
'MEMBER_OF',
'https://example.com/my_org',
), (
'https://example.com/mbsimpson',
'ADMIN_OF',
'https://example.com/my_org',
), (
'https://example.com/kbroflovski',
'UNAFFILIATED',
Expand Down

0 comments on commit 8af1a58

Please sign in to comment.