Skip to content

Commit

Permalink
Merge pull request #512 from 3cham/feature/gcp-config
Browse files Browse the repository at this point in the history
Introduce google cloud config for logsmith
  • Loading branch information
redvox authored Apr 2, 2024
2 parents 1e45697 + dbb9fa4 commit a265f8e
Show file tree
Hide file tree
Showing 13 changed files with 193 additions and 18 deletions.
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# logsmith
Logsmith is a desktop trayicon to assume your favorite aws roles.
Logsmith is a desktop trayicon to:
- assume your favorite aws roles, and
- login & configure your gcloud config

```
“Who are you and how did you get in here?” -
Expand Down Expand Up @@ -35,10 +37,36 @@ productive:
- profile: live
account: '123456789123'
role: developer

# for google cloud:
# - gcp project is the profile group name
# - region and type are mandatory
# - profiles section is no longer needed
gcp-project-dev:
color: '#FF0000'
team: teama
region: europe-west1
type: gcp

gcp-project-prd:
color: '#388E3C'
team: teama
region: europe-west1
type: gcp
```
If you have account ids with leading zeros, please make sure to put them in quotes.
### Google Cloud login
Click on the project that you want to use, this will trigger the typical login flow for user and application
credentials using browser.
If you have multiple browser profiles, please select the correct active browser.
The login flow will be automatically stopped after 60 seconds of inactivity or not completion.
It will trigger the login flow again after 8 hours.
### Chain Assume
You may add a "source" profile which will be used to assume a given role.
Expand Down
17 changes: 17 additions & 0 deletions app/assets/google-cloud.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 9 additions & 7 deletions app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ def __init__(self, name, group: dict):
self.region: str = group.get('region', None)
self.color: str = group.get('color', None)
self.profiles: List[Profile] = []
self.type: str = group.get("type", "aws") # only aws (default) & gcp as values are allowed

for profile in group.get('profiles', []):
self.profiles.append(Profile(self, profile))

Expand All @@ -74,8 +76,8 @@ def validate(self) -> (bool, str):
return False, f'{self.name} has no region'
if not self.color:
return False, f'{self.name} has no color'
if len(self.profiles) == 0:
return False, f'{self.name} has no profiles'
if self.type == "aws" and len(self.profiles) == 0:
return False, f'aws "{self.name}" has no profiles'
for profile in self.profiles:
valid, error = profile.validate()
if not valid:
Expand All @@ -94,15 +96,15 @@ def get_default_profile(self):
return next((profile for profile in self.profiles if profile.default), None)

def to_dict(self):
profiles = []
for profile in self.profiles:
profiles.append(profile.to_dict())
return {
result_dict = {
'color': self.color,
'team': self.team,
'region': self.region,
'profiles': profiles,
'profiles': [profile.to_dict() for profile in self.profiles],
}
if self.type != "aws":
result_dict["type"] = self.type
return result_dict


class Profile:
Expand Down
43 changes: 43 additions & 0 deletions app/core/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from app.core.config import Config, ProfileGroup
from app.core.result import Result
from app.yubico import mfa
from app.gcp import login, config

logger = logging.getLogger('logsmith')

Expand Down Expand Up @@ -49,6 +50,48 @@ def login(self, profile_group: ProfileGroup, mfa_callback: Callable) -> Result:
result.set_success()
return result

def login_gcp(self, profile_group: ProfileGroup) -> Result:

result = Result()
self.active_profile_group = profile_group
logger.info('gcp login detected')

# first login
user_login = login.gcloud_auth_login()
if user_login is None:
result.error("gcloud auth login command failed")
return result

# second login for application default credentials
adc_login = login.gcloud_auth_application_login()
if adc_login is None:
result.error("gcloud auth application-default login command failed")
return result

# set project
config_project = config.set_default_project(project=profile_group.name)
if config_project is None:
result.error("config gcp project failed")
return result

# set region
config_region = config.set_default_region(region=profile_group.region)
if config_region is None:
result.error("config gcp region failed")
return result

# set quota-project
config_quota_project = config.set_default_quota_project(project=profile_group.name)
if config_quota_project is None:
result.error("config gcp quota-project failed")
return result

logger.info('login success')
self._handle_support_files(profile_group)
result.set_success()

return result

def logout(self):
result = Result()
logger.info(f'start logout')
Expand Down
22 changes: 22 additions & 0 deletions app/gcp/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import logging

from app.shell import shell

logger = logging.getLogger('logsmith')


def set_default_project(project: str):
logger.info(f'set default project to: {project}')
return shell.run(f"gcloud config set project {project}", timeout=5)


def set_default_quota_project(project: str):
logger.info(f'set default quota-project to: {project}')
return shell.run(f"gcloud auth application-default set-quota-project {project}", timeout=5)


def set_default_region(region: str):

logger.info(f'set default region to: {region}')
return shell.run(f"gcloud config set compute/region {region}", timeout=5)

15 changes: 15 additions & 0 deletions app/gcp/login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import logging

from app.shell import shell

logger = logging.getLogger('logsmith')


def gcloud_auth_login():
logger.info(f'run gcloud auth login and wait for 60 seconds to complete login flow!')
return shell.run("gcloud auth login", timeout=60)


def gcloud_auth_application_login():
logger.info(f'run gcloud auth application-default login and wait for 60 seconds to complete login flow!')
return shell.run("gcloud auth application-default login", timeout=60)
3 changes: 3 additions & 0 deletions app/gui/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def __init__(self):
self.cloud = self._resource_path('assets/cloud.svg')
self.cloud_outline = self._resource_path('assets/cloud-outline.svg')
self.cloud_done = self._resource_path('assets/cloud-done.svg')
self.cloud_google = self._resource_path('assets/google-cloud.svg')
self.bug = self._resource_path('assets/bug.svg')
self.standard = self.get_icon()

Expand All @@ -20,6 +21,8 @@ def get_icon(self, style='full', color_code='#FFFFFF'):
return self._color_icon(self.cloud_outline, color_code)
if style == 'error':
return self._color_icon(self.bug, color_code)
if style == 'gcp':
return self._color_icon(self.cloud_google, color_code)
else:
return self._color_icon(self.cloud, color_code)

Expand Down
24 changes: 20 additions & 4 deletions app/gui/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
from app.gui.key_rotation_dialog import RotateKeyDialog
from app.gui.log_dialog import LogDialog
from app.gui.trayicon import SystemTrayIcon
from core.core import Core
from gui.mfa_dialog import MfaDialog
from gui.repeater import Repeater
from app.core.core import Core
from app.gui.mfa_dialog import MfaDialog
from app.gui.repeater import Repeater

logger = logging.getLogger('logsmith')

Expand Down Expand Up @@ -55,6 +55,21 @@ def login(self, profile_group: ProfileGroup):
delay_seconds=300)
self._to_login_state()

def login_gcp(self, profile_group: ProfileGroup):
self._to_reset_state()
self.tray_icon.disable_actions(True)

result = self.core.login_gcp(profile_group=profile_group)
if not self._check_and_signal_error(result):
self._to_error_state()
return

logger.info('start repeater to remind login in 8 hours')
prepare_login = partial(self.login_gcp, profile_group=profile_group)
self.login_repeater.start(task=prepare_login,
delay_seconds=8 * 60 * 60)
self._to_login_state()

def logout(self):
result = self.core.logout()
self._check_and_signal_error(result)
Expand Down Expand Up @@ -109,7 +124,8 @@ def show_logs(self):
self.log_dialog.show_dialog(logs_as_text)

def _to_login_state(self):
self.tray_icon.setIcon(self.assets.get_icon(color_code=self.core.get_active_profile_color()))
style = "full" if self.core.active_profile_group.type == "aws" else "gcp"
self.tray_icon.setIcon(self.assets.get_icon(style=style, color_code=self.core.get_active_profile_color()))
self.tray_icon.disable_actions(False)
self.tray_icon.update_last_login(self.get_timestamp())

Expand Down
20 changes: 15 additions & 5 deletions app/gui/trayicon.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,28 @@ def populate_context_menu(self, profile_list: List[ProfileGroup]):

self.actions = []
for profile_group in profile_list:
action = menu.addAction(profile_group.name)
action.triggered.connect(partial(self.gui.login,
profile_group=profile_group))
action.setIcon(self.assets.get_icon(style='full', color_code=profile_group.color))
self.actions.append(action)
if profile_group.type == "aws":
action = menu.addAction(profile_group.name)
action.triggered.connect(partial(self.gui.login,
profile_group=profile_group))
action.setIcon(self.assets.get_icon(style='full', color_code=profile_group.color))
self.actions.append(action)

# log out
action = menu.addAction('logout')
action.triggered.connect(self.gui.logout)
action.setIcon(self.assets.get_icon(style='outline', color_code='#FFFFFF'))
self.actions.append(action)

menu.addSeparator()
for profile_group in profile_list:
if profile_group.type == "gcp":
action = menu.addAction("[GCP] " + profile_group.name)
action.triggered.connect(partial(self.gui.login_gcp,
profile_group=profile_group))
action.setIcon(self.assets.get_icon(style='gcp', color_code=profile_group.color))
self.actions.append(action)

menu.addSeparator()
# active region
self.active_region = menu.addAction('not logged in')
Expand Down
Binary file modified example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 9 additions & 1 deletion tests/test_core/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ def test_save_to_disk(self, mock_save_accounts_file, mock_save_config_file):
'role': 'readonly'
}
]
},
'gcp-project-dev': {
'color': '#FF0000',
'team': 'another-team',
'region': 'europe-west1',
'type': 'gcp',
'profiles': [], # this will be automatically added
}
}
)]
Expand All @@ -81,14 +88,15 @@ def test_save_to_disk(self, mock_save_accounts_file, mock_save_config_file):
def test_set_accounts(self):
self.config.set_accounts(get_test_accounts())

groups = ['development', 'live']
groups = ['development', 'live', 'gcp-project-dev']
self.assertEqual(groups, list(self.config.profile_groups.keys()))

development_group = self.config.get_group('development')
self.assertEqual('development', development_group.name)
self.assertEqual('awesome-team', development_group.team)
self.assertEqual('us-east-1', development_group.region)
self.assertEqual('#388E3C', development_group.color)
self.assertEqual('aws', development_group.type)

profile = development_group.profiles[0]
self.assertEqual(development_group, profile.group)
Expand Down
5 changes: 5 additions & 0 deletions tests/test_core/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ def test_get_region__region_overwrite(self):
region = self.core.get_region()
self.assertEqual('eu-north-1', region)

def test_get_region__gcp(self):
self.core.active_profile_group = self.config.get_group('gcp-project-dev')
region = self.core.get_region()
self.assertEqual('europe-west1', region)

@mock.patch('app.core.core.mfa')
@mock.patch('app.core.core.credentials')
def test__renew_session__token_from_shell(self, mock_credentials, mock_mfa_shell):
Expand Down
6 changes: 6 additions & 0 deletions tests/test_data/test_accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ def get_test_accounts() -> dict:
'role': 'readonly',
}
]
},
'gcp-project-dev': {
'color': '#FF0000',
'team': 'another-team',
'region': 'europe-west1',
'type': 'gcp',
}
}

Expand Down

0 comments on commit a265f8e

Please sign in to comment.