Skip to content

Commit 59f407d

Browse files
authored
Merge pull request #3 from jitsecurity/sc-19672-create-a-tool-to-generate-teams-from-github-part3
Sc 19672 create a tool to generate teams from GitHub part3
2 parents e2904e0 + 920b844 commit 59f407d

12 files changed

Lines changed: 401 additions & 129 deletions

File tree

Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,18 @@ configure:
1313
read -p "Enter JIT API client ID: " client_id; \
1414
read -p "Enter JIT API client secret: " client_secret; \
1515
read -p "Enter GitHub Personal token (PAT): " github_token; \
16+
read -p "Enter comma separated topic wildcards to exclude the creation of teams(example: *dev*, *test*): " topics_to_exclude; \
1617
echo "ORGANIZATION_NAME=$$org_name" > .env; \
1718
echo "JIT_CLIENT_ID=$$client_id" >> .env; \
1819
echo "JIT_CLIENT_SECRET=$$client_secret" >> .env; \
1920
echo "GITHUB_API_TOKEN=$$github_token" >> .env
21+
echo "TEAM_WILDCARD_TO_EXCLUDE=topics_to_exclude" >> .env
2022

2123
create-teams:
2224
source venv-jit/bin/activate && \
2325
export PYTHONPATH=$(CURDIR) && \
2426
python src/utils/github_topics_to_json_file.py && \
25-
python src/scripts/create_teams.py --input teams.json
27+
python src/scripts/create_teams.py teams.json
2628

2729
help:
2830
@echo "Usage: make [target]"

README.md

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ jit-customer-scripts/
3636
- Python 3.x
3737
- Git
3838

39+
## Generating API Keys
40+
41+
* To generate Github Personal Access Token(PAT) refer to
42+
this [guide](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic)
43+
* To generate a Jit API Key Go to Settings -> Users & Permissions -> API Tokens in your
44+
Jit [dashboard](https://platform.jit.io).
45+
3946
## Installation
4047

4148
1. Clone the repository:
@@ -84,23 +91,29 @@ make create-teams
8491

8592
This command is a convenience utility that extracts the teams to generate from Github topics. \
8693
It runs these commands:
94+
8795
```bash
8896
python src/utils/github_topics_to_json_file.py
89-
python src/scripts/create_teams.py --input teams.json
97+
python src/scripts/create_teams.py teams.json
9098
```
9199

92-
This command will fetch the repository names and topics from the GitHub API and generate the JSON file. And then it will create the teams and update the assets.
100+
This command will fetch the repository names and topics from the GitHub API and generate the JSON file. And then it will
101+
create the teams and update the assets.
93102

94103
### Using External JSON File
95104

96-
You can also provide a JSON file containing team details using the `--input` argument. The JSON file should have the following structure:
105+
You can also provide a JSON file containing team details using a command line argument directly. The JSON file should
106+
have the following structure:
97107

98108
```json
99109
{
100110
"teams": [
101111
{
102112
"name": "Team 1",
103-
"members": ["user1", "user2"],
113+
"members": [
114+
"user1",
115+
"user2"
116+
],
104117
"resources": [
105118
{
106119
"type": "{resource_type}",
@@ -114,7 +127,10 @@ You can also provide a JSON file containing team details using the `--input` arg
114127
},
115128
{
116129
"name": "Team 2",
117-
"members": ["user3", "user4"],
130+
"members": [
131+
"user3",
132+
"user4"
133+
],
118134
"resources": [
119135
{
120136
"type": "{resource_type}",
@@ -126,16 +142,27 @@ You can also provide a JSON file containing team details using the `--input` arg
126142
}
127143
```
128144

129-
To use the `--input` argument, run the following command:
145+
You can run the command like this:
130146

131147
```shell
132-
python scripts/create_teams.py --input path/to/teams.json
148+
python scripts/create_teams.py path/to/teams.json
133149
```
134150

135151
Replace `path/to/teams.json` with the actual path to your JSON file.
136152

137-
## Development
153+
## Excluding Topics
154+
155+
You can exclude certain topics from being considered when creating teams. \
156+
To exclude topics, you could add them in the `make configure` command or update this env var in
157+
the `.env` file: `TEAM_WILDCARD_TO_EXCLUDE`.
138158

139-
To override the default Frontegg authentication endpoint, you can set the `FRONTEGG_AUTH_ENDPOINT` environment variable. If the variable is not set, the default value will be used.
159+
For example, to exclude topics that contain the word "test", you can set the variable as follows:
160+
161+
TEAM_WILDCARD_TO_EXCLUDE=*test*
162+
163+
This will exclude topics with names like "test", "test123", and "abc-testing".
164+
165+
## Development
140166

141-
To override Jit's API endpoint, you can set the `JIT_API_ENDPOINT` environment variable. If the variable is not set, the default value will be used.
167+
To override Jit's API endpoint, you can set the `JIT_API_ENDPOINT` environment variable. If the variable is not set, the
168+
default value will be used.

requirements-test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
pytest==7.4.0
33
pytest-mock==3.11.1
44
pytest-cov==4.0.0
5+
polyfactory==2.7.2
56
flake8>=3.9.2

src/scripts/create_teams.py

Lines changed: 86 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,18 @@
77
from dotenv import load_dotenv
88
from loguru import logger
99
from pydantic import ValidationError
10-
1110
from src.shared.clients.jit import get_existing_teams, create_teams, list_assets, add_teams_to_asset, delete_teams, \
1211
get_jit_jwt_token
1312
from src.shared.diff_tools import get_different_items_in_lists
14-
from src.shared.models import Asset, TeamObject, Organization, TeamTemplate
13+
from src.shared.models import Asset, TeamAttributes, Organization, TeamStructure, ResourceType
1514

1615
# Load environment variables from .env file.
1716
load_dotenv()
1817

18+
logger.remove() # Remove default handler
19+
logger_format = "<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | {message}"
20+
logger.add(sys.stderr, format=logger_format)
21+
1922

2023
def parse_input_file() -> Organization:
2124
"""
@@ -27,39 +30,34 @@ def parse_input_file() -> Organization:
2730
# Create the argument parser
2831
parser = argparse.ArgumentParser(description="Retrieve teams and assets")
2932

30-
# Add the --input argument
31-
parser.add_argument("--input", help="Path to a JSON file")
33+
# Add the file argument
34+
parser.add_argument("file", help="Path to a JSON file")
3235

3336
# Parse the command line arguments
3437
args = parser.parse_args()
3538

36-
# Check if the --input argument is provided
37-
if args.input:
38-
# Check if the file exists and is a JSON file
39-
if not os.path.isfile(args.input):
40-
logger.error("Error: File does not exist.")
41-
sys.exit(1)
42-
if not args.input.endswith(".json"):
43-
logger.error("Error: File is not a JSON file.")
44-
sys.exit(1)
45-
46-
# Read the JSON file
47-
with open(args.input, "r") as file:
48-
json_data = file.read()
49-
50-
# Parse the JSON data
51-
try:
52-
data = json.loads(json_data)
53-
return Organization(teams=[TeamTemplate(**team) for team in data["teams"]])
54-
except ValidationError as e:
55-
logger.error(f"Failed to validate input file: {e}")
56-
sys.exit(1)
57-
else:
58-
logger.error("No input file provided.")
39+
# Check if the file exists and is a JSON file
40+
if not os.path.isfile(args.file):
41+
logger.error("Error: File does not exist.")
42+
sys.exit(1)
43+
if not args.file.endswith(".json"):
44+
logger.error("Error: File is not a JSON file.")
5945
sys.exit(1)
6046

47+
# Read the JSON file
48+
with open(args.file, "r") as file:
49+
json_data = file.read()
50+
51+
# Parse the JSON data
52+
try:
53+
data = json.loads(json_data)
54+
return Organization(teams=[TeamStructure(**team) for team in data["teams"]])
55+
except (ValidationError, KeyError) as e:
56+
logger.error(f"Failed to validate input file: {e}")
57+
sys.exit(1)
6158

62-
def update_assets(token, organization):
59+
60+
def update_assets(token, assets: List[Asset], organization):
6361
"""
6462
Update the assets with the teams specified in the organization.
6563
@@ -71,13 +69,23 @@ def update_assets(token, organization):
7169
None
7270
"""
7371
logger.info("Updating assets.")
74-
assets: List[Asset] = list_assets(token)
72+
7573
asset_to_team_map = get_teams_for_assets(organization)
74+
existing_teams: List[str] = [t.name for t in get_existing_teams(token)]
7675
for asset in assets:
7776
teams_to_update = asset_to_team_map.get(asset.asset_name, [])
7877
if teams_to_update:
79-
logger.info(f"Adding teams {teams_to_update} to asset {asset.asset_name}")
78+
excluded_teams = get_different_items_in_lists(teams_to_update, existing_teams)
79+
if excluded_teams:
80+
logger.info(f"Excluding team(s) {excluded_teams} for asset '{asset.asset_name}'")
81+
teams_to_update = list(set(teams_to_update) - set(excluded_teams))
82+
logger.info(f"Syncing team(s) {teams_to_update} to asset '{asset.asset_name}'")
8083
add_teams_to_asset(token, asset, teams_to_update)
84+
else:
85+
asset_has_teams_tag = asset.tags and "team" in [t.name for t in asset.tags]
86+
if asset_has_teams_tag:
87+
logger.info(f"Removing all teams from asset '{asset.asset_name}'")
88+
add_teams_to_asset(token, asset, teams_to_update)
8189

8290

8391
def get_teams_to_create(topic_names: List[str], existing_team_names: List[str]) -> List[str]:
@@ -108,7 +116,43 @@ def get_teams_to_delete(topic_names: List[str], existing_team_names: List[str])
108116
return get_different_items_in_lists(existing_team_names, topic_names)
109117

110118

111-
def process_teams(token, organization):
119+
def get_desired_teams(assets: List[Asset], organization: Organization) -> List[str]:
120+
"""
121+
Get the desired teams based on the assets and organization.
122+
Also filter out teams that match the TEAM_WILDCARD_TO_EXCLUDE environment variable.
123+
124+
Args:
125+
assets (List[Asset]): The list of assets.
126+
organization (Organization): The organization object.
127+
128+
Returns:
129+
List[str]: The names of the desired teams.
130+
"""
131+
desired_teams = []
132+
for team in organization.teams:
133+
team_resources = []
134+
for resource in team.resources:
135+
if resource.type == ResourceType.GithubRepo and resource.name in [asset.asset_name for asset in assets]:
136+
team_resources.append(resource.name)
137+
if team_resources:
138+
desired_teams.append(team.name)
139+
140+
wildcards_to_exclude = os.getenv("TEAM_WILDCARD_TO_EXCLUDE", "").split(",")
141+
final_desired_teams = []
142+
for team_name in desired_teams:
143+
exclude_team = False
144+
for wildcard in wildcards_to_exclude:
145+
wildcard = wildcard.strip().strip("*")
146+
if wildcard and wildcard in team_name:
147+
exclude_team = True
148+
break
149+
if not exclude_team:
150+
final_desired_teams.append(team_name)
151+
152+
return final_desired_teams
153+
154+
155+
def process_teams(token, organization, assets: List[Asset]) -> List[str]:
112156
"""
113157
Process the teams in the organization and create or delete teams as necessary.
114158
We will delete the teams at a later stage to avoid possible synchronization issues.
@@ -121,13 +165,14 @@ def process_teams(token, organization):
121165
List[str]: The names of the teams to delete.
122166
"""
123167
logger.info("Determining required changes in teams.")
124-
desired_teams = [t.name for t in organization.teams]
125-
existing_teams: List[TeamObject] = get_existing_teams(token)
168+
169+
desired_teams = get_desired_teams(assets, organization)
170+
existing_teams: List[TeamAttributes] = get_existing_teams(token)
126171
existing_team_names = [team.name for team in existing_teams]
127172
teams_to_create = get_teams_to_create(desired_teams, existing_team_names)
128173
teams_to_delete = get_teams_to_delete(desired_teams, existing_team_names)
129174
if teams_to_create:
130-
logger.info(f"Creating {len(teams_to_create)} teams: {teams_to_create}")
175+
logger.info(f"Creating {len(teams_to_create)} team(s): {teams_to_create}")
131176
create_teams(token, teams_to_create)
132177
return teams_to_delete
133178

@@ -145,7 +190,7 @@ def get_teams_for_assets(organization: Organization) -> Dict[str, List[str]]:
145190
asset_to_team_map = {}
146191
for team in organization.teams:
147192
for resource in team.resources:
148-
if resource.type == "github_repo":
193+
if resource.type == ResourceType.GithubRepo:
149194
asset_name = resource.name
150195
if asset_name in asset_to_team_map:
151196
asset_to_team_map[asset_name].append(team.name)
@@ -166,15 +211,17 @@ def main():
166211
logger.error("Failed to parse input file. Exiting...")
167212
return
168213

169-
teams_to_delete = process_teams(jit_token, organization)
214+
assets: List[Asset] = list_assets(jit_token)
215+
216+
teams_to_delete = process_teams(jit_token, organization, assets)
170217

171-
update_assets(jit_token, organization)
218+
update_assets(jit_token, assets, organization)
172219

173220
if teams_to_delete:
174-
logger.info(f"Deleting {len(teams_to_delete)} teams: {teams_to_delete}")
221+
logger.info(f"Deleting {len(teams_to_delete)} team(s): {teams_to_delete}")
175222
delete_teams(jit_token, teams_to_delete)
223+
logger.info("Successfully completed teams sync.")
176224

177225

178226
if __name__ == '__main__':
179-
logger.add("app.log", rotation="5 MB", level="INFO")
180227
main()

src/shared/clients/github.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,19 @@
11
import os
22

3-
from dotenv import load_dotenv
43
from github import Github
54
from loguru import logger
65

7-
from src.shared.models import TeamTemplate, Resource, Organization
8-
9-
# Load environment variables from .env file.
10-
load_dotenv(".env")
11-
12-
ORGANIZATION_NAME = os.getenv("ORGANIZATION_NAME")
13-
GITHUB_TOKEN = os.getenv("GITHUB_API_TOKEN")
6+
from src.shared.models import TeamStructure, Resource, Organization, ResourceType
147

158

169
def get_teams_from_github_topics() -> Organization:
1710
try:
18-
logger.info(f"Trying to communicate with Github to get information from Org: {ORGANIZATION_NAME}")
11+
logger.info(f"Trying to communicate with Github to get information from Org: {os.getenv('ORGANIZATION_NAME')}")
1912
# Create a GitHub instance using the token
20-
github = Github(GITHUB_TOKEN)
13+
github = Github(os.getenv("GITHUB_API_TOKEN"))
2114

2215
# Get the organization
23-
organization = github.get_organization(ORGANIZATION_NAME)
16+
organization = github.get_organization(os.getenv('ORGANIZATION_NAME'))
2417

2518
# Dictionary to store team templates
2619
teams = {}
@@ -40,16 +33,16 @@ def get_teams_from_github_topics() -> Organization:
4033
# Check if the topic already exists in the teams dictionary
4134
if topic in teams:
4235
# Add the repository to the existing team
43-
teams[topic].resources.append(Resource(type="github_repo", name=repo_name))
36+
teams[topic].resources.append(Resource(type=ResourceType.GithubRepo, name=repo_name))
4437
else:
4538
# Create a new team template for the topic
46-
team_template = TeamTemplate(name=topic, members=[],
47-
resources=[Resource(type="github_repo", name=repo_name)])
39+
team_template = TeamStructure(name=topic, members=[],
40+
resources=[Resource(type=ResourceType.GithubRepo, name=repo_name)])
4841

4942
# Add the team template to the teams dictionary
5043
teams[topic] = team_template
5144

52-
logger.info(f"Retrieved ({len(teams.keys())}) teams {list(teams.keys())} from GitHub successfully.")
45+
logger.info(f"Retrieved ({len(teams.keys())}) team(s) {list(teams.keys())} from GitHub successfully.")
5346
return Organization(teams=list(teams.values()))
5447
except Exception as e:
5548
logger.error(f"Failed to retrieve teams from GitHub: {str(e)}")

0 commit comments

Comments
 (0)