Skip to content

Commit 2eab7b4

Browse files
neelmehta247fantavlik
authored andcommitted
Add ServicePrincipalAuthManager
1 parent 5778cf2 commit 2eab7b4

8 files changed

Lines changed: 126 additions & 9 deletions

File tree

.gitlab-ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
include:
22
- project: 'devplat-pr-32/gitlab-central'
3-
ref: '738bbbe05f297917a13f00438750d15b6d4303d9'
3+
ref: 'c4fa68ec4048428bbc5dff3a40e1b5227b8c5e9e'
44
file: '/splunk-cloud-sdk-python/.gitlab-ci.yml'

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ asn1crypto==0.24.0
22
atomicwrites==1.3.0
33
attrs==19.1.0
44
certifi==2019.3.9
5-
coverage==4.5.3
65
cffi==1.12.3
76
chardet==3.0.4
7+
coverage==4.5.3
88
cryptography==2.6.1
99
entrypoints==0.3
1010
flake8==3.7.7
@@ -16,6 +16,7 @@ py==1.8.0
1616
pycodestyle==2.5.0
1717
pycparser==2.19
1818
pyflakes==2.1.1
19+
PyJWT==1.7.1
1920
pytest==4.5.0
2021
pytest-asyncio==0.10.0
2122
requests==2.22.0

splunk_sdk/auth/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
from splunk_sdk.auth.auth_manager import AuthManager, PKCEAuthManager, ClientAuthManager, TokenAuthManager
2-
1+
from splunk_sdk.auth.auth_manager import AuthManager, PKCEAuthManager, ClientAuthManager, TokenAuthManager, \
2+
ServicePrincipalAuthManager

splunk_sdk/auth/auth_manager.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
import base64
1313
import hashlib
1414
import json
15+
import jwt
1516
import os
1617
import urllib
1718
import time
18-
from datetime import datetime
19+
import uuid
20+
from datetime import datetime, timezone, timedelta
1921
from abc import ABC, abstractmethod
2022
from typing import Optional
2123

@@ -428,6 +430,7 @@ def __init__(self, access_token, token_type='Bearer', expires_in=None,
428430
def authenticate(self) -> AuthContext:
429431
return self._context
430432

433+
431434
class RefreshTokenAuthManager(AuthManager):
432435
def __init__(self, client_id, refresh_token, host, scope="openid", requests_hooks=None):
433436
super().__init__(host, client_id, requests_hooks=requests_hooks)
@@ -448,3 +451,47 @@ def authenticate(self):
448451

449452
response = self._post_token(**data)
450453
return AuthContext(**response.json())
454+
455+
456+
class ServicePrincipalAuthManager(AuthManager):
457+
def __init__(self, host, principal_name, key, kid, algorithm="ES256", **kwargs):
458+
"""
459+
Creates an AuthManager that uses Service Principals to authenticate.
460+
461+
principal_name is the principal_name of the authenticating service principal
462+
key is the PEM formatted private key registered with the service principal
463+
kid is the key_id of `key`
464+
algorithm is the algorithm that generated `key`
465+
"""
466+
super().__init__(host=host, client_id=None, **kwargs)
467+
self._principal_name = principal_name
468+
self._key = key
469+
self._kid = kid
470+
self._algorithm = algorithm
471+
472+
def authenticate(self):
473+
"""Authenticate using the "client assertion" flow."""
474+
if not self._principal_name:
475+
raise ValueError("missing principal_name")
476+
if not self._key:
477+
raise ValueError("missing key")
478+
if not self._kid:
479+
raise ValueError("missing kid")
480+
if not self._algorithm:
481+
raise ValueError("missing algorithm")
482+
483+
# Client assertion expires in 10 minutes
484+
ten_minutes_from_now = datetime.now(timezone.utc) + timedelta(minutes=10)
485+
jwt_payload = {"sub": self._principal_name, "iss": self._principal_name, "jti": str(uuid.uuid4()),
486+
"exp": int(ten_minutes_from_now.timestamp()), "aud": [self._url(PATH_TOKEN)]}
487+
488+
client_assertion = jwt.encode(payload=jwt_payload, key=self._key, algorithm=self._algorithm,
489+
headers={"kid": self._kid})
490+
491+
data = {"grant_type": "client_credentials", "client_assertion": client_assertion.decode("utf-8"),
492+
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"}
493+
494+
response = self._post_token(**data)
495+
if response.status_code != 200:
496+
raise AuthnError("Unable to authenticate. Check credentials.", response)
497+
return AuthContext(**response.json())

test/auth/test_auth_manager.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from splunk_sdk.auth import PKCEAuthManager, ClientAuthManager
1111
from test.fixtures import get_auth_manager as pkce_auth_manager # NOQA
1212
from test.fixtures import get_client_auth_manager as client_auth_manager # NOQA
13+
from test.fixtures import get_service_principal_auth_manager as service_principal_auth_manager # NOQA
1314

1415

1516
@pytest.mark.usefixtures('pkce_auth_manager') # NOQA
@@ -180,11 +181,48 @@ def _assert_pkce_auth_context(auth_context):
180181
assert ('email' in auth_context.scope)
181182

182183

183-
def _assert_client_credentials_auth_context(auth_context):
184+
def _assert_client_credentials_auth_context(auth_context, expires_in=43200):
184185
assert (auth_context is not None)
185186
assert (auth_context.access_token is not None)
186187
# assert(auth_context.id_token is None)
187-
assert (auth_context.expires_in == 43200)
188+
assert (auth_context.expires_in == expires_in)
188189
assert (auth_context.token_type == 'Bearer')
189190
assert (auth_context.refresh_token is None)
190191
assert (auth_context.scope == 'client_credentials')
192+
193+
194+
def _assert_sp_credentials_auth_context(auth_context):
195+
_assert_client_credentials_auth_context(auth_context, 3600)
196+
197+
198+
@pytest.mark.usefixtures('service_principal_auth_manager') # NOQA
199+
def test_sp_authenticate(service_principal_auth_manager):
200+
auth_context = service_principal_auth_manager.authenticate()
201+
_assert_sp_credentials_auth_context(auth_context)
202+
203+
204+
@pytest.mark.usefixtures('service_principal_auth_manager') # NOQA
205+
def test_sp_token_authenticate(service_principal_auth_manager):
206+
auth_context = service_principal_auth_manager.authenticate()
207+
_assert_sp_credentials_auth_context(auth_context)
208+
209+
# use existing token from auth_context
210+
token_mgr = TokenAuthManager(access_token=auth_context.access_token,
211+
token_type=auth_context.token_type,
212+
expires_in=auth_context.expires_in,
213+
scope=auth_context.scope,
214+
id_token=auth_context.id_token,
215+
refresh_token=auth_context.refresh_token)
216+
217+
new_auth_context = token_mgr.authenticate()
218+
assert (auth_context.access_token == new_auth_context.access_token)
219+
assert (auth_context.token_type == new_auth_context.token_type)
220+
assert (auth_context.expires_in == new_auth_context.expires_in)
221+
assert (auth_context.scope == new_auth_context.scope)
222+
assert (auth_context.id_token == new_auth_context.id_token)
223+
assert (auth_context.refresh_token == new_auth_context.refresh_token)
224+
225+
context = Context(host=os.environ.get("SPLUNK_HOST"), tenant=os.environ.get("SPLUNK_TENANT"))
226+
base_client = BaseClient(context=context, auth_manager=token_mgr)
227+
idc = IdentityAndAccessControl(base_client)
228+
assert (idc.validate_token().name.lower() == os.environ.get("SPLUNK_APP_PRINCIPAL_NAME").lower())

test/fixtures.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import pytest
1414
import sys
1515

16-
from splunk_sdk.auth import ClientAuthManager, PKCEAuthManager
16+
from splunk_sdk.auth import ClientAuthManager, PKCEAuthManager, ServicePrincipalAuthManager
1717
from splunk_sdk.common.context import Context
1818
from splunk_sdk.base_client import get_client
1919

@@ -139,6 +139,13 @@ def get_client_auth_manager():
139139
return auth_manager
140140

141141

142+
@pytest.fixture(scope='session')
143+
def get_service_principal_auth_manager():
144+
auth_manager = _get_principal_manager()
145+
assert (auth_manager is not None)
146+
return auth_manager
147+
148+
142149
def _get_pkce_manager():
143150
return PKCEAuthManager(host=os.environ.get('SPLUNK_AUTH_HOST'),
144151
client_id=os.environ.get('SPLUNK_APP_CLIENT_ID'),
@@ -154,3 +161,11 @@ def _get_client_manager():
154161
client_secret=os.environ.get(
155162
'SPLUNK_APP_CLIENT_CRED_SECRET'),
156163
scope=os.environ.get('SPLUNK_SCOPE'))
164+
165+
166+
def _get_principal_manager():
167+
return ServicePrincipalAuthManager(host=os.environ.get('SPLUNK_AUTH_HOST'),
168+
principal_name=os.environ.get('SPLUNK_APP_PRINCIPAL_NAME'),
169+
key=os.environ.get('SPLUNK_APP_PRINCIPAL_PRIVATE_KEY'),
170+
kid=os.environ.get('SPLUNK_APP_PRINCIPAL_KEY_ID'),
171+
algorithm=os.environ.get('SPLUNK_APP_PRINCIPAL_KEY_ALG'))

test/test_base_client.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
import pytest
1313

1414
from test.auth.test_auth_manager import \
15-
_assert_client_credentials_auth_context, _assert_pkce_auth_context # NOQA
15+
_assert_client_credentials_auth_context, _assert_pkce_auth_context, _assert_sp_credentials_auth_context # NOQA
1616
from test.fixtures import get_auth_manager as pkce_auth_manager # NOQA
1717
from test.fixtures import get_client_auth_manager as client_auth_manager # NOQA
18+
from test.fixtures import get_service_principal_auth_manager as service_principal_auth_manager # NOQA
1819
from splunk_sdk.common.context import Context
1920
from splunk_sdk.base_client import BaseClient
2021
from splunk_sdk.identity import Identity as IdentityAndAccessControl
@@ -83,3 +84,14 @@ def test_base_client_instance_with_client_auth(client_auth_manager):
8384
assert (default_config is not None)
8485
assert (base_client is not None)
8586
_assert_client_credentials_auth_context(base_client.auth_manager.context)
87+
88+
89+
@pytest.mark.usefixtures("service_principal_auth_manager") # NOQA
90+
def test_base_client_instance_with_sp_auth(service_principal_auth_manager):
91+
"""Get a base client with a context and a client auth manager."""
92+
default_config = Context()
93+
base_client = BaseClient(context=default_config,
94+
auth_manager=service_principal_auth_manager)
95+
assert (default_config is not None)
96+
assert (base_client is not None)
97+
_assert_sp_credentials_auth_context(base_client.auth_manager.context)

tox.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ passenv = SPLUNK_AUTH_HOST
1818
SPLUNK_SCOPE
1919
SPLUNK_REDIRECT_URL
2020
SPLUNK_DEBUG
21+
SPLUNK_APP_PRINCIPAL_NAME
22+
SPLUNK_APP_PRINCIPAL_PRIVATE_KEY
23+
SPLUNK_APP_PRINCIPAL_KEY_ID
24+
SPLUNK_APP_PRINCIPAL_KEY_ALG
2125
ARTIFACT_USERNAME
2226
ARTIFACT_PASSWORD
2327
ARTIFACTORY_NPM_REGISTRY

0 commit comments

Comments
 (0)