Merge branch 'agw'
This commit is contained in:
commit
089b48aad1
|
@ -116,6 +116,18 @@ build-passbook-static:
|
||||||
services:
|
services:
|
||||||
- postgres:latest
|
- postgres:latest
|
||||||
- redis:latest
|
- redis:latest
|
||||||
|
build-passbook-gatekeeper:
|
||||||
|
stage: build
|
||||||
|
image:
|
||||||
|
name: gcr.io/kaniko-project/executor:debug
|
||||||
|
entrypoint: [""]
|
||||||
|
before_script:
|
||||||
|
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
|
||||||
|
script:
|
||||||
|
- /kaniko/executor --context $CI_PROJECT_DIR/gatekeeper --dockerfile $CI_PROJECT_DIR/gatekeeper/Dockerfile --destination docker.beryju.org/passbook/gatekeeper:latest --destination docker.beryju.org/passbook/gatekeeper:0.7.2-beta
|
||||||
|
only:
|
||||||
|
- tags
|
||||||
|
- /^version/.*$/
|
||||||
|
|
||||||
package-helm:
|
package-helm:
|
||||||
image: debian:stretch-slim
|
image: debian:stretch-slim
|
||||||
|
|
8
gatekeeper/Dockerfile
Normal file
8
gatekeeper/Dockerfile
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
FROM quay.io/pusher/oauth2_proxy
|
||||||
|
|
||||||
|
COPY templates /templates
|
||||||
|
|
||||||
|
ENV OAUTH2_PROXY_EMAIL_DOMAINS=*
|
||||||
|
ENV OAUTH2_PROXY_PROVIDER=oidc
|
||||||
|
ENV OAUTH2_PROXY_CUSTOM_TEMPLATES_DIR=/templates
|
||||||
|
ENV OAUTH2_PROXY_HTTP_ADDRESS=:4180
|
18
gatekeeper/templates/error.html
Normal file
18
gatekeeper/templates/error.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{{define "error.html"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" charset="utf-8">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>{{.Title}}</h2>
|
||||||
|
<p>{{.Message}}</p>
|
||||||
|
<hr>
|
||||||
|
<p><a href="{{.ProxyPrefix}}/sign_in">Sign In</a></p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
{{end}}
|
119
gatekeeper/templates/sign_in.html
Normal file
119
gatekeeper/templates/sign_in.html
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
{{define "sign_in.html"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" charset="utf-8">
|
||||||
|
<head>
|
||||||
|
<title>Sign In with passbook</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.42857143;
|
||||||
|
color: #333;
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signin {
|
||||||
|
display: block;
|
||||||
|
margin: 20px auto;
|
||||||
|
max-width: 400px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #428bca;
|
||||||
|
border: 1px solid #357ebd;
|
||||||
|
-webkit-border-radius: 4;
|
||||||
|
-moz-border-radius: 4;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #3071a9;
|
||||||
|
border-color: #285e8e;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 34px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.42857143;
|
||||||
|
color: #555;
|
||||||
|
background-color: #fff;
|
||||||
|
background-image: none;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
|
||||||
|
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
|
||||||
|
-webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
|
||||||
|
-o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||||
|
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||||
|
margin: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
display: block;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #aaa;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer a {
|
||||||
|
display: inline-block;
|
||||||
|
height: 25px;
|
||||||
|
line-height: 25px;
|
||||||
|
color: #aaa;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer a:hover {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="signin center">
|
||||||
|
<form method="GET" action="{{.ProxyPrefix}}/start">
|
||||||
|
<input type="hidden" name="rd" value="{{.Redirect}}">
|
||||||
|
<button type="submit" class="btn">Sign in with passbook</button><br />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
if (window.location.hash) {
|
||||||
|
(function () {
|
||||||
|
var inputs = document.getElementsByName('rd');
|
||||||
|
for (var i = 0; i < inputs.length; i++) {
|
||||||
|
inputs[i].value += window.location.hash;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
{{end}}
|
|
@ -1,18 +1,42 @@
|
||||||
"""ApplicationGatewayProvider API Views"""
|
"""ApplicationGatewayProvider API Views"""
|
||||||
|
from oauth2_provider.generators import (generate_client_id,
|
||||||
|
generate_client_secret)
|
||||||
|
from oidc_provider.models import Client
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from passbook.providers.app_gw.models import ApplicationGatewayProvider
|
from passbook.providers.app_gw.models import ApplicationGatewayProvider
|
||||||
|
from passbook.providers.oidc.api import OpenIDProviderSerializer
|
||||||
|
|
||||||
|
|
||||||
class ApplicationGatewayProviderSerializer(ModelSerializer):
|
class ApplicationGatewayProviderSerializer(ModelSerializer):
|
||||||
"""ApplicationGatewayProvider Serializer"""
|
"""ApplicationGatewayProvider Serializer"""
|
||||||
|
|
||||||
|
client = OpenIDProviderSerializer()
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
instance = super().create(validated_data)
|
||||||
|
instance.client = Client.objects.create(
|
||||||
|
client_id=generate_client_id(),
|
||||||
|
client_secret=generate_client_secret())
|
||||||
|
instance.save()
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
self.instance.client.name = self.instance.name
|
||||||
|
self.instance.client.redirect_uris = [
|
||||||
|
f"http://{self.instance.host}/oauth2/callback",
|
||||||
|
f"https://{self.instance.host}/oauth2/callback",
|
||||||
|
]
|
||||||
|
self.instance.client.scope = ['openid', 'email']
|
||||||
|
self.instance.client.save()
|
||||||
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = ApplicationGatewayProvider
|
model = ApplicationGatewayProvider
|
||||||
fields = ['pk', 'server_name', 'upstream', 'enabled', 'authentication_header',
|
fields = ['pk', 'name', 'host', 'client']
|
||||||
'default_content_type', 'upstream_ssl_verification', 'property_mappings']
|
read_only_fields = ['client']
|
||||||
|
|
||||||
class ApplicationGatewayProviderViewSet(ModelViewSet):
|
class ApplicationGatewayProviderViewSet(ModelViewSet):
|
||||||
"""ApplicationGatewayProvider Viewset"""
|
"""ApplicationGatewayProvider Viewset"""
|
||||||
|
|
|
@ -1,67 +1,37 @@
|
||||||
"""passbook Application Security Gateway Forms"""
|
"""passbook Application Security Gateway Forms"""
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
from oauth2_provider.generators import (generate_client_id,
|
||||||
from django.forms import ValidationError
|
generate_client_secret)
|
||||||
from django.utils.translation import gettext as _
|
from oidc_provider.models import Client
|
||||||
|
|
||||||
from passbook.lib.fields import DynamicArrayField
|
from passbook.providers.app_gw.models import ApplicationGatewayProvider
|
||||||
from passbook.providers.app_gw.models import (ApplicationGatewayProvider,
|
|
||||||
RewriteRule)
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationGatewayProviderForm(forms.ModelForm):
|
class ApplicationGatewayProviderForm(forms.ModelForm):
|
||||||
"""Security Gateway Provider form"""
|
"""Security Gateway Provider form"""
|
||||||
|
|
||||||
def clean_server_name(self):
|
def save(self, *args, **kwargs):
|
||||||
"""Check if server_name is in DB already, since
|
if not self.instance.pk:
|
||||||
Postgres ArrayField doesn't suppport keys."""
|
# New instance, so we create a new OIDC client with random keys
|
||||||
current = self.cleaned_data.get('server_name')
|
self.instance.client = Client.objects.create(
|
||||||
if ApplicationGatewayProvider.objects \
|
client_id=generate_client_id(),
|
||||||
.filter(server_name__overlap=current) \
|
client_secret=generate_client_secret())
|
||||||
.exclude(pk=self.instance.pk).exists():
|
self.instance.client.name = self.instance.name
|
||||||
raise ValidationError(_("Server Name already in use."))
|
self.instance.client.redirect_uris = [
|
||||||
return current
|
f"http://{self.instance.host}/oauth2/callback",
|
||||||
|
f"https://{self.instance.host}/oauth2/callback",
|
||||||
def clean_upstream(self):
|
]
|
||||||
"""Check that upstream begins with http(s)"""
|
self.instance.client.scope = ['openid', 'email']
|
||||||
for upstream in self.cleaned_data.get('upstream'):
|
self.instance.client.save()
|
||||||
_parsed_url = urlparse(upstream)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
if _parsed_url.scheme not in ('http', 'https'):
|
|
||||||
raise ValidationError(_("URL Scheme must be either http or https"))
|
|
||||||
return self.cleaned_data.get('upstream')
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = ApplicationGatewayProvider
|
model = ApplicationGatewayProvider
|
||||||
fields = ['server_name', 'upstream', 'enabled', 'authentication_header',
|
fields = [
|
||||||
'default_content_type', 'upstream_ssl_verification', 'property_mappings']
|
'name', 'host'
|
||||||
widgets = {
|
]
|
||||||
'authentication_header': forms.TextInput(),
|
|
||||||
'default_content_type': forms.TextInput(),
|
|
||||||
'property_mappings': FilteredSelectMultiple(_('Property Mappings'), False)
|
|
||||||
}
|
|
||||||
field_classes = {
|
|
||||||
'server_name': DynamicArrayField,
|
|
||||||
'upstream': DynamicArrayField
|
|
||||||
}
|
|
||||||
labels = {
|
|
||||||
'upstream_ssl_verification': _('Verify upstream SSL Certificates?'),
|
|
||||||
'property_mappings': _('Rewrite Rules')
|
|
||||||
}
|
|
||||||
|
|
||||||
class RewriteRuleForm(forms.ModelForm):
|
|
||||||
"""Rewrite Rule Form"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = RewriteRule
|
|
||||||
fields = ['name', 'match', 'halt', 'replacement', 'redirect', 'conditions']
|
|
||||||
widgets = {
|
widgets = {
|
||||||
'name': forms.TextInput(),
|
'name': forms.TextInput(),
|
||||||
'match': forms.TextInput(attrs={'data-is-monospace': True}),
|
'host': forms.TextInput(),
|
||||||
'replacement': forms.TextInput(attrs={'data-is-monospace': True}),
|
|
||||||
'conditions': FilteredSelectMultiple(_('Conditions'), False)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-11-11 17:03
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('passbook_core', '0005_merge_20191025_2022'),
|
||||||
|
('passbook_providers_app_gw', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='rewriterule',
|
||||||
|
name='conditions',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='rewriterule',
|
||||||
|
name='propertymapping_ptr',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='ApplicationGatewayProvider',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='RewriteRule',
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-11-11 17:08
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('passbook_core', '0005_merge_20191025_2022'),
|
||||||
|
('oidc_provider', '0026_client_multiple_response_types'),
|
||||||
|
('passbook_providers_app_gw', '0002_auto_20191111_1703'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ApplicationGatewayProvider',
|
||||||
|
fields=[
|
||||||
|
('provider_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Provider')),
|
||||||
|
('name', models.TextField()),
|
||||||
|
('host', models.TextField()),
|
||||||
|
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.Client')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Application Gateway Provider',
|
||||||
|
'verbose_name_plural': 'Application Gateway Providers',
|
||||||
|
},
|
||||||
|
bases=('passbook_core.provider',),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,74 +1,39 @@
|
||||||
"""passbook app_gw models"""
|
"""passbook app_gw models"""
|
||||||
import re
|
import string
|
||||||
|
from random import SystemRandom
|
||||||
|
|
||||||
from django.contrib.postgres.fields import ArrayField
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
from oidc_provider.models import Client
|
||||||
|
|
||||||
from passbook.core.models import Policy, PropertyMapping, Provider
|
from passbook import __version__
|
||||||
|
from passbook.core.models import Provider
|
||||||
|
|
||||||
|
|
||||||
class ApplicationGatewayProvider(Provider):
|
class ApplicationGatewayProvider(Provider):
|
||||||
"""Virtual server which proxies requests to any hostname in server_name to upstream"""
|
"""This provider uses oauth2_proxy with the OIDC Provider."""
|
||||||
|
|
||||||
server_name = ArrayField(models.TextField())
|
name = models.TextField()
|
||||||
upstream = ArrayField(models.TextField())
|
host = models.TextField()
|
||||||
enabled = models.BooleanField(default=True)
|
|
||||||
|
|
||||||
authentication_header = models.TextField(default='X-Remote-User', blank=True)
|
client = models.ForeignKey(Client, on_delete=models.CASCADE)
|
||||||
default_content_type = models.TextField(default='application/octet-stream')
|
|
||||||
upstream_ssl_verification = models.BooleanField(default=True)
|
|
||||||
|
|
||||||
form = 'passbook.providers.app_gw.forms.ApplicationGatewayProviderForm'
|
form = 'passbook.providers.app_gw.forms.ApplicationGatewayProviderForm'
|
||||||
|
|
||||||
@property
|
def html_setup_urls(self, request):
|
||||||
def name(self):
|
"""return template and context modal with URLs for authorize, token, openid-config, etc"""
|
||||||
"""since this model has no name property, return a joined list of server_names as name"""
|
cookie_secret = ''.join(SystemRandom().choice(
|
||||||
return ', '.join(self.server_name)
|
string.ascii_uppercase + string.digits) for _ in range(50))
|
||||||
|
return "app_gw/setup_modal.html", {
|
||||||
|
'provider': self,
|
||||||
|
'cookie_secret': cookie_secret,
|
||||||
|
'version': __version__
|
||||||
|
}
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Application Gateway %s" % ', '.join(self.server_name)
|
return f"Application Gateway {self.name}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
verbose_name = _('Application Gateway Provider')
|
verbose_name = _('Application Gateway Provider')
|
||||||
verbose_name_plural = _('Application Gateway Providers')
|
verbose_name_plural = _('Application Gateway Providers')
|
||||||
|
|
||||||
|
|
||||||
class RewriteRule(PropertyMapping):
|
|
||||||
"""Rewrite requests matching `match` with `replacement`, if all polcies in `conditions` apply"""
|
|
||||||
|
|
||||||
REDIRECT_INTERNAL = 'internal'
|
|
||||||
REDIRECT_PERMANENT = 301
|
|
||||||
REDIRECT_FOUND = 302
|
|
||||||
|
|
||||||
REDIRECTS = (
|
|
||||||
(REDIRECT_INTERNAL, _('Internal')),
|
|
||||||
(REDIRECT_PERMANENT, _('Moved Permanently')),
|
|
||||||
(REDIRECT_FOUND, _('Found')),
|
|
||||||
)
|
|
||||||
|
|
||||||
match = models.TextField()
|
|
||||||
halt = models.BooleanField(default=False)
|
|
||||||
conditions = models.ManyToManyField(Policy, blank=True)
|
|
||||||
replacement = models.TextField() # python formatted strings, use {match.1}
|
|
||||||
redirect = models.CharField(max_length=50, choices=REDIRECTS)
|
|
||||||
|
|
||||||
form = 'passbook.providers.app_gw.forms.RewriteRuleForm'
|
|
||||||
|
|
||||||
_matcher = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def compiled_matcher(self):
|
|
||||||
"""Cache the compiled regex in memory"""
|
|
||||||
if not self._matcher:
|
|
||||||
self._matcher = re.compile(self.match)
|
|
||||||
return self._matcher
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "Rewrite Rule %s" % self.name
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
verbose_name = _('Rewrite Rule')
|
|
||||||
verbose_name_plural = _('Rewrite Rules')
|
|
||||||
|
|
64
passbook/providers/app_gw/templates/app_gw/k8s-manifest.yaml
Normal file
64
passbook/providers/app_gw/templates/app_gw/k8s-manifest.yaml
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
k8s-app: passbook-gatekeeper
|
||||||
|
name: passbook-gatekeeper
|
||||||
|
namespace: kube-system
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
k8s-app: passbook-gatekeeper
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
k8s-app: passbook-gatekeeper
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- args:
|
||||||
|
- --upstream=file:///dev/null
|
||||||
|
env:
|
||||||
|
- name: OAUTH2_PROXY_CLIENT_ID
|
||||||
|
value: {{ provider.client.client_id }}
|
||||||
|
- name: OAUTH2_PROXY_CLIENT_SECRET
|
||||||
|
value: {{ provider.client.client_secret }}
|
||||||
|
- name: OAUTH2_PROXY_COOKIE_SECRET
|
||||||
|
value: {{ cookie_secret }}
|
||||||
|
image: docker.beryju.org/passbook/gatekeeper:{{ version }}
|
||||||
|
imagePullPolicy: Always
|
||||||
|
name: passbook-gatekeeper
|
||||||
|
ports:
|
||||||
|
- containerPort: 4180
|
||||||
|
protocol: TCP
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
k8s-app: passbook-gatekeeper
|
||||||
|
name: passbook-gatekeeper
|
||||||
|
namespace: kube-system
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 4180
|
||||||
|
protocol: TCP
|
||||||
|
targetPort: 4180
|
||||||
|
selector:
|
||||||
|
k8s-app: passbook-gatekeeper
|
||||||
|
---
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: passbook-gatekeeper
|
||||||
|
namespace: kube-system
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: {{ provider.host }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
serviceName: passbook-gatekeeper
|
||||||
|
servicePort: 4180
|
||||||
|
path: /oauth2
|
90
passbook/providers/app_gw/templates/app_gw/setup_modal.html
Normal file
90
passbook/providers/app_gw/templates/app_gw/setup_modal.html
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<script src="{% static 'codemirror/lib/codemirror.js' %}"></script>
|
||||||
|
<script src="{% static 'codemirror/addon/display/autorefresh.js' %}"></script>
|
||||||
|
<link rel="stylesheet" href="{% static 'codemirror/lib/codemirror.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'codemirror/theme/monokai.css' %}">
|
||||||
|
<script src="{% static 'codemirror/mode/yaml/yaml.js' %}"></script>
|
||||||
|
|
||||||
|
<div class="dropdown" style="display: inline-block;">
|
||||||
|
<button class="btn btn-default btn-sm dropdown-toggle" type="button" id="setupDropdown-{{ provider.pk }}" data-toggle="dropdown">
|
||||||
|
{% trans 'Setup with...' %}
|
||||||
|
<span class="caret"></span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu" role="menu" aria-labelledby="setupDropdown-{{ provider.pk }}">
|
||||||
|
<li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#docker-compose-{{ provider.pk }}">{% trans 'docker-compose' %}</a></li>
|
||||||
|
<li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#k8s-{{ provider.pk }}">{% trans 'Kubernetes' %}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="docker-compose-{{ provider.pk }}" tabindex="-1" role="dialog" aria-labelledby="{{ provider.pk }}Label" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true" aria-label="Close">
|
||||||
|
<span class="pficon pficon-close"></span>
|
||||||
|
</button>
|
||||||
|
<h4 class="modal-title" id="{{ provider.pk }}Label">{% trans 'Setup with docker-compose' %}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
{% trans 'Add the following snippet to your docker-compose file.' %}
|
||||||
|
<textarea class="codemirror">version: "3.5"
|
||||||
|
|
||||||
|
services:
|
||||||
|
passbook_gatekeeper:
|
||||||
|
container_name: gatekeeper
|
||||||
|
image: docker.beryju.org/passbook/gatekeeper:{{ version }}
|
||||||
|
ports:
|
||||||
|
- 4180:4180
|
||||||
|
environment:
|
||||||
|
OAUTH2_PROXY_CLIENT_ID: {{ provider.client.client_id }}
|
||||||
|
OAUTH2_PROXY_CLIENT_SECRET: {{ provider.client.client_secret }}
|
||||||
|
OAUTH2_PROXY_REDIRECT_URL: https://{{ provider.host }}/oauth2/callback
|
||||||
|
OAUTH2_PROXY_OIDC_ISSUER_URL: https://{{ request.META.host }}/application/oidc
|
||||||
|
OAUTH2_PROXY_COOKIE_SECRET: {{ cookie_secret }}
|
||||||
|
OAUTH2_PROXY_UPSTREAM: http://{{ provider.host }}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" data-dismiss="modal">{% trans 'Close' %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal fade" id="k8s-{{ provider.pk }}" tabindex="-1" role="dialog" aria-labelledby="{{ provider.pk }}Label" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true" aria-label="Close">
|
||||||
|
<span class="pficon pficon-close"></span>
|
||||||
|
</button>
|
||||||
|
<h4 class="modal-title" id="{{ provider.pk }}Label">{% trans 'Setup with Kubernetes' %}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>{% trans 'Download the manifest to create the Gatekeeper deployment and service:' %}</p>
|
||||||
|
<a href="{% url 'passbook_providers_app_gw:k8s-manifest' provider=provider.pk %}">{% trans 'Here' %}</a>
|
||||||
|
<p>{% trans 'Afterwards, add the following annotations to the Ingress you want to secure:' %}</p>
|
||||||
|
<textarea class="codemirror">
|
||||||
|
nginx.ingress.kubernetes.io/auth-url: "https://{{ provider.host }}/oauth2/auth"
|
||||||
|
nginx.ingress.kubernetes.io/auth-signin: "https://{{ provider.host }}/oauth2/start?rd=$escaped_request_uri"
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" data-dismiss="modal">{% trans 'Close' %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let attributes = document.getElementsByClassName('codemirror');
|
||||||
|
for (let attrib of attributes) {
|
||||||
|
let myCodeMirror = CodeMirror.fromTextArea(attrib, {
|
||||||
|
mode: 'yaml',
|
||||||
|
theme: 'monokai',
|
||||||
|
lineNumbers: false,
|
||||||
|
readOnly: true,
|
||||||
|
autoRefresh: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,8 +1,8 @@
|
||||||
"""passbook app_gw urls"""
|
"""passbook app_gw urls"""
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from passbook.providers.app_gw.views import NginxCheckView
|
from passbook.providers.app_gw.views import K8sManifestView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('nginx/', NginxCheckView.as_view())
|
path('<int:provider>/k8s-manifest/', K8sManifestView.as_view(), name='k8s-manifest'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,49 +1,33 @@
|
||||||
"""passbook app_gw views"""
|
"""passbook app_gw views"""
|
||||||
from urllib.parse import urlparse
|
import string
|
||||||
|
from random import SystemRandom
|
||||||
|
|
||||||
from django.conf import settings
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.core.cache import cache
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.shortcuts import get_object_or_404, render
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from passbook.core.views.access import AccessMixin
|
from passbook import __version__
|
||||||
from passbook.providers.app_gw.models import ApplicationGatewayProvider
|
from passbook.providers.app_gw.models import ApplicationGatewayProvider
|
||||||
|
|
||||||
ORIGINAL_URL = 'HTTP_X_ORIGINAL_URL'
|
ORIGINAL_URL = 'HTTP_X_ORIGINAL_URL'
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
def cache_key(session_cookie: str, request: HttpRequest) -> str:
|
def get_cookie_secret():
|
||||||
"""Cache Key for request fingerprinting"""
|
"""Generate random 50-character string for cookie-secret"""
|
||||||
fprint = '_'.join([
|
return ''.join(SystemRandom().choice(
|
||||||
session_cookie,
|
string.ascii_uppercase + string.digits) for _ in range(50))
|
||||||
request.META.get('HTTP_HOST'),
|
|
||||||
request.META.get('PATH_INFO'),
|
|
||||||
])
|
|
||||||
return f"app_gw_{fprint}"
|
|
||||||
|
|
||||||
class NginxCheckView(AccessMixin, View):
|
|
||||||
"""View used by nginx's auth_request module"""
|
|
||||||
|
|
||||||
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
class K8sManifestView(LoginRequiredMixin, View):
|
||||||
session_cookie = request.COOKIES.get(settings.SESSION_COOKIE_NAME, '')
|
"""Generate K8s Deployment and SVC for gatekeeper"""
|
||||||
_cache_key = cache_key(session_cookie, request)
|
|
||||||
if cache.get(_cache_key):
|
def get(self, request: HttpRequest, provider: int) -> HttpResponse:
|
||||||
return HttpResponse(status=202)
|
"""Render deployment template"""
|
||||||
parsed_url = urlparse(request.META.get(ORIGINAL_URL))
|
provider = get_object_or_404(ApplicationGatewayProvider, pk=provider)
|
||||||
# request.session[AuthenticationView.SESSION_ALLOW_ABSOLUTE_NEXT] = True
|
return render(request, 'app_gw/k8s-manifest.yaml', {
|
||||||
# request.session[AuthenticationView.SESSION_FORCE_COOKIE_HOSTNAME] = parsed_url.hostname
|
'provider': provider,
|
||||||
if not request.user.is_authenticated:
|
'cookie_secret': get_cookie_secret(),
|
||||||
return HttpResponse(status=401)
|
'version': __version__
|
||||||
matching = ApplicationGatewayProvider.objects.filter(
|
}, content_type='text/yaml')
|
||||||
server_name__contains=[parsed_url.hostname])
|
|
||||||
if not matching.exists():
|
|
||||||
LOGGER.debug("Couldn't find matching application", host=parsed_url.hostname)
|
|
||||||
return HttpResponse(status=403)
|
|
||||||
application = self.provider_to_application(matching.first())
|
|
||||||
has_access, _ = self.user_has_access(application, request.user)
|
|
||||||
if has_access:
|
|
||||||
cache.set(_cache_key, True)
|
|
||||||
return HttpResponse(status=202)
|
|
||||||
LOGGER.debug("User not passing", user=request.user)
|
|
||||||
return HttpResponse(status=401)
|
|
||||||
|
|
Reference in a new issue