docs: Add more STS docs with dex and python example (#10047)

master
Harshavardhana 4 years ago committed by GitHub
parent 00d3cc4b69
commit ec91fa55db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 104
      docs/sts/.gitignore
  2. 2
      docs/sts/README.md
  3. 4
      docs/sts/client-grants.go
  4. 48
      docs/sts/client-grants.py
  5. 144
      docs/sts/client_grants/__init__.py
  6. 90
      docs/sts/client_grants/sts_element.py
  7. 98
      docs/sts/dex.md
  8. 78
      docs/sts/dex.yaml
  9. 8
      docs/sts/keycloak.md
  10. 10
      docs/sts/web-identity.go
  11. 111
      docs/sts/web-identity.py

104
docs/sts/.gitignore vendored

@ -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/

@ -14,7 +14,7 @@ Following are advantages for using temporary credentials:
|AuthN | Description |
| :---------------------- | ------------------------------------------ |
| [**Client grants**](https://github.com/minio/minio/blob/master/docs/sts/client-grants.md) | Let applications request `client_grants` using any well-known third party identity provider such as KeyCloak, Okta. This is known as the client grants approach to temporary access. Using this approach helps clients keep MinIO credentials to be secured. MinIO STS supports client grants, tested against identity providers such as KeyCloak, Okta. |
| [**WebIdentity**](https://github.com/minio/minio/blob/master/docs/sts/web-identity.md) | Let users request temporary credentials using any OpenID(OIDC) compatible web identity providers such as KeyCloak, Facebook, Google etc. |
| [**WebIdentity**](https://github.com/minio/minio/blob/master/docs/sts/web-identity.md) | Let users request temporary credentials using any OpenID(OIDC) compatible web identity providers such as KeyCloak, Dex, Facebook, Google etc. |
| [**AssumeRole**](https://github.com/minio/minio/blob/master/docs/sts/assume-role.md) | Let MinIO users request temporary credentials using user access and secret keys. |
| [**AD/LDAP**](https://github.com/minio/minio/blob/master/docs/sts/ldap.md) | Let AD/LDAP users request temporary credentials using AD/LDAP username and password. |

@ -1,7 +1,7 @@
// +build ignore
/*
* MinIO Cloud Storage, (C) 2018 MinIO, Inc.
* MinIO Cloud Storage, (C) 2019,2020 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -48,7 +48,7 @@ var (
func init() {
flag.StringVar(&stsEndpoint, "sts-ep", "http://localhost:9000", "STS endpoint")
flag.StringVar(&idpEndpoint, "idp-ep", "https://localhost:9443/oauth2/token", "IDP endpoint")
flag.StringVar(&idpEndpoint, "idp-ep", "http://localhost:8080/auth/realms/minio/protocol/openid-connect/token", "IDP token endpoint")
flag.StringVar(&clientID, "cid", "", "Client ID")
flag.StringVar(&clientSecret, "csec", "", "Client secret")
}

@ -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"

@ -34,7 +34,7 @@ Configure and install keycloak server by following [Keycloak Installation Guide]
- `Claim JSON Type` is `string`
- Save
- Open http://localhost:8080/auth/realms/demo/.well-known/openid-configuration to verify OpenID discovery document, verify it has `authorization_endpoint` and `jwks_uri`
- Open http://localhost:8080/auth/realms/minio/.well-known/openid-configuration to verify OpenID discovery document, verify it has `authorization_endpoint` and `jwks_uri`
### Configure MinIO
```
@ -77,7 +77,7 @@ MINIO_IDENTITY_OPENID_COMMENT (sentence) optionally add a comment to this
Set `identity_openid` config with `config_url`, `client_id` and restart MinIO
```
~ mc admin config set myminio identity_openid config_url="http://localhost:8080/auth/realms/demo/.well-known/openid-configuration" client_id="account"
~ mc admin config set myminio identity_openid config_url="http://localhost:8080/auth/realms/minio/.well-known/openid-configuration" client_id="account"
```
> NOTE: You can configure the `scopes` parameter to restrict the OpenID scopes requested by minio to the IdP, for example, `"openid,policy_role_attribute"`, being `policy_role_attribute` a client_scope / client_mapper that maps a role attribute called policy to a `policy` claim returned by Keycloak
@ -87,10 +87,10 @@ mc admin service restart myminio
```
### Using WebIdentiy API
Client ID can be found by clicking any of the clients listed [here](http://localhost:8080/auth/admin/master/console/#/realms/demo/clients). If you have followed the above steps docs, the default Client ID will be `account`.
Client ID can be found by clicking any of the clients listed [here](http://localhost:8080/auth/admin/master/console/#/realms/minio/clients). If you have followed the above steps docs, the default Client ID will be `account`.
```
$ go run docs/sts/web-identity.go -cid account -csec 072e7f00-4289-469c-9ab2-bbe843c7f5a8 -config-ep "http://localhost:8080/auth/realms/demo/.well-known/openid-configuration" -port 8888
$ go run docs/sts/web-identity.go -cid account -csec 072e7f00-4289-469c-9ab2-bbe843c7f5a8 -config-ep "http://localhost:8080/auth/realms/minio/.well-known/openid-configuration" -port 8888
2018/12/26 17:49:36 listening on http://localhost:8888/
```

@ -1,7 +1,7 @@
// +build ignore
/*
* MinIO Cloud Storage, (C) 2019 MinIO, Inc.
* MinIO Cloud Storage, (C) 2019,2020 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -85,7 +85,7 @@ var (
)
// DiscoveryDoc - parses the output from openid-configuration
// for example https://accounts.google.com/.well-known/openid-configuration
// for example http://localhost:8080/auth/realms/minio/.well-known/openid-configuration
type DiscoveryDoc struct {
Issuer string `json:"issuer,omitempty"`
AuthEndpoint string `json:"authorization_endpoint,omitempty"`
@ -129,7 +129,7 @@ func parseDiscoveryDoc(ustr string) (DiscoveryDoc, error) {
func init() {
flag.StringVar(&stsEndpoint, "sts-ep", "http://localhost:9000", "STS endpoint")
flag.StringVar(&configEndpoint, "config-ep",
"https://accounts.google.com/.well-known/openid-configuration",
"http://localhost:8080/auth/realms/minio/.well-known/openid-configuration",
"OpenID discovery document endpoint")
flag.StringVar(&clientID, "cid", "", "Client ID")
flag.StringVar(&clientSec, "csec", "", "Client Secret")
@ -151,9 +151,9 @@ func main() {
return
}
scopes := ddoc.ScopesSupported
scopes := ddoc.ScopesSupported
if clientScopes != "" {
scopes = strings.Split(clientScopes, ",");
scopes = strings.Split(clientScopes, ",")
}
ctx := context.Background()

@ -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…
Cancel
Save