Skip to content

Commit f362284

Browse files
authored
Merge pull request teamhephy#54 from jianxiaoguo/pr
feat(oauth): using passport authentication
2 parents 542481e + 8f9bef7 commit f362284

23 files changed

+434
-849
lines changed

charts/controller/templates/_helpers.tpl

+31-2
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,13 @@ env:
8282
secretKeyRef:
8383
name: database-creds
8484
key: password
85+
- name: DRYCC_DATABASE_NAME
86+
valueFrom:
87+
secretKeyRef:
88+
name: database-creds
89+
key: controller-database-name
8590
- name: DRYCC_DATABASE_URL
86-
value: "postgres://$(DRYCC_DATABASE_USER):$(DRYCC_DATABASE_PASSWORD)@$(DRYCC_DATABASE_SERVICE_HOST):$(DRYCC_DATABASE_SERVICE_PORT)/$(DRYCC_DATABASE_USER)"
91+
value: "postgres://$(DRYCC_DATABASE_USER):$(DRYCC_DATABASE_PASSWORD)@$(DRYCC_DATABASE_SERVICE_HOST):$(DRYCC_DATABASE_SERVICE_PORT)/$(DRYCC_DATABASE_NAME)"
8792
{{- end }}
8893
- name: WORKFLOW_NAMESPACE
8994
valueFrom:
@@ -149,6 +154,30 @@ env:
149154
- name: "DRYCC_RABBITMQ_URL"
150155
value: "amqp://$(DRYCC_RABBITMQ_USERNAME):$(DRYCC_RABBITMQ_PASSWORD)@drycc-rabbitmq-0.drycc-rabbitmq.{{$.Release.Namespace}}.svc.{{$.Values.global.cluster_domain}}:5672/drycc"
151156
{{- end }}
157+
{{- if eq .Values.global.passport_location "on-cluster"}}
158+
- name: "DRYCC_PASSPORT_DOMAIN"
159+
value: drycc-passport.{{ .Values.global.platform_domain }}
160+
- name: "SOCIAL_AUTH_DRYCC_AUTHORIZATION_URL"
161+
value: "$(DRYCC_PASSPORT_DOMAIN)/oauth/token/"
162+
- name: "SOCIAL_AUTH_DRYCC_ACCESS_API_URL"
163+
value: "$(DRYCC_PASSPORT_DOMAIN)/users/"
164+
- name: "SOCIAL_AUTH_DRYCC_USERINFO_URL"
165+
value: "$(DRYCC_PASSPORT_DOMAIN)/oauth/userinfo/"
166+
- name: "SOCIAL_AUTH_DRYCC_JWKS_URI"
167+
value: "$(DRYCC_PASSPORT_DOMAIN)/oauth/.well-known/jwks.json"
168+
- name: "SOCIAL_AUTH_DRYCC_OIDC_ENDPOINT"
169+
value: "$(DRYCC_PASSPORT_DOMAIN)/oauth"
170+
- name: SOCIAL_AUTH_DRYCC_CONTROLLER_KEY
171+
valueFrom:
172+
secretKeyRef:
173+
name: passport-creds
174+
key: social-auth-drycc-controller-key
175+
- name: SOCIAL_AUTH_DRYCC_CONTROLLER_SECRET
176+
valueFrom:
177+
secretKeyRef:
178+
name: passport-creds
179+
key: social-auth-drycc-controller-secret
180+
{{- end }}
152181
{{- range $key, $value := .Values.environment }}
153182
- name: {{ $key }}
154183
value: {{ $value | quote }}
@@ -168,4 +197,4 @@ resources:
168197
memory: {{.Values.limits_memory}}
169198
{{- end }}
170199
{{- end }}
171-
{{- end }}
200+
{{- end }}

charts/controller/templates/controller-cronjob-daily.yaml

+3-2
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ metadata:
77
annotations:
88
component.drycc.cc/version: {{ .Values.image_tag }}
99
spec:
10-
failedJobsHistoryLimit: 1
1110
schedule: "0 0 * * *"
12-
successfulJobsHistoryLimit: 3
11+
concurrencyPolicy: {{ .Values.concurrency_policy }}
12+
successfulJobsHistoryLimit: 1
13+
failedJobsHistoryLimit: 1
1314
jobTemplate:
1415
spec:
1516
template:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
apiVersion: batch/v1beta1
2+
kind: CronJob
3+
metadata:
4+
name: drycc-controller-cronjob-daily
5+
labels:
6+
heritage: drycc
7+
annotations:
8+
component.drycc.cc/version: {{ .Values.image_tag }}
9+
spec:
10+
schedule: "0 */1 * * *"
11+
concurrencyPolicy: {{ .Values.concurrency_policy }}
12+
successfulJobsHistoryLimit: 1
13+
failedJobsHistoryLimit: 1
14+
jobTemplate:
15+
spec:
16+
template:
17+
spec:
18+
restartPolicy: OnFailure
19+
serviceAccount: drycc-controller
20+
containers:
21+
- image: {{.Values.image_registry}}/{{.Values.org}}/controller:{{.Values.image_tag}}
22+
imagePullPolicy: {{.Values.pull_policy}}
23+
name: drycc-controller-measure-app
24+
command:
25+
- /bin/bash
26+
- -c
27+
args:
28+
- python -u /app/manage.py measure_app
29+
{{- include "controller.envs" . | indent 12 }}
30+
{{- include "controller.volumeMounts" . | indent 12 }}
31+
{{- include "controller.volumes" . | indent 10 }}

charts/controller/values.yaml

+16
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ app_storage_class: ""
2222
replicas: 1
2323
# Set celery replicas
2424
celery_replicas: 1
25+
# Set cronjob concurrencyPolicy
26+
# Allow (default): The cron job allows concurrently running jobs
27+
# Forbid: The cron job does not allow concurrent runs; if it is time for a new job run and the previous job run hasn't finished yet, the cron job skips the new job run
28+
# Replace: If it is time for a new job run and the previous job run hasn't finished yet, the cron job replaces the currently running job run with a new job run
29+
concurrency_policy: "Replace"
2530
# Configuring this will no longer use the built-in database component
2631
database_url: ""
2732

@@ -30,6 +35,16 @@ database_url: ""
3035
# this is usually a non required setting.
3136
environment:
3237
RESERVED_NAMES: "drycc, drycc-builder, drycc-monitor-grafana"
38+
## Oauth settings. If passport_location is off-cluster, set the following environment variables
39+
# LOGIN_REDIRECT_URL: ""
40+
# SOCIAL_AUTH_DRYCC_AUTHORIZATION_URL: ""
41+
# SOCIAL_AUTH_DRYCC_ACCESS_TOKEN_URL: ""
42+
# SOCIAL_AUTH_DRYCC_ACCESS_API_URL: ""
43+
# SOCIAL_AUTH_DRYCC_USERINFO_URL: ""
44+
# SOCIAL_AUTH_DRYCC_JWKS_URI: ""
45+
# SOCIAL_AUTH_DRYCC_OIDC_ENDPOINT: ""
46+
# SOCIAL_AUTH_DRYCC_KEY: ""
47+
# SOCIAL_AUTH_DRYCC_SECRET: ""
3348

3449
redis:
3550
replicas: 1
@@ -79,3 +94,4 @@ global:
7994
platform_domain: ""
8095
# Whether cert_manager is enabled to automatically generate controller certificates
8196
cert_manager_enabled: "true"
97+
passport_location: "on-cluster"

rootfs/Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ RUN apk add --update --virtual .build-deps \
99
cargo \
1010
libffi-dev \
1111
musl-dev \
12-
openldap-dev \
12+
openssl-dev \
1313
&& pip3 install --disable-pip-version-check --no-cache-dir -r /app/requirements.txt \
1414
&& runDeps="$( \
1515
scanelf --needed --nobanner --format '%n#p' --recursive /usr/local \

rootfs/api/apps_extra/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
from urllib.parse import quote
2+
3+
from social_core.utils import sanitize_redirect, user_is_authenticated, \
4+
user_is_active, partial_pipeline_data, setting_url
5+
6+
7+
def do_auth(backend, redirect_name='next'):
8+
# Save any defined next value into session
9+
data = backend.strategy.request_data(merge=False)
10+
11+
# Save extra data into session.
12+
for field_name in backend.setting('FIELDS_STORED_IN_SESSION', []):
13+
if field_name in data:
14+
backend.strategy.session_set(field_name, data[field_name])
15+
else:
16+
backend.strategy.session_set(field_name, None)
17+
# uri = None
18+
if redirect_name in data:
19+
# Check and sanitize a user-defined GET/POST next field value
20+
redirect_uri = data[redirect_name]
21+
if backend.setting('SANITIZE_REDIRECTS', True):
22+
allowed_hosts = backend.setting('ALLOWED_REDIRECT_HOSTS', []) + \
23+
[backend.strategy.request_host()]
24+
redirect_uri = sanitize_redirect(allowed_hosts, redirect_uri)
25+
backend.strategy.session_set(
26+
redirect_name,
27+
redirect_uri or backend.setting('LOGIN_REDIRECT_URL')
28+
)
29+
response = backend.start()
30+
url = response.url.split('?')[1]
31+
32+
def form2json(form_data):
33+
from urllib.parse import parse_qs, urlparse
34+
query = urlparse('?' + form_data).query
35+
params = parse_qs(query)
36+
return {key: params[key][0] for key in params}
37+
from django.core.cache import cache
38+
cache.set("oidc_key_" + data.get('key', ''), form2json(url).get('state'), 60 * 10)
39+
return response
40+
41+
42+
def do_complete(backend, login, user=None, redirect_name='next',
43+
*args, **kwargs):
44+
data = backend.strategy.request_data()
45+
46+
is_authenticated = user_is_authenticated(user)
47+
user = user if is_authenticated else None
48+
49+
partial = partial_pipeline_data(backend, user, *args, **kwargs)
50+
if partial:
51+
user = backend.continue_pipeline(partial)
52+
# clean partial data after usage
53+
backend.strategy.clean_partial_pipeline(partial.token)
54+
else:
55+
user = backend.complete(user=user, *args, **kwargs)
56+
57+
# pop redirect value before the session is trashed on login(), but after
58+
# the pipeline so that the pipeline can change the redirect if needed
59+
redirect_value = backend.strategy.session_get(redirect_name, '') or \
60+
data.get(redirect_name, '')
61+
62+
# check if the output value is something else than a user and just
63+
# return it to the client
64+
user_model = backend.strategy.storage.user.user_model()
65+
if user and not isinstance(user, user_model):
66+
return user
67+
68+
if is_authenticated:
69+
if not user:
70+
url = setting_url(backend, redirect_value, 'LOGIN_REDIRECT_URL')
71+
else:
72+
url = setting_url(backend, redirect_value,
73+
'NEW_ASSOCIATION_REDIRECT_URL',
74+
'LOGIN_REDIRECT_URL')
75+
elif user:
76+
if user_is_active(user):
77+
# catch is_new/social_user in case login() resets the instance
78+
is_new = getattr(user, 'is_new', False)
79+
social_user = user.social_user
80+
login(backend, user, social_user)
81+
# store last login backend name in session
82+
backend.strategy.session_set('social_auth_last_login_backend',
83+
social_user.provider)
84+
85+
if is_new:
86+
url = setting_url(backend,
87+
'NEW_USER_REDIRECT_URL',
88+
redirect_value,
89+
'LOGIN_REDIRECT_URL')
90+
else:
91+
url = setting_url(backend, redirect_value,
92+
'LOGIN_REDIRECT_URL')
93+
else:
94+
if backend.setting('INACTIVE_USER_LOGIN', False):
95+
social_user = user.social_user
96+
login(backend, user, social_user)
97+
url = setting_url(backend, 'INACTIVE_USER_URL', 'LOGIN_ERROR_URL',
98+
'LOGIN_URL')
99+
else:
100+
url = setting_url(backend, 'LOGIN_ERROR_URL', 'LOGIN_URL')
101+
102+
if redirect_value and redirect_value != url:
103+
redirect_value = quote(redirect_value)
104+
url += ('&' if '?' in url else '?') + \
105+
'{0}={1}'.format(redirect_name, redirect_value)
106+
107+
if backend.setting('SANITIZE_REDIRECTS', True):
108+
allowed_hosts = backend.setting('ALLOWED_REDIRECT_HOSTS', []) + \
109+
[backend.strategy.request_host()]
110+
url = sanitize_redirect(allowed_hosts, url) or \
111+
backend.setting('LOGIN_REDIRECT_URL')
112+
response = backend.strategy.redirect(url)
113+
social_auth = user.social_auth.filter(provider='drycc').\
114+
order_by('-modified').last()
115+
response.set_cookie("name", user.username,
116+
max_age=social_auth.extra_data.get('expires_in'))
117+
response.set_cookie("id_token", social_auth.extra_data.get('id_token'),
118+
max_age=social_auth.extra_data.get('expires_in'))
119+
from django.core.cache import cache
120+
cache.set("oidc_state_" + data.get('state'),
121+
{'token': social_auth.extra_data.get('id_token', 'fail'),
122+
'username': user.username},
123+
60 * 10)
124+
return response

rootfs/api/authentication.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ def authenticate(self, request):
3535
return AnonymousUser(), None
3636

3737

38-
class DryccTokenAuthentication(TokenAuthentication):
38+
class DryccOIDCAuthentication(TokenAuthentication):
3939
def authenticate(self, request):
40-
if 'manager' in request.META.get('HTTP_USER_AGENT', ''):
40+
if 'Drycc' in request.META.get('HTTP_USER_AGENT', ''):
4141
auth = get_authorization_header(request).split()
4242

4343
if not auth or auth[0].lower() != self.keyword.lower().encode():
@@ -57,7 +57,7 @@ def authenticate(self, request):
5757
raise exceptions.AuthenticationFailed(msg)
5858
return cache.get_or_set(
5959
token, lambda: self._get_user(token), settings.OAUTH_CACHE_USER_TIME), None # noqa
60-
return super(DryccTokenAuthentication, self).authenticate(request) # noqa
60+
return super(DryccOIDCAuthentication, self).authenticate(request) # noqa
6161

6262
@staticmethod
6363
def _get_user(key):

rootfs/api/backend.py

+72
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
from django.conf import settings
12
from django.contrib.auth import get_user_model
23
from django.core.exceptions import ObjectDoesNotExist
34

5+
from social_core.backends.oauth import BaseOAuth2
6+
from social_core.backends.open_id_connect import OpenIdConnectAuth
7+
48
from api import serializers
59
from api.oauth import OAuthManager
610

@@ -29,3 +33,71 @@ def get_user(self, user_id):
2933
except ObjectDoesNotExist:
3034
pass
3135
return user
36+
37+
38+
class DryccOAuth(BaseOAuth2):
39+
"""Drycc OAuth authentication backend"""
40+
name = 'drycc'
41+
AUTHORIZATION_URL = settings.SOCIAL_AUTH_DRYCC_AUTHORIZATION_URL
42+
ACCESS_TOKEN_URL = settings.SOCIAL_AUTH_DRYCC_ACCESS_TOKEN_URL
43+
ACCESS_TOKEN_METHOD = 'POST'
44+
SCOPE_SEPARATOR = ','
45+
EXTRA_DATA = [
46+
('id', 'id'),
47+
('access_token', 'access_token'),
48+
('refresh_token', 'refresh_token'),
49+
('expires_in', 'expires_in'),
50+
('token_type', 'token_type'),
51+
('id_token', 'id_token'),
52+
('scope', 'scope'),
53+
]
54+
55+
def get_user_details(self, response):
56+
"""Return user details from GitHub account"""
57+
print(response)
58+
return {
59+
'username': response.get('username'),
60+
'email': response.get('email') or '',
61+
'first_name': response.get('first_name'),
62+
'last_name': response.get('last_name'),
63+
'is_superuser': response.get('is_superuser'),
64+
'is_staff': response.get('is_staff'),
65+
'is_active': response.get('is_active'),
66+
}
67+
68+
def user_data(self, access_token, *args, **kwargs):
69+
"""Loads user data from service"""
70+
url = settings.SOCIAL_AUTH_DRYCC_ACCESS_API_URL
71+
return self.get_json(url, headers={
72+
'authorization': 'Bearer ' + access_token})
73+
74+
def get_user_id(self, details, response):
75+
"""Use user account id as unique id"""
76+
return response.get('id')
77+
78+
79+
class DryccOIDC(OpenIdConnectAuth):
80+
"""Drycc Openid Connect authentication backend"""
81+
name = 'drycc'
82+
AUTHORIZATION_URL = settings.SOCIAL_AUTH_DRYCC_AUTHORIZATION_URL
83+
ACCESS_TOKEN_URL = settings.SOCIAL_AUTH_DRYCC_ACCESS_TOKEN_URL
84+
USERINFO_URL = settings.SOCIAL_AUTH_DRYCC_USERINFO_URL
85+
JWKS_URI = settings.SOCIAL_AUTH_DRYCC_JWKS_URI
86+
OIDC_ENDPOINT = settings.SOCIAL_AUTH_DRYCC_OIDC_ENDPOINT
87+
DEFAULT_SCOPE = ['openid']
88+
EXTRA_DATA = [
89+
('id', 'id'),
90+
('access_token', 'access_token'),
91+
('refresh_token', 'refresh_token'),
92+
('expires_in', 'expires_in'),
93+
('token_type', 'token_type'),
94+
('id_token', 'id_token'),
95+
('scope', 'scope'),
96+
]
97+
98+
from social_core.utils import cache
99+
100+
@cache(ttl=86400)
101+
def oidc_config(self):
102+
return self.get_json(self.OIDC_ENDPOINT +
103+
'/.well-known/openid-configuration/')

rootfs/api/influxdb.py

+3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ def _query_stream(flux_script: str) -> Iterator[FluxRecord]:
2929
except ApiException as e:
3030
logger.exception(e)
3131
yield from []
32+
except Exception as e:
33+
logger.exception(e)
34+
yield from []
3235
else:
3336
yield from records
3437

0 commit comments

Comments
 (0)