Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • acides/hiboo
  • frju365/hiboo
  • pascoual/hiboo
  • thedarky/hiboo
  • jeremy/hiboo
  • cyrinux/hiboo
  • a.f/hiboo
  • mickge/hiboo
  • llaq/hiboo
  • vaguelysalaried/hiboo
  • felinn-glotte/hiboo
  • AntoninDelFabbro/hiboo
  • docemmetbrown/hiboo
13 results
Show changes
Showing
with 832 additions and 4 deletions
<p>PeerTube supports OIDC authentication using the official
<a href="https://framagit.org/framasoft/peertube/official-plugins/-/tree/master/peertube-plugin-auth-openid-connect">
auth-openid-connect</a>.</p>
<p>Once the plugin is installed, to configure OIDC authentication, you should fill the following settings:</p>
<dl>
<dt>Discover URL</dt>
<dd><code>{{ url_for("sso.oidc_discovery", service_uuid=service.uuid, _external=True) }}</code></dd>
<dt>Client ID</dt>
<dd><code>{{ service.config["client_id"] }}</code></dd>
<dt>Client secret</dt>
<dd><code>{{ service.config["client_secret"] }}</code></dd>
<dt>Scope</dt>
<dd><code>openid email profile</code></dd>
<dt>Username property</dt>
<dd><code>preferred_username</code></dd>
<dt>Email property</dt>
<dd><code>email</code></dd>
</dl>
{% include "application_oidc.html" %}
{% extends "base.html" %}
{% block title %}
{% trans %}Select application type{% endtrans %}
{% endblock %}
{% block content %}
<div class="row g-3">
{% for application_id, application in applications.items() %}
<div class="col-md-4 col-s-6 col">
<div class="card">
<div class="card-header text-white"
style="background-color: var(--bs-{{ macros.colors[loop.index0 % 7] }}">
<h3>{{ application.name }}</h3>
</div>
<div class="card-body">
<p class="card-text">{{ application.__doc__ }}</p>
<a href="{{ url_for(route, application_id=application_id, **kwargs) }}"
class="btn btn-secondary">{% trans %}Select{% endtrans %}</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
<h4>Detailed SAML settings</h4>
<dl>
<!-- WARNING: do not split those tags into multiple lines, it would break auth tests -->
<dt id="metadata">{% trans %}SAML Metadata{% endtrans %}</dt>
<dd aria-labelledby="metadata"><code>{{ url_for("sso.saml_metadata", service_uuid=service.uuid, _external=True) }}</code></dd>
<dt id="redirect">{% trans %}SSO redirect binding{% endtrans %}</dt>
<dd aria-labelledby="redirect"><code>{{ url_for("sso.saml_redirect", service_uuid=service.uuid, _external=True) }}</code></dd>
<dt id="acs">{% trans %}ACS{% endtrans %}</dt>
<dd aria-labelledby="acs"><code>{{ service.config["acs"] }}</code></dd>
<dt id="idp-cert">{% trans %}IDP certificate{% endtrans %}</dt>
<dd aria-labelledby="idp-cert"><pre class="bg-dark-subtle rounded p-2"><code>{{ service.config["idp_cert"] }}</code></pre></dd>
<dt id="sp-cert">{% trans %}SP certificate{% endtrans %}</dt>
<dd aria-labelledby="sp-cert"><pre class="bg-dark-subtle rounded p-2"><code>{{ service.config["sp_cert"] }}</code></pre></dd>
<dt id="sp-key">{% trans %}SP private key{% endtrans %}</dt>
<dd aria-labelledby="sp-key"><pre class="bg-dark-subtle rounded p-2"><code>{{ service.config["sp_key"] }}</code></pre></dd>
</dl>
<p>Seafile supports OIDC authentication through its Seahub frontend.</p>
<p>In order to enable OIDC, you may add the following settings to your <code>seahub_settings.py</code> file.</p>
<pre class="bg-dark-subtle rounded p-2"><code># Authentication
ENABLE_OAUTH = True
OAUTH_ENABLE_INSECURE_TRANSPORT = True
OAUTH_CLIENT_ID = "{{ service.config["client_id"] }}"
OAUTH_CLIENT_SECRET = "{{ service.config["client_secret"] }}"
OAUTH_REDIRECT_URL = "{{ service.config["redirect_uris"][0] }}"
OAUTH_PROVIDER_DOMAIN = "{{ url_for('account.home', _external=True).split(':')[1].split('/')[0] }}"
OAUTH_AUTHORIZATION_URL = "{{ url_for("sso.oidc_authorize", service_uuid=service.uuid, _external=True) }}"
OAUTH_TOKEN_URL = "{{ url_for("sso.oidc_token", service_uuid=service.uuid, _external=True) }}"
OAUTH_USER_INFO_URL = "{{ url_for("sso.oidc_userinfo", service_uuid=service.uuid, _external=True) }}"
OAUTH_SCOPE = ["openid", "profile", "email"]
OAUTH_ATTRIBUTE_MAP = {
"id": (False, "no_destination"),
"name": (True, "name"),
"email": (True, "email")
}</code></pre>
<h4>Migrating accounts</h4>
<p>If you are running an existing Seafile server, you may import your existing accounts as claimable
profiles under Hiboo.</p>
<p>Accounts are stored in the <i>EmailUser</i> table of the <i>ccnet_db</i> database. However, we recommend
that profiles be named after the username instead of the email address. The following SQL query exports
username, password hash, and user email as alternate claim to a CSV file. It dynamically converts the password
to use a proper crypt context hash identifier, so that Hiboo will recognize the hash.</p>
<pre class="bg-dark-subtle rounded p-2"><code>select
profile.nickname,
user.email,
CONCAT('$pbkdf2-sha256$10000$', SUBSTRING(TO_BASE64(UNHEX(SUBSTRING(user.passwd,20,64))),1,43), '$', SUBSTRING(TO_BASE64(UNHEX(SUBSTRING(user.passwd,85,64))),1,43)) as password
from
ccnet_db.EmailUser as user
left join
seahub_db.profile_profile as profile
on
profile.user=user.email
into
outfile '/tmp/users.csv'
fields terminated by ',';</code></pre>
<p>Please grab the exported CSV file, copy it next to Hiboo, and run
the following command to import these profiles as unclaimed:</p>
<p><code>flask profile csv-unclaimed {{ service.uuid }} /tmp/users.csv</code></p>
{% include "application_oidc.html" %}
<p>Synapse relies on the pysaml2 SAML implementation for SAML2 authentication.</p>
<p>In order to configure SAML for Synapse, you may copy then paste the following
lines directly into your homeserver configuration file.</p>
<pre class="bg-dark-subtle rounded p-2"><code>saml2_config:
enabled: true
sp_config:
metadata:
remote:
- url: {{ url_for("sso.saml_metadata", service_uuid=service.uuid, _external=True) }}</code></pre>
<p>You should also disable password authentication if you wish to avoid desynchronization and username conflicts.</p>
{% include "application_saml.html" %}
<div class="row">
<div class="col-12">
<div class="table-responsive">
<table class="table table-striped table-head-fixed table-hover">
<thead>
<tr>
<th>{% trans %}Media{% endtrans %}</th>
<th>{% trans %}Thumbnail{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for full, thumb in media.items() %}
<tr>
<td>
<a href="{{ full }}">{{ full }}</a>
</td>
<td>
<img src="{{ thumb }}">
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{{ render_form(form) }}
{% if room and room.get("room_id") %}
<div class="row">
<div class="col-md-6">
<a href="{{ url_for("service.action", service_uuid=service.uuid, action="list_media", roomid=room["room_id"]) }}">Media</a>
<dl>
{% for name, value in room.items() %}
<dt>{{ name }}</dt>
<dd>
{{ value }}
</dd>
{% endfor %}
</dl>
</div>
<div class="col-md-6">
<div class="table-responsive p-0">
<table class="table table-striped table-head-fixed text-nowrap">
<thead>
<tr>
<th>{% trans %}Member{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for member in members.get("members", []) %}
<tr>
<td>
<a href="{{ url_for("service.action", service_uuid=service.uuid, action="get_user", mxid=member) }}">{{ member }}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
{{ render_form(form) }}
{% if rooms %}
<div class="row">
<div class="col">
<div class="table-responsive">
<table class="table table-striped table-head-fixed table-hover">
<thead>
<tr>
<th>{% trans %}RoomID{% endtrans %}</th>
<th>{% trans %}Alias{% endtrans %}</th>
<th>{% trans %}Version{% endtrans %}</th>
<th>{% trans %}Name{% endtrans %}</th>
<th>{% trans %}Members (local){% endtrans %}</th>
<th>{% trans %}Properties{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for room in rooms %}
<tr>
<td>
<a href="{{ url_for("service.action", service_uuid=service.uuid, action="get_room", roomid=room["room_id"]) }}">{{ room["room_id"] }}
</a>
</td>
<td>{{ room["canonical_alias"] }}</td>
<td>{{ room["name"] }}</td>
<td>{{ room["version"] }}</td>
<td>{{ room["joined_members"] }} ({{ room["joined_local_members"] }})</td>
<td>{{ room["join_rules"] }}, {{ room["history_visibility"] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
{{ render_form(form) }}
{% if user %}
<div class="row">
<div class="col-md-6">
<p>
<a href="{{ url_for("service.action", service_uuid=service.uuid, action="list_rooms", mxid=user["name"]) }}">Rooms</a>
<a href="{{ url_for("service.action", service_uuid=service.uuid, action="list_media", mxid=user["name"]) }}">Media</a>
</p>
<dl>
{% for name, value in user.items() %}
<dt>{{ name }}</dt>
<dd>
{{ value }}
</dd>
{% endfor %}
</dl>
</div>
<div class="col-md-6">
<div class="table-responsive">
<table class="table table-striped table-head-fixed table-hover">
<thead>
<tr>
<th>{% trans %}Devices{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for device in devices["devices"] %}
<tr>
<td>{{ device["device_id"] }}</td>
<td>{{ device["display_name"] }}</td>
<td>{{ device["last_seen_ip"] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
<div class="card-header">
<h3>Setting up WriteFreely</h3>
</div>
<div class="card-body">
<p>WriteFreely supports OIDC authentication through Oauth2.</p>
<p>In order to configure Oauth2 for WriteFreely, you may copy then paste the
following lines directly into your WriteFreely config.ini and restart.</p>
<pre class="bg-dark-subtle rounded p-2"><code>[oauth.generic]
client_id = {{ service.config["client_id"] }}
client_secret = {{ service.config["client_secret"] }}
host = {{ url_for('account.home', _external=True).split('/account')[0] }}
display_name = Hiboo
callback_proxy =
callback_proxy_api =
token_endpoint = {{ url_for("sso.oidc_token", service_uuid=service.uuid, _external=False) }}
inspect_endpoint = {{ url_for("sso.oidc_userinfo", service_uuid=service.uuid, _external=False) }}
auth_endpoint = {{ url_for("sso.oidc_authorize", service_uuid=service.uuid, _external=False) }}</code></pre>
<h4>Migrating accounts</h4>
<p>If you are running an existing WriteFreely server, you may import your
existing accounts as claimable profiles under Hiboo.</p>
<p>Accounts are stored in the <code>users</code> table of the database.
The following SQL query exports username and password hash to a CSV file.
Hiboo will recognize the hash as it use a proper crypt context hash identifier</p>
<pre class="bg-dark-subtle rounded p-2"><code>use writefreely;
select
username, password from users
into
outfile 'usersWF.csv'
fields terminated by ',';</code></pre>
<p>Run the following command to import these profiles as unclaimed:</p>
<p><code>flask profile csv-unclaimed {{ service.uuid }} /var/lib/mysql/writefreely/usersWF.csv</code></p>
<p>Now we need to attach created unclaimed profiles to Writefreely users (as writefreely don't use Oauth2
profile name but only profile uuid).<br>Click on "View profiles" on top of this page and click on
"Export unclaimed profiles", save the csv file in <code>/var/lib/mysql/writefreely/unclaimedWF.csv</code>.
Use this script on Writefreely database:</p>
<pre class="bg-dark-subtle rounded p-2"><code>use writefreely;
CREATE TABLE tmp_hiboo (
service_client_id VARCHAR(36) NOT NULL,
profile_name VARCHAR(36) NOT NULL,
profile_uuid VARCHAR(36) NOT NULL,
PRIMARY KEY (profile_uuid)
);
LOAD DATA INFILE 'unclaimedWF.csv'
INTO TABLE tmp_hiboo
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 ROWS;
INSERT INTO oauth_users (user_id, remote_user_id, provider, client_id)
SELECT wf.id, h.profile_uuid, 'generic', h.service_client_id
FROM tmp_hiboo h
left join
users wf
ON h.profile_name = wf.username
where NOT EXISTS
(
SELECT 1
FROM oauth_users oau
WHERE wf.id = oau.user_id
);
drop table tmp_hiboo;</code></pre>
{% include "application_oidc.html" %}
</div>
File moved
from wtforms import widgets
from hiboo import captcha
from hiboo.captcha import fields
from flask_babel import lazy_gettext as _
import random
import string
import io
import os
import base64
import markupsafe
from PIL import Image, ImageDraw, ImageFont
class DummyCaptchaField(fields.CaptchaField):
""" Dummy captcha that displays a number and checks for it
This is meant as an example of a captcha implementation.
"""
# This is the rendered widget. If you wish something more specific, you
# may use custom widgets or implement __call__() for the field to render
# HTML directly
widget = widgets.TextInput()
def _value(self):
""" This is required by the TextInput widget (displayed value)
"""
return ""
def challenge(self):
""" Generate a challenge, here we generate a random integer and update
the label accordingly
"""
value = random.randint(100,1000)
self.label = "Please write down the number {}".format(value)
return value
def check(self, challenge, form, field):
""" Check that the field was properly filled with the challenge
"""
return (
field.data.isdigit() and
challenge == int(field.data)
)
class ImageCaptchaField(fields.CaptchaField):
""" Image-based captcha base class, displays an image and requests that the
user inputs some text deduced from the image.
"""
widget = widgets.TextInput()
def __init__(self, *args, **kwargs):
font_path = os.path.join(captcha.__path__[0], "captcha.ttf")
self.font = ImageFont.truetype(font_path, 20)
self.bgcolor = "#ffffff"
self.fgcolor = "#000000"
super(ImageCaptchaField, self).__init__(*args, **kwargs)
def __call__(self, **kwargs):
png = io.BytesIO()
self.make_image(self.context["challenge"]).save(png, "PNG")
output = base64.b64encode(png.getvalue()).decode("ascii")
return markupsafe.Markup(
'<p><img src="data:image/png;base64,{}"></p>'.format(output)
) + super(ImageCaptchaField, self).__call__(**kwargs)
def get_size(self, text, scale=(1, 1)):
left, top, right, bottom = self.font.getbbox(text)
sx, sy = scale
return (int(sx * (right-left)), int(sy * (bottom-top)))
def make_char(self, char, rotation_range=15):
""" Renders a character
"""
drawn = " {} ".format(char)
rotation = random.randrange(-rotation_range, rotation_range)
bbox = self.font.getbbox(drawn)
image = Image.new("L", self.get_size(drawn, (2, 2)), self.bgcolor)
draw = ImageDraw.Draw(image)
draw.text((0, 0), drawn, font=self.font, fill=self.fgcolor)
return image.rotate(rotation, expand=True, fillcolor=self.bgcolor, resample=Image.BICUBIC)
def make_text(self, text, **kwargs):
""" Renders a text
"""
size = self.get_size(text, (4, 3))
image = Image.new("RGB", size, self.bgcolor)
xpos = 2
for char in text:
charimage = self.make_char(char)
image.paste(charimage, (xpos, 4, xpos + charimage.size[0], 4 + charimage.size[1]))
xpos = xpos + 2 + charimage.size[0]
return image
def make_image(self, challenge):
pass
def _value(self):
return ""
class GeneratedTextImageCaptchaField(ImageCaptchaField):
""" Generate a random text and build a fuzzy image based on that text.
This is the most common historical captcha type, although pretty easily
defeated by OCR nowadays.
"""
charset = string.ascii_uppercase + string.digits
def challenge(self):
length = random.randint(5,8)
return "".join(random.choice(self.charset) for _ in range(length))
def check(self, challenge, form, field):
return challenge.lower() == field.data.lower()
def make_image(self, challenge):
return self.make_text(challenge + " ")
File added
from wtforms import validators, fields, widgets, utils
from joserfc import jwk, jwt
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
import flask
import random
import datetime
class ContextTokenField(fields.Field):
""" Field that has a token hidden field that holds a stateless context.
"""
@classmethod
def encode(cls, context):
"""JOSE authenticating encryption"""
kdf = HKDF(hashes.SHA512(), length=32, salt=None, info=None)
dkey = kdf.derive(flask.current_app.config["SECRET_KEY"].encode())
key = jwk.OctKey.import_key(dkey)
header = {"alg": "A256KW", "enc": "A256GCM"}
expired = datetime.datetime.now() + datetime.timedelta(minutes=3)
claims = {
"exp": int(expired.timestamp()),
"aud": flask.url_for('account.signup'),
"context": context
}
return jwt.encode(header, claims, key)
@classmethod
def decode(cls, token):
"""JOSE decryption"""
kdf = HKDF(hashes.SHA512(), length=32, salt=None, info=None)
dkey = kdf.derive(flask.current_app.config["SECRET_KEY"].encode())
key = jwk.OctKey.import_key(dkey)
decoded_token = jwt.decode(token, key)
claims_requests = jwt.JWTClaimsRegistry(
now=int(datetime.datetime.now().timestamp()),
aud={'essential': True, 'value': flask.url_for('account.signup')},
exp={'essential': True},
context={'essential': True}
)
claims_requests.validate(decoded_token.claims)
return decoded_token.claims["context"]
def __init__(self, *args, **kwargs):
self.received_context = {}
self.context = {}
# This is an internal field used to store stateless context between
# executions, context is protected using JOSE
self.token = fields.HiddenField("token")
super(ContextTokenField, self).__init__(*args, **kwargs)
def process(self, formdata=None, data=utils.unset_value, **kwargs):
""" Initialize the internal token field
"""
self.bound_token = self.token.bind(
form=None,
name="{}_token".format(self.name),
id="{}_token".format(self.id),
_meta=self.meta,
translations=self._translations
)
self.bound_token.process(formdata, data)
super(ContextTokenField, self).process(formdata, data)
def pre_validate(self, form):
""" Validate that the context properly decodes
"""
try:
decoded = ContextTokenField.decode(self.bound_token.data)
self.received_context = decoded
except:
raise validators.ValidationError("Could not decode the context")
def __call__(self, **kwargs):
""" Display the context token before the field itself
"""
self.bound_token.data = ContextTokenField.encode(self.context)
return (
self.bound_token() +
super(ContextTokenField, self).__call__(**kwargs)
)
class CaptchaField(ContextTokenField):
""" Generic captcha field that requires the implementation of two
functions for simple captchas.
"""
def __init__(self, label=None, validators=None, *args, **kwargs):
validators = validators or [self.validate_captcha]
super(CaptchaField, self).__init__(label, validators, *args, **kwargs)
def process(self, formdata=None, data=utils.unset_value, **kwargs):
""" Initializes a challenge
"""
self.context["challenge"] = self.challenge()
super(CaptchaField, self).process(formdata, data, **kwargs)
def validate_captcha(self, form, field):
if "challenge" not in self.received_context:
raise validators.ValidationError("CAPTCHA session expired")
if not self.check(self.received_context["challenge"], form, field):
raise validators.ValidationError("Wrong CAPTCHA")
def challenge(self):
""" Implement this method to generate a challenge that will later be
rendered and validated.
"""
return None
def check(self, challenge, form, field):
""" Implement this method to check that the challenge has been resolved
properly.
"""
return True
from flask import cli
from hiboo import models
from hiboo.profile import common
import time
tasks = cli.AppGroup("tasks")
@tasks.command("once")
def tasks_once():
""" Run regular tasks
"""
common.apply_all_transitions()
models.User.delete_unused()
@tasks.command("loop")
def tasks_loop():
""" Run regular tasks at interval
"""
while True:
common.apply_all_transitions()
models.User.delete_unused()
time.sleep(30)
import os
import ast
import logging
from hiboo.security import username_blocklist
DEFAULT_CONFIG = {
......@@ -6,9 +10,19 @@ DEFAULT_CONFIG = {
'DEBUG': False,
'BABEL_DEFAULT_LOCALE': 'en',
'BABEL_DEFAULT_TIMEZONE': 'UTC',
'SQLALCHEMY_DATABASE_URI': 'sqlite:///./trurt.db',
'SQLALCHEMY_DATABASE_URI': 'sqlite:////tmp/hiboo.db',
'SQLALCHEMY_TRACK_MODIFICATIONS': False,
'CACHE_TYPE': 'RedisCache',
'CACHE_REDIS_URL': 'redis://redis:6379/0',
'SECRET_KEY': 'changeMe',
'BOOTSTRAP_SERVE_LOCAL': True,
'TEMPLATES_AUTO_RELOAD': False,
'MAIL_DOMAIN': 'tedomum.net',
'WEBSITE_NAME': 'Hiboo',
'OPEN_SIGNUP': True,
'USER_TIMEOUT': 86400,
'API_TOKEN': 'changeMe',
'USERNAME_BLOCKLIST': username_blocklist(),
}
class ConfigManager(dict):
......@@ -23,16 +37,26 @@ class ConfigManager(dict):
return True
elif isinstance(value, str) and value.lower() in ('false', 'no'):
return False
elif isinstance(value, str) and value[0] in "[{(" and value[-1] in ")}]":
return ast.literal_eval(value)
return value
def init_app(self, app):
self.config.update(app.config)
# get environment variables
# Get environment variables for all keys
self.config.update({
key: self.__coerce_value(os.environ.get(key, value))
for key, value in DEFAULT_CONFIG.items()
})
# update the app config itself
# Ensure environment variables for flask-caching CACHE_ keys are handled
self.config.update({
key: self.__coerce_value(os.environ.get(key, DEFAULT_CONFIG.get(key)))
for key in os.environ.keys() if key.startswith('CACHE_')
})
# Update the logging config if necessary
if self.config.get("DEBUG", False):
logging.basicConfig(level=logging.DEBUG)
# Update the app config itself
app.config = self
def setdefault(self, key, value):
......@@ -54,3 +78,8 @@ class ConfigManager(dict):
def __contains__(self, key):
return key in self.config
def get_config_by_prefix(self, prefix):
return {
key: value for key, value in self.config.items() if key.startswith(prefix)
}
import flask_debugtoolbar
from werkzeug.contrib import profiler as werkzeug_profiler
from werkzeug.middleware import profiler as werkzeug_profiler
# Debugging toolbar
......@@ -14,4 +14,5 @@ class Profiler(object):
app.wsgi_app, restrictions=[30]
)
profiler = Profiler()
import flask
blueprint = flask.Blueprint("docs", __name__, template_folder="../static/docs/")
from hiboo.docs import views
import os
import sys
import flask
from werkzeug.utils import safe_join
from hiboo import security
from hiboo.utils import get_locale
from hiboo.docs import blueprint
@blueprint.route("/")
@blueprint.route("/<lang>/")
@blueprint.route("/<path:path>")
@blueprint.route("/<lang>/<path:path>/")
@security.authentication_required()
def index(lang: str = "en", path: str = "") -> flask.Response:
"""
Serve documentation templates from the static/docs folder.
This route handles requests for the builtin documentation. It use
`safe_join` to get the absolute path of requested URL and raise a 404 if
it's not found.
Args:
lang (str): The mkdocs localized template managed by mkdocs-i18n
path (str): The relative path of the requested template obtained from
the URL. Defaults to the documentation root page `""`.
Returns:
flask.Response: Jinja2 rendered for the template.
- 200 OK: If the template exists.
- 404 Not Found: If the template does not exist.
Notes:
If the requested URL is a path, "index.html" is added to the request to
match the Mkdocs files strucutre.
If the requested lang is not english (default), a "/" is added to the
request to serve all the localized template folder
Exemple:
Request: GET /reference/docs/views
Response: Template `../static/docs/reference/docs/views/index.html`.
"""
docs_base_path = os.path.join(flask.current_app.static_folder, "docs")
l10n = str(get_locale())[0:2]
full_path = safe_join(docs_base_path, path)
if l10n != "en" and lang == "en":
return flask.redirect(flask.url_for(".index", lang=l10n))
if lang != "en":
full_path = safe_join(docs_base_path, lang, path)
if os.path.isdir(full_path):
full_path = safe_join(full_path, "index.html")
if not os.path.exists(full_path):
flask.abort(404)
relative_path = os.path.relpath(full_path, docs_base_path)
return flask.render_template(relative_path)
@blueprint.route("/assets/<path:path>")
@security.authentication_required()
def assets(path: str) -> flask.Response:
"""
Serve documentation assets from the `static/docs/assets/` folder.
This route handles requests for assets-related resources, such as CSS and
JS files required by mkdocs theme. The `send_from_directory` ensure the
file exists or raise a 404.
Args:
path (str): The relative path of the requested file within the `search`
directory, obtained from the request URL.
Returns:
flask.Response: The requested file, or a 404 error if the file is not
found.
"""
return flask.send_from_directory("static/docs/assets/", path)
@blueprint.route("/search/<path:path>")
@security.authentication_required()
def search(path: str) -> flask.Response:
"""
Serve documentation search files from the `static/docs/search/` folder.
This route handles requests for search-related resources, such as JSON
index file required by the mkdocs's search functionality. The
`send_from_directory` ensure the file exists or raise a 404.
Args:
path (str): The relative path of the requested file within the `search`
directory, obtained from the request URL.
Returns:
flask.Response: The requested file, or a 404 error if the file is not
found.
"""
return flask.send_from_directory("static/docs/search/", path)
from wtforms import validators
from flask_babel import lazy_gettext as _
from hiboo.security import username_blocklist
import string
class NameFormat(object):
registry = {}
regex = ".*"
allowed = string.printable
transform = str
message = ""
min_length = 3
max_length = 30
@classmethod
def register(cls, *format_ids):
""" Class decorator
"""
def register_function(format):
for format_id in format_ids:
cls.registry[format_id] = format
return format
return register_function
@classmethod
def validators(cls, blocklist=True):
""" Return a username validator for wtforms
"""
return [
validators.DataRequired(),
validators.Length(
min=cls.min_length, max=cls.max_length,
message=(_("The name must be at least {} and at most {} characters long".format(cls.min_length, cls.max_length)))
),
validators.Regexp(
"^{}$".format(cls.regex),
message=cls.message
),
validators.NoneOf(
username_blocklist() if blocklist else (),
message=(_("Sorry, this username is not available"))
)
]
@classmethod
def coalesce(cls, username):
""" Transform a username into its valid form
"""
return ''.join(filter(cls.allowed.__contains__, cls.transform(username)))
@classmethod
def alternatives(cls, username):
""" Generate alternate usernames for a given username
"""
yield username
index = 1
while True:
yield username + str(index)
index += 1
register = NameFormat.register
@register("lowercase")
class LowercaseAlphanumPunct(NameFormat):
""" Lowercase name, including digits and very basic punctuation
"""
regex = r"[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?"
allowed = string.digits + string.ascii_lowercase + ".-_"
transform = str.lower
# Translators: "it" refers to a user/group/profile name
message = (_("It can only include lowercase letters, digits, dots, hyphens\
and underscores, but may not begin or end with dots or\
hyphens."))
@register("alnum")
class AlphanumPunct(NameFormat):
""" Alphanum name, including some very basic punctuation
"""
regex = r"[a-zA-Z0-9_]+([a-zA-Z0-9_\.-]+[a-zA-Z0-9_]+)?"
allowed = string.digits + string.ascii_letters + ".-_"
# Translators: "it" refers to a user/group/profile name
message = (_("It can only include letters, digits, dots, hyphens and\
underscores, but may not begin or end with dots or hyphens."))
@register("alnumSpace")
class AlphanumPunctSpace(NameFormat):
""" Alphanum name, including some very basic punctuation and space
"""
regex = r"[a-zA-Z0-9_]+([a-zA-Z0-9_\.\s-]+[a-zA-Z0-9_]+)?"
allowed = string.digits + string.ascii_letters + ". -_"
# Translators: "it" refers to a user/group/profile name
message = (_("It can only include letters, digits, dots, hyphens,\
underscores and spaces, but may not begin or end with dots,\
hyphens or spaces."))