diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..5ae766c --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,29 @@ +name: Python Package & Deploy to PyPI +on: + push: + branches: + - main +jobs: + deploy-to-pypi: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: '3.10' + - name: Install dependencies + run: | + pip install twine + - name: Bump Version + run: | + sed -i "s/0.0.0/$(date +"%Y.%m%d.${{github.run_number}}")/g" setup.py + - name: Create Python Package + run: | + python3 setup.py sdist + - name: Upload to Twine + run: | + twine upload dist/* + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.gitignore b/.gitignore index 871e7e5..6ae8d0b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ .vscode/ aws_google_saml.egg-info/ dist/ -__pycache__/ \ No newline at end of file +__pycache__/ +.DS_Store +aws-google-saml.rb diff --git a/README.md b/README.md index 74875d1..80b64e9 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,14 @@ brew install python You'll then need to configure profiles to use in your `~/.aws/config` file. An example below: -``` +```conf [profile profile-name] region = ap-southeast-2 -account = 453559030913 -google_config.google_idp_id = C01g1l5do -google_config.role_name = assumed-ins-tech-lead -google_config.google_sp_id = 705835944086 +account = 123456789012 +google_config.google_idp_id = ABCDE1234 +google_config.role_name = production-engineer +google_config.google_sp_id = 000000000000 ``` @@ -34,3 +34,7 @@ Ready? Start the app with the following command ```sh python3 google-saml-auth.py --profile profile-name ``` + +### Administrator Instructions + +// TODO: How to setup application in Google SAML Console diff --git a/aws-google-saml/__main__.py b/aws_google_saml/__main__.py similarity index 84% rename from aws-google-saml/__main__.py rename to aws_google_saml/__main__.py index 18f34e5..ae130d8 100644 --- a/aws-google-saml/__main__.py +++ b/aws_google_saml/__main__.py @@ -6,7 +6,7 @@ import xml.etree.ElementTree as ET -import webbrowser, base64, botocore.session, boto3, configparser, os, getopt, sys +import webbrowser, base64, botocore.session, boto3, configparser, os, getopt, sys, pkgutil, datetime boto_session = None @@ -56,9 +56,12 @@ def authenticate_aws(options, saml_assertion): .setSamlAssertion(saml_assertion) def get_html_page_contents(): - with open('authed.html') as htmlFile: - htmlContents = htmlFile.read() - return htmlContents + try: + return pkgutil.get_data(__name__, 'authed.html').decode('utf-8') # Required when packaged + except: + with open('aws_google_saml/authed.html') as htmlFile: # Used for local development + htmlContents = htmlFile.read() + return htmlContents def HandlerWrapper(options): class CustomHandler(BaseHTTPRequestHandler): @@ -115,6 +118,8 @@ class Options: session_name = None session_duration = None + skip_if_already_authenticated = False + port = 35002 def setProfile(self, profile): @@ -129,6 +134,10 @@ def setGoogleSpId(self, google_sp_id): self.google_sp_id = google_sp_id return self + def setSkipIfAlreadyAuthenticated(self, skip_if_already_authenticated): + self.skip_if_already_authenticated = skip_if_already_authenticated + return self + def setAwsAccountId(self, aws_account_id): self.aws_account_id = aws_account_id return self @@ -242,17 +251,36 @@ def enrichOptionsFromAwsConfiguration(options): return options +def isAlreadyAuthenticated(options): + credentials_parser = configparser.RawConfigParser() + credentials_parser.read(os.path.expanduser(boto_session.get_config_variable('credentials_file'))) + + if credentials_parser.has_section(options.profile): + if credentials_parser.has_option(options.profile, 'aws_session_expiration'): + aws_session_expiration = credentials_parser.get(options.profile, 'aws_session_expiration') + return aws_session_expiration > datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S%z') + + return False + + def enrichOptionsFromArgs(cliArguments, options): - optionsList, arguments = getopt.getopt(cliArguments,"hp:",["profile=", "port="]) + optionsList, arguments = getopt.getopt(cliArguments,"hp:",["help", "profile=", "port=", "skip-if-authed"]) for option, argument in optionsList: - if option == '-h': - print ('aws-google-saml.py --profile ') + if option == '-h' or option == '--help': + print ('\nUsage: aws-google-saml.py --profile ') + print ('\nOptions:') + print ('\t --profile (required)\t\t\t eg: --profile my-profile') + print ('\t --port (optional, default 35002)\t\t eg: --port 35002') + print ('\t --skip-if-authed (optional, default false)\t\t eg: --skip-if-authed') sys.exit() if option == '--profile': options.setProfile(argument) if option == '--port': options.setPort(argument) + + if option == '--skip-if-authed': + options.setSkipIfAlreadyAuthenticated(True) return options @@ -267,8 +295,8 @@ def validateOptions(options): return validationErrors -if __name__ == "__main__": - +def main(): + global boto_session options = Options() boto_session = botocore.session.Session() @@ -284,8 +312,17 @@ def validateOptions(options): print("Exiting due to improper usage. Use -h for help.") sys.exit() + if options.skip_if_already_authenticated and isAlreadyAuthenticated(options): + print(f"You are already authenticated to the {options.profile} profile. Exiting early.") + sys.exit() + Thread(target=start_server, args=(options,)).start() webbrowser.open(f"https://accounts.google.com/o/saml2/initsso?idpid={options.google_idp_id}&spid={options.google_sp_id}&forceauthn=false") # That's it. Now we wait for the HTTP callback + + +if __name__ == "__main__": + main() + \ No newline at end of file diff --git a/authed.html b/aws_google_saml/authed.html similarity index 94% rename from authed.html rename to aws_google_saml/authed.html index 23d17c9..6269010 100644 --- a/authed.html +++ b/aws_google_saml/authed.html @@ -71,7 +71,7 @@ margin-top: 100px; position: absolute; color: #fff; - width: 1000px; + width: 100%; line-height: 2.5em; } @@ -120,11 +120,7 @@

You're Auth'ed

You're Auth'ed

- into the __REPLACED_PROFILE_NAME_HERE__ - profile for the next X hours XX mins XX seconds + into the __REPLACED_PROFILE_NAME_HERE__ profile for the next X hours XX mins XX seconds
you can close this window now
diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..3bd1b87 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,33 @@ +# Deploy script to automate process of release + +rm -rf dist + +python3 setup.py sdist + +twine upload dist/* + +rm -rf venv # Just incase it wasn't cleared before + +sleep 20 # need to wait for PyPi to be ready with new package + +python3 -m venv venv + +source venv/bin/activate + +pip3 install --no-cache-dir aws-google-saml homebrew-pypi-poet + +poet --formula aws-google-saml > aws-google-saml.rb + +deactivate + +rm -rf venv + +# Replace 'virtualenv_create(libexec, "python3")' with nothing +sed -i '' 's/virtualenv_create(libexec, "python3")//g' aws-google-saml.rb + +# replace 'Shiny new formula' with 'A user browser driven SAML authentication tool for AWS' +sed -i '' 's/Shiny new formula/A user browser driven SAML authentication tool for AWS/g' aws-google-saml.rb + +code aws-google-saml.rb + +# Copy and upload to Github diff --git a/setup.py b/setup.py index 47f9c47..ef6b2f9 100644 --- a/setup.py +++ b/setup.py @@ -2,8 +2,8 @@ from distutils.core import setup setup( name='aws-google-saml', - packages=['aws-google-saml'], - version='0.7.9', + packages=['aws_google_saml'], + version='0.0.0', license='MIT', description='A user-browser driven SAML authentication tool for AWS', author='bengieeee', @@ -11,11 +11,9 @@ download_url='https://github.com/bengieeee/aws-google-saml/archive/refs/tags/0.7.1.tar.gz', # Keywords that define your package best keywords=['aws', 'aws-cli', 'saml', 'google', 'google-saml', 'google-saml-aws'], - install_requires=[ # I get to this in a second - 'ET', + install_requires=[ 'botocore', - 'boto3', - 'configparser', + 'boto3' ], classifiers=[ 'Development Status :: 3 - Alpha', @@ -24,4 +22,15 @@ 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3', ], + # To provide executable scripts, use entry points in preference to the + # "scripts" keyword. Entry points provide cross-platform support and allow + # pip to create the appropriate form of executable for the target platform. + entry_points={ + 'console_scripts': [ + 'aws-google-saml=aws_google_saml.__main__:main', + ], + }, + package_data={ + 'aws_google_saml': ['authed.html'], + } )