Skip to content
Snippets Groups Projects
Commit 47e89871 authored by Mart van Santen's avatar Mart van Santen
Browse files

Removed old panels and update gitlab ci to stop building those

parent 89c1aa2c
No related branches found
No related tags found
2 merge requests!68Merge loginpanel into main and release 0.5.0,!56Resolve "Remove old panel from code"
Pipeline #9714 failed
Showing
with 4 additions and 564 deletions
......@@ -9,39 +9,6 @@ stages:
- application-test
- integration-test
consent_provider:
stage: build
variables:
KANIKO_CONTEXT: "consent_provider"
KANIKO_BUILD_IMAGENAME: $CI_JOB_NAME
extends: .kaniko_build
only:
changes:
- consent_provider/**/*
- .gitlab-ci.yml
logout_provider:
stage: build
variables:
KANIKO_CONTEXT: "logout_provider"
KANIKO_BUILD_IMAGENAME: $CI_JOB_NAME
extends: .kaniko_build
only:
changes:
- logout_provider/**/*
- .gitlab-ci.yml
login_provider:
stage: build
variables:
KANIKO_CONTEXT: "login_provider"
KANIKO_BUILD_IMAGENAME: $CI_JOB_NAME
extends: .kaniko_build
only:
changes:
- login_provider/**/*
- .gitlab-ci.yml
integration_test_app:
stage: build-test-images
variables:
......
[submodule "user-panel"]
path = user-panel
url = https://open.greenhost.net/stackspin/user-panel.git
FROM python:3.9-alpine
RUN apk add gcc libc-dev libffi-dev
WORKDIR /usr/src/app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ARG FLASK_PORT=5001
EXPOSE $FLASK_PORT
ENV FLASK_ENV production
ENV FLASK_RUN_PORT $FLASK_PORT
ENV FLASK_RUN_HOST 0.0.0.0
ENV HYDRA_ADMIN_URL http://localhost:4445
CMD [ "flask", "run" ]
from flask import abort, Flask, redirect, request
from flask.views import View
from os import urandom, environ
from hydra_client import HydraAdmin
import hydra_client
from db import User, BackendConnectionError
import logging
HYDRA_ADMIN_URL = environ['HYDRA_ADMIN_URL']
HYDRA = HydraAdmin(HYDRA_ADMIN_URL)
app = Flask(__name__)
app.logger.setLevel(logging.INFO)
@app.route('/consent', methods=['GET'])
def home():
"""Checks user app permission
Checks user app permission by loading a consent object via the Hydra admin API and
validating that the user triggering the request has sufficient permissions by querying
the GraphQL API. If the user is allowed to use the app the request is accepted and openID
claims are sent to Hydra.
Args:
consent_challenge: Reference to a consent challenge object in form of an alphanumeric
String. Can be used to retrieve the consent challenge object via the Hydra Admin API (GET)
Returns:
Redirect to the url that is provided by the consent challenge object.
"""
challenge = request.args.get("consent_challenge")
if not challenge:
abort(403)
try:
consent_request = HYDRA.consent_request(challenge)
except hydra_client.exceptions.NotFound:
app.logger.error("Not Found. Consent request not found. challenge={0}".format(challenge))
abort(404)
except hydra_client.exceptions.HTTPError:
app.logger.error("Conflict. Consent request has been used already. challenge={0}".format(challenge))
abort(503)
app_name = consent_request.client.client_name
username = consent_request.subject
try:
user = User(username)
except BackendConnectionError as error:
app.logger.error(
"Retrieving user object from GraphQL server failed {0}".format(error))
return redirect(consent_request.reject(
"Permission denied",
error_description="Login request was denied due to an internal server error"))
access_granted = user.has_app_permission(app_name)
if access_granted:
app.logger.info("{0} was granted access to {1}".format(username, app_name))
session = user.get_oauth_session()
return redirect(consent_request.accept(
grant_scope=consent_request.requested_scope,
grant_access_token_audience=consent_request.requested_access_token_audience,
session=session,
))
app.logger.info("{0} was denied access to {1}".format(username, app_name))
return redirect(consent_request.reject(
"Permission denied",
error_description="Login request was denied due to missing application permission"))
if __name__ == '__main__':
app.run()
from os import environ
from hydra_client import HydraAdmin
from graphqlclient import GraphQLClient
import urllib
import json
GRAPHQL_URL = environ['GRAPHQL_URL']
GRAPHQL_CLIENT = GraphQLClient(GRAPHQL_URL)
class User():
def __init__(self, username):
self.username = username
try:
self._load_remote_user_info()
except urllib.error.HTTPError as error:
raise BackendConnectionError(
error.code,
error.headers,
("Error during retrieval of userdata - " + error.reason))
def _load_remote_user_info(self):
querystring = '''{{
getUser(username: "{0}"){{
email,
applications{{
edges{{
node{{
name
}}
}}
}},
roles{{
edges{{
node{{
name
}}
}}
}}
}}}}'''.format(self.username)
result = json.loads(GRAPHQL_CLIENT.execute(querystring))
data = result["data"]["getUser"]
self.applications = list(map(lambda x: x["node"]["name"],
data["applications"]["edges"]))
self.roles = list(map(lambda x: x["node"]["name"],
data["roles"]["edges"]))
self.email = data["email"]
def has_app_permission(self, appname):
return appname in self.applications
def get_oauth_session(self):
"""Create openID Connect token
Use the userdata stored in the user object to create an OpenID Connect token.
The token will be passed to Hydra, which will store it and serve it to all OpenID Connect
Clients, that successfully query the /userinfo endpoint. Every field in the "id_token"
dictionary can be accessed through standard scopes and claims.
See https://openid.net/specs/openid-connect-core-1_0.html#Claims
Returns:
OpenID Connect token of type dict
"""
return {
"id_token": {
"name": self.username,
"preferred_username": self.username,
"email" : self.email,
"stackspin_roles": self.roles}
}
class BackendConnectionError(Exception):
"""Raised when requests to the backend server fail
Attributes:
code -- http response code
headers -- http response headers
reason -- reson for the error
"""
def __init__(self, code, headers, reason):
self.code = code
self.headers = headers
self.reason = reason
Flask
hydra-client
graphqlclient
<!DOCTYPE html>
<html lang="en">
<head>
<title>{%block title %}python-login-consent{% endblock %}</title>
</head>
<body>
{% block content %}
<h1>Welcome to the Python login/consent app for Hydra</h1>
{% endblock %}
</body>
</html>
{% extends "base.html" %}
{% block title %}Consent{% endblock %}
{% block content %}
<dl>
<dt>User</dt>
<dd>{{ user }}</dd>
<dt>Client</dt>
<dd>{{ client }}</dd>
</dl>
<form method="POST" action="/consent">
{{ form.hidden_tag() }}
{{ form.accept }}
</form>
{% endblock %}
......@@ -27,23 +27,6 @@ services:
- LOG_LEVEL=debug
- LOG_LEAK_SENSITIVE_VALUES=true
restart: unless-stopped
consent:
build: consent_provider/
environment:
- HYDRA_ADMIN_URL=http://hydra:4445
- GRAPHQL_URL=http://backend:5000/graphql
- FLASK_ENV=development
ports:
- "5001:5001"
restart: unless-stopped
logout:
build: logout_provider/
environment:
- HYDRA_ADMIN_URL=http://hydra:4445
- FLASK_ENV=development
ports:
- "5002:5002"
restart: unless-stopped
login:
build: login_provider/
environment:
......@@ -53,18 +36,6 @@ services:
ports:
- "5000:5000"
restart: unless-stopped
backend:
build: user-panel/backend/
environment:
- DEBUG=True
- HYDRA_ADMIN_URL=http://hydra:4445
- DATABASE_USER=postgres
- DATABASE_PASSWORD=secret
- DATABASE_NAME=postgres
- DATABASE_HOST=psql
ports:
- "5003:5000"
restart: unless-stopped
psql:
image: postgres:11
environment:
......
File moved
......@@ -7,13 +7,13 @@ Welcome to single-sign-on's documentation!
==========================================
This project provides a single sign on solution based on
[hydra](https://github.com/ory/hydra) and in combination with a [user
panel](https://open.greenhost.net/stackspin/user-panel).
[hydra](https://github.com/ory/hydra), [kratos](https://github.com/ory/kratos)
and in combination with a [login panel](https://open.greenhost.net/stackspin/single-sign-on).
.. toctree::
:maxdepth: 2
:caption: Contents:
usage
helmchart
local_development
development
# Usage
## Installlation instructions
Clone the repo and make sure to also fetch the submodules.
```
git submodule update --init
```
Installation should be done via the helm using the helmchart contained in
`./helmchart`. Make sure to edit the values in
`./helmchart/single-sign-on/values.yaml` according to your needs. To install the
helm chart, make sure Helm is properly set up in your Kubernetes cluster, then:
```
cd helmchart # Enter the helm chart directory
helm dep update # Download helm charts this chart relies on
helm install . # Run the installation command
```
For Details on how to configure the chart. Refer to [the Helm chart
chapter](helmchart)
### Managing users
`single-sign-on` includese a simple `user-panel` to manage users. To read more
about this, see the [user panel
documentation](https://docs.stackspin.net/projects/user-panel/)
export FLASK_SECRET_KEY="CHANGEME"
export FLASK_RUN_HOST="0.0.0.0"
export FLASK_RUN_PORT="5000"
export GRAPHQL_URL="http://localhost:5002/graphql"
export HYDRA_ADMIN_URL="http://localhost:4445"
export FLASK_ENV=development
FROM python:3.9-alpine
RUN apk add gcc libc-dev libffi-dev g++
WORKDIR /usr/src/app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
ENV FLASK_ENV production
ENV FLASK_RUN_HOST 0.0.0.0
ENV FLASK_RUN_PORT 5000
ENV HYDRA_ADMIN_URL http://localhost:4445
ENV GRAPHQL_URL http://localhost:5002/graphql
CMD [ "flask", "run" ]
# Configuration
To enable the `debug` mode, set the environment variable `FLASK_ENV` to `development`.
```
export FLASK_ENV=development
# or
docker login-provider:latest build . && docker run -e FLASK_ENV=development login-provider
```
You can do the same with the following variables.
* **FLASK_SECRET_KEY** A secret key that will be used for securely signing the session cookie.
* **FLASK_RUN_HOST** IP Address that the server will open a socket on.
*Default*: 0.0.0.0
* **FLASK_RUN_PORT** Port of the socket that the server will listen on.
*Default*: 5000
* **GRAPHQL_URL** URL to the server that runs the graphql backend API
*Default*: http://localhost:5002/graphql
* **HYDRA_ADMIN_URL** URl to the Hydra admin server
*Default*: http://localhost:4445
from flask import abort, Flask, redirect, request, render_template
from os import urandom, environ
from hydra_client import HydraAdmin
import hydra_client
from db import User, BackendConnectionError
from forms import LoginForm
import logging
HYDRA_ADMIN_URL = environ['HYDRA_ADMIN_URL']
HYDRA = HydraAdmin(HYDRA_ADMIN_URL)
app = Flask(__name__)
app.config['SECRET_KEY'] = urandom(16)
app.debug = True if "FLASK_ENV" in environ and environ["FLASK_ENV"] == "development" else False
app.logger.setLevel(logging.INFO)
@app.route('/login', methods=['GET', 'POST'])
def login():
"""Provides login form and handles login attempt
Args:
login_form: contains login data submitted by a user (POST)
challenge: id that identifies the request from oauth client. passed by hydra
Returns:
Error page if no challenge id is present
or Login Form if user hasn't authenticated
or redirect to callback url provided by hydra if login was successful
"""
login_form = LoginForm()
redirect_to = None
# Retrieve the challenge id from the request. Depending on the method it is saved in the
# form (POST) or in a GET variable.
if request.method == 'GET':
challenge = request.args.get("login_challenge")
if not challenge:
return abort(400)
elif login_form.validate_on_submit():
challenge = login_form.challenge.data
# Now that we have the challenge id, we can request the challenge object from the hydra
# admin API
try:
login_request = HYDRA.login_request(challenge)
except hydra_client.exceptions.NotFound:
app.logger.error("Not Found. Login request not found. challenge={0}".format(challenge))
abort(404)
except hydra_client.exceptions.HTTPError:
app.logger.error("Conflict. Login request has been used already. challenge={0}".format(challenge))
abort(503)
# We need to decide here whether we want to accept or decline the login request.
# if a login form was submitted, we need to confirm that the userdata, the agent
# send us via POST is valid
if login_form.validate_on_submit():
try:
user = User(login_form.username.data)
except BackendConnectionError as error:
app.logger.error(
"Retrieving user object from GraphQL server failed {0}".format(error))
return redirect(login_request.reject(
"Login denied",
error_description="Login request was denied due to an internal server error"))
if user.authenticate(login_form.password.data):
redirect_to = login_request.accept(
user.username,
remember=login_form.remember.data,
# Remember session for 12h
remember_for=60*60*12)
app.logger.info("{0} logged in successfully".format(user.username))
else:
redirect_to = login_request.reject(
"Login denied",
error_description="Invalid username or password")
app.logger.info("{0} failed to login".format(user.username))
return redirect(redirect_to)
# Skip, if true, let's us know that Hydra has already successfully authenticated
# the user. we don't need to check anything and we can accept the request right away.
elif login_request.skip:
skip = request.args.get("skip")
logout = request.args.get("logout")
if skip:
app.logger.info("{0} is already logged in. Skip authentication".format(login_request.subject))
return redirect(login_request.accept(login_request.subject))
elif logout:
login_form.challenge.data = challenge
HYDRA.invalidate_login_sessions(login_request.subject);
return redirect(login_request.reject(
"Login cancelled",
error_description="Login was cancelled and user session was terminated"))
else:
return render_template('skip.html', challenge=challenge, logo=login_request.client.logo_uri, application_name=login_request.client.client_name, username=login_request.subject)
# If Skip is not true and the user has not submitted any data via a form, we need
# to display a login form for the user to type in their username and password.
# as a reference we save the challenge id in a hidden field of the form.
else:
login_form.challenge.data = challenge
return render_template('login.html', login_form=login_form, logo=login_request.client.logo_uri, application_name=login_request.client.client_name)
if __name__ == '__main__':
app.run()
from os import environ
from hydra_client import HydraAdmin
from flask_login import UserMixin
from graphqlclient import GraphQLClient
import urllib
import json
GRAPHQL_URL = environ['GRAPHQL_URL']
graphql_client = GraphQLClient(GRAPHQL_URL)
class User(UserMixin):
def __init__(self, username):
self.id = username
self.username = username
self.active = False
try:
self._load_remote_user_info()
except urllib.error.HTTPError as error:
raise BackendConnectionError(
error.code,
error.headers,
("Error during retrieval of userdata - " + error.reason))
def _load_remote_user_info(self):
"""Loads userdata from remote GraphQL API"""
querystring = '''{{
getUser(username: "{0}"){{
email,
active
}}}}'''.format(self.username)
result = json.loads(graphql_client.execute(querystring))
if "data" in result and result["data"]["getUser"] is not None:
self.active = result["data"]["getUser"]["active"]
self.email = result["data"]["getUser"]["email"]
def _verify_password(self, password):
"""Verifies cleartext password
Sends the cleartext `password` to the GraphQL API
which verifies the password by hashing it and comparing it to a stored password
hash.
Args:
password: cleartext password
Returns:
Boolean result of password verification
"""
password = password.replace('"', '\\"')
querystring = '''{{
verifyPassword(
username: "{0}",
password: "{1}")
}}'''.format(self.username, password)
result = json.loads(graphql_client.execute(querystring))
verified = False
if "data" in result:
verified = result["data"]["verifyPassword"]
return verified
def authenticate(self, password):
return self.active and self._verify_password(password)
class BackendConnectionError(Exception):
"""Raised when requests to the backend server fail
Attributes:
code -- http response code
headers -- http response headers
reason -- reson for the error
"""
def __init__(self, code, headers, reason):
self.code = code
self.headers = headers
self.reason = reason
from wtforms import SubmitField, StringField, PasswordField, BooleanField, HiddenField, validators
from flask_wtf import FlaskForm
class LoginForm(FlaskForm):
username = StringField("Username", validators=[validators.input_required()],)
password = PasswordField("Password", validators=[validators.input_required()])
challenge = HiddenField("challenge")
remember = BooleanField("Remember me")
submit = SubmitField("Sign in")
Flask
flask-wtf
flask-login
hydra-client
Flask-SQLAlchemy
bcrypt
graphqlclient
<!doctype html>
<title>Stackspin authentication service</title>
<div style='margin: 0 auto ; width: 350px; padding:20px; border-style:solid; border-color:#6c757d; border-width: 1px; background-color: #f8f9fa; font-family: "Segoe UI", Roboto; font-family: "Helvetica Neue", Arial; font-family: "Noto Sans", sans-serif;'>
{% if logo %}
<div style="position:relative; width: 350px; height:100px">
<img style="overflow: auto; top: 0; left: 0; bottom: 0; right: 0; position: absolute; margin: auto;max-width: 300px; max-height: 100px" src="{{logo}}" alt="Logo of application"></img>
</div>
{% endif %}
<h1>Log in to {{ application_name }}</h1>
<form method="POST" action="/login">
{{ login_form.csrf_token }}
{{ login_form.challenge }}
{{ login_form.username(placeholder="Username", autofocus=true) }}<br>
<div style="margin-top:5px">{{ login_form.password(placeholder="Password") }}</div> <br>
{{ login_form.remember }}{{ login_form.remember.label }} <br>
<div style="margin-top:5px">{{ login_form.submit }}</div>
</form>
</div>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment