Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.0.0/>`
Current (Unreleased)
--------------------

- Add mocked compatibility tests, ``auth status`` / ``auth logout``, and optional domain-wide delegation ``--subject`` without changing existing commands or store keys.
- Record packaging updates and install from the lockfile in CI. (`#93 <https://github.com/elastic/SWAT/pull/93>`_) [`@terrancedejesus <https://github.com/terrancedejesus>`_]
- Ignore local AI and planning markdown files. (`#90 <https://github.com/elastic/SWAT/pull/90>`_) [`@terrancedejesus <https://github.com/terrancedejesus>`_]
- Pin Python dependencies to current 3.10-compatible versions and add ``poetry.lock``. (`#91 <https://github.com/elastic/SWAT/pull/91>`_) [`@terrancedejesus <https://github.com/terrancedejesus>`_]
- Refresh ReadTheDocs requirements to the pinned Sphinx versions. (`#92 <https://github.com/elastic/SWAT/pull/92>`_) [`@terrancedejesus <https://github.com/terrancedejesus>`_]
Expand Down
19 changes: 12 additions & 7 deletions docs/auth.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Google Workspace Authentication Mechanisms

- **Service Accounts**: Representing non-human users, these accounts authenticate via dedicated credentials. Service accounts are ideal for server-to-server interactions or acting on behalf of a user. Service accounts require domain-wide delegation to access Google Workspace APIs. Unlike OAuth, service account scopes are defined at the time of creation or based on the IAM role assigned. Therefore to use service accounts in SWAT, the user must create a service account with the appropriate scopes and download the JSON key file.

- **API Keys**: These keys authenticate requests to Google Workspace APIs.
- **API Keys**: Google API keys are not used for SWAT authentication. SWAT authenticates with OAuth client credentials or service accounts.

For SWAT, both OAuth credentials and Service Account credentials are supported.

Expand Down Expand Up @@ -51,11 +51,16 @@ The `Cred` class pairs these credentials with an active session if one exists. T
Command Execution with `auth.py`
--------------------------------

`auth.py` provides the executable logic, allowing the user to:
``auth.py`` provides the executable logic. Existing commands stay in place:

- Initiate authentication using either OAuth or Service Account.
- Fetch, store, or remove credentials in the `CredStore`.
- Authenticates and authorizes to Google Workspace services with a `Cred` object.
- ``auth session`` authenticates with OAuth or a service account and optionally stores the session under a key.
- ``auth list`` lists stored sessions.
- ``auth status`` shows stored keys, credential type, session state, and a non-secret identity. Use ``--key`` to inspect one entry.
- ``auth logout [KEY]`` clears the stored session for a key and leaves the credential in the store. The default key is ``default``. Logout is local only; it does not revoke tokens at Google.

``auth session --service-account --subject USER@DOMAIN`` impersonates that Workspace user through domain-wide delegation. ``--subject`` requires ``--service-account``.

Credentials are added and removed with ``creds add/remove/list``. Those commands are unchanged.

Google Workspace Scopes
-----------------------
Expand Down Expand Up @@ -84,7 +89,7 @@ For more information on how to authenticate or authorize with credentials in SWA
Persistence of Credentials
--------------------------

Credentials are stored in a serialized file, `swat/etc/.cred_store.pkl`, by default. Both credentials and valid sessions are stored in this file and are loaded at runtime. This allows for persistent authentication and authorization without the need to re-authenticate each time for every user or service account. This can be disabled by changed the `store_on_exit` value in the `etc/config.yaml` to `False`.
Credentials are stored in a serialized file, `swat/etc/.cred_store.pkl`, by default. Both credentials and valid sessions are stored in this file and are loaded at runtime. This allows for persistent authentication and authorization without the need to re-authenticate each time for every user or service account. This can be disabled by changing the `save_on_exit` value in the `etc/config.yaml` to `False`.

Recommendations
---------------
Expand All @@ -94,7 +99,7 @@ Recommendations
- Some emulations require a "3rd-party" google workspace account, separate from the organization being targeted/tested. It is therefore recommended to setup a separate and external Google Workspace account for this purpose.
- For the "3rd-party" Google Workspace account, it is recommended to use OAuth credentials as well and store them in the credential store with the key name `external` as such ``auth session --store-key external --creds PATH_TO_CREDS``.
- It is recommended to have all Google Chrome profiles established and pre-authenticated for users where OAuth credentials are used. This will be useful when the OAuth consent screen appears and the user can select the appropriate profile to authenticate with.
- It is recommended to keep the `store_on_exit` value in the `etc/config.yaml` to `True` to ensure credentials are stored and available for future use.
- It is recommended to keep the `save_on_exit` value in the `etc/config.yaml` to `True` to ensure credentials are stored and available for future use.
- Most OOTB emulations require 1-2 users with OAuth creds to emulate either internal or external user activity.


Expand Down
20 changes: 11 additions & 9 deletions docs/how_to_guides.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ By default, SWAT will save the ``CredentialStore`` to a local file named ``.cred
Authentication and Authorization
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

- Add Creds, Authenticate and Store Session: Use ``auth session --creds PATH_TO_CREDENTIALS_FILE --store NAME`` (`--service-account` if service account).
- Remove Creds & Sessions from Store: Use ``creds remove NAME``.
- List Credentials: Use ``creds list`` to view the credential store.
- List Auth Sessions: Use ``auth list`` to view valid sessions.
- Add Additional Creds and Session to Store:** Repeat the first note above with separate credentials.
- Add creds, authenticate, and store a session: Use ``auth session --creds PATH_TO_CREDENTIALS_FILE --store-key NAME`` (``--service-account`` if service account). For domain-wide delegation, add ``--subject USER@DOMAIN``.
- Check stored keys and session state: Use ``auth status``.
- Clear a session without removing creds: Use ``auth logout NAME``.
- Remove creds and sessions from the store: Use ``creds remove NAME``.
- List credentials: Use ``creds list`` to view the credential store.
- List auth sessions: Use ``auth list`` to view valid sessions.
- Add additional creds and sessions to the store: Repeat the first note above with separate credentials.

Specific Authentication Steps
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand All @@ -54,8 +56,8 @@ For most of the SWAT commands and emulations, a single OAuth validated session w

1. Run ``auth list`` - There should be no active sessions available, unless your credential store has been loaded from previous session
2. Run ``creds list`` - There should be no saved credentials, unless your credential store has been loaded from a previous session
3. Run ``auth session --store default --creds PATH_TO_CREDS`` - where `default` is the key in the credential store for these creds and session
#. This can also be used to store credentials, authenticate/authorize and store the session in the Credential Store at once. So if you want to bypass steps 1 and 2, you can run ``auth session --store default --creds PATH_TO_CREDS``.
3. Run ``auth session --store-key default --creds PATH_TO_CREDS`` - where `default` is the key in the credential store for these creds and session
#. This can also be used to store credentials, authenticate/authorize and store the session in the Credential Store at once. So if you want to bypass steps 1 and 2, you can run ``auth session --store-key default --creds PATH_TO_CREDS``.
4. Follow the Google Workspace OAuth prompt and consent screen with a valid account.

.. image:: _static/auth_session_store.png
Expand Down Expand Up @@ -85,7 +87,7 @@ Once complete, you may continue to the following workflows, however be aware tha
#. From the `Google Cloud Console <https://console.cloud.google.com/>`_, navigate to the service account you want to use and click **Add Key**.
#. Select **JSON** and click **Create**. This will download the service account credentials to your local machine.
#. You can also create a new service account in the console and assign access to users whom this service account will make API calls on behalf of.
2. Authenticate and Authorized: Run ``auth session --store-key KEYNAME --creds PATH_TO_CREDS --service-account``
2. Authenticate and authorize: Run ``auth session --store-key KEYNAME --creds PATH_TO_CREDS --service-account --subject USER@DOMAIN``
3. Run ``auth list`` to check that session is saved
4. Run ``creds list`` to ensure creds are saved
5. Within your emulation, pass your session from the credential store to build your service in the initialization method ``self.service = build('drive', 'v3', credentials=self.obj.cred_store.store['KEYNAME'].session)``
Expand All @@ -108,7 +110,7 @@ Your `self.service` will now be active and authenticated and can be used anywher
:width: 600px
:alt: Add Service Account Credentials to Credential Store

4. Run ``auth session --key KEYNAME --store-key KEYNAME --service-account``
4. Run ``auth session --key KEYNAME --store-key KEYNAME --service-account --subject USER@DOMAIN``
6. Build your service in the initialization method of the emulation: ``self.service = build('drive', 'v3', credentials=self.obj.cred_store.store['KEYNAME'].session``

.. image:: _static/auth_session_from_cred_store.png
Expand Down
7 changes: 7 additions & 0 deletions swat/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,13 @@ def remove(self, key: str) -> bool:
"""Remove cred by key and type."""
return self.store.pop(key, None) is not None

def clear_session(self, key: str) -> bool:
"""Clear a stored session while leaving the credential in place."""
if key not in self.store or self.store[key].session is None:
return False
self.store[key].session = None
return True

def get(self, key: str, validate_type: Optional[Literal['oauth', 'service']] = None,
missing_error: bool = True) -> Optional[Cred]:
if key not in self.store:
Expand Down
54 changes: 54 additions & 0 deletions swat/commands/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,32 @@ class Command(BaseCommand):
parser_session.add_argument('--creds', type=Path, help='Path to the credentials file')
parser_session.add_argument('--service-account', action='store_true', help='Authenticate a service account')
parser_session.add_argument('--store-key', type=str, help='Add authenticated session to credential store with key')
parser_session.add_argument('--subject', help='Workspace user email to impersonate with domain-wide delegation')
parser_list = subparsers.add_parser('list', description='List credential sessions within the cred store',
help='List credential sessions within the cred store')
parser_status = subparsers.add_parser('status', description='Show stored credentials and session state',
help='Show stored credentials and session state')
parser_status.add_argument('--key', help='Show a single stored key')
parser_logout = subparsers.add_parser('logout', description='Clear a stored session without removing credentials',
help='Clear a stored session without removing credentials')
parser_logout.add_argument('key', nargs='?', default='default',
help='Name of key to clear the session from (default: default)')

def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.parser_session.set_defaults(func=self.authenticate)
self.parser_list.set_defaults(func=self.list_sessions)
self.parser_status.set_defaults(func=self.status)
self.parser_logout.set_defaults(func=self.logout)

self.args = validate_args(self.parser, self.args)

def authenticate(self) -> Optional[Credentials]:
"""Authenticate with Google Workspace using OAuth2.0."""
cred_type = 'service' if self.args.service_account else 'oauth'
if self.args.subject and not self.args.service_account:
self.logger.info('The --subject flag requires --service-account.')
return None
if self.args.key:
cred = self.obj.cred_store.get(self.args.key, validate_type=cred_type)
self.logger.info(f'Using stored credentials with key: {self.args.key}')
Expand Down Expand Up @@ -87,6 +100,9 @@ def authenticate(self) -> Optional[Credentials]:
self.logger.info(f'Missing key or credentials file.')
return None

if session and self.args.subject:
session = session.with_subject(self.args.subject)

self.logger.info(f'Authenticated successfully.' if session else f'Failed to authenticate.')
if self.args.store_key:
self.obj.cred_store.add(self.args.store_key, creds=cred, session=session, type=cred_type, override=True)
Expand All @@ -96,5 +112,43 @@ def list_sessions(self):
cred_sessions = self.obj.cred_store.list_sessions()
self.logger.info(f'Stored auth sessions: {", ".join(cred_sessions) if cred_sessions else None}')

def status(self):
store = self.obj.cred_store.store
keys = [self.args.key] if self.args.key else list(store)
if self.args.key and self.args.key not in store:
self.logger.info(f'No credentials found with key: {self.args.key}')
return
if not keys:
self.logger.info('No credentials stored.')
return
for key in keys:
self.logger.info(self._format_status(key, store[key]))

def logout(self):
cleared = self.obj.cred_store.clear_session(self.args.key)
if cleared:
self.logger.info(f'Cleared session for key: {self.args.key}')
elif self.args.key not in self.obj.cred_store.store:
self.logger.info(f'No credentials found with key: {self.args.key}')
else:
self.logger.info(f'No session stored for key: {self.args.key}')

@staticmethod
def _format_status(key: str, cred) -> str:
if isinstance(cred.creds, OAuthCreds):
cred_type = 'oauth'
identity = cred.client_id
elif isinstance(cred.creds, ServiceAccountCreds):
cred_type = 'service'
identity = cred.creds.client_email
else:
cred_type = 'unknown'
identity = cred.client_id
if cred.session:
session_state = 'expired' if getattr(cred.session, 'expired', False) else 'active'
else:
session_state = 'none'
return f'{key}: type={cred_type} session={session_state} identity={identity}'

def execute(self) -> None:
self.args.func()
Loading
Loading