docs: Add more STS docs with dex and python example (#10047)
parent
00d3cc4b69
commit
ec91fa55db
@ -0,0 +1,104 @@ |
||||
# Byte-compiled / optimized / DLL files |
||||
__pycache__/ |
||||
*.py[cod] |
||||
*$py.class |
||||
|
||||
# C extensions |
||||
*.so |
||||
|
||||
# Distribution / packaging |
||||
.Python |
||||
build/ |
||||
develop-eggs/ |
||||
dist/ |
||||
downloads/ |
||||
eggs/ |
||||
.eggs/ |
||||
lib/ |
||||
lib64/ |
||||
parts/ |
||||
sdist/ |
||||
var/ |
||||
wheels/ |
||||
*.egg-info/ |
||||
.installed.cfg |
||||
*.egg |
||||
MANIFEST |
||||
|
||||
# PyInstaller |
||||
# Usually these files are written by a python script from a template |
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it. |
||||
*.manifest |
||||
*.spec |
||||
|
||||
# Installer logs |
||||
pip-log.txt |
||||
pip-delete-this-directory.txt |
||||
|
||||
# Unit test / coverage reports |
||||
htmlcov/ |
||||
.tox/ |
||||
.coverage |
||||
.coverage.* |
||||
.cache |
||||
nosetests.xml |
||||
coverage.xml |
||||
*.cover |
||||
.hypothesis/ |
||||
.pytest_cache/ |
||||
|
||||
# Translations |
||||
*.mo |
||||
*.pot |
||||
|
||||
# Django stuff: |
||||
*.log |
||||
local_settings.py |
||||
db.sqlite3 |
||||
|
||||
# Flask stuff: |
||||
instance/ |
||||
.webassets-cache |
||||
|
||||
# Scrapy stuff: |
||||
.scrapy |
||||
|
||||
# Sphinx documentation |
||||
docs/_build/ |
||||
|
||||
# PyBuilder |
||||
target/ |
||||
|
||||
# Jupyter Notebook |
||||
.ipynb_checkpoints |
||||
|
||||
# pyenv |
||||
.python-version |
||||
|
||||
# celery beat schedule file |
||||
celerybeat-schedule |
||||
|
||||
# SageMath parsed files |
||||
*.sage.py |
||||
|
||||
# Environments |
||||
.env |
||||
.venv |
||||
env/ |
||||
venv/ |
||||
ENV/ |
||||
env.bak/ |
||||
venv.bak/ |
||||
|
||||
# Spyder project settings |
||||
.spyderproject |
||||
.spyproject |
||||
|
||||
# Rope project settings |
||||
.ropeproject |
||||
|
||||
# mkdocs documentation |
||||
/site |
||||
|
||||
# mypy |
||||
.mypy_cache/ |
@ -0,0 +1,48 @@ |
||||
#!/usr/bin/env python |
||||
# -*- coding: utf-8 -*- |
||||
# Licensed under the Apache License, Version 2.0 (the "License"); |
||||
# you may not use this file except in compliance with the License. |
||||
# You may obtain a copy of the License at |
||||
# |
||||
# http://www.apache.org/licenses/LICENSE-2.0 |
||||
# |
||||
# Unless required by applicable law or agreed to in writing, software |
||||
# distributed under the License is distributed on an "AS IS" BASIS, |
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
# See the License for the specific language governing permissions and |
||||
# limitations under the License. |
||||
|
||||
import logging |
||||
|
||||
import boto3 |
||||
from boto3.session import Session |
||||
from botocore.session import get_session |
||||
|
||||
from client_grants import ClientGrantsCredentialProvider |
||||
|
||||
boto3.set_stream_logger('boto3.resources', logging.DEBUG) |
||||
|
||||
bc_session = get_session() |
||||
bc_session.get_component('credential_provider').insert_before( |
||||
'env', |
||||
ClientGrantsCredentialProvider('NZLOOFRSluw9RfIkuHGqfk1HFp4a', |
||||
'0Z4VTG8uJBSekn42HE40DK9vQb4a'), |
||||
) |
||||
|
||||
boto3_session = Session(botocore_session=bc_session) |
||||
s3 = boto3_session.resource('s3', endpoint_url='http://localhost:9000') |
||||
|
||||
with open('/etc/hosts', 'rb') as data: |
||||
s3.meta.client.upload_fileobj(data, |
||||
'testbucket', |
||||
'hosts', |
||||
ExtraArgs={'ServerSideEncryption': 'AES256'}) |
||||
|
||||
# Upload with server side encryption, using temporary credentials |
||||
s3.meta.client.upload_file('/etc/hosts', |
||||
'testbucket', |
||||
'hosts', |
||||
ExtraArgs={'ServerSideEncryption': 'AES256'}) |
||||
|
||||
# Download encrypted object using temporary credentials |
||||
s3.meta.client.download_file('testbucket', 'hosts', '/tmp/hosts') |
@ -0,0 +1,144 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Licensed under the Apache License, Version 2.0 (the "License"); |
||||
# you may not use this file except in compliance with the License. |
||||
# You may obtain a copy of the License at |
||||
# |
||||
# http://www.apache.org/licenses/LICENSE-2.0 |
||||
# |
||||
# Unless required by applicable law or agreed to in writing, software |
||||
# distributed under the License is distributed on an "AS IS" BASIS, |
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
# See the License for the specific language governing permissions and |
||||
# limitations under the License. |
||||
|
||||
import json |
||||
# standard. |
||||
import os |
||||
|
||||
import certifi |
||||
# Dependencies |
||||
import urllib3 |
||||
from botocore.credentials import CredentialProvider, RefreshableCredentials |
||||
from botocore.exceptions import CredentialRetrievalError |
||||
from dateutil.parser import parse |
||||
|
||||
from .sts_element import STSElement |
||||
|
||||
|
||||
class ClientGrantsCredentialProvider(CredentialProvider): |
||||
""" |
||||
ClientGrantsCredentialProvider implements CredentialProvider compatible |
||||
implementation to be used with boto_session |
||||
""" |
||||
METHOD = 'assume-role-client-grants' |
||||
CANONICAL_NAME = 'AssumeRoleClientGrants' |
||||
|
||||
def __init__(self, cid, csec, |
||||
idp_ep='http://localhost:8080/auth/realms/minio/protocol/openid-connect/token', |
||||
sts_ep='http://localhost:9000'): |
||||
self.cid = cid |
||||
self.csec = csec |
||||
self.idp_ep = idp_ep |
||||
self.sts_ep = sts_ep |
||||
|
||||
# Load CA certificates from SSL_CERT_FILE file if set |
||||
ca_certs = os.environ.get('SSL_CERT_FILE') |
||||
if not ca_certs: |
||||
ca_certs = certifi.where() |
||||
|
||||
self._http = urllib3.PoolManager( |
||||
timeout=urllib3.Timeout.DEFAULT_TIMEOUT, |
||||
maxsize=10, |
||||
cert_reqs='CERT_NONE', |
||||
ca_certs=ca_certs, |
||||
retries=urllib3.Retry( |
||||
total=5, |
||||
backoff_factor=0.2, |
||||
status_forcelist=[500, 502, 503, 504] |
||||
) |
||||
) |
||||
|
||||
def load(self): |
||||
""" |
||||
Search for credentials with client_grants |
||||
""" |
||||
if self.cid is not None: |
||||
fetcher = self._create_credentials_fetcher() |
||||
return RefreshableCredentials.create_from_metadata( |
||||
metadata=fetcher(), |
||||
refresh_using=fetcher, |
||||
method=self.METHOD, |
||||
) |
||||
else: |
||||
return None |
||||
|
||||
def _create_credentials_fetcher(self): |
||||
method = self.METHOD |
||||
|
||||
def fetch_credentials(): |
||||
# HTTP headers are case insensitive filter out |
||||
# all duplicate headers and pick one. |
||||
headers = {} |
||||
headers['content-type'] = 'application/x-www-form-urlencoded' |
||||
headers['authorization'] = urllib3.make_headers( |
||||
basic_auth='%s:%s' % (self.cid, self.csec))['authorization'] |
||||
|
||||
response = self._http.urlopen('POST', self.idp_ep, |
||||
body="grant_type=client_credentials", |
||||
headers=headers, |
||||
preload_content=True, |
||||
) |
||||
if response.status != 200: |
||||
message = "Credential refresh failed, response: %s" |
||||
raise CredentialRetrievalError( |
||||
provider=method, |
||||
error_msg=message % response.status, |
||||
) |
||||
|
||||
creds = json.loads(response.data) |
||||
|
||||
query = {} |
||||
query['Action'] = 'AssumeRoleWithClientGrants' |
||||
query['Token'] = creds['access_token'] |
||||
query['DurationSeconds'] = creds['expires_in'] |
||||
query['Version'] = '2011-06-15' |
||||
|
||||
query_components = [] |
||||
for key in query: |
||||
if query[key] is not None: |
||||
query_components.append("%s=%s" % (key, query[key])) |
||||
|
||||
query_string = '&'.join(query_components) |
||||
sts_ep_url = self.sts_ep |
||||
if query_string: |
||||
sts_ep_url = self.sts_ep + '?' + query_string |
||||
|
||||
response = self._http.urlopen( |
||||
'POST', sts_ep_url, preload_content=True) |
||||
if response.status != 200: |
||||
message = "Credential refresh failed, response: %s" |
||||
raise CredentialRetrievalError( |
||||
provider=method, |
||||
error_msg=message % response.status, |
||||
) |
||||
|
||||
return parse_grants_response(response.data) |
||||
|
||||
def parse_grants_response(data): |
||||
""" |
||||
Parser for AssumeRoleWithClientGrants response |
||||
|
||||
:param data: Response data for AssumeRoleWithClientGrants request |
||||
:return: dict |
||||
""" |
||||
root = STSElement.fromstring( |
||||
'AssumeRoleWithClientGrantsResponse', data) |
||||
result = root.find('AssumeRoleWithClientGrantsResult') |
||||
creds = result.find('Credentials') |
||||
return dict( |
||||
access_key=creds.get_child_text('AccessKeyId'), |
||||
secret_key=creds.get_child_text('SecretAccessKey'), |
||||
token=creds.get_child_text('SessionToken'), |
||||
expiry_time=parse(creds.get_child_text('Expiration')).isoformat()) |
||||
|
||||
return fetch_credentials |
@ -0,0 +1,90 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Licensed under the Apache License, Version 2.0 (the "License"); |
||||
# you may not use this file except in compliance with the License. |
||||
# You may obtain a copy of the License at |
||||
# |
||||
# http://www.apache.org/licenses/LICENSE-2.0 |
||||
# |
||||
# Unless required by applicable law or agreed to in writing, software |
||||
# distributed under the License is distributed on an "AS IS" BASIS, |
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
# See the License for the specific language governing permissions and |
||||
# limitations under the License. |
||||
|
||||
from xml.etree import cElementTree |
||||
from xml.etree.cElementTree import ParseError |
||||
|
||||
if hasattr(cElementTree, 'ParseError'): |
||||
_ETREE_EXCEPTIONS = (ParseError, AttributeError, ValueError, TypeError) |
||||
else: |
||||
_ETREE_EXCEPTIONS = (SyntaxError, AttributeError, ValueError, TypeError) |
||||
|
||||
_STS_NS = {'sts': 'https://sts.amazonaws.com/doc/2011-06-15/'} |
||||
|
||||
|
||||
class STSElement(object): |
||||
"""STS aware XML parsing class. Wraps a root element name and |
||||
cElementTree.Element instance. Provides STS namespace aware parsing |
||||
functions. |
||||
|
||||
""" |
||||
|
||||
def __init__(self, root_name, element): |
||||
self.root_name = root_name |
||||
self.element = element |
||||
|
||||
@classmethod |
||||
def fromstring(cls, root_name, data): |
||||
"""Initialize STSElement from name and XML string data. |
||||
|
||||
:param name: Name for XML data. Used in XML errors. |
||||
:param data: string data to be parsed. |
||||
:return: Returns an STSElement. |
||||
""" |
||||
try: |
||||
return cls(root_name, cElementTree.fromstring(data)) |
||||
except _ETREE_EXCEPTIONS as error: |
||||
raise InvalidXMLError( |
||||
'"{}" XML is not parsable. Message: {}'.format( |
||||
root_name, error.message |
||||
) |
||||
) |
||||
|
||||
def findall(self, name): |
||||
"""Similar to ElementTree.Element.findall() |
||||
|
||||
""" |
||||
return [ |
||||
STSElement(self.root_name, elem) |
||||
for elem in self.element.findall('sts:{}'.format(name), _STS_NS) |
||||
] |
||||
|
||||
def find(self, name): |
||||
"""Similar to ElementTree.Element.find() |
||||
|
||||
""" |
||||
elt = self.element.find('sts:{}'.format(name), _STS_NS) |
||||
return STSElement(self.root_name, elt) if elt is not None else None |
||||
|
||||
def get_child_text(self, name, strict=True): |
||||
"""Extract text of a child element. If strict, and child element is |
||||
not present, raises InvalidXMLError and otherwise returns |
||||
None. |
||||
|
||||
""" |
||||
if strict: |
||||
try: |
||||
return self.element.find('sts:{}'.format(name), _STS_NS).text |
||||
except _ETREE_EXCEPTIONS as error: |
||||
raise InvalidXMLError( |
||||
('Invalid XML provided for "{}" - erroring tag <{}>. ' |
||||
'Message: {}').format(self.root_name, name, error.message) |
||||
) |
||||
else: |
||||
return self.element.findtext('sts:{}'.format(name), None, _STS_NS) |
||||
|
||||
def text(self): |
||||
"""Fetch the current node's text |
||||
|
||||
""" |
||||
return self.element.text |
@ -0,0 +1,98 @@ |
||||
# Dex Quickstart Guide [![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io) |
||||
|
||||
Dex is an identity service that uses OpenID Connect to drive authentication for apps. Dex acts as a portal to other identity providers through "connectors." This lets dex defer authentication to LDAP servers, SAML providers, or established identity providers like GitHub, Google, and Active Directory. Clients write their authentication logic once to talk to dex, then dex handles the protocols for a given backend. |
||||
|
||||
## Prerequisites |
||||
|
||||
Install Dex by following [Dex Getting Started Guide](https://github.com/dexidp/dex/blob/master/Documentation/getting-started.md) |
||||
|
||||
### Start Dex |
||||
|
||||
``` |
||||
~ ./bin/dex serve dex.yaml |
||||
time="2020-07-12T20:45:50Z" level=info msg="config issuer: http://127.0.0.1:5556/dex" |
||||
time="2020-07-12T20:45:50Z" level=info msg="config storage: sqlite3" |
||||
time="2020-07-12T20:45:50Z" level=info msg="config static client: Example App" |
||||
time="2020-07-12T20:45:50Z" level=info msg="config connector: mock" |
||||
time="2020-07-12T20:45:50Z" level=info msg="config connector: local passwords enabled" |
||||
time="2020-07-12T20:45:50Z" level=info msg="config response types accepted: [code token id_token]" |
||||
time="2020-07-12T20:45:50Z" level=info msg="config using password grant connector: local" |
||||
time="2020-07-12T20:45:50Z" level=info msg="config signing keys expire after: 3h0m0s" |
||||
time="2020-07-12T20:45:50Z" level=info msg="config id tokens valid for: 3h0m0s" |
||||
time="2020-07-12T20:45:50Z" level=info msg="listening (http) on 0.0.0.0:5556" |
||||
``` |
||||
|
||||
### Configure MinIO server with Dex |
||||
``` |
||||
~ export MINIO_IDENTITY_OPENID_CLAIM_NAME=name |
||||
~ export MINIO_IDENTITY_OPENID_CONFIG_URL=http://127.0.0.1:5556/dex/.well-known/openid-configuration |
||||
~ minio server ~/test |
||||
``` |
||||
|
||||
### Run the `web-identity.go` |
||||
|
||||
``` |
||||
~ go run web-identity.go -cid example-app -csec ZXhhbXBsZS1hcHAtc2VjcmV0 \ |
||||
-config-ep http://127.0.0.1:5556/dex/.well-known/openid-configuration \ |
||||
-cscopes groups,openid,email,profile |
||||
``` |
||||
|
||||
``` |
||||
~ mc admin policy add admin allaccess.json |
||||
``` |
||||
|
||||
Contents of `allaccess.json` |
||||
```json |
||||
{ |
||||
"Version": "2012-10-17", |
||||
"Statement": [ |
||||
{ |
||||
"Effect": "Allow", |
||||
"Action": [ |
||||
"s3:*" |
||||
], |
||||
"Resource": [ |
||||
"arn:aws:s3:::*" |
||||
] |
||||
} |
||||
] |
||||
} |
||||
``` |
||||
|
||||
### Visit http://localhost:8080 |
||||
You will be redirected to dex login screen - click "Login with email", enter username password |
||||
> username: admin@example.com |
||||
> password: password |
||||
|
||||
and then click "Grant access" |
||||
|
||||
On the browser now you shall see the list of buckets output, along with your temporary credentials obtained from MinIO. |
||||
``` |
||||
{ |
||||
"buckets": [ |
||||
"dl.minio.equipment", |
||||
"dl.minio.service-fulfillment", |
||||
"testbucket" |
||||
], |
||||
"credentials": { |
||||
"AccessKeyID": "Q31CVS1PSCJ4OTK2YVEM", |
||||
"SecretAccessKey": "rmDEOKARqKYmEyjWGhmhLpzcncyu7Jf8aZ9bjDic", |
||||
"SessionToken": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJRMzFDVlMxUFNDSjRPVEsyWVZFTSIsImF0X2hhc2giOiI4amItZFE2OXRtZEVueUZaMUttNWhnIiwiYXVkIjoiZXhhbXBsZS1hcHAiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6IjE1OTQ2MDAxODIiLCJpYXQiOjE1OTQ1ODkzODQsImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6NTU1Ni9kZXgiLCJuYW1lIjoiYWRtaW4iLCJzdWIiOiJDaVF3T0dFNE5qZzBZaTFrWWpnNExUUmlOek10T1RCaE9TMHpZMlF4TmpZeFpqVTBOallTQld4dlkyRnMifQ.nrbzIJz99Om7TvJ04jnSTmhvlM7aR9hMM1Aqjp2ONJ1UKYCvegBLrTu6cYR968_OpmnAGJ8vkd7sIjUjtR4zbw", |
||||
"SignerType": 1 |
||||
} |
||||
} |
||||
``` |
||||
|
||||
Now you have successfully configured Dex IdP with MinIO. |
||||
|
||||
> NOTE: Dex supports groups with external connectors so you can use `groups` as policy claim instead of `name`. |
||||
``` |
||||
export MINIO_IDENTITY_OPENID_CLAIM_NAME=groups |
||||
``` |
||||
|
||||
and add relevant policies on MinIO using `mc admin policy add myminio/ <group_name> group-access.json` |
||||
|
||||
## Explore Further |
||||
|
||||
- [MinIO STS Quickstart Guide](https://docs.min.io/docs/minio-sts-quickstart-guide) |
||||
- [The MinIO documentation website](https://docs.min.io) |
@ -0,0 +1,78 @@ |
||||
# The base path of dex and the external name of the OpenID Connect service. |
||||
# This is the canonical URL that all clients MUST use to refer to dex. If a |
||||
# path is provided, dex's HTTP service will listen at a non-root URL. |
||||
issuer: http://127.0.0.1:5556/dex |
||||
|
||||
# The storage configuration determines where dex stores its state. Supported |
||||
# options include SQL flavors and Kubernetes third party resources. |
||||
# |
||||
# See the storage document at Documentation/storage.md for further information. |
||||
storage: |
||||
type: sqlite3 |
||||
config: |
||||
file: examples/dex.db |
||||
|
||||
# Configuration for the HTTP endpoints. |
||||
web: |
||||
http: 0.0.0.0:5556 |
||||
# Uncomment for HTTPS options. |
||||
# https: 127.0.0.1:5554 |
||||
# tlsCert: /etc/dex/tls.crt |
||||
# tlsKey: /etc/dex/tls.key |
||||
|
||||
# Configuration for telemetry |
||||
telemetry: |
||||
http: 0.0.0.0:5558 |
||||
|
||||
# Uncomment this block to enable configuration for the expiration time durations. |
||||
expiry: |
||||
signingKeys: "3h" |
||||
idTokens: "3h" |
||||
|
||||
# Options for controlling the logger. |
||||
logger: |
||||
level: "debug" |
||||
format: "text" # can also be "json" |
||||
|
||||
# Default values shown below |
||||
oauth2: |
||||
# use ["code", "token", "id_token"] to enable implicit flow for web-only clients |
||||
responseTypes: [ "code", "token", "id_token" ] # also allowed are "token" and "id_token" |
||||
# By default, Dex will ask for approval to share data with application |
||||
# (approval for sharing data from connected IdP to Dex is separate process on IdP) |
||||
skipApprovalScreen: false |
||||
# If only one authentication method is enabled, the default behavior is to |
||||
# go directly to it. For connected IdPs, this redirects the browser away |
||||
# from application to upstream provider such as the Google login page |
||||
alwaysShowLoginScreen: false |
||||
# Uncommend the passwordConnector to use a specific connector for password grants |
||||
passwordConnector: local |
||||
|
||||
# Instead of reading from an external storage, use this list of clients. |
||||
# |
||||
# If this option isn't chosen clients may be added through the gRPC API. |
||||
staticClients: |
||||
- id: example-app |
||||
redirectURIs: |
||||
- 'http://localhost:8080/oauth2/callback' |
||||
name: 'Example App' |
||||
secret: ZXhhbXBsZS1hcHAtc2VjcmV0 |
||||
|
||||
connectors: |
||||
- type: mockCallback |
||||
id: mock |
||||
name: Example |
||||
|
||||
# Let dex keep a list of passwords which can be used to login to dex. |
||||
enablePasswordDB: true |
||||
|
||||
# A static list of passwords to login the end user. By identifying here, dex |
||||
# won't look in its underlying storage for passwords. |
||||
# |
||||
# If this option isn't chosen users may be added through the gRPC API. |
||||
staticPasswords: |
||||
- email: "admin@example.com" |
||||
# bcrypt hash of the string "password" |
||||
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" |
||||
username: "admin" |
||||
userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" |
@ -0,0 +1,111 @@ |
||||
#!/usr/bin/env python |
||||
# -*- coding: utf-8 -*- |
||||
# Licensed under the Apache License, Version 2.0 (the "License"); |
||||
# you may not use this file except in compliance with the License. |
||||
# You may obtain a copy of the License at |
||||
# |
||||
# http://www.apache.org/licenses/LICENSE-2.0 |
||||
# |
||||
# Unless required by applicable law or agreed to in writing, software |
||||
# distributed under the License is distributed on an "AS IS" BASIS, |
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
# See the License for the specific language governing permissions and |
||||
# limitations under the License. |
||||
|
||||
import json |
||||
import logging |
||||
import urllib |
||||
from uuid import uuid4 |
||||
|
||||
import boto3 |
||||
import requests |
||||
from botocore.client import Config |
||||
from flask import Flask, request |
||||
|
||||
boto3.set_stream_logger('boto3.resources', logging.DEBUG) |
||||
|
||||
authorize_url = "http://localhost:8080/auth/realms/minio/protocol/openid-connect/auth" |
||||
token_url = "http://localhost:8080/auth/realms/minio/protocol/openid-connect/token" |
||||
|
||||
# callback url specified when the application was defined |
||||
callback_uri = "http://localhost:8000/oauth2/callback" |
||||
|
||||
# keycloak id and secret |
||||
client_id = 'account' |
||||
client_secret = 'daaa3008-80f0-40f7-80d7-e15167531ff0' |
||||
|
||||
sts_client = boto3.client( |
||||
'sts', |
||||
region_name='us-east-1', |
||||
use_ssl=False, |
||||
endpoint_url='http://localhost:9000', |
||||
) |
||||
|
||||
app = Flask(__name__) |
||||
|
||||
|
||||
@app.route('/') |
||||
def homepage(): |
||||
text = '<a href="%s">Authenticate with keycloak</a>' |
||||
return text % make_authorization_url() |
||||
|
||||
|
||||
def make_authorization_url(): |
||||
# Generate a random string for the state parameter |
||||
# Save it for use later to prevent xsrf attacks |
||||
|
||||
state = str(uuid4()) |
||||
params = {"client_id": client_id, |
||||
"response_type": "code", |
||||
"state": state, |
||||
"redirect_uri": callback_uri, |
||||
"scope": "openid"} |
||||
|
||||
url = authorize_url + "?" + urllib.parse.urlencode(params) |
||||
return url |
||||
|
||||
|
||||
@app.route('/oauth2/callback') |
||||
def callback(): |
||||
error = request.args.get('error', '') |
||||
if error: |
||||
return "Error: " + error |
||||
|
||||
authorization_code = request.args.get('code') |
||||
|
||||
data = {'grant_type': 'authorization_code', |
||||
'code': authorization_code, 'redirect_uri': callback_uri} |
||||
access_token_response = requests.post( |
||||
token_url, data=data, verify=False, allow_redirects=False, auth=(client_id, client_secret)) |
||||
|
||||
print('body: ' + access_token_response.text) |
||||
|
||||
# we can now use the access_token as much as we want to access protected resources. |
||||
tokens = json.loads(access_token_response.text) |
||||
access_token = tokens['access_token'] |
||||
|
||||
response = sts_client.assume_role_with_web_identity( |
||||
RoleArn='arn:aws:iam::123456789012:user/svc-internal-api', |
||||
RoleSessionName='test', |
||||
WebIdentityToken=access_token, |
||||
DurationSeconds=3600 |
||||
) |
||||
|
||||
s3_resource = boto3.resource('s3', |
||||
endpoint_url='http://localhost:9000', |
||||
aws_access_key_id=response['Credentials']['AccessKeyId'], |
||||
aws_secret_access_key=response['Credentials']['SecretAccessKey'], |
||||
aws_session_token=response['Credentials']['SessionToken'], |
||||
config=Config(signature_version='s3v4'), |
||||
region_name='us-east-1') |
||||
|
||||
bucket = s3_resource.Bucket('testbucket') |
||||
|
||||
for obj in bucket.objects.all(): |
||||
print(obj) |
||||
|
||||
return "success" |
||||
|
||||
|
||||
if __name__ == '__main__': |
||||
app.run(debug=True, port=8000) |
Loading…
Reference in new issue