diff --git a/cartography/models/github/users.py b/cartography/models/github/users.py new file mode 100644 index 000000000..b83116338 --- /dev/null +++ b/cartography/models/github/users.py @@ -0,0 +1,88 @@ +from dataclasses import dataclass +from typing import Optional + +from cartography.models.core.common import PropertyRef +from cartography.models.core.nodes import CartographyNodeProperties +from cartography.models.core.nodes import CartographyNodeSchema +from cartography.models.core.relationships import CartographyRelProperties +from cartography.models.core.relationships import CartographyRelSchema +from cartography.models.core.relationships import LinkDirection +from cartography.models.core.relationships import make_target_node_matcher +from cartography.models.core.relationships import OtherRelationships +from cartography.models.core.relationships import TargetNodeMatcher + + +@dataclass(frozen=True) +class GitHubUserNodeProperties(CartographyNodeProperties): + id: PropertyRef = PropertyRef('url') + lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True) + fullname: PropertyRef = PropertyRef('name') + username: PropertyRef = PropertyRef('login', extra_index=True) + # optional because some enterprise owners do not have this property specified + has_2fa_enabled: Optional[PropertyRef] = PropertyRef('hasTwoFactorEnabled') + role: PropertyRef = PropertyRef('role') + is_site_admin: PropertyRef = PropertyRef('isSiteAdmin') + is_enterprise_owner: PropertyRef = PropertyRef('isEnterpriseOwner') + email: PropertyRef = PropertyRef('email') + company: PropertyRef = PropertyRef('company') + +@dataclass(frozen=True) +class GitHubUserToOrganizationRelProperties(CartographyRelProperties): + lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True) + + +@dataclass(frozen=True) +class GitHubUserMemberOfOrganizationRel(CartographyRelSchema): + target_node_label: str = 'GitHubOrganization' + target_node_matcher: TargetNodeMatcher = make_target_node_matcher( + {'id': PropertyRef('org_url', set_in_kwargs=True)}, + ) + direction: LinkDirection = LinkDirection.OUTWARD + rel_label: str = "MEMBER_OF" + properties: GitHubUserToOrganizationRelProperties = GitHubUserToOrganizationRelProperties() + + +@dataclass(frozen=True) +class GitHubUserUnaffiliatedOrganizationRel(CartographyRelSchema): + """ + See docs in GitHubUserSchema for more on what 'UNAFFILIATED' means. + """ + target_node_label: str = 'GitHubOrganization' + target_node_matcher: TargetNodeMatcher = make_target_node_matcher( + {'id': PropertyRef('org_url', set_in_kwargs=True)}, + ) + direction: LinkDirection = LinkDirection.OUTWARD + rel_label: str = "UNAFFILIATED" + properties: GitHubUserToOrganizationRelProperties = GitHubUserToOrganizationRelProperties() + +@dataclass(frozen=True) +class GitHubUserSchema(CartographyNodeSchema): + """ + Note the relationship between GitHubUser and GitHubOrganization is implemented via 'other_relationships' and not via + the 'sub_resource_relationship' field as might be expected. + + The 'sub_resource_relationship' typically describes the relationship of a node to its tenant (the org, project, or + other resource to which other nodes belong). An assumption of that relationship is that if the tenant goes + away, all nodes related to it should be cleaned up. + + In GitHub, though the GitHubUser's tenant seems to be GitHubOrganization, users actually exist independently. There + is a concept of 'UNAFFILIATED' users, like Enterprise Owners who are related to an org even if they are not direct + members of it. You would not want them to be cleaned up, if an org goes away, and you could want them in your graph + even if they are not members of any org in the enterprise. + + To allow for this in the schema, this relationship is treated as any other node-to-node relationship, via + 'other_relationships', instead of as the typical 'sub_resource_relationship'. + + See: + * https://docs.github.com/en/graphql/reference/enums#roleinorganization + * https://docs.github.com/en/enterprise-cloud@latest/admin/managing-accounts-and-repositories/managing-users-in-your-enterprise/roles-in-an-enterprise#enterprise-owners + """ + label: str = 'GitHubUser' + properties: GitHubUserNodeProperties = GitHubUserNodeProperties() + other_relationships: OtherRelationships = OtherRelationships( + [ + GitHubUserMemberOfOrganizationRel(), + GitHubUserUnaffiliatedOrganizationRel(), + ], + ) + sub_resource_relationship = None