Merge branch 'agw'
This commit is contained in:
commit
089b48aad1
|
@ -116,6 +116,18 @@ build-passbook-static:
|
|||
services:
|
||||
- postgres: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:
|
||||
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"""
|
||||
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.viewsets import ModelViewSet
|
||||
|
||||
from passbook.providers.app_gw.models import ApplicationGatewayProvider
|
||||
from passbook.providers.oidc.api import OpenIDProviderSerializer
|
||||
|
||||
|
||||
class ApplicationGatewayProviderSerializer(ModelSerializer):
|
||||
"""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:
|
||||
|
||||
model = ApplicationGatewayProvider
|
||||
fields = ['pk', 'server_name', 'upstream', 'enabled', 'authentication_header',
|
||||
'default_content_type', 'upstream_ssl_verification', 'property_mappings']
|
||||
fields = ['pk', 'name', 'host', 'client']
|
||||
read_only_fields = ['client']
|
||||
|
||||
class ApplicationGatewayProviderViewSet(ModelViewSet):
|
||||
"""ApplicationGatewayProvider Viewset"""
|
||||
|
|
|
@ -1,67 +1,37 @@
|
|||
"""passbook Application Security Gateway Forms"""
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django import forms
|
||||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||
from django.forms import ValidationError
|
||||
from django.utils.translation import gettext as _
|
||||
from oauth2_provider.generators import (generate_client_id,
|
||||
generate_client_secret)
|
||||
from oidc_provider.models import Client
|
||||
|
||||
from passbook.lib.fields import DynamicArrayField
|
||||
from passbook.providers.app_gw.models import (ApplicationGatewayProvider,
|
||||
RewriteRule)
|
||||
from passbook.providers.app_gw.models import ApplicationGatewayProvider
|
||||
|
||||
|
||||
class ApplicationGatewayProviderForm(forms.ModelForm):
|
||||
"""Security Gateway Provider form"""
|
||||
|
||||
def clean_server_name(self):
|
||||
"""Check if server_name is in DB already, since
|
||||
Postgres ArrayField doesn't suppport keys."""
|
||||
current = self.cleaned_data.get('server_name')
|
||||
if ApplicationGatewayProvider.objects \
|
||||
.filter(server_name__overlap=current) \
|
||||
.exclude(pk=self.instance.pk).exists():
|
||||
raise ValidationError(_("Server Name already in use."))
|
||||
return current
|
||||
|
||||
def clean_upstream(self):
|
||||
"""Check that upstream begins with http(s)"""
|
||||
for upstream in self.cleaned_data.get('upstream'):
|
||||
_parsed_url = urlparse(upstream)
|
||||
|
||||
if _parsed_url.scheme not in ('http', 'https'):
|
||||
raise ValidationError(_("URL Scheme must be either http or https"))
|
||||
return self.cleaned_data.get('upstream')
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.instance.pk:
|
||||
# New instance, so we create a new OIDC client with random keys
|
||||
self.instance.client = Client.objects.create(
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret())
|
||||
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().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = ApplicationGatewayProvider
|
||||
fields = ['server_name', 'upstream', 'enabled', 'authentication_header',
|
||||
'default_content_type', 'upstream_ssl_verification', 'property_mappings']
|
||||
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']
|
||||
fields = [
|
||||
'name', 'host'
|
||||
]
|
||||
widgets = {
|
||||
'name': forms.TextInput(),
|
||||
'match': forms.TextInput(attrs={'data-is-monospace': True}),
|
||||
'replacement': forms.TextInput(attrs={'data-is-monospace': True}),
|
||||
'conditions': FilteredSelectMultiple(_('Conditions'), False)
|
||||
'host': forms.TextInput(),
|
||||
}
|
||||
|
|
|
@ -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"""
|
||||
import re
|
||||
import string
|
||||
from random import SystemRandom
|
||||
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db import models
|
||||
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):
|
||||
"""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())
|
||||
upstream = ArrayField(models.TextField())
|
||||
enabled = models.BooleanField(default=True)
|
||||
name = models.TextField()
|
||||
host = models.TextField()
|
||||
|
||||
authentication_header = models.TextField(default='X-Remote-User', blank=True)
|
||||
default_content_type = models.TextField(default='application/octet-stream')
|
||||
upstream_ssl_verification = models.BooleanField(default=True)
|
||||
client = models.ForeignKey(Client, on_delete=models.CASCADE)
|
||||
|
||||
form = 'passbook.providers.app_gw.forms.ApplicationGatewayProviderForm'
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""since this model has no name property, return a joined list of server_names as name"""
|
||||
return ', '.join(self.server_name)
|
||||
def html_setup_urls(self, request):
|
||||
"""return template and context modal with URLs for authorize, token, openid-config, etc"""
|
||||
cookie_secret = ''.join(SystemRandom().choice(
|
||||
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):
|
||||
return "Application Gateway %s" % ', '.join(self.server_name)
|
||||
return f"Application Gateway {self.name}"
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _('Application Gateway Provider')
|
||||
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"""
|
||||
from django.urls import path
|
||||
|
||||
from passbook.providers.app_gw.views import NginxCheckView
|
||||
from passbook.providers.app_gw.views import K8sManifestView
|
||||
|
||||
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"""
|
||||
from urllib.parse import urlparse
|
||||
import string
|
||||
from random import SystemRandom
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.views import View
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.views.access import AccessMixin
|
||||
from passbook import __version__
|
||||
from passbook.providers.app_gw.models import ApplicationGatewayProvider
|
||||
|
||||
ORIGINAL_URL = 'HTTP_X_ORIGINAL_URL'
|
||||
LOGGER = get_logger()
|
||||
|
||||
def cache_key(session_cookie: str, request: HttpRequest) -> str:
|
||||
"""Cache Key for request fingerprinting"""
|
||||
fprint = '_'.join([
|
||||
session_cookie,
|
||||
request.META.get('HTTP_HOST'),
|
||||
request.META.get('PATH_INFO'),
|
||||
])
|
||||
return f"app_gw_{fprint}"
|
||||
def get_cookie_secret():
|
||||
"""Generate random 50-character string for cookie-secret"""
|
||||
return ''.join(SystemRandom().choice(
|
||||
string.ascii_uppercase + string.digits) for _ in range(50))
|
||||
|
||||
class NginxCheckView(AccessMixin, View):
|
||||
"""View used by nginx's auth_request module"""
|
||||
|
||||
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||
session_cookie = request.COOKIES.get(settings.SESSION_COOKIE_NAME, '')
|
||||
_cache_key = cache_key(session_cookie, request)
|
||||
if cache.get(_cache_key):
|
||||
return HttpResponse(status=202)
|
||||
parsed_url = urlparse(request.META.get(ORIGINAL_URL))
|
||||
# request.session[AuthenticationView.SESSION_ALLOW_ABSOLUTE_NEXT] = True
|
||||
# request.session[AuthenticationView.SESSION_FORCE_COOKIE_HOSTNAME] = parsed_url.hostname
|
||||
if not request.user.is_authenticated:
|
||||
return HttpResponse(status=401)
|
||||
matching = ApplicationGatewayProvider.objects.filter(
|
||||
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)
|
||||
class K8sManifestView(LoginRequiredMixin, View):
|
||||
"""Generate K8s Deployment and SVC for gatekeeper"""
|
||||
|
||||
def get(self, request: HttpRequest, provider: int) -> HttpResponse:
|
||||
"""Render deployment template"""
|
||||
provider = get_object_or_404(ApplicationGatewayProvider, pk=provider)
|
||||
return render(request, 'app_gw/k8s-manifest.yaml', {
|
||||
'provider': provider,
|
||||
'cookie_secret': get_cookie_secret(),
|
||||
'version': __version__
|
||||
}, content_type='text/yaml')
|
||||
|
|
Reference in a new issue