def test_list_grants(setup): # type: (SetupTest) -> None with setup.transaction(): create_graph(setup) mock_ui = MockUI() usecase = setup.usecase_factory.create_list_grants_usecase(mock_ui) usecase.list_grants() expected = { "not-gary": UniqueGrantsOfPermission(users={"*****@*****.**": ["foo"]}, role_users={}, service_accounts={}), "other-permission": UniqueGrantsOfPermission(users={"*****@*****.**": [""]}, role_users={}, service_accounts={}), "some-permission": UniqueGrantsOfPermission( users={ "*****@*****.**": ["bar", "foo"], "*****@*****.**": ["*"] }, role_users={"*****@*****.**": ["foo", "role"]}, service_accounts={"*****@*****.**": ["*"]}, ), "twice": UniqueGrantsOfPermission(users={"*****@*****.**": ["*"]}, role_users={}, service_accounts={}), } assert mock_ui.grants == expected assert mock_ui.grants_of_permission == {}
def test_broken_service_account_grants(setup): # type: (SetupTest) -> None """Test correct handling of a bug in service account membership. It's currently possible to add the user underlying a service account directly to a group. This was not intended behavior, but unfortunately some code depends on this behavior, so we can't fix it (yet). Until then, we want to suppress any permissions derived from such membership from the graph underlying /grants to maintain separation between service account permissions and user permissions. This tests that we do so correctly. TODO(rra): Remove this test once we've cleaned up service account membership handling. """ with setup.transaction(): setup.create_service_account("*****@*****.**", "some-group") setup.grant_permission_to_service_account("service", "bar", "*****@*****.**") setup.grant_permission_to_group("some-permission", "foo", "another-group") setup.add_user_to_group("*****@*****.**", "another-group") mock_ui = MockUI() usecase = setup.usecase_factory.create_list_grants_usecase(mock_ui) usecase.list_grants() assert mock_ui.grants == { "service": UniqueGrantsOfPermission( users={}, role_users={}, service_accounts={"*****@*****.**": ["bar"]}) }
def test_np_owner_grants(setup): # type: (SetupTest) -> None """Test special behavior of np-owner. np-owner roles should not cause permission grants to pass on to the user with that role, either as a direct member or as np-owner of a group that in turn is a member of the group with the permission. To make things even trickier, the user who is an np-owner on the shortest path should still get the permission if there is some other path that doesn't involve np-owner. This sets up a graph to test this behavior. """ with setup.transaction(): setup.add_user_to_group("*****@*****.**", "group") setup.add_user_to_group("*****@*****.**", "np-group", "np-owner") setup.grant_permission_to_group("permission", "direct", "np-group") setup.add_group_to_group("np-group", "parent-group") setup.grant_permission_to_group("permission", "parent", "parent-group") setup.add_group_to_group("group", "intermediate-group") setup.add_group_to_group("intermediate-group", "grandparent-group") setup.add_group_to_group("np-group", "grandparent-group") setup.grant_permission_to_group("permission", "grandparent", "grandparent-group") mock_ui = MockUI() usecase = setup.usecase_factory.create_list_grants_usecase(mock_ui) usecase.list_grants() assert mock_ui.grants == { "permission": UniqueGrantsOfPermission(users={"*****@*****.**": ["grandparent"]}, role_users={}, service_accounts={}) }
def test_unknown_permission(setup): # type: (SetupTest) -> None mock_ui = MockUI() usecase = setup.usecase_factory.create_list_grants_usecase(mock_ui) usecase.list_grants_of_permission("unknown-permission") assert mock_ui.grants == {} assert mock_ui.grants_of_permission == { "unknown-permission": UniqueGrantsOfPermission(users={}, role_users={}, service_accounts={}) }
def all_grants_of_permission(self, permission): # type: (str) -> UniqueGrantsOfPermission empty_grants = UniqueGrantsOfPermission(users={}, role_users={}, service_accounts={}) return self._grants_by_permission.get(permission, empty_grants)
def _get_grants_by_permission( permission_graph, # type: DiGraph group_grants, # type: Dict[str, List[GroupPermissionGrant]] service_account_grants, # type: Dict[str, List[ServiceAccountPermissionGrant]] user_metadata, # type: Dict[str, Any] ): # type: (...) -> Dict[str, UniqueGrantsOfPermission] """Build a map of permissions to users and service accounts with grants.""" service_grants = defaultdict( lambda: defaultdict(set)) # type: Dict[str, Dict[str, Set[str]]] for account, service_grant_list in iteritems(service_account_grants): for service_grant in service_grant_list: service_grants[service_grant.permission][account].add( service_grant.argument) # For each group that has a permission grant, determine all of its users from the graph, # and then record each permission grant of that group as a grant to all of those users. # Use a set for the arguments in our intermediate data structure to handle uniqueness. We # have to separate role users from non-role users here, since they're otherwise identical # and are both handled by the same graph. # # TODO(rra): We currently have a bug that erroneously allows service accounts to be added # as regular members of groups, causing them to show up in the user graph. Work around # this by skipping such users based on user metadata. This special-case can be removed # once we enforce that the user underlying service accounts cannot be added as a member of # groups. (A better place to put this is to remove service accounts from the nodes in the # graph, but this will break some arguably broken software that (ab)used the membership of # service accounts in groups, so we'll do that later when we fix that bug.) role_user_grants = defaultdict( lambda: defaultdict(set)) # type: Dict[str, Dict[str, Set[str]]] user_grants = defaultdict( lambda: defaultdict(set)) # type: Dict[str, Dict[str, Set[str]]] for group, grant_list in iteritems(group_grants): members = set() # type: Set[str] paths = single_source_shortest_path(permission_graph, ("Group", group)) for member, path in iteritems(paths): member_type, member_name = member if member_type != "User": continue if "service_account" in user_metadata[member_name]: continue members.add(member_name) for grant in grant_list: for member in members: if user_metadata[member]["role_user"]: role_user_grants[grant.permission][member].add( grant.argument) else: user_grants[grant.permission][member].add( grant.argument) # Now, assemble the service_grants, role_user_grants, and user_grants dicts into a single # dictionary of permission names to UniqueGrantsOfPermission named tuples. defaultdicts # don't compare easily to dicts and the API server wants to return lists, so convert to a # regular dict with list values for ease of testing. (The performance loss should be # insignificant.) all_grants = {} # type: Dict[str, UniqueGrantsOfPermission] for permission in set(user_grants.keys()) | set(service_grants.keys()): grants = UniqueGrantsOfPermission( users={ k: sorted(v) for k, v in iteritems(user_grants[permission]) }, role_users={ k: sorted(v) for k, v in iteritems(role_user_grants[permission]) }, service_accounts={ k: sorted(v) for k, v in iteritems(service_grants[permission]) }, ) all_grants[permission] = grants return all_grants