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
  • uffd/uffd
  • rixx/uffd
  • thies/uffd
  • leona/uffd
  • enbewe/uffd
  • strifel/uffd
  • thies/uffd-2
7 results
Show changes
Showing
with 3333 additions and 561 deletions
{% extends 'base.html' %}
{% extends 'base_narrow.html' %}
{% block body %}
<form action="{{ url_for("mfa.auth_finish", ref=ref) }}" method="POST">
<div class="row mt-2 justify-content-center">
<div class="col-lg-6 col-md-10" style="background: #f7f7f7; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); padding: 30px;">
<div class="text-center">
<img alt="CCC logo" src="{{ url_for("static", filename="chaosknoten.png") }}" class="col-lg-8 col-md-12" >
</div>
<div class="col-12 mb-3">
<h2 class="text-center">Two-Factor Authentication</h2>
</div>
{% if webauthn_methods %}
<noscript>
<div class="form-group col-12">
<div id="webauthn-nojs" class="alert alert-warning" role="alert">Enable javascript for authentication with U2F/FIDO2 devices</div>
</div>
</noscript>
<div id="webauthn-unsupported" class="form-group col-12 d-none">
<div class="alert alert-warning" role="alert">Authentication with U2F/FIDO2 devices is not supported by your browser</div>
</div>
<div class="form-group col-12 d-none webauthn-group">
<div id="webauthn-alert" class="alert alert-warning d-none" role="alert"></div>
<button type="button" id="webauthn-btn" class="btn btn-primary btn-block">
<span id="webauthn-spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
<span id="webauthn-btn-text">Authenticate with U2F/FIDO2 device</span>
</button>
</div>
<div class="text-center text-muted d-none webauthn-group mb-3">- or -</div>
{% endif %}
<div class="form-group col-12 mb-2">
<input type="text" class="form-control" id="mfa-code" name="code" required="required" placeholder="Code from your authenticator app or recovery code" autocomplete="off" autofocus>
</div>
<div class="form-group col-12">
<button type="submit" class="btn btn-primary btn-block">Verify</button>
</div>
<form action="{{ url_for("session.mfa_auth_finish", ref=ref) }}" method="POST" autocomplete="off">
<div class="col-12 mb-3">
<h2 class="text-center">{{_("Two-Factor Authentication")}}</h2>
</div>
{% if request.user_pre_mfa.mfa_webauthn_methods %}
<noscript>
<div class="form-group col-12">
<a href="{{ url_for("session.logout") }}" class="btn btn-secondary btn-block">Cancel</a>
<div id="webauthn-nojs" class="alert alert-warning" role="alert">{{_("Enable javascript for authentication with U2F/FIDO2 devices")}}</div>
</div>
</noscript>
<div id="webauthn-unsupported" class="form-group col-12 d-none">
<div class="alert alert-warning" role="alert">{{_("Authentication with U2F/FIDO2 devices is not supported by your browser")}}</div>
</div>
<div class="form-group col-12 d-none webauthn-group">
<div id="webauthn-alert" class="alert alert-warning d-none" role="alert"></div>
<button type="button" id="webauthn-btn" class="btn btn-primary btn-block">
<span id="webauthn-spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
<span id="webauthn-btn-text">{{_("Authenticate with U2F/FIDO2 device")}}</span>
</button>
</div>
<div class="text-center text-muted d-none webauthn-group mb-3">- {{_("or")}} -</div>
{% endif %}
<div class="form-group col-12 mb-2">
<input type="text" class="form-control" id="mfa-code" name="code" required="required" placeholder="{{_("Code from your authenticator app or recovery code")}}" autofocus>
</div>
<div class="form-group col-12">
<button type="submit" class="btn btn-primary btn-block">{{_("Verify")}}</button>
</div>
<div class="form-group col-12">
<a href="{{ url_for("session.logout") }}" class="btn btn-secondary btn-block">{{_("Cancel")}}</a>
</div>
</div>
</form>
{% if webauthn_supported and webauthn_methods %}
{% if webauthn_supported and request.user_pre_mfa.mfa_webauthn_methods %}
<script src="{{ url_for('static', filename="cbor.js") }}"></script>
<script>
function begin_webauthn() {
$('#webauthn-alert').addClass('d-none');
$('#webauthn-spinner').removeClass('d-none');
$('#webauthn-btn-text').text('Fetching credential data');
$('#webauthn-btn-text').text({{ _('Contacting server')|tojson }});
$('#webauthn-btn').prop('disabled', true);
fetch({{ url_for('mfa.auth_webauthn_begin')|tojson }}, {
fetch({{ url_for('session.mfa_auth_webauthn_begin')|tojson }}, {
method: 'POST',
}).then(function(response) {
if (response.ok) {
return response.arrayBuffer();
} else if (response.status == 403) {
window.location = {{ request.url|tojson }}; /* reload */
throw new Error('Session timed out');
throw new Error({{ _('Session timed out')|tojson }});
} else if (response.status == 404) {
throw new Error('You have not registered any U2F/FIDO2 devices for your account');
throw new Error({{ _('You have not registered any U2F/FIDO2 devices for your account')|tojson }});
} else {
throw new Error('Server error');
throw new Error({{ _('Server error')|tojson }});
}
}).then(CBOR.decode).then(function(options) {
$('#webauthn-btn-text').text('Waiting for response from your device');
$('#webauthn-btn-text').text({{ _('Waiting for device')|tojson }});
return navigator.credentials.get(options);
}).then(function(assertion) {
$('#webauthn-btn-text').text('Verifing response');
return fetch({{ url_for('mfa.auth_webauthn_complete')|tojson }}, {
$('#webauthn-btn-text').text({{ _('Verifing response')|tojson }});
return fetch({{ url_for('session.mfa_auth_webauthn_complete')|tojson }}, {
method: 'POST',
headers: {'Content-Type': 'application/cbor'},
body: CBOR.encode({
......@@ -81,37 +73,37 @@ function begin_webauthn() {
}).then(function(response) {
if (response.ok) {
$('#webauthn-spinner').addClass('d-none');
$('#webauthn-btn-text').text('Success, redirecting');
$('#webauthn-btn-text').text({{ _('Success, redirecting')|tojson }});
window.location = {{ (ref or url_for('index'))|tojson }};
} else if (response.status == 403) {
window.location = {{ request.url|tojson }}; /* reload */
throw new Error('Session timed out');
throw new Error({{ _('Session timed out')|tojson }});
} else {
throw new Error('Response from authenticator rejected');
throw new Error({{ _('Invalid response from device')|tojson }});
}
}).catch(function(err) {
console.log(err);
/* various webauthn errors */
if (err.name == 'NotAllowedError')
$('#webauthn-alert').text('Authentication timed out, was aborted or not allowed');
$('#webauthn-alert').text({{ _('Authentication timed out, was aborted or not allowed')|tojson }});
else if (err.name == 'InvalidStateError')
$('#webauthn-alert').text('Device is not registered for your account');
$('#webauthn-alert').text({{ _('Device is not registered for your account')|tojson }});
else if (err.name == 'AbortError')
$('#webauthn-alert').text('Authentication was aborted');
$('#webauthn-alert').text({{ _('Authentication was aborted')|tojson }});
else if (err.name == 'NotSupportedError')
$('#webauthn-alert').text('U2F and FIDO2 devices are not supported by your browser');
$('#webauthn-alert').text({{ _('U2F and FIDO2 devices are not supported by your browser')|tojson }});
/* errors from fetch() */
else if (err.name == 'TypeError')
$('#webauthn-alert').text('Could not connect to server');
$('#webauthn-alert').text({{ _('Could not connect to server')|tojson }});
/* our own errors */
else if (err.name == 'Error')
$('#webauthn-alert').text(err.message);
/* fallback */
else
$('#webauthn-alert').text('Authentication failed ('+err+')');
$('#webauthn-alert').text({{ _('Authentication failed ')|tojson }}+'('+err+')');
$('#webauthn-alert').removeClass('d-none');
$('#webauthn-spinner').addClass('d-none');
$('#webauthn-btn-text').text('Try FIDO token again');
$('#webauthn-btn-text').text({{ _('Retry authenticate with U2F/FIDO2 device')|tojson }});
$('#webauthn-btn').prop('disabled', false);
});
}
......
{% extends 'base_narrow.html' %}
{% block body %}
<form action="{{ url_for(".signup_confirm_submit", signup_id=signup.id, token=signup.token) }}" method="POST">
<div class="col-12">
<h2 class="text-center">{{_('Complete Registration')}}</h2>
</div>
<div class="form-group col-12">
<label for="user-password1">{{_('Please enter your password to complete the account registration')}}</label>
<input type="password" autocomplete="current-password" class="form-control" id="user-password1" name="password" required="required">
</div>
<div class="form-group col-12">
<button type="submit" class="btn btn-primary btn-block">{{_('Complete Account Registration')}}</button>
</div>
</form>
{% endblock %}
Hi {{ signup.displayname }},
an account was created on the {{ config.ORGANISATION_NAME }} infrastructure with this mail address.
Please visit the following url to complete the account registration:
{{ url_for('signup.signup_confirm', signup_id=signup.id, token=signup.token, _external=True) }}
**The link is valid for 48h**
{% if config.WELCOME_TEXT %}
{{ config.WELCOME_TEXT }}
{% endif -%}
If you have not requested an account on the {{ config.ORGANISATION_NAME }} infrastructure, you can ignore this mail.
{% extends 'base_narrow.html' %}
{% block body %}
<form action="{{ url_for('.signup_submit') }}" method="POST" onInput="password2.setCustomValidity(password1.value != password2.value ? 'Passwords do not match.' : '') ">
<div class="col-12">
<h2 class="text-center">{{_('Account Registration')}}</h2>
</div>
<div class="form-group col-12">
<label for="user-loginname">{{_('Login Name')}}</label>
<div class="js-only-input-group">
<input type="text" autocomplete="username" class="form-control" id="user-loginname" name="loginname" aria-describedby="loginname-feedback" value="{{ request.form.loginname }}" minlength=1 maxlength=32 pattern="[a-z0-9_-]*" required>
<div class="js-only-input-group-append d-none">
<button class="btn btn-outline-secondary rounded-right" type="button" id="check-loginname">{{_('Check')}}</button>
</div>
<div id="loginname-feedback" class="invalid-feedback"></div>
</div>
<small class="form-text text-muted">
{{_('At least one and at most 32 lower-case characters, digits, dashes ("-") or underscores ("_"). <b>Cannot be changed later!</b>')|safe}}
</small>
</div>
<div class="form-group col-12">
<label for="user-displayname">{{_('Display Name')}}</label>
<input type="text" autocomplete="nickname" class="form-control" id="user-displayname" name="displayname" value="{{ request.form.displayname }}" minlength=1 maxlength=128 required>
<small class="form-text text-muted">
{{_('At least one and at most 128 characters, no other special requirements.')}}
</small>
</div>
<div class="form-group col-12">
<label for="user-mail">{{_('E-Mail Address')}}</label>
<input type="email" autocomplete="email" class="form-control" id="user-mail" name="mail" value="{{ request.form.mail }}" required>
<small class="form-text text-muted">
{{_('We will send a confirmation mail to this address that you need to complete the registration.')}}
</small>
</div>
<div class="form-group col-12">
<label for="user-password1">{{_('Password')}}</label>
<input type="password" autocomplete="new-password" class="form-control" id="user-password1" name="password1" minlength={{ User.PASSWORD_MINLEN }} maxlength={{ User.PASSWORD_MAXLEN }} pattern="{{ User.PASSWORD_REGEX }}" required>
<small class="form-text text-muted">
{{ User.PASSWORD_DESCRIPTION|safe }}
</small>
</div>
<div class="form-group col-12">
<label for="user-password2">{{_('Repeat Password')}}</label>
<input type="password" autocomplete="new-password" class="form-control" id="user-password2" name="password2" required>
</div>
<div class="form-group col-12">
<button type="submit" class="btn btn-primary btn-block">{{_('Create Account')}}</button>
</div>
</form>
<script>
$(".js-only-input-group").addClass("input-group");
$(".js-only-input-group-append").removeClass("d-none");
$(".js-only-input-group-append").addClass("input-group-append");
let checkreq;
$("#check-loginname").on("click", function () {
if (checkreq)
checkreq.abort();
$("#user-loginname").removeClass("is-valid");
$("#user-loginname").removeClass("is-invalid");
$("#check-loginname").prop("disabled", true);
checkreq = $.ajax({
url: {{ url_for('.signup_check')|tojson }},
method: "POST",
data: {"loginname": $("#user-loginname").val()},
success: function (resp) {
checkreq = null;
$("#check-loginname").prop("disabled", false);
if (resp.status == "ok") {
$("#user-loginname").addClass("is-valid");
$("#loginname-feedback").text("");
} else if (resp.status == 'exists') {
$("#loginname-feedback").text({{_("The name is already taken")|tojson}});
$("#user-loginname").addClass("is-invalid");
} else if (resp.status == 'ratelimited') {
$("#loginname-feedback").text({{_("Too many requests! Please wait a bit before trying again!")|tojson}});
$("#user-loginname").addClass("is-invalid");
} else {
$("#loginname-feedback").text({{_("The name is invalid")|tojson}});
$("#user-loginname").addClass("is-invalid");
}
}
});
});
$("#user-loginname").on("input", function () {
if (checkreq)
checkreq.abort();
checkreq = null;
$("#user-loginname").removeClass("is-valid");
$("#user-loginname").removeClass("is-invalid");
$("#check-loginname").prop("disabled", false);
});
</script>
{% endblock %}
{% extends 'base_narrow.html' %}
{% block body %}
<div class="col-12 mb-3">
<h2 class="text-center">{{_('Confirm your E-Mail Address')}}</h2>
</div>
<p>{{_('We sent a confirmation mail to <b>%(signup_mail)s</b>. You need to confirm your mail address within 48 hours to complete the account registration.', signup_mail=signup.mail|e)|safe}}</p>
<p>{{_("If you mistyped your mail address or don't receive the confirmation mail for another reason, retry the registration procedure from the beginning.")}}</p>
{% endblock %}
......@@ -3,44 +3,47 @@
{% block body %}
<div class="row">
<div class="col">
<p class="text-right">
<a class="btn btn-primary" href="{{ url_for("user.show") }}">
<i class="fa fa-plus" aria-hidden="true"></i> {{_("New")}}
</a>
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#csvimport">
<i class="fa fa-file-csv" aria-hidden="true"></i> {{_("CSV import")}}
</button>
</p>
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">uid</th>
<th scope="col">login name</th>
<th scope="col">display name</th>
<th scope="col">
<p class="text-right">
<a class="btn btn-primary" href="{{ url_for("user.show") }}">
<i class="fa fa-plus" aria-hidden="true"></i> New
</a>
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#csvimport">
<i class="fa fa-file-csv" aria-hidden="true"></i> CSV import
</button>
</p>
</th>
<th scope="col">{{_("UID")}}</th>
<th scope="col">{{_("Login Name")}}</th>
<th scope="col">{{_("Display Name")}}</th>
<th scope="col">{{_("Roles")}}</th>
</tr>
</thead>
<tbody>
{% for user in users|sort(attribute="uid") %}
<tr id="user-{{ user.uid }}">
{% for user in users|sort(attribute="unix_uid") %}
<tr id="user-{{ user.id }}">
<th scope="row">
{{ user.uid }}
{{ user.unix_uid }}
</th>
<td>
<a href="{{ url_for("user.show", uid=user.uid) }}">
<a href="{{ url_for("user.show", id=user.id) }}">
{{ user.loginname }}
</a>
{% if user.is_service_user %}
<span class="badge badge-secondary">{{_('service')}}</span>
{% endif %}
{% if user.is_deactivated %}
<span class="badge badge-danger">{{ _('deactivated') }}</span>
{% endif %}
</td>
<td>
{{ user.displayname }}
</td>
<td>
<p class="text-right">
<a href="{{ url_for("user.show", uid=user.uid) }}" class="btn btn-primary">
<i class="fa fa-edit" aria-hidden="true"></i> Edit
</a>
</p>
{% for role in user.roles|sort(attribute="name") %}
<a href="{{ url_for("role.show", roleid=role.id) }}">{{ role.name }}</a>{% if not loop.last %}, {% endif %}
{% endfor %}
</td>
</tr>
{% endfor %}
......@@ -54,16 +57,14 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Import a csv formated list of users</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<h5 class="modal-title" id="exampleModalLabel">{{_("Import a csv formated list of users")}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="{{_('Close')}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>
The format should be "loginname,mailaddres,roleid1;roleid2".
Neither setting the display name nor setting passwords is supported (yet).
Example:
{{_('The format should be "loginname,mailaddres,roleid1;roleid2". Neither setting the display name nor setting passwords is supported (yet). Example:')}}
</p>
<pre>
testuser1,foobar@example.com,5;2;6
......@@ -72,10 +73,14 @@ testuser5,foobadfar@example.com,0;5;2
testuser2,foobaadsfr@example.com,5;2
</pre>
<textarea rows="10" class="form-control" name="csv"></textarea>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="ignore-loginname-blocklist" name="ignore-loginname-blocklist" value="1" aria-label="enabled">
<label class="form-check-label" for="ignore-loginname-blocklist">{{_("Ignore login name blocklist")}}</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Import</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{_("Close")}}</button>
<button type="submit" class="btn btn-primary">{{_("Import")}}</button>
</div>
</div>
</div>
......
{% extends 'base.html' %}
{% macro new_email_row(tmp_id) %}
<tr>
<td>
<input class="form-control form-control-sm" type="email" name="newemail-{{ tmp_id }}-address" placeholder="{{ _('New address') }}">
</td>
<td class="text-center">
<input type="checkbox" value="1" name="newemail-{{ tmp_id }}-verified">
</td>
<td class="text-center">
<button type="button" class="btn btn-sm delete-new-email-row d-none"><i class="fas fa-times"></i></button>
</td>
</tr>
{% endmacro %}
{% block body %}
{% if user.id %}
<form action="{{ url_for("user.update", id=user.id) }}" method="POST" autocomplete="off">
{% else %}
<form action="{{ url_for("user.create") }}" method="POST" autocomplete="off">
{% endif %}
<div class="align-self-center">
{% if user.id and user.is_deactivated %}
<div class="alert alert-warning">
{{ _('This account is deactivated. The user cannot login and existing sessions are not usable. The user cannot log into services, but existing sessions on services might still be active.') }}
</div>
{% endif %}
<div class="clearfix pb-2"><div class="float-sm-right">
<button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> {{_("Save")}}</button>
<a href="{{ url_for("user.index") }}" class="btn btn-secondary">{{_("Cancel")}}</a>
{% if user.id and not user.is_deactivated and user != request.user %}
<a href="{{ url_for("user.deactivate", id=user.id) }}" class="btn btn-secondary">{{ _("Deactivate") }}</a>
{% elif user.id and user.is_deactivated %}
<a href="{{ url_for("user.activate", id=user.id) }}" class="btn btn-primary">{{ _("Activate") }}</a>
{% else %}
<a href="#" class="btn btn-secondary disabled">{{ _("Deactivate") }}</a>
{% endif %}
{% if user.id and user != request.user %}
<a href="{{ url_for("user.delete", id=user.id) }}" onClick='return confirm({{_("Are you sure?")|tojson}});' class="btn btn-danger"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a>
{% else %}
<a href="#" class="btn btn-danger disabled"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a>
{% endif %}
</div></div>
<ul class="nav nav-tabs pt-2 border-0" id="tablist" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="profile-tab" data-toggle="tab" href="#profile" role="tab" aria-controls="profile" aria-selected="true">{{_("Profile")}}</a>
</li>
<li class="nav-item">
<a class="nav-link" id="roles-tab" data-toggle="tab" href="#roles" role="tab" aria-controls="roles" aria-selected="false">{{_("Roles")}}</a>
</li>
</ul>
<div class="tab-content border mb-2 pt-2" id="tabcontent">
<div class="tab-pane fade show active" id="profile" role="tabpanel" aria-labelledby="roles-tab">
<div class="form-group col">
<label for="user-uid">
{{_('User ID')}}
{% if user.is_service_user %}
<span class="badge badge-secondary">{{_('service')}}</span>
{% endif %}
</label>
{% if user.id %}
<input type="number" class="form-control" id="user-uid" name="uid" value="{{ user.unix_uid }}" readonly>
{% else %}
<input type="text" class="form-control" id="user-uid" name="uid" placeholder="{{_('will be choosen')}}" readonly>
{% endif %}
</div>
{% if not user.id %}
<div class="form-group col">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="user-serviceaccount" name="serviceaccount" value="1" aria-label="enabled">
<label class="form-check-label" for="user-serviceaccount">{{_('Service User')}}</label>
</div>
</div>
{% endif %}
<div class="form-group col">
<label for="user-loginname">{{_("Login Name")}}</label>
<input type="text" class="form-control" id="user-loginname" name="loginname" value="{{ user.loginname or '' }}" {% if user.id %}readonly{% endif %}>
<small class="form-text text-muted">
{{_("Only letters, numbers, dashes (\"-\") and underscores (\"_\") are allowed. At most 32, at least 2 characters. There is a word blocklist. Must be unique.")}}
</small>
</div>
{% if not user.id %}
<div class="form-group col">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="ignore-loginname-blocklist" name="ignore-loginname-blocklist" value="1" aria-label="enabled">
<label class="form-check-label" for="ignore-loginname-blocklist">{{_('Ignore login name blocklist')}}</label>
</div>
</div>
{% endif %}
<div class="form-group col">
<label for="user-loginname">{{_("Display Name")}}</label>
<input type="text" class="form-control" id="user-displayname" name="displayname" value="{{ user.displayname or '' }}">
<small class="form-text text-muted">
{{_("If you leave this empty it will be set to the login name. At most 128, at least 2 characters. No character restrictions.")}}
</small>
</div>
{% if not user.id %}
<div class="form-group col">
<label for="user-email">{{_("E-Mail Address")}}</label>
<input type="email" class="form-control" id="user-email" name="email" value="">
<small class="form-text text-muted">
{{_("Make sure the address is correct! Services might use e-mail addresses as account identifiers and rely on them being unique and verified.")}}
</small>
</div>
{% else %}
<div class="form-group col">
<span>{{_("E-Mail Addresses")}}</span>
<table class="table table-sm mt-2">
<thead>
<tr>
<th scope="col">{{_("Address")}}</th>
<th scope="col" class="text-center">{{_("Verified")}}</th>
<th scope="col" class="text-center">{{_("Delete")}}</th>
</tr>
</thead>
<tbody id="email-rows">
{% for email in user.all_emails %}
<tr>
<td>
<input type="hidden" name="email-{{ email.id }}-present" value="1">
{{ email.address }}
{% if email == user.primary_email %}
<span class="badge badge-primary">{{ _('primary') }}</span>
{% endif %}
</td>
<td class="text-center">
<input type="checkbox" value="1" name="email-{{ email.id }}-verified"{{ ' checked disabled' if email.verified }}>
</td>
<td class="text-center">
<input type="checkbox" value="1" name="email-{{ email.id }}-delete"{{ ' disabled' if email == user.primary_email }}>
</td>
</tr>
{% endfor %}
{{ new_email_row(0) }}
</tbody>
</table>
<small class="form-text text-muted">
{{_("Make sure that addresses you add are correct! Services might use e-mail addresses as account identifiers and rely on them being unique and verified.")}}
</small>
</div>
<div class="form-group col">
<label>{{_("Primary E-Mail Address")}}</label>
<select name="primary_email" class="form-control">
{% for email in user.all_emails if email.verified %}
<option value="{{ email.id }}"{{ ' selected' if email == user.primary_email }}>{{ email.address }}</option>
{% endfor %}
</select>
</div>
<div class="form-group col">
<label>{{_("Recovery E-Mail Address")}}</label>
<select name="recovery_email" class="form-control">
<option value="primary"{{ ' selected' if not user.recovery_email }}>{{ _('Use primary address') }}</option>
{% for email in user.all_emails if email.verified %}
<option value="{{ email.id }}" {{ 'selected' if email == user.recovery_email }}>{{ email.address }}</option>
{% endfor %}
</select>
</div>
{% for service_user in user.service_users if service_user.has_email_preferences %}
<div class="form-group col">
<label>{{ _("Address for %(name)s", name=service_user.service.name) }}</label>
<select name="service_{{ service_user.service.id }}_email" class="form-control">
<option value="primary" {{ 'selected' if not service_user.service_email }}>{{ _('Use primary address') }}</option>
{% for email in user.all_emails if email.verified %}
<option value="{{ email.id }}" {{ 'selected' if email == service_user.service_email }}>{{ email.address }}</option>
{% endfor %}
</select>
</div>
{% endfor %}
{% endif %}
<div class="form-group col">
<label for="user-loginname">{{_("Password")}}</label>
{% if user.id %}
<input type="password" autocomplete="new-password" class="form-control" id="user-password" name="password" placeholder="●●●●●●●●" minlength={{ User.PASSWORD_MINLEN }} maxlength={{ User.PASSWORD_MAXLEN }} pattern="{{ User.PASSWORD_REGEX }}">
{% else %}
<input type="text" class="form-control" id="user-password" name="password" placeholder="{{_("E-Mail to set it will be sent")}}" readonly>
{% endif %}
<small class="form-text text-muted">
{{ User.PASSWORD_DESCRIPTION|safe }}
</small>
</div>
{% if user.id %}
<div class="form-group col">
<div class="mb-1">{{_("Two-Factor Authentication")}}</div>
<p>
{{ _("Status:") }} {{ _("Enabled") if user.mfa_enabled else _("Disabled") }}<br>
{{ user.mfa_recovery_codes|length }} {{ _("Recovery Codes") }}, {{ user.mfa_totp_methods|length }} {{ _("Authenticator Apps (TOTP)") }}, {{ user.mfa_webauthn_methods|length }} {{ _("U2F and FIDO2 Devices") }}
</p>
<a href="{{ url_for("user.disable_mfa", id=user.id) }}" onClick='return confirm({{_("Are you sure?")|tojson}});' class="btn btn-secondary">{{_("Reset 2FA")}}</a>
</div>
<div class="form-group col">
<div class="mb-1">{{_("Sessions")}}</div>
<p>{{ _("%(session_count)d active sessions", session_count=user.sessions|rejectattr('expired')|list|length) }}</p>
<a href="{{ url_for("user.revoke_sessions", id=user.id) }}" onClick='return confirm({{_("Are you sure?")|tojson}});' class="btn btn-secondary">{{_("Revoke all sessions")}}</a>
</div>
{% endif %}
</div>
<div class="tab-pane fade" id="roles" role="tabpanel" aria-labelledby="roles-tab">
<div class="form-group col">
<span>{{_("Roles")}}:</span>
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">{{_("Name")}}</th>
<th scope="col">{{_("Description")}}</th>
</tr>
</thead>
<tbody>
{% for role in roles|sort(attribute="name") %}
<tr id="role-{{ role.id }}">
<td>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="role-{{ role.id }}-checkbox" name="role-{{ role.id }}" value="1" aria-label="enabled"
{% if user in role.members %}checked {% endif %}
{% if role.is_default and not user.is_service_user %}disabled {% endif %}>
</div>
</td>
<td>
<a href="{{ url_for("role.show", roleid=role.id) }}">
{{ role.name }}
</a>
</td>
<td>
{{ role.description }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="form-group col">
<span>{{_("Resulting groups (only updated after save)")}}:</span>
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">{{_("Name")}}</th>
<th scope="col">{{_("Description")}}</th>
</tr>
</thead>
<tbody>
{% for group in user.groups|sort(attribute="name") %}
<tr id="group-{{ group.id }}">
<td>
<a href="{{ url_for("group.show", id=group.id) }}">
{{ group.name }}
</a>
</td>
<td>
{{ group.description }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</form>
<script>
$('#email-rows').on('click', '.delete-new-email-row', function () {
$(this).closest('tr').remove()
});
let new_email_id = 1;
$('#email-rows').on('input', 'tr:last input', function () {
$('#email-rows tr:last button.delete-new-email-row').removeClass('d-none');
$('#email-rows').append({{ new_email_row('TMPID')|tojson }}.replace(/TMPID/g, new_email_id));
new_email_id ++;
});
</script>
{% endblock %}
File added
# German translations for uffd.
# Copyright (C) 2021 ORGANIZATION
# This file is distributed under the same license as the uffd project.
# Milan Höllner <milan.hoellner@posteo.de>, 2021.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-03-24 18:37+0100\n"
"PO-Revision-Date: 2021-05-25 21:18+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de\n"
"Language-Team: de <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.10.3\n"
#: uffd/models/invite.py:84 uffd/models/invite.py:107 uffd/models/invite.py:112
msgid "Invite link is invalid"
msgstr "Einladungslink ist ungültig"
#: uffd/models/invite.py:86
msgid "Invite link does not grant any roles"
msgstr "Einladungslink weist keine Rollen zu"
#: uffd/models/invite.py:88
msgid "Invite link does not grant any new roles"
msgstr "Einladungslink weist keine neuen Rollen zu"
#: uffd/models/invite.py:93 uffd/models/signup.py:122
#: uffd/templates/mfa/setup.html:225
msgid "Success"
msgstr "Erfolgreich"
#: uffd/models/ratelimit.py:76
msgid "a few seconds"
msgstr "ein paar Sekunden"
#: uffd/models/ratelimit.py:78
msgid "30 seconds"
msgstr "30 Sekunden"
#: uffd/models/ratelimit.py:80
msgid "one minute"
msgstr "eine Minute"
#: uffd/models/ratelimit.py:82
#, python-format
msgid "%(minutes)d minutes"
msgstr "%(minutes)d Minuten"
#: uffd/models/ratelimit.py:84
msgid "one hour"
msgstr "eine Stunde"
#: uffd/models/ratelimit.py:85
#, python-format
msgid "%(hours)d hours"
msgstr "%(hours)d Stunden"
#: uffd/models/session.py:62 uffd/models/session.py:74
#: uffd/models/session.py:80 uffd/models/session.py:90
msgid "Unknown"
msgstr "Unbekannt"
#: uffd/models/signup.py:78 uffd/models/signup.py:103
msgid "Invalid signup request"
msgstr "Ungültiger Account-Registrierungs-Link"
#: uffd/models/signup.py:80
msgid "Login name is invalid"
msgstr "Anmeldename ist ungültig"
#: uffd/models/signup.py:82
msgid "Display name is invalid"
msgstr "Anzeigename ist ungültig"
#: uffd/models/signup.py:84 uffd/views/selfservice.py:112 uffd/views/user.py:51
#: uffd/views/user.py:99
msgid "E-Mail address is invalid"
msgstr "Ungültige E-Mail-Adresse"
#: uffd/models/signup.py:86 uffd/views/selfservice.py:49
msgid "Invalid password"
msgstr "Passwort ungültig"
#: uffd/models/signup.py:88 uffd/models/signup.py:107
msgid "A user with this login name already exists"
msgstr "Ein Account mit diesem Anmeldenamen existiert bereits"
#: uffd/models/signup.py:89
msgid "Valid"
msgstr "Gültig"
#: uffd/models/signup.py:105 uffd/views/signup.py:104
msgid "Wrong password"
msgstr "Falsches Passwort"
#: uffd/models/signup.py:115 uffd/views/user.py:62
msgid "Login name or e-mail address is already in use"
msgstr "Der Anmeldename oder die E-Mail-Adresse wird bereits verwendet"
#: uffd/models/user.py:119
#, python-format
msgid ""
"At least %(minlen)d and at most %(maxlen)d characters. Only letters, "
"digits, spaces and some symbols (<code>%(symbols)s</code>) allowed. "
"Please use a password manager."
msgstr ""
"%(minlen)d bis %(maxlen)d Zeichen. Nur Buchstaben, Ziffern, Leerzeichen "
"und manche Symbole (<code>%(symbols)s</code>), keine Umlaute. Bitte "
"verwende einen Passwort-Manager."
#: uffd/templates/403.html:10
msgid "Access Denied"
msgstr "Zugriff verweigert"
#: uffd/templates/403.html:17
msgid "You don't have the permission to access this page."
msgstr "Du bist nicht berechtigt, auf diese Seite zuzugreifen."
#: uffd/templates/base.html:85
msgid "Change"
msgstr "Ändern"
#: uffd/templates/base.html:93 uffd/templates/session/deviceauth.html:12
msgid "Authorize Device Login"
msgstr "Gerätelogin erlauben"
#: uffd/templates/base.html:94 uffd/templates/session/devicelogin.html:6
msgid "Device Login"
msgstr "Gerätelogin"
#: uffd/templates/base.html:100 uffd/templates/oauth2/logout.html:5
msgid "Logout"
msgstr "Abmelden"
#: uffd/templates/base.html:107 uffd/templates/service/overview.html:15
#: uffd/templates/session/login.html:6 uffd/templates/session/login.html:20
msgid "Login"
msgstr "Anmelden"
#: uffd/templates/base.html:143
msgid "About uffd"
msgstr "Über uffd"
#: uffd/templates/group/list.html:8 uffd/templates/invite/list.html:6
#: uffd/templates/mail/list.html:8 uffd/templates/role/list.html:8
#: uffd/templates/service/index.html:8 uffd/templates/service/show.html:106
#: uffd/templates/service/show.html:134 uffd/templates/user/list.html:8
msgid "New"
msgstr "Neu"
#: uffd/templates/group/list.html:14
msgid "GID"
msgstr "GID"
#: uffd/templates/group/list.html:15 uffd/templates/group/show.html:26
#: uffd/templates/invite/new.html:35 uffd/templates/mail/list.html:14
#: uffd/templates/mail/show.html:7 uffd/templates/mfa/setup.html:98
#: uffd/templates/mfa/setup.html:99 uffd/templates/mfa/setup.html:107
#: uffd/templates/mfa/setup.html:157 uffd/templates/mfa/setup.html:158
#: uffd/templates/mfa/setup.html:169 uffd/templates/role/list.html:14
#: uffd/templates/rolemod/list.html:9 uffd/templates/rolemod/show.html:44
#: uffd/templates/selfservice/self.html:239
#: uffd/templates/service/index.html:14 uffd/templates/service/show.html:20
#: uffd/templates/service/show.html:140 uffd/templates/user/show.html:212
#: uffd/templates/user/show.html:244
msgid "Name"
msgstr "Name"
#: uffd/templates/group/list.html:16 uffd/templates/group/show.html:33
#: uffd/templates/invite/new.html:36 uffd/templates/role/list.html:15
#: uffd/templates/role/show.html:48 uffd/templates/rolemod/list.html:10
#: uffd/templates/rolemod/show.html:26 uffd/templates/selfservice/self.html:240
#: uffd/templates/user/show.html:213 uffd/templates/user/show.html:245
msgid "Description"
msgstr "Beschreibung"
#: uffd/templates/group/show.html:8 uffd/templates/mail/show.html:27
#: uffd/templates/role/show.html:13 uffd/templates/rolemod/show.html:8
#: uffd/templates/service/api.html:15 uffd/templates/service/oauth2.html:15
#: uffd/templates/service/show.html:16 uffd/templates/user/show.html:31
msgid "Save"
msgstr "Speichern"
#: uffd/templates/group/show.html:9 uffd/templates/invite/new.html:56
#: uffd/templates/mail/show.html:28 uffd/templates/mfa/auth.html:33
#: uffd/templates/role/show.html:14 uffd/templates/rolemod/show.html:9
#: uffd/templates/service/api.html:9 uffd/templates/service/oauth2.html:9
#: uffd/templates/service/show.html:10
#: uffd/templates/session/deviceauth.html:39
#: uffd/templates/session/deviceauth.html:49
#: uffd/templates/session/devicelogin.html:29 uffd/templates/user/show.html:32
msgid "Cancel"
msgstr "Abbrechen"
#: uffd/templates/group/show.html:11 uffd/templates/role/show.html:19
#: uffd/templates/role/show.html:21 uffd/templates/selfservice/self.html:61
#: uffd/templates/selfservice/self.html:210
#: uffd/templates/selfservice/self.html:254 uffd/templates/service/api.html:11
#: uffd/templates/service/oauth2.html:11 uffd/templates/service/show.html:12
#: uffd/templates/user/show.html:41 uffd/templates/user/show.html:193
#: uffd/templates/user/show.html:199
msgid "Are you sure?"
msgstr "Wirklich fortfahren?"
#: uffd/templates/group/show.html:11 uffd/templates/group/show.html:13
#: uffd/templates/mail/show.html:30 uffd/templates/mail/show.html:32
#: uffd/templates/mfa/setup.html:117 uffd/templates/mfa/setup.html:179
#: uffd/templates/role/show.html:21 uffd/templates/role/show.html:24
#: uffd/templates/selfservice/self.html:62 uffd/templates/service/api.html:12
#: uffd/templates/service/oauth2.html:12 uffd/templates/service/show.html:13
#: uffd/templates/user/show.html:41 uffd/templates/user/show.html:43
#: uffd/templates/user/show.html:116
msgid "Delete"
msgstr "Löschen"
#: uffd/templates/group/show.html:18
msgid "Group ID"
msgstr "Gruppen ID"
#: uffd/templates/group/show.html:29 uffd/templates/signup/start.html:18
msgid ""
"At least one and at most 32 lower-case characters, digits, dashes (\"-\")"
" or underscores (\"_\"). <b>Cannot be changed later!</b>"
msgstr ""
"1 bis 32 Kleinbuchstaben, Zahlen, Binde- (\"-\") und Unterstriche "
"(\"_\"). <b>Kann später nicht geändert werden!</b>"
#: uffd/templates/group/show.html:40 uffd/templates/role/show.html:71
#: uffd/templates/rolemod/show.html:16
msgid "Members"
msgstr "Mitglieder"
#: uffd/templates/invite/list.html:12
msgid "Link"
msgstr "Link"
#: uffd/templates/invite/list.html:13
msgid "Created by"
msgstr "Erstellt durch"
#: uffd/templates/invite/list.html:14 uffd/templates/service/api.html:34
#: uffd/templates/service/show.html:141
msgid "Permissions"
msgstr "Berechtigungen"
#: uffd/templates/invite/list.html:15
msgid "Usages"
msgstr "Verwendungen"
#: uffd/templates/invite/list.html:16
msgid "Status"
msgstr "Status"
#: uffd/templates/invite/list.html:26
msgid "Copy link to clipboard"
msgstr "Link kopieren"
#: uffd/templates/invite/list.html:27
msgid "Show link as QR code"
msgstr "Link als QR-Code anzeigen"
#: uffd/templates/invite/list.html:40
msgid "Signup"
msgstr "Account-Registrierung"
#: uffd/templates/invite/list.html:44
msgid "user signups"
msgstr "Account-Registrierungen"
#: uffd/templates/invite/list.html:49 uffd/templates/user/show.html:190
msgid "Disabled"
msgstr "Deaktiviert"
#: uffd/templates/invite/list.html:51
msgid "Voided"
msgstr "Verbraucht"
#: uffd/templates/invite/list.html:53
msgid "Expired"
msgstr "Abgelaufen"
#: uffd/templates/invite/list.html:55
msgid "Invalid, unpermitted creator"
msgstr "Ungültig, erstellt durch unberechtigten Account"
#: uffd/templates/invite/list.html:57
msgid "Invalid"
msgstr "Ungültig"
#: uffd/templates/invite/list.html:59
#, python-format
msgid "Valid once, expires %(expiry_date)s"
msgstr "Einmal verwendbar, gültig bis %(expiry_date)s"
#: uffd/templates/invite/list.html:61
#, python-format
msgid "Valid, expires %(expiry_date)s"
msgstr "Gültig bis %(expiry_date)s"
#: uffd/templates/invite/list.html:78
msgid "Invite Link Details"
msgstr "Details zum Einladungslink"
#: uffd/templates/invite/list.html:85
msgid "Type:"
msgstr "Typ:"
#: uffd/templates/invite/list.html:85
msgid "Single-use"
msgstr "Einmal verwendbar"
#: uffd/templates/invite/list.html:85 uffd/templates/invite/new.html:9
msgid "Multi-use"
msgstr "Mehrfach verwendbar"
#: uffd/templates/invite/list.html:86
msgid "Created:"
msgstr "Erstellt:"
#: uffd/templates/invite/list.html:87
msgid "Expires:"
msgstr "Ablaufdatum:"
#: uffd/templates/invite/list.html:88
msgid "Permissions:"
msgstr "Berechtigungen:"
#: uffd/templates/invite/list.html:91 uffd/templates/invite/new.html:21
msgid "Link allows account registration"
msgstr "Link erlaubt Account-Registrierung"
#: uffd/templates/invite/list.html:93 uffd/templates/invite/new.html:22
msgid "No account registration allowed"
msgstr "Keine Account-Registrierung möglich"
#: uffd/templates/invite/list.html:96
#, python-format
msgid "Link grants users the role \"%(name)s\""
msgstr "Link gibt Accounts die Rolle \"%(name)s\""
#: uffd/templates/invite/list.html:102
msgid "Never used"
msgstr "Keine Verwendungen"
#: uffd/templates/invite/list.html:106
#, python-format
msgid "Registration of user <a href=\"%(user_url)s\">%(user_name)s</a>"
msgstr "Account-Registrierung von <a href=\"%(user_url)s\">%(user_name)s</a>"
#: uffd/templates/invite/list.html:109
#, python-format
msgid "Roles granted to <a href=\"%(user_url)s\">%(user_name)s</a>"
msgstr "Rollen an <a href=\"%(user_url)s\">%(user_name)s</a> vergeben"
#: uffd/templates/invite/list.html:120
msgid "Disable Link"
msgstr "Link deaktivieren"
#: uffd/templates/invite/list.html:124
msgid "Reenable Link"
msgstr "Link reaktivieren"
#: uffd/templates/invite/list.html:138
msgid "Invite"
msgstr "Einladungslink"
#: uffd/templates/invite/new.html:6
msgid "Link Type"
msgstr "Link Typ"
#: uffd/templates/invite/new.html:8
msgid "Valid for a single successful use"
msgstr "Für eine erfolgreiche Verwendung gültig"
#: uffd/templates/invite/new.html:13
msgid "Valid Until"
msgstr "Ablaufdatum"
#: uffd/templates/invite/new.html:15
#, python-format
msgid "Must be within the next %(max_valid_days)d days"
msgstr "Muss innerhalb der nächsten %(max_valid_days)d Tage liegen"
#: uffd/templates/invite/new.html:19 uffd/templates/signup/start.html:6
msgid "Account Registration"
msgstr "Account-Registrierung"
#: uffd/templates/invite/new.html:30
msgid "Granted Roles"
msgstr "Enthaltene Rollen"
#: uffd/templates/invite/new.html:55
msgid "Create Link"
msgstr "Link erstellen"
#: uffd/templates/invite/use.html:5
msgid "Invite Link"
msgstr "Einladungslink"
#: uffd/templates/invite/use.html:8
#, python-format
msgid "Welcome to the %(org_name)s Single-Sign-On!"
msgstr "Willkommen im %(org_name)s Single-Sign-On!"
#: uffd/templates/invite/use.html:12
msgid ""
"With this link you can register a new user account with the following "
"roles or add the roles to an existing account:"
msgstr ""
"Mit diesem Link kannst du einen Account mit den folgenden Rollen "
"erstellen oder diese Rollen zu einem existierenden Account hinzufügen:"
#: uffd/templates/invite/use.html:14
msgid "With this link you can add the following roles to an existing account:"
msgstr ""
"Mit diesem Link kannst du die folgenden Rollen zu einem existierenden "
"Account hinzufügen:"
#: uffd/templates/invite/use.html:16
msgid "With this link you can register a new user account."
msgstr "Mit diesem Link kannst du einen Account registieren."
#: uffd/templates/invite/use.html:28
msgid "Add the roles to your account now"
msgstr "Rollen jetzt zu deinem Account hinzufügen"
#: uffd/templates/invite/use.html:30
msgid "Logout and switch to a different account"
msgstr "Abmelden und zu einem anderen Account wechseln"
#: uffd/templates/invite/use.html:33
msgid "Logout to register a new account"
msgstr "Abmelden um einen neuen Account zu registrieren"
#: uffd/templates/invite/use.html:37
msgid "Register a new account"
msgstr "Neuen Account registrieren"
#: uffd/templates/invite/use.html:40
msgid "Login and add the roles to your account"
msgstr "Anmelden und die Rollen zu deinem Account hinzufügen"
#: uffd/templates/mail/list.html:15 uffd/templates/mail/show.html:13
msgid "Receiving addresses"
msgstr "Empfangsadressen"
#: uffd/templates/mail/list.html:16 uffd/templates/mail/show.html:20
msgid "Destinations"
msgstr "Zieladressen"
#: uffd/templates/mail/show.html:16
msgid ""
"One address pattern (local+ext@domain, local@domain, local, @domain) per "
"line. Only lower-case ASCII letters, digits and symbols."
msgstr ""
"Ein Adressmuster (local+ext@domain, local@domain, local, @domain) pro "
"Zeile. Nur ASCII-Kleinbuchstaben, -Ziffern und -Symbole."
#: uffd/templates/mail/show.html:23
msgid "One address per line"
msgstr "Eine Adresse pro Zeile"
#: uffd/templates/mfa/auth.html:6 uffd/templates/selfservice/self.html:159
#: uffd/templates/user/show.html:188
msgid "Two-Factor Authentication"
msgstr "Zwei-Faktor-Authentifizierung"
#: uffd/templates/mfa/auth.html:11
msgid "Enable javascript for authentication with U2F/FIDO2 devices"
msgstr "Aktiviere Javascript zur Authentifizierung mit U2F/FIDO2 Geräten"
#: uffd/templates/mfa/auth.html:15
msgid "Authentication with U2F/FIDO2 devices is not supported by your browser"
msgstr ""
"Authentifizierung mit U2F/FIDO2 Geräten wird von deinem Browser nicht "
"unterstützt"
#: uffd/templates/mfa/auth.html:21
msgid "Authenticate with U2F/FIDO2 device"
msgstr "Authentifiziere dich mit einem U2F/FIDO2-Gerät"
#: uffd/templates/mfa/auth.html:24
msgid "or"
msgstr "oder"
#: uffd/templates/mfa/auth.html:27
msgid "Code from your authenticator app or recovery code"
msgstr "Code aus deiner Authentifikator-App oder Wiederherstellungscode"
#: uffd/templates/mfa/auth.html:30
msgid "Verify"
msgstr "Verifizieren"
#: uffd/templates/mfa/auth.html:43 uffd/templates/mfa/setup.html:199
msgid "Contacting server"
msgstr "Verbinde mit Server"
#: uffd/templates/mfa/auth.html:52 uffd/templates/mfa/auth.html:80
msgid "Session timed out"
msgstr "Sitzung abgelaufen"
#: uffd/templates/mfa/auth.html:54
msgid "You have not registered any U2F/FIDO2 devices for your account"
msgstr "Es sind keine U2F/FIDO2-Geräte für deinen Account registriert"
#: uffd/templates/mfa/auth.html:56 uffd/templates/mfa/setup.html:208
msgid "Server error"
msgstr "Serverfehler"
#: uffd/templates/mfa/auth.html:59 uffd/templates/mfa/setup.html:210
msgid "Waiting for device"
msgstr "Warte auf Gerät"
#: uffd/templates/mfa/auth.html:62
msgid "Verifing response"
msgstr "Überprüfe Antwort"
#: uffd/templates/mfa/auth.html:76
msgid "Success, redirecting"
msgstr "Erfolg, leite weiter"
#: uffd/templates/mfa/auth.html:82 uffd/templates/mfa/setup.html:228
msgid "Invalid response from device"
msgstr "Ungültige Antwort von Gerät"
#: uffd/templates/mfa/auth.html:88
msgid "Authentication timed out, was aborted or not allowed"
msgstr "Authentifikation abgelaufen, abgebrochen oder nicht erlaubt"
#: uffd/templates/mfa/auth.html:90
msgid "Device is not registered for your account"
msgstr "Gerät ist nicht für deinen Account registriert"
#: uffd/templates/mfa/auth.html:92
msgid "Authentication was aborted"
msgstr "Authentifikation abgebrochen"
#: uffd/templates/mfa/auth.html:94 uffd/templates/mfa/setup.html:240
#: uffd/templates/mfa/setup.html:264
msgid "U2F and FIDO2 devices are not supported by your browser"
msgstr "U2F- und FIDO2-Geräte werden vom Webbrowser nicht unterstüzt"
#: uffd/templates/mfa/auth.html:97 uffd/templates/mfa/setup.html:243
msgid "Could not connect to server"
msgstr "Verbindung zum Server fehlgeschlagen"
#: uffd/templates/mfa/auth.html:103
msgid "Authentication failed "
msgstr "Authentifikation fehlgeschlagen"
#: uffd/templates/mfa/auth.html:106
msgid "Retry authenticate with U2F/FIDO2 device"
msgstr "Authentifikation mit U2F/FIDO2-Gerät nochmal versuchen"
#: uffd/templates/mfa/disable.html:6
msgid ""
"When you proceed, all recovery codes, registered authenticator "
"applications and devices will be invalidated.\n"
"\tYou can later generate new recovery codes and setup your applications "
"and devices again."
msgstr ""
"Wenn du fortfährst werden alle Wiederherstellungscodes, registrierte "
"Authentifikator-Apps und Geräte ungültig gemacht. Du kannst später neue "
"Wiederherstellungscodes generieren und das Setup der Anwendungen und "
"Geräte erneut durchführen."
#: uffd/templates/mfa/disable.html:11 uffd/templates/mfa/setup.html:32
msgid "Disable two-factor authentication"
msgstr "Zwei-Faktor-Authentifizierung (2FA) deaktivieren"
#: uffd/templates/mfa/setup.html:18 uffd/templates/selfservice/self.html:165
msgid "Two-factor authentication is currently <strong>enabled</strong>."
msgstr "Die Zwei-Faktor-Authentifizierung ist derzeit <strong>aktiviert</strong>."
#: uffd/templates/mfa/setup.html:20 uffd/templates/selfservice/self.html:167
msgid "Two-factor authentication is currently <strong>disabled</strong>."
msgstr ""
"Die Zwei-Faktor-Authentifizierung ist derzeit "
"<strong>deaktiviert</strong>."
#: uffd/templates/mfa/setup.html:23
msgid ""
"You need to generate recovery codes and setup at least one authentication"
" method to enable two-factor authentication."
msgstr ""
"Du musst Wiederherstellungscodes generieren und mindestens eine "
"Authentifizierungsmethode hinzufügen um Zwei-Faktor-Authentifizierung "
"nutzen zu können."
#: uffd/templates/mfa/setup.html:25
msgid ""
"You need to setup at least one authentication method to enable two-factor"
" authentication."
msgstr ""
"Du musst mindestens eine Authentifizierungsmethode hinzufügen um Zwei-"
"Faktor-Authentifizierung nutzen zu können."
#: uffd/templates/mfa/setup.html:36
msgid "Reset two-factor configuration"
msgstr "Zwei-Faktor-Authentifizierung zurücksetzen"
#: uffd/templates/mfa/setup.html:46 uffd/templates/mfa/setup_recovery.html:5
#: uffd/templates/user/show.html:191
msgid "Recovery Codes"
msgstr "Wiederherstellungscodes"
#: uffd/templates/mfa/setup.html:48
msgid ""
"Recovery codes allow you to login and setup new two-factor methods when "
"you lost your registered second factor."
msgstr ""
"Wiederherstellungscodes erlauben die Anmeldung und das erneute Hinzufügen"
" einer Zwei-Faktor-Methode, falls der Zweite Faktor verloren geht."
#: uffd/templates/mfa/setup.html:52
msgid ""
"You need to setup recovery codes before you can setup up authenticator "
"apps or U2F/FIDO2 devices."
msgstr ""
"Du musst Wiederherstellungscodes generieren bevor du einen "
"Authentifikator-App oder ein U2F/FIDO2 Gerät hinzufen kannst."
#: uffd/templates/mfa/setup.html:54
msgid "Each code can only be used once."
msgstr "Jeder Code kann nur einmal verwendet werden."
#: uffd/templates/mfa/setup.html:62
msgid "Generate recovery codes to enable two-factor authentication"
msgstr ""
"Generiere Wiederherstellungscodes um die Zwei-Faktor-Authentifizierung zu"
" aktivieren"
#: uffd/templates/mfa/setup.html:66
msgid "Generate new recovery codes"
msgstr "Generiere neue Wiederherstellungscodes"
#: uffd/templates/mfa/setup.html:75
msgid "You have no remaining recovery codes."
msgstr "Du hast keine Wiederherstellungscodes übrig."
#: uffd/templates/mfa/setup.html:85 uffd/templates/user/show.html:191
msgid "Authenticator Apps (TOTP)"
msgstr "Authentifikator-Apps (TOTP)"
#: uffd/templates/mfa/setup.html:87
msgid "Use an authenticator application on your mobile device as a second factor."
msgstr "Nutze eine Authentifikator-App auf deinem Mobilgerät als zweiten Faktor."
#: uffd/templates/mfa/setup.html:90
msgid ""
"The authenticator app generates a 6-digit one-time code each time you "
"login.\n"
"\t\t\tCompatible apps are freely available for most phones."
msgstr ""
"Die Authentifikator-App generiert ein 6-stelliges Einmalpasswort für "
"jeden Login. Passende Apps sind kostenlos verfügbar für die meisten "
"Mobilgeräte."
#: uffd/templates/mfa/setup.html:100
msgid "Setup new app"
msgstr "Neue App hinzufügen"
#: uffd/templates/mfa/setup.html:108 uffd/templates/mfa/setup.html:170
msgid "Registered On"
msgstr "Registriert am"
#: uffd/templates/mfa/setup.html:122
msgid "No authenticator apps registered yet"
msgstr "Bisher keine Authentifikator-Apps registriert"
#: uffd/templates/mfa/setup.html:134 uffd/templates/user/show.html:191
msgid "U2F and FIDO2 Devices"
msgstr "U2F und FIDO2 Geräte"
#: uffd/templates/mfa/setup.html:136
msgid "Use an U2F or FIDO2 compatible hardware security key as a second factor."
msgstr "Nutze einen U2F oder FIDO2 kompatiblen Key als zweiten Faktor."
#: uffd/templates/mfa/setup.html:139
msgid ""
"U2F and FIDO2 devices are not supported by all browsers and can be "
"particularly difficult to use on mobile\n"
"\t\t\tdevices. <strong>It is strongly recommended to also setup an "
"authenticator app</strong> to be able to login on all\n"
"\t\t\tbrowsers."
msgstr ""
"U2F und FIDO2 Geräte werden nicht von allen Browsern unterstützt und "
"können besonders auf mobilen Geräten schwer zu nutzen sein. <strong>Es "
"wird dringend empfohlen ebenfalls eine Authentifikator-App "
"hinzuzufügen</strong> um einen Login mit allen Browsern zu ermöglichen."
#: uffd/templates/mfa/setup.html:147
msgid "U2F/FIDO2 support not enabled"
msgstr "U2F/FIDO2 Unterstützung nicht aktiviert"
#: uffd/templates/mfa/setup.html:151
msgid "Enable javascript in your browser to use U2F and FIDO2 devices!"
msgstr ""
"Aktiviere Javascript in deinem Browser, um U2F und FIDO2 Geräte nutzen zu"
" können!"
#: uffd/templates/mfa/setup.html:161
msgid "Setup new device"
msgstr "Neues Gerät hinzufügen"
#: uffd/templates/mfa/setup.html:184
msgid "No U2F/FIDO2 devices registered yet"
msgstr "Bisher kein U2F/FIDO2 Gerät registriert"
#: uffd/templates/mfa/setup.html:207
msgid "You need to generate recovery codes first"
msgstr "Du musst erst Wiederherstellungscodes generieren"
#: uffd/templates/mfa/setup.html:234
msgid "Registration timed out, was aborted or not allowed"
msgstr "Registrierung abgelaufen, abgebrochen oder nicht erlaubt"
#: uffd/templates/mfa/setup.html:236
msgid "Device already registered"
msgstr "Gerät bereits registriert"
#: uffd/templates/mfa/setup.html:238
msgid "Registration was aborted"
msgstr "Registrierung abgebrochen"
#: uffd/templates/mfa/setup.html:249
msgid "Registration failed"
msgstr "Registrierung fehlgeschlagen"
#: uffd/templates/mfa/setup.html:252
msgid "Retry registration"
msgstr "Registrierung nochmal versuchen"
#: uffd/templates/mfa/setup_recovery.html:8
msgid ""
"Recovery codes allow you to login when you lose access to your "
"authenticator app or U2F/FIDO device. Each code can\n"
"\tonly be used once."
msgstr ""
"Wiederherstellungscodes erlauben den Login, wenn der Zugriff auf die "
"Authentifikator-App oder das U2F/FIDO2 Gerät verloren geht. Jeder Code "
"kann nur einmal verwendet werden."
#: uffd/templates/mfa/setup_recovery.html:21
msgid ""
"These are your new recovery codes. Make sure to store them in a safe "
"place or you risk losing access to your\n"
"\taccount. All previous recovery codes are now invalid."
msgstr ""
"Dies sind deine Wiederherstellungscodes. Speichere sie an einem sicheren "
"Ort, sonst könntest du den Zugriff auf dein Konto verlieren. Alle "
"vorherigen Wiederherstellungscodes sind nun ungültig."
#: uffd/templates/mfa/setup_recovery.html:26
#: uffd/templates/session/deviceauth.html:36
#: uffd/templates/session/devicelogin.html:25
msgid "Continue"
msgstr "Weiter"
#: uffd/templates/mfa/setup_recovery.html:28
msgid "Download codes"
msgstr "Codes herunterladen"
#: uffd/templates/mfa/setup_recovery.html:30
msgid "Print codes"
msgstr "Codes ausdrucken"
#: uffd/templates/mfa/setup_totp.html:6
msgid ""
"Install an authenticator application on your mobile device like FreeOTP "
"or Google Authenticator and scan this QR\n"
"\tcode. On Apple devices you can use an app called \"Authenticator\"."
msgstr ""
"Installiere eine Authentifikator-App auf deinem Mobilgerät wie FreeOTP "
"oder Google Authenticator and scanne diesen QR Code. Auf Geräten von "
"Apple kann die App \"Authenticator\" verwendet werden."
#: uffd/templates/mfa/setup_totp.html:18
msgid ""
"If you are on your mobile device and cannot scan the code, you can click "
"on it to open it with your\n"
"\t\t\tauthenticator app. If that does not work, enter the following "
"details manually into your authenticator\n"
"\t\t\tapp:"
msgstr ""
"Falls du ein Mobilgerät verwendest und den Code nicht scannen kannst, "
"kannst du drauf klicken um ihn damit direkt in der Authentifikator-App zu"
" öffnen. Wenn das nicht funktioniert, gib die folgenden Angaben manuell "
"in die Authentifikator-App ein:"
#: uffd/templates/mfa/setup_totp.html:23
msgid "Issuer"
msgstr "Herausgeber"
#: uffd/templates/mfa/setup_totp.html:24
msgid "Account"
msgstr "Konto"
#: uffd/templates/mfa/setup_totp.html:25
msgid "Secret"
msgstr "Geheimnis"
#: uffd/templates/mfa/setup_totp.html:26
msgid "Type"
msgstr "Typ"
#: uffd/templates/mfa/setup_totp.html:27
msgid "Digits"
msgstr "Zeichen"
#: uffd/templates/mfa/setup_totp.html:28
msgid "Hash algorithm"
msgstr "Hash-Algorithmus"
#: uffd/templates/mfa/setup_totp.html:29
msgid "Interval/period"
msgstr "Intervall/Dauer"
#: uffd/templates/mfa/setup_totp.html:29
msgid "seconds"
msgstr "Sekunden"
#: uffd/templates/mfa/setup_totp.html:37
msgid "Code"
msgstr "Code"
#: uffd/templates/mfa/setup_totp.html:38
msgid "Verify and complete setup"
msgstr "Verifiziere und beende das Setup"
#: uffd/templates/oauth2/logout.html:10
msgid "Javascript is required for automatic logout"
msgstr "Für das automatische Abmelden muss Javascript aktiviert sein"
#: uffd/templates/oauth2/logout.html:12
msgid ""
"While you successfully logged out of the Single-Sign-On service, you may "
"still be logged in on these services:"
msgstr ""
"Während du nun aus dem Single-Sign-On abgemeldet bist, bist du eventuell "
"weiterhin in folgenden Diensten angemeldet:"
#: uffd/templates/oauth2/logout.html:25
msgid ""
"Please wait until you have been automatically logged out of all services "
"or make sure of this yourself."
msgstr ""
"Bitte warte, bis das automatische Abmelden bei allen Diensten "
"abgeschlossen ist oder melde dich überall manuell ab."
#: uffd/templates/oauth2/logout.html:29
msgid "Logging you out on all services ..."
msgstr "Abmeldung bei allen Diensten ..."
#: uffd/templates/oauth2/logout.html:33
msgid "Skip this and continue"
msgstr "Automatisches Abmelden überspringen"
#: uffd/templates/oauth2/logout.html:72
msgid "Done, redirecting ..."
msgstr "Abgeschlossen, leite weiter ..."
#: uffd/templates/oauth2/logout.html:76
msgid "Log out failed on some services. Retry?"
msgstr ""
"Automatisches Abmelden bei einigen Diensten fehlgeschlagen. Nochmal "
"versuchen?"
#: uffd/templates/role/list.html:23
msgid "<empty name>"
msgstr "<leerer Name>"
#: uffd/templates/role/show.html:6
msgid ""
"Name, moderator group, included roles and groups of this role are managed"
" externally."
msgstr ""
"Name, Moderationsgruppe, enthaltene Rollen und Gruppen dieser Rolle "
"werden extern verwaltet."
#: uffd/templates/role/show.html:17
msgid ""
"All non-service users will be removed as members from this role and get "
"its permissions implicitly. Are you sure?"
msgstr ""
"Alle Nicht-Service-Accounts verlieren diese Rolle und erhalten dessen "
"Berechtigungen implizit."
#: uffd/templates/role/show.html:17 uffd/templates/role/show.html:23
msgid "Set as default"
msgstr "Als Default setzen"
#: uffd/templates/role/show.html:19
msgid "Unset as default"
msgstr "Nicht mehr als Default setzen"
#: uffd/templates/role/show.html:29
msgid "Settings"
msgstr "Einstellungen"
#: uffd/templates/role/show.html:32
msgid "Included roles"
msgstr "Enthaltene Rollen"
#: uffd/templates/role/show.html:35 uffd/templates/role/show.html:122
msgid "Included groups"
msgstr "Enthaltene Gruppen"
#: uffd/templates/role/show.html:42
msgid "Role Name"
msgstr "Rollenname"
#: uffd/templates/role/show.html:54
msgid "Moderator Group"
msgstr "Moderationsgruppe"
#: uffd/templates/role/show.html:56
msgid "No Moderator Group"
msgstr "Keine Moderationsgruppe"
#: uffd/templates/role/show.html:63
msgid "Moderators"
msgstr "Accounts mit Moderationsrechten"
#: uffd/templates/role/show.html:81
msgid "Roles to include groups from recursively"
msgstr "Rollen, deren Gruppen rekursiv enthalten sein sollen"
#: uffd/templates/role/show.html:86 uffd/templates/role/show.html:127
msgid "name"
msgstr "Name"
#: uffd/templates/role/show.html:87 uffd/templates/role/show.html:128
msgid "description"
msgstr "Beschreibung"
#: uffd/templates/role/show.html:88
msgid "currently includes groups"
msgstr "derzeit enthaltene Gruppen"
#: uffd/templates/role/show.html:129
msgid "2FA required"
msgstr "2FA erforderlich"
#: uffd/templates/rolemod/show.html:7
msgid "Invite Members"
msgstr "Mitglieder einladen"
#: uffd/templates/rolemod/show.html:13
msgid "Overview"
msgstr "Übersicht"
#: uffd/templates/rolemod/show.html:22
msgid "Role name"
msgstr "Rollenname"
#: uffd/templates/rolemod/show.html:30
msgid "Moderators:"
msgstr "Accounts mit Moderationsrechten:"
#: uffd/templates/rolemod/show.html:40
msgid "Role members:"
msgstr "Mitglieder:"
#: uffd/templates/rolemod/show.html:53
msgid "Remove"
msgstr "Entfernen"
#: uffd/templates/selfservice/forgot_password.html:6
msgid "Forgot password"
msgstr "Passwort vergessen"
#: uffd/templates/selfservice/forgot_password.html:9
#: uffd/templates/selfservice/self.html:21 uffd/templates/session/login.html:12
#: uffd/templates/signup/start.html:9 uffd/templates/user/list.html:18
#: uffd/templates/user/show.html:78
msgid "Login Name"
msgstr "Anmeldename"
#: uffd/templates/selfservice/forgot_password.html:13
msgid "Mail Address"
msgstr "E-Mail-Adresse"
#: uffd/templates/selfservice/forgot_password.html:17
msgid "Send password reset mail"
msgstr "Passwort-Zurücksetzen-Mail versenden"
#: uffd/templates/selfservice/self.html:7
msgid ""
"Some permissions require you to setup two-factor authentication.\n"
"\tThese permissions are not in effect until you do that."
msgstr ""
"Einige deiner Berechtigungen erfordern das Einrichten von Zwei-Faktor-"
"Authentifizierung.\n"
"\tDiese Berechtigungen werden erst aktiv, wenn du dies getan hast."
#: uffd/templates/selfservice/self.html:14 uffd/templates/user/show.html:48
msgid "Profile"
msgstr "Profil"
#: uffd/templates/selfservice/self.html:15
msgid ""
"Your profile information is used by all services that are integrated into"
" the Single-Sign-On."
msgstr ""
"Deine Profilangaben werden von allen Diensten verwendet, die an das "
"Single-Sign-On angeschlossen sind."
#: uffd/templates/selfservice/self.html:16
msgid "Changes may take several minutes to be visible in all services."
msgstr "Änderungen sind erst nach einigen Minuten in allen Diensten sichtbar."
#: uffd/templates/selfservice/self.html:25 uffd/templates/signup/start.html:22
#: uffd/templates/user/list.html:19 uffd/templates/user/show.html:93
msgid "Display Name"
msgstr "Anzeigename"
#: uffd/templates/selfservice/self.html:28
msgid "Update Profile"
msgstr "Änderungen speichern"
#: uffd/templates/selfservice/self.html:37 uffd/templates/user/show.html:110
msgid "E-Mail Addresses"
msgstr "E-Mail-Adressen"
#: uffd/templates/selfservice/self.html:38
msgid ""
"Add and delete addresses associated with your account. You will need to "
"verify new addresses by opening a link set to them."
msgstr ""
"Füge neue Adressen zu deinem Account hinzu oder löschen vorhandene. Neue "
"Adressen müssen bestätigt werden, bevor sie verwendet werden können. Dazu"
" erhälst du eine E-Mail mit einem Bestätigungslink."
#: uffd/templates/selfservice/self.html:43
msgid "Email"
msgstr "E-Mail"
#: uffd/templates/selfservice/self.html:44
msgid "New E-Mail Address"
msgstr "Neue E-Mail-Adresse"
#: uffd/templates/selfservice/self.html:45
msgid "Add address"
msgstr "Adresse hinzufügen"
#: uffd/templates/selfservice/self.html:55 uffd/templates/user/show.html:126
msgid "primary"
msgstr "primär"
#: uffd/templates/selfservice/self.html:57
msgid "unverified"
msgstr "nicht bestätigt"
#: uffd/templates/selfservice/self.html:62 uffd/views/selfservice.py:175
msgid "Cannot delete primary e-mail address"
msgstr "Primäre E-Mail-Adresse kann nicht gelöscht werden"
#: uffd/templates/selfservice/self.html:65
msgid "Retry verification"
msgstr "Bestätigungslink neusenden"
#: uffd/templates/selfservice/self.html:81
msgid "E-Mail Preferences"
msgstr "E-Mail-Einstellungen"
#: uffd/templates/selfservice/self.html:83
msgid ""
"Choose your primary e-mail address and the address password recovery "
"e-mails will be sent to."
msgstr ""
"Wähle deine primäre Adresse und die Adresse für Passwort-"
"Zurücksetzen-E-Mails aus."
#: uffd/templates/selfservice/self.html:85
msgid "You can also select different addresses for different services."
msgstr ""
"Du kannst für unterschiedliche Dienste unterschiedliche Adressen "
"verwenden."
#: uffd/templates/selfservice/self.html:88
msgid "Adresses must be verified before you can select them here."
msgstr "Adressen müssen bestätigt sein, damit du sie hier auswählen kannst."
#: uffd/templates/selfservice/self.html:93
msgid "Primary Address"
msgstr "Primäre Adresse"
#: uffd/templates/selfservice/self.html:101
msgid "Address for Password Reset E-Mails"
msgstr "Adresse für Passwort-Zurücksetzen-E-Mails"
#: uffd/templates/selfservice/self.html:103
#: uffd/templates/selfservice/self.html:116 uffd/templates/user/show.html:155
#: uffd/templates/user/show.html:165
msgid "Use primary address"
msgstr "Primäre Adresse verwenden"
#: uffd/templates/selfservice/self.html:114
#, python-format
msgid "Address for Service \"%(name)s\""
msgstr "Adresse für Dienst „%(name)s“"
#: uffd/templates/selfservice/self.html:125
msgid "Show more settings ..."
msgstr "Weitere Einstellungen anzeigen ..."
#: uffd/templates/selfservice/self.html:127
msgid "Update E-Mail Preferences"
msgstr "E-Mail-Einstellungen speichern"
#: uffd/templates/selfservice/self.html:136
#: uffd/templates/session/login.html:16 uffd/templates/signup/start.html:36
#: uffd/templates/user/show.html:175
msgid "Password"
msgstr "Passwort"
#: uffd/templates/selfservice/self.html:137
msgid ""
"Your login password for the Single-Sign-On. Only enter it on the Single-"
"Sign-On login page! No other legit websites will ask you for this "
"password. We do not ever need your password to assist you."
msgstr ""
"Dein Passwort zur Anmeldung im Single-Sign-On. Gib dieses Passwort "
"ausschließlich auf der Anmeldeseite des Single-Sign-Ons ein! Keine andere"
" Webseite wird dich nach diesem Passwort fragen. Es wird auch niemals für"
" Support-Anfragen benötigt."
#: uffd/templates/selfservice/self.html:142
#: uffd/templates/selfservice/set_password.html:9
msgid "New Password"
msgstr "Neues Passwort"
#: uffd/templates/selfservice/self.html:148
#: uffd/templates/selfservice/set_password.html:16
#: uffd/templates/signup/start.html:43
msgid "Repeat Password"
msgstr "Passwort wiederholen"
#: uffd/templates/selfservice/self.html:150
msgid "Change Password"
msgstr "Passwort ändern"
#: uffd/templates/selfservice/self.html:160
msgid ""
"Setting up Two-Factor Authentication (2FA) adds an additional step to the"
" Single-Sign-On login and increases the security of your account "
"significantly."
msgstr ""
"Zwei-Faktor-Authentifizierung (2FA) fügt einen zusätzlichen Schritt zur "
"Anmeldung im Single-Sign-On hinzu und verbessert damit die Sicherheit "
"deines Accounts erheblich."
#: uffd/templates/selfservice/self.html:170
msgid "Manage two-factor authentication"
msgstr "Zwei-Faktor-Authentifizierung (2FA) verwalten"
#: uffd/templates/selfservice/self.html:178
msgid "Active Sessions"
msgstr "Aktive Sitzungen"
#: uffd/templates/selfservice/self.html:179
msgid "Your active login sessions on this device and other devices."
msgstr "Deine aktiven Sitzungen auf diesem und anderen Geräten."
#: uffd/templates/selfservice/self.html:180
msgid ""
"Revoke a session to log yourself out on another device. Note that this is"
" limited to the Single-Sign-On session and <b>does not affect login "
"sessions on services.</b>"
msgstr ""
"Widerrufe eine Sitzung, um dich auf einem anderen Gerät abzumelden. "
"Beachte dass dies auf deine Sitzung im Single-Sign-On beschränkt ist und "
"sich <b>nicht auf Sitzungen an Diensten auswirkt.</b>"
#: uffd/templates/selfservice/self.html:186
msgid "Last used"
msgstr "Zuletzt verwendet"
#: uffd/templates/selfservice/self.html:187
msgid "Device"
msgstr "Gerät"
#: uffd/templates/selfservice/self.html:193
#: uffd/templates/selfservice/self.html:202
msgid "Just now"
msgstr "Gerade eben"
#: uffd/templates/selfservice/self.html:211
msgid "Revoke"
msgstr "Widerrufen"
#: uffd/templates/selfservice/self.html:227 uffd/templates/user/list.html:20
#: uffd/templates/user/show.html:51 uffd/templates/user/show.html:207
#: uffd/views/role.py:21
msgid "Roles"
msgstr "Rollen"
#: uffd/templates/selfservice/self.html:228
msgid ""
"Aside from a set of base permissions, your roles determine the "
"permissions of your account."
msgstr ""
"Deine Berechtigungen werden, von einigen Basis-Berechtigungen abgesehen, "
"von deinen Rollen bestimmt"
#: uffd/templates/selfservice/self.html:230
#, python-format
msgid ""
"See <a href=\"%(services_url)s\">Services</a> for an overview of your "
"current permissions."
msgstr ""
"Auf <a href=\"%(services_url)s\">Dienste</a> erhälst du einen Überblick "
"über deine aktuellen Berechtigungen."
#: uffd/templates/selfservice/self.html:234
msgid "Administrators and role moderators can invite you to new roles."
msgstr ""
"Accounts mit Adminrechten oder Rollen-Moderationsrechten können dich zu "
"Rollen einladen."
#: uffd/templates/selfservice/self.html:249
msgid ""
"Some permissions in this role require you to setup two-factor "
"authentication"
msgstr ""
"Einige Berechtigungen dieser Rolle erfordern das Einrichten von Zwei-"
"Faktor-Authentifikation"
#: uffd/templates/selfservice/self.html:255
msgid "Leave"
msgstr "Verlassen"
#: uffd/templates/selfservice/self.html:262
msgid "You currently don't have any roles"
msgstr "Du hast derzeit keine Rollen"
#: uffd/templates/selfservice/set_password.html:6
msgid "Reset password"
msgstr "Passwort zurücksetzen"
#: uffd/templates/selfservice/set_password.html:20
msgid "Set password"
msgstr "Passwort setzen"
#: uffd/templates/service/api.html:20
msgid "Authentication Username"
msgstr "Authentifikations-Name"
#: uffd/templates/service/api.html:25
msgid "Authentication Password"
msgstr "Authentifikations-Passwort"
#: uffd/templates/service/api.html:37
msgid "Access user and group data"
msgstr "Zugriff auf Account- und Gruppen-Daten"
#: uffd/templates/service/api.html:41
msgid "Verify user passwords"
msgstr "Passwörter von Nutzeraccounts verifizieren"
#: uffd/templates/service/api.html:45
msgid "Access mail aliases"
msgstr "Zugriff auf Mail-Weiterleitungen"
#: uffd/templates/service/api.html:49
msgid "Resolve remailer addresses"
msgstr "Auflösen von Remailer-Adressen"
#: uffd/templates/service/api.html:51 uffd/templates/service/show.html:55
msgid "This option has no effect: Remailer config options are unset"
msgstr "Diese Option hat keine Auswirkung: Remailer ist nicht konfiguriert"
#: uffd/templates/service/api.html:56
msgid "Access uffd metrics"
msgstr "Zugriff auf uffd-Metriken"
#: uffd/templates/service/oauth2.html:20 uffd/templates/service/show.html:112
msgid "Client ID"
msgstr "Client-ID"
#: uffd/templates/service/oauth2.html:25
msgid "Client Secret"
msgstr "Client-Secret"
#: uffd/templates/service/oauth2.html:34
msgid "Redirect URIs"
msgstr "Redirect-URIs"
#: uffd/templates/service/oauth2.html:37
msgid "One URI per line"
msgstr "Eine URI pro Zeile"
#: uffd/templates/service/oauth2.html:42
msgid "Logout URIs"
msgstr "Abmelde-URIs"
#: uffd/templates/service/oauth2.html:49
msgid "One URI per line, prefixed with space-separated method (GET/POST)"
msgstr ""
"Eine URI pro Zeile, vorangestellt die mit Leerzeichen getrennte HTTP-"
"Methode (GET/POST)"
#: uffd/templates/service/overview.html:11
msgid ""
"Some services may not be publicly listed! Log in to see all services you "
"have access to."
msgstr ""
"Einige Dienste sind eventuell nicht öffentlich aufgelistet! Melde dich an"
" um alle Dienste zu sehen, auf die du Zugriff hast."
#: uffd/templates/service/overview.html:36
#, python-format
msgid "Logo for %(service_title)s"
msgstr "Logo für %(service_title)s"
#: uffd/templates/service/overview.html:55
msgid "No access"
msgstr "Kein Zugriff"
#: uffd/templates/service/overview.html:75
msgid "Manage OAuth2 and API clients"
msgstr "OAuth2- und API-Clients verwalten"
#: uffd/templates/service/overview.html:95 uffd/templates/user/list.html:61
#: uffd/templates/user/list.html:82
msgid "Close"
msgstr "Schließen"
#: uffd/templates/service/show.html:24
msgid "Access Restriction"
msgstr "Zugriffsbeschränkungen"
#: uffd/templates/service/show.html:26
msgid "No user has access"
msgstr "Kein Account hat Zugriff"
#: uffd/templates/service/show.html:27
msgid "All users have access (legacy)"
msgstr "Alle Account haben Zugriff (veraltet)"
#: uffd/templates/service/show.html:29
#, python-format
msgid "Members of group \"%(group_name)s\" have access"
msgstr "Mitglieder der Gruppe \"%(group_name)s\" haben Zugriff"
#: uffd/templates/service/show.html:37
msgid "Hide deactivated users from service"
msgstr "Deaktivierte Nutzer verstecken"
#: uffd/templates/service/show.html:44
msgid ""
"Allow users with access to select a different e-mail address for this "
"service"
msgstr ""
"Ermögliche Nutzern mit Zugriff auf diesen Dienst eine andere E-Mail-"
"Adresse auszuwählen"
#: uffd/templates/service/show.html:46
msgid "If disabled, the service always uses the primary e-mail address."
msgstr "Wenn deaktiviert, wird immer die primäre E-Mail-Adresse verwendet."
#: uffd/templates/service/show.html:53
msgid "Hide e-mail addresses with remailer"
msgstr "E-Mail-Adressen mit Remailer verstecken"
#: uffd/templates/service/show.html:60 uffd/templates/service/show.html:83
msgid "Remailer disabled"
msgstr "Remailer deaktiviert"
#: uffd/templates/service/show.html:63 uffd/templates/service/show.html:86
msgid "Remailer enabled"
msgstr "Remailer aktiviert"
#: uffd/templates/service/show.html:66 uffd/templates/service/show.html:89
msgid "Remailer enabled (deprecated, case-sensitive format)"
msgstr ""
"Remailer aktiviert (veraltetes, Groß-/Kleinschreibung-unterscheidendes "
"Format)"
#: uffd/templates/service/show.html:70
msgid ""
"Some services notify users about changes to their e-mail address. "
"Modifying this setting immediatly affects the e-mail addresses of all "
"users and can cause masses of notification e-mails."
msgstr ""
"Einige Dienste benachrichtigen Nutzer bei Änderungen ihrer E-Mail-"
"Adresse. Diese Einstellung zu verändern wirkt sich unmittelbar auf die E"
"-Mail-Adressen aller Nutzer aus und kann zu massenhaftem Versand von "
"Benachrichtigungs-E-Mails führen."
#: uffd/templates/service/show.html:76
msgid "Overwrite remailer setting for specific users"
msgstr "Überschreibe Remailer-Einstellung für ausgewählte Nutzer"
#: uffd/templates/service/show.html:79
msgid "Login names"
msgstr "Anmeldenamen"
#: uffd/templates/service/show.html:94
msgid ""
"Useful for testing remailer before enabling it for all users. Specify "
"users as a comma-seperated list of login names."
msgstr ""
"Hilfreich zum Testen des Remailers vor dem Aktivieren für alle Nutzer. Um"
" Nutzer auszuwählen, liste ihre Anmeldenamen mit Komma getrennt auf."
#: uffd/templates/session/deviceauth.html:15
msgid "Log into a service on another device without entering your password."
msgstr ""
"Melde dich an einem Dienst auf einem anderen Gerät an ohne dein Password "
"eingeben zu müssen."
#: uffd/templates/session/deviceauth.html:18
#: uffd/templates/session/devicelogin.html:13
msgid "Initiation Code"
msgstr "Startcode"
#: uffd/templates/session/deviceauth.html:27
#: uffd/templates/session/devicelogin.html:18
msgid "Confirmation Code"
msgstr "Bestätigungscode"
#: uffd/templates/session/deviceauth.html:33
msgid ""
"Start logging into a service on the other device and chose \"Device "
"Login\" on the login page. Enter the displayed initiation code in the box"
" above."
msgstr ""
"Beginne die Anmeldung an einem Dienst auf dem anderen Gerät und wähle "
"\"Gerätelogin\" auf der Anmeldeseite aus. Gib den angezeigten Startcode "
"oben ein."
#: uffd/templates/session/deviceauth.html:43
#, python-format
msgid "Authorize the login for service <b>%(service_name)s</b>?"
msgstr "Anmeldung an Dienst <b>%(service_name)s</b> erlauben?"
#: uffd/templates/session/deviceauth.html:46
msgid "Authorize Login"
msgstr "Anmeldung erlauben"
#: uffd/templates/session/deviceauth.html:53
msgid ""
"Enter the confirmation code on the other device and complete the login. "
"Click <em>Finish</em> afterwards."
msgstr ""
"Gib den Bestätigungscode auf dem anderen Gerät ein und schließe die "
"Anmeldung ab. Clicke danach auf <em>Abschließen</em>."
#: uffd/templates/session/deviceauth.html:56
msgid "Finish"
msgstr "Beenden"
#: uffd/templates/session/devicelogin.html:9
msgid ""
"Use a login session on another device (e.g. your laptop) to log into a "
"service without entering your password."
msgstr ""
"Nutze eine Login-Sitzung auf einem anderen Gerät (z.B. deinem Laptop) um "
"dich bei einem Dienst anzumelden."
#: uffd/templates/session/devicelogin.html:22
#, python-format
msgid ""
"Open <code><a href=\"%(deviceauth_url)s\">%(deviceauth_url)s</a></code> "
"on the other device and enter the initiation code there. Then enter the "
"confirmation code in the box above."
msgstr ""
"Öffne <code><a href=\"%(deviceauth_url)s\">%(deviceauth_url)s</a></code> "
"auf dem anderen Gerät und gib dort den obenstehenden Startcode ein. Geben"
" anschließend den Bestätigungscode hier ein."
#: uffd/templates/session/login.html:23
msgid "- or -"
msgstr "- oder -"
#: uffd/templates/session/login.html:25
msgid "Login with another device"
msgstr "Über anderes Gerät anmelden"
#: uffd/templates/session/login.html:30
msgid "Register"
msgstr "Registrieren"
#: uffd/templates/session/login.html:32
msgid "Forgot Password?"
msgstr "Passwort vergessen?"
#: uffd/templates/signup/confirm.html:6
msgid "Complete Registration"
msgstr "Account-Registrierung abschließen"
#: uffd/templates/signup/confirm.html:9
msgid "Please enter your password to complete the account registration"
msgstr "Bitte gib dein Passwort ein, um die Account-Registrierung abzuschließen"
#: uffd/templates/signup/confirm.html:13
msgid "Complete Account Registration"
msgstr "Account-Registrierung abschließen"
#: uffd/templates/signup/start.html:13
msgid "Check"
msgstr "Überprüfen"
#: uffd/templates/signup/start.html:25
msgid "At least one and at most 128 characters, no other special requirements."
msgstr "Mindestens 1 und maximal 128 Zeichen, keine weiteren Einschränkungen."
#: uffd/templates/signup/start.html:29 uffd/templates/user/show.html:102
msgid "E-Mail Address"
msgstr "E-Mail-Adresse"
#: uffd/templates/signup/start.html:32
msgid ""
"We will send a confirmation mail to this address that you need to "
"complete the registration."
msgstr ""
"Wir werden eine Bestätigungsmail an diese Adresse senden. Du benötigst "
"sie, um die Account-Registrierung abzuschließen."
#: uffd/templates/signup/start.html:47
msgid "Create Account"
msgstr "Account registrieren"
#: uffd/templates/signup/start.html:74
msgid "The name is already taken"
msgstr "Dieser Name wird bereits verwendet"
#: uffd/templates/signup/start.html:77
msgid "Too many requests! Please wait a bit before trying again!"
msgstr "Zu viele Anfragen! Bitte warte etwas, bevor du es erneut versuchst!"
#: uffd/templates/signup/start.html:80
msgid "The name is invalid"
msgstr "Name ungültig"
#: uffd/templates/signup/submitted.html:5
msgid "Confirm your E-Mail Address"
msgstr "E-Mail-Adresse bestätigen"
#: uffd/templates/signup/submitted.html:7
#, python-format
msgid ""
"We sent a confirmation mail to <b>%(signup_mail)s</b>. You need to "
"confirm your mail address within 48 hours to complete the account "
"registration."
msgstr ""
"Eine Bestätigungsmail wurde an <b>%(signup_mail)s</b> gesendet. Du must "
"deine E-Mail-Adresse innerhalb von 48 Stunden bestätigen, um die Account-"
"Registrierung abzuschließen."
#: uffd/templates/signup/submitted.html:8
msgid ""
"If you mistyped your mail address or don't receive the confirmation mail "
"for another reason, retry the registration procedure from the beginning."
msgstr ""
"Falls du dich bei deiner E-Mail-Adresse vertippt hast oder du aus anderen"
" Gründen keine Bestätigungsmail erhalten hast, kannst du den Prozess "
"einfach von Vorne beginnen."
#: uffd/templates/user/list.html:11
msgid "CSV import"
msgstr "CSV-Import"
#: uffd/templates/user/list.html:17
msgid "UID"
msgstr "UID"
#: uffd/templates/user/list.html:34 uffd/templates/user/show.html:60
msgid "service"
msgstr "service"
#: uffd/templates/user/list.html:37
msgid "deactivated"
msgstr "deaktiviert"
#: uffd/templates/user/list.html:60
msgid "Import a csv formated list of users"
msgstr "Importiere eine als CSV formatierte Liste von Accounts"
#: uffd/templates/user/list.html:67
msgid ""
"The format should be \"loginname,mailaddres,roleid1;roleid2\". Neither "
"setting the display name nor setting passwords is supported (yet). "
"Example:"
msgstr ""
"Das Format sollte \"loginname,mailaddres,roleid1;roleid2\" sein. Der "
"Anzeigename oder das Password können (derzeit) nicht gesetzt werden. "
"Beispiel:"
#: uffd/templates/user/list.html:78 uffd/templates/user/show.html:88
msgid "Ignore login name blocklist"
msgstr "Liste der nicht erlaubten Anmeldenamen ignorieren"
#: uffd/templates/user/list.html:83
msgid "Import"
msgstr "Importieren"
#: uffd/templates/user/show.html:6
msgid "New address"
msgstr "Neue Adresse"
#: uffd/templates/user/show.html:27
msgid ""
"This account is deactivated. The user cannot login and existing sessions "
"are not usable. The user cannot log into services, but existing sessions "
"on services might still be active."
msgstr ""
"Dieser Account ist deaktiviert. Der Nutzer kann sich nicht neu anmelden. "
"Existierende Sitzungen sind nicht nutzbar. Eine Anmeldung bei Diensten "
"ist nicht möglich, allerdings könnten bestehende Sitzungen weiterhin "
"aktiv sein."
#: uffd/templates/user/show.html:34 uffd/templates/user/show.html:38
msgid "Deactivate"
msgstr "Deaktivieren"
#: uffd/templates/user/show.html:36
msgid "Activate"
msgstr "Aktivieren"
#: uffd/templates/user/show.html:58
msgid "User ID"
msgstr "Account ID"
#: uffd/templates/user/show.html:66
msgid "will be choosen"
msgstr "wird automatisch bestimmt"
#: uffd/templates/user/show.html:73
msgid "Service User"
msgstr "Service-Account"
#: uffd/templates/user/show.html:81
msgid ""
"Only letters, numbers, dashes (\"-\") and underscores (\"_\") are "
"allowed. At most 32, at least 2 characters. There is a word blocklist. "
"Must be unique."
msgstr ""
"Nur Buchstaben, Zahlen, Binde- (\"-\") und Unterstriche (\"_\") sind "
"erlaubt. Maximal 32, mindestens 2 Zeichen. Es gibt eine Liste nicht "
"erlaubter Namen. Muss einmalig sein."
#: uffd/templates/user/show.html:96
msgid ""
"If you leave this empty it will be set to the login name. At most 128, at"
" least 2 characters. No character restrictions."
msgstr ""
"Wenn das Feld leer bleibt, wird der Anmeldename verwendet. Maximal 128, "
"mindestens 2 Zeichen. Keine Zeichenbeschränkung."
#: uffd/templates/user/show.html:105
msgid ""
"Make sure the address is correct! Services might use e-mail addresses as "
"account identifiers and rely on them being unique and verified."
msgstr ""
"Stelle sicher, dass die Adresse korrekt ist! Manche Dienste verwenden die"
" E-Mail-Adresse um Accounts zu identifizieren und verlassen sich darauf, "
"dass diese verifiziert und einzigartig sind."
#: uffd/templates/user/show.html:114
msgid "Address"
msgstr "Adresse"
#: uffd/templates/user/show.html:115
msgid "Verified"
msgstr "Verifiziert"
#: uffd/templates/user/show.html:141
msgid ""
"Make sure that addresses you add are correct! Services might use e-mail "
"addresses as account identifiers and rely on them being unique and "
"verified."
msgstr ""
"Stelle sicher, dass Adressen, die du hinzufügst, korrekt sind! Manche "
"Dienste verwenden die E-Mail-Adresse um Accounts zu identifizieren und "
"verlassen sich darauf, dass diese verifiziert und einzigartig sind."
#: uffd/templates/user/show.html:145
msgid "Primary E-Mail Address"
msgstr "Primäre E-Mail-Adresse"
#: uffd/templates/user/show.html:153
msgid "Recovery E-Mail Address"
msgstr "Wiederherstellungs-E-Mail-Adresse"
#: uffd/templates/user/show.html:163
#, python-format
msgid "Address for %(name)s"
msgstr "Adresse für %(name)s"
#: uffd/templates/user/show.html:179
msgid "E-Mail to set it will be sent"
msgstr "Mail zum Setzen wird versendet"
#: uffd/templates/user/show.html:190
msgid "Status:"
msgstr "Status:"
#: uffd/templates/user/show.html:190
msgid "Enabled"
msgstr "Aktiv"
#: uffd/templates/user/show.html:193
msgid "Reset 2FA"
msgstr "2FA zurücksetzen"
#: uffd/templates/user/show.html:197
msgid "Sessions"
msgstr "Sitzungen"
#: uffd/templates/user/show.html:198
#, python-format
msgid "%(session_count)d active sessions"
msgstr "%(session_count)d aktive Sitzungen"
#: uffd/templates/user/show.html:199
msgid "Revoke all sessions"
msgstr "Alle Sitzungen widerrufen"
#: uffd/templates/user/show.html:240
msgid "Resulting groups (only updated after save)"
msgstr "Resultierende Gruppen (wird nur aktualisiert beim Speichern)"
#: uffd/views/group.py:22
msgid "Groups"
msgstr "Gruppen"
#: uffd/views/group.py:42
msgid "GID is already in use or was used in the past"
msgstr "GID wird oder wurde bereits verwendet"
#: uffd/views/group.py:45
msgid "Invalid name"
msgstr "Ungültiger Name"
#: uffd/views/group.py:56
msgid "Group with this name or id already exists"
msgstr "Gruppe mit diesem Namen oder dieser ID existiert bereits"
#: uffd/views/group.py:61
msgid "Group created"
msgstr "Gruppe erstellt"
#: uffd/views/group.py:63
msgid "Group updated"
msgstr "Gruppe aktualisiert"
#: uffd/views/group.py:72
msgid "Deleted group"
msgstr "Gruppe gelöscht"
#: uffd/views/invite.py:43
msgid "Invites"
msgstr "Einladungslinks"
#: uffd/views/invite.py:75
msgid "The \"Expires After\" date is too far in the future"
msgstr "Das Ablaufdatum liegt zu weit in der Zukunft"
#: uffd/views/invite.py:78
msgid "You are not allowed to create invite links with these permissions"
msgstr "Dir fehlen Berechtigungen um diesen Einladungslink zu erstellen"
#: uffd/views/invite.py:81
msgid "Invite link must either allow signup or grant at least one role"
msgstr ""
"Einladungslink muss entweder Account-Registrierung erlauben oder Rollen "
"vergeben"
#: uffd/views/invite.py:111 uffd/views/invite.py:146
msgid "Invalid invite link"
msgstr "Ungültiger Einladungslink"
#: uffd/views/invite.py:129
msgid "Roles successfully updated"
msgstr "Rollen erfolgreich geändert"
#: uffd/views/invite.py:149
msgid "Invite link does not allow signup"
msgstr "Einladungslink erlaubt keine Account-Registrierung"
#: uffd/views/invite.py:175 uffd/views/selfservice.py:44
#: uffd/views/signup.py:47
msgid "Passwords do not match"
msgstr "Die Passwörter stimmen nicht überein"
#: uffd/views/invite.py:181 uffd/views/signup.py:53
#, python-format
msgid "Too many signup requests with this mail address! Please wait %(delay)s."
msgstr ""
"Zu viele Account-Registrierungen mit dieser E-Mail-Adresse! Bitte warte "
"%(delay)s."
#: uffd/views/invite.py:184 uffd/views/signup.py:56
#, python-format
msgid "Too many requests! Please wait %(delay)s."
msgstr "Zu viele Anfragen! Bitte warte %(delay)s."
#: uffd/views/invite.py:199 uffd/views/signup.py:74
msgid "Could not send mail"
msgstr "Mailversand fehlgeschlagen"
#: uffd/views/mail.py:21
msgid "Forwardings"
msgstr "Weiterleitungen"
#: uffd/views/mail.py:46
#, python-format
msgid "Invalid receive address: %(mail_address)s"
msgstr "Ungültige Empfangsadresse: %(mail_address)s"
#: uffd/views/mail.py:50
msgid "Mail mapping updated."
msgstr "Mailweiterleitung geändert."
#: uffd/views/mail.py:59
msgid "Deleted mail mapping."
msgstr "Mailweiterleitung gelöscht."
#: uffd/views/mfa.py:49
msgid "Two-factor authentication was reset"
msgstr "Zwei-Faktor-Authentifizierung wurde zurückgesetzt"
#: uffd/views/mfa.py:78
msgid "Generate recovery codes first!"
msgstr "Generiere zuerst die Wiederherstellungscodes!"
#: uffd/views/mfa.py:86
msgid "Code is invalid"
msgstr "Wiederherstellungscode ist ungültig"
#: uffd/views/mfa.py:105
#, python-format
msgid ""
"2FA WebAuthn support disabled because import of the fido2 module failed "
"(%s)"
msgstr ""
"2FA WebAuthn Unterstützung deaktiviert, da das fido2 Modul nicht geladen "
"werden konnte (%s)"
#: uffd/views/mfa.py:216
#, python-format
msgid "We received too many invalid attempts! Please wait at least %s."
msgstr "Wir haben zu viele fehlgeschlagene Versuche! Bitte warte mindestens %s."
#: uffd/views/mfa.py:231
msgid "You have exhausted your recovery codes. Please generate new ones now!"
msgstr "Du hast keine Wiederherstellungscode mehr. Bitte generiere diese jetzt!"
#: uffd/views/mfa.py:234
msgid ""
"You only have a few recovery codes remaining. Make sure to generate new "
"ones before they run out."
msgstr ""
"Du hast nur noch wenige Wiederherstellungscodes übrig. Bitte generiere "
"diese erneut bevor keine mehr übrig sind."
#: uffd/views/mfa.py:238
msgid "Two-factor authentication failed"
msgstr "Zwei-Faktor-Authentifizierung fehlgeschlagen"
#: uffd/views/oauth2.py:267 uffd/views/selfservice.py:66
#: uffd/views/session.py:86
#, python-format
msgid ""
"We received too many requests from your ip address/network! Please wait "
"at least %(delay)s."
msgstr ""
"Wir haben zu viele Anfragen von deiner IP-Adresses bzw. aus deinem "
"Netzwerk empfangen! Bitte warte mindestens %(delay)s."
#: uffd/views/oauth2.py:278
msgid "Device login is currently not available. Try again later!"
msgstr "Geräte-Login ist gerade nicht verfügbar. Versuche es später nochmal!"
#: uffd/views/oauth2.py:296
msgid "Device login failed"
msgstr "Gerätelogin fehlgeschlagen"
#: uffd/views/oauth2.py:304
msgid "You need to login to access this service"
msgstr "Du musst dich anmelden, um auf diesen Dienst zugreifen zu können"
#: uffd/views/oauth2.py:335
#, python-format
msgid ""
"You don't have the permission to access the service "
"<b>%(service_name)s</b>."
msgstr ""
"Du bist nicht berechtigt, auf den Dienst <b>%(service_name)s</b> "
"zuzugreifen."
#: uffd/views/role.py:68
msgid "Locked roles cannot be deleted"
msgstr "Gesperrte Rollen können nicht gelöscht werden"
#: uffd/views/rolemod.py:22
msgid "Moderation"
msgstr "Moderation"
#: uffd/views/rolemod.py:42
msgid "Description too long"
msgstr "Beschreibung zu lang"
#: uffd/views/rolemod.py:59
msgid "Member removed"
msgstr "Mitglied entfernt"
#: uffd/views/selfservice.py:22
msgid "Selfservice"
msgstr "Selfservice"
#: uffd/views/selfservice.py:33
msgid "Display name changed."
msgstr "Anzeigename geändert."
#: uffd/views/selfservice.py:35
msgid "Display name is not valid."
msgstr "Anzeigename ist nicht valide."
#: uffd/views/selfservice.py:47
msgid "Password changed"
msgstr "Passwort geändert"
#: uffd/views/selfservice.py:64
#, python-format
msgid ""
"We received too many password reset requests for this user! Please wait "
"at least %(delay)s."
msgstr ""
"Wir haben zu viele fehlgeschlagene Anmeldeversuche für diesen Account! "
"Bitte warte mindestens %(delay)s."
#: uffd/views/selfservice.py:70
msgid ""
"We sent a mail to this user's mail address if you entered the correct "
"mail and login name combination"
msgstr ""
"Falls E-Mail-Adresse und Anmeldename richtig waren, wurde eine E-Mail an "
"die Adresse gesendet."
#: uffd/views/selfservice.py:87 uffd/views/selfservice.py:138
#: uffd/views/selfservice.py:143
msgid "Link invalid or expired"
msgstr "Link ist ungültig oder abgelaufen"
#: uffd/views/selfservice.py:92
msgid "You need to set a password, please try again."
msgstr "Password fehlt, bitte versuche es erneut."
#: uffd/views/selfservice.py:95
msgid "Passwords do not match, please try again."
msgstr "Die Passwörter stimmen nicht überein, bitte versuche es erneut"
#: uffd/views/selfservice.py:100
msgid "Password ist not valid, please try again."
msgstr "Ungültiges Passwort, bitte versuche es erneut"
#: uffd/views/selfservice.py:104
msgid "New password set"
msgstr "Passwort geändert"
#: uffd/views/selfservice.py:117
msgid "E-Mail address already exists"
msgstr "E-Mail-Adresse existiert bereits"
#: uffd/views/selfservice.py:124 uffd/views/selfservice.py:162
#: uffd/views/selfservice.py:237
#, python-format
msgid "E-Mail to \"%(mail_address)s\" could not be sent!"
msgstr "E-Mail an \"%(mail_address)s\" konnte nicht gesendet werden!"
#: uffd/views/selfservice.py:126 uffd/views/selfservice.py:164
msgid "We sent you an email, please verify your mail address."
msgstr "Wir haben dir eine E-Mail gesendet, bitte prüfe deine E-Mail-Adresse."
#: uffd/views/selfservice.py:141
msgid ""
"This link was generated for another user. Login as the correct user to "
"continue."
msgstr ""
"Dieser Link wurde für einen anderen Account erstellt. Melde dich mit dem "
"richtigen Account an um Fortzufahren."
#: uffd/views/selfservice.py:150
msgid "E-Mail address is already used by another account"
msgstr "E-Mail-Adresse wird bereits von einem anderen Account verwendet"
#: uffd/views/selfservice.py:152
msgid "E-Mail address verified"
msgstr "E-Mail-Adresse verifiziert"
#: uffd/views/selfservice.py:177
msgid "E-Mail address deleted"
msgstr "E-Mail-Adresse gelöscht"
#: uffd/views/selfservice.py:198
msgid "E-Mail preferences updated"
msgstr "E-Mail-Einstellungen geändert"
#: uffd/views/selfservice.py:208
msgid "Session revoked"
msgstr "Sitzung widerrufen"
#: uffd/views/selfservice.py:219
#, python-format
msgid "You left role %(role_name)s"
msgstr "Rolle %(role_name)s verlassen"
#: uffd/views/service.py:34
msgid "Services"
msgstr "Dienste"
#: uffd/views/session.py:84
#, python-format
msgid ""
"We received too many invalid login attempts for this user! Please wait at"
" least %(delay)s."
msgstr ""
"Wir haben zu viele fehlgeschlagene Anmeldeversuche für diesen Account "
"erhalten! Bitte warte mindestens %(delay)s."
#: uffd/views/session.py:93
msgid "Login name or password is wrong"
msgstr "Der Anmeldename oder das Passwort ist falsch"
#: uffd/views/session.py:96
#, python-format
msgid "Your account is deactivated. Contact %(contact_email)s for details."
msgstr ""
"Dein Account ist deaktiviert. Kontaktiere %(contact_email)s für weitere "
"Informationen."
#: uffd/views/session.py:102
msgid "You do not have access to this service"
msgstr "Du hast keinen Zugriff auf diesen Service"
#: uffd/views/session.py:114 uffd/views/session.py:125
msgid "You need to login first"
msgstr "Du musst dich erst anmelden"
#: uffd/views/session.py:146 uffd/views/session.py:156
msgid "Initiation code is no longer valid"
msgstr "Startcode ist nicht mehr gültig"
#: uffd/views/session.py:160
msgid "Invalid confirmation code"
msgstr "Ungültiger Bestätigungscode"
#: uffd/views/session.py:172 uffd/views/session.py:183
msgid "Invalid initiation code"
msgstr "Ungültiger Startcode"
#: uffd/views/signup.py:21
msgid "Signup not enabled"
msgstr "Account-Registrierung ist deaktiviert"
#: uffd/views/signup.py:84 uffd/views/signup.py:92
msgid "Invalid signup link"
msgstr "Ungültiger Account-Registrierungs-Link"
#: uffd/views/signup.py:97
#, python-format
msgid "Too many failed attempts! Please wait %(delay)s."
msgstr "Zu viele fehlgeschlagene Versuche! Bitte warte mindestens %(delay)s."
#: uffd/views/signup.py:113
msgid "Your account was successfully created"
msgstr "Account erfolgreich erstellt"
#: uffd/views/user.py:30
msgid "Users"
msgstr "Accounts"
#: uffd/views/user.py:48
msgid "Login name does not meet requirements"
msgstr "Anmeldename entspricht nicht den Anforderungen"
#: uffd/views/user.py:55 uffd/views/user.py:129
msgid "Display name does not meet requirements"
msgstr "Anzeigename entspricht nicht den Anforderungen"
#: uffd/views/user.py:74
msgid "Service user created"
msgstr "Service-Account erstellt"
#: uffd/views/user.py:77
msgid "User created. We sent the user a password reset link by e-mail"
msgstr ""
"Account erstellt. E-Mail mit einem Link zum Setzen des Passworts wurde "
"versendet."
#: uffd/views/user.py:106
msgid "E-Mail address already exists or is used by another account"
msgstr ""
"E-Mail-Adresse existiert bereits oder wird von einem anderen Account "
"verwendet"
#: uffd/views/user.py:134
msgid "Password is invalid"
msgstr "Passwort ist ungültig"
#: uffd/views/user.py:146
msgid "User updated"
msgstr "Account aktualisiert"
#: uffd/views/user.py:155
msgid "User deactivated"
msgstr "Account deaktiviert"
#: uffd/views/user.py:164
msgid "User activated"
msgstr "Account aktiviert"
#: uffd/views/user.py:173
msgid "Sessions revoked"
msgstr "Sitzungen widerrufen"
#: uffd/views/user.py:183
msgid "Deleted user"
msgstr "Account gelöscht"
from .views_user import bp as bp_user
from .views_group import bp as bp_group
bp = [bp_user, bp_group]
import secrets
from ldap3 import MODIFY_REPLACE, MODIFY_DELETE, MODIFY_ADD, HASHED_SALTED_SHA512
from ldap3.utils.hashed import hashed
from flask import current_app
from uffd import ldap
class User():
def __init__(self, uid=None, loginname='', displayname='', mail='', groups=None, dn=None, uuid=None): # pylint: disable=too-many-arguments
self.uid = uid
self.loginname = loginname
self.displayname = displayname
self.mail = mail
self.newpassword = None
self.dn = dn
self.uuid = uuid
self.groups_ldap = groups or []
self.initial_groups_ldap = groups or []
self.groups_changed = False
self._groups = None
@classmethod
def from_ldap(cls, ldapobject):
return User(
uid=ldapobject['uidNumber'].value,
loginname=ldapobject['uid'].value,
displayname=ldapobject['cn'].value,
mail=ldapobject['mail'].value,
groups=ldap.get_ldap_array_attribute_safe(ldapobject, 'memberOf'),
dn=ldapobject.entry_dn,
# The LDAP mock does not generate UUIDs for newly created LDAP objects,
# so we use a dummy value if the attribute is missing (only for testing!)
uuid=ldapobject['entryUUID'].value \
if 'entryUUID' in ldapobject.entry_attributes_as_dict \
or not current_app.config.get('LDAP_SERVICE_MOCK', False) \
else '00000000-0000-0000-0000-000000000000'
)
@classmethod
def from_ldap_dn(cls, dn):
conn = ldap.get_conn()
conn.search(dn, '(objectClass=person)')
if not len(conn.entries) == 1:
return None
return User.from_ldap(conn.entries[0])
def to_ldap(self, new=False):
conn = ldap.get_conn()
if new:
self.uid = ldap.get_next_uid()
attributes = {
'uidNumber': self.uid,
'gidNumber': current_app.config['LDAP_USER_GID'],
'homeDirectory': '/home/'+self.loginname,
'sn': ' ',
'userPassword': hashed(HASHED_SALTED_SHA512, secrets.token_hex(128)),
# same as for update
'givenName': self.displayname,
'displayName': self.displayname,
'cn': self.displayname,
'mail': self.mail,
}
dn = ldap.loginname_to_dn(self.loginname)
result = conn.add(dn, current_app.config['LDAP_USER_OBJECTCLASSES'], attributes)
else:
attributes = {
'givenName': [(MODIFY_REPLACE, [self.displayname])],
'displayName': [(MODIFY_REPLACE, [self.displayname])],
'cn': [(MODIFY_REPLACE, [self.displayname])],
'mail': [(MODIFY_REPLACE, [self.mail])],
}
if self.newpassword:
attributes['userPassword'] = [(MODIFY_REPLACE, [hashed(HASHED_SALTED_SHA512, self.newpassword)])]
dn = ldap.uid_to_dn(self.uid)
result = conn.modify(dn, attributes)
self.dn = dn
group_conn = ldap.get_conn()
for group in self.initial_groups_ldap:
if not group in self.groups_ldap:
group_conn.modify(group, {'uniqueMember': [(MODIFY_DELETE, [self.dn])]})
for group in self.groups_ldap:
if not group in self.initial_groups_ldap:
group_conn.modify(group, {'uniqueMember': [(MODIFY_ADD, [self.dn])]})
self.groups_changed = False
return result
def get_groups(self):
if self._groups:
return self._groups
groups = []
for i in self.groups_ldap:
newgroup = Group.from_ldap_dn(i)
if newgroup:
groups.append(newgroup)
self._groups = groups
return groups
def replace_group_dns(self, values):
self._groups = None
self.groups_ldap = values
self.groups_changed = True
def is_in_group(self, name):
if not name:
return True
groups = self.get_groups()
for i in groups:
if i.name == name:
return True
return False
def set_loginname(self, value):
if not ldap.loginname_is_safe(value):
return False
self.loginname = value
self.dn = ldap.loginname_to_dn(self.loginname)
return True
def set_displayname(self, value):
if len(value) > 128 or len(value) < 1:
return False
self.displayname = value
return True
def set_password(self, value):
if len(value) < 8 or len(value) > 256:
return False
self.newpassword = value
return True
def set_mail(self, value):
if len(value) < 3 or '@' not in value:
return False
self.mail = value
return True
class Group():
def __init__(self, gid=None, name='', members=None, description='', dn=None):
self.gid = gid
self.name = name
self.members_ldap = members
self._members = None
self.description = description
self.dn = dn
@classmethod
def from_ldap(cls, ldapobject):
return Group(
gid=ldapobject['gidNumber'].value,
name=ldapobject['cn'].value,
members=ldap.get_ldap_array_attribute_safe(ldapobject, 'uniqueMember'),
description=ldap.get_ldap_attribute_safe(ldapobject, 'description') or '',
dn=ldapobject.entry_dn,
)
@classmethod
def from_ldap_dn(cls, dn):
conn = ldap.get_conn()
conn.search(dn, '(objectClass=groupOfUniqueNames)')
if not len(conn.entries) == 1:
return None
return Group.from_ldap(conn.entries[0])
@classmethod
def from_ldap_all(cls):
conn = ldap.get_conn()
conn.search(current_app.config["LDAP_BASE_GROUPS"], '(objectclass=groupOfUniqueNames)')
groups = []
for i in conn.entries:
groups.append(Group.from_ldap(i))
return groups
def to_ldap(self, new):
pass
def get_members(self):
if self._members:
return self._members
members = []
for i in self.members_ldap:
newmember = User.from_ldap_dn(i)
if newmember:
members.append(newmember)
self._members = members
return members
{% extends 'base.html' %}
{% block body %}
<form action="{{ url_for("group.show", gid=group.gid) }}" method="POST">
<div class="align-self-center">
<div class="form-group col">
<label for="group-gid">gid</label>
<input type="number" class="form-control" id="group-gid" name="gid" value="{{ group.gid }}" readonly>
</div>
<div class="form-group col">
<label for="group-loginname">name</label>
<input type="text" class="form-control" id="group-loginname" name="loginname" value="{{ group.name }}" readonly>
</div>
<div class="col">
<span>Members:</span>
<ul class="list-group">
{% for member in group.get_members() %}
<li class="list-group-item"><a href="{{ url_for("user.show", uid=member.uid) }}">{{ member.loginname }}</a></li>
{% endfor %}
</ul>
</div>
</div>
</form>
{% endblock %}
{% extends 'base.html' %}
{% block body %}
<form action="{{ url_for("user.update", uid=user.uid) }}" method="POST">
<div class="align-self-center">
<div class="float-sm-right pb-2">
<button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> Save</button>
<a href="{{ url_for("user.index") }}" class="btn btn-secondary">Cancel</a>
{% if user.uid %}
<a href="{{ url_for("mfa.admin_disable", uid=user.uid) }}" class="btn btn-secondary">Reset 2FA</a>
<a href="{{ url_for("user.delete", uid=user.uid) }}" class="btn btn-danger"><i class="fa fa-trash" aria-hidden="true"></i> Delete</a>
{% else %}
<a href="#" class="btn btn-danger disabled"><i class="fa fa-trash" aria-hidden="true"></i> Delete</a>
{% endif %}
</div>
<ul class="nav nav-tabs pt-2 border-0" id="tablist" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="profile-tab" data-toggle="tab" href="#profile" role="tab" aria-controls="profile" aria-selected="true">Profile</a>
</li>
<li class="nav-item">
<a class="nav-link" id="roles-tab" data-toggle="tab" href="#roles" role="tab" aria-controls="roles" aria-selected="false">Roles</a>
</li>
<li class="nav-item">
<a class="nav-link" id="ldif-tab" data-toggle="tab" href="#ldif" role="tab" aria-controls="ldif" aria-selected="false">LDIF</a>
</li>
</ul>
<div class="tab-content border mb-2 pt-2" id="tabcontent">
<div class="tab-pane fade show active" id="profile" role="tabpanel" aria-labelledby="roles-tab">
<div class="form-group col">
<label for="user-uid">uid</label>
{% if user.uid %}
<input type="number" class="form-control" id="user-uid" name="uid" value="{{ user.uid }}" readonly>
{% else %}
<input type="text" class="form-control" id="user-uid" name="uid" placeholder="will be choosen" readonly>
{% endif %}
</div>
<div class="form-group col">
<label for="user-loginname">Login Name</label>
<input type="text" class="form-control" id="user-loginname" name="loginname" value="{{ user.loginname }}" {% if user.uid %}readonly{% endif %}>
<small class="form-text text-muted">
Only letters, numbers and underscore ("_") are allowed. At most 32, at least 2 characters. There is a word blacklist. Musst be unique.
</small>
</div>
<div class="form-group col">
<label for="user-loginname">Display Name</label>
<input type="text" class="form-control" id="user-displayname" name="displayname" value="{{ user.displayname }}">
<small class="form-text text-muted">
If you leave this empty it will be set to the login name. At most 128, at least 2 characters. No character restrictions.
</small>
</div>
<div class="form-group col">
<label for="user-mail">Mail</label>
<input type="email" class="form-control" id="user-mail" name="mail" value="{{ user.mail }}">
<small class="form-text text-muted">
Do a sanity check here. A user can take over another account if both have the same mail address set.
</small>
</div>
<div class="form-group col">
<label for="user-loginname">Password</label>
{% if user.uid %}
<input type="password" class="form-control" id="user-password" name="password" placeholder="●●●●●●●●">
{% else %}
<input type="password" class="form-control" id="user-password" name="password" placeholder="mail to set it will be sent" readonly>
{% endif %}
<small class="form-text text-muted">
At least 8 and at most 256 characters, no other special requirements. But please don't be stupid and use a password manager.
</small>
</div>
</div>
<div class="tab-pane fade" id="roles" role="tabpanel" aria-labelledby="roles-tab">
<div class="form-group col">
<span>Roles:</span>
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">name</th>
<th scope="col">description</th>
</tr>
</thead>
<tbody>
{% for role in roles %}
<tr id="role-{{ role.id }}">
<td>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="role-{{ role.id }}-checkbox" name="role-{{ role.id }}" value="1" aria-label="enabled"
{% if user.dn in role.member_dns() or role.name in config["ROLES_BASEROLES"] %}checked {% endif %}
{% if role.name in config["ROLES_BASEROLES"] %}disabled {% endif %}>
</div>
</td>
<td>
<a href="{{ url_for("role.show", roleid=role.id) }}">
{{ role.name }}
</a>
</td>
<td>
{{ role.description }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="form-group col">
<span>Resulting groups (only updated after save):</span>
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">name</th>
<th scope="col">description</th>
</tr>
</thead>
<tbody>
{% for group in user.get_groups()|sort(attribute="name") %}
<tr id="group-{{ group.gid }}">
<td>
<a href="{{ url_for("group.show", gid=group.gid) }}">
{{ group.name }}
</a>
</td>
<td>
{{ group.description }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="tab-pane fade" id="ldif" role="tabpanel" aria-labelledby="ldif-tab">
<div class="form-group col">
<pre>{{ user_ldif }}</pre>
</div>
</div>
</div>
</div>
</form>
{% endblock %}
from flask import Blueprint, render_template, url_for, redirect, flash, current_app
from uffd.navbar import register_navbar
from uffd.ldap import get_conn, escape_filter_chars
from uffd.session import login_required, is_valid_session, get_current_user
from .models import Group
bp = Blueprint("group", __name__, template_folder='templates', url_prefix='/group/')
@bp.before_request
@login_required()
def group_acl(): #pylint: disable=inconsistent-return-statements
if not group_acl_check():
flash('Access denied')
return redirect(url_for('index'))
def group_acl_check():
return is_valid_session() and get_current_user().is_in_group(current_app.config['ACL_ADMIN_GROUP'])
@bp.route("/")
@register_navbar('Groups', icon='layer-group', blueprint=bp, visible=group_acl_check)
def index():
return render_template('group_list.html', groups=Group.from_ldap_all())
@bp.route("/<int:gid>")
def show(gid):
conn = get_conn()
conn.search(current_app.config["LDAP_BASE_GROUPS"], '(&(objectclass=groupOfUniqueNames)(gidNumber={}))'.format((escape_filter_chars(gid))))
assert len(conn.entries) == 1
group = Group.from_ldap(conn.entries[0])
return render_template('group.html', group=group)
import csv
import io
from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app
from uffd.navbar import register_navbar
from uffd.csrf import csrf_protect
from uffd.selfservice import send_passwordreset
from uffd.ldap import get_conn, escape_filter_chars
from uffd.session import login_required, is_valid_session, get_current_user
from uffd.role.models import Role
from uffd.role.utils import recalculate_user_groups
from uffd.database import db
from .models import User
bp = Blueprint("user", __name__, template_folder='templates', url_prefix='/user/')
@bp.before_request
@login_required()
def user_acl(): #pylint: disable=inconsistent-return-statements
if not user_acl_check():
flash('Access denied')
return redirect(url_for('index'))
def user_acl_check():
return is_valid_session() and get_current_user().is_in_group(current_app.config['ACL_ADMIN_GROUP'])
@bp.route("/")
@register_navbar('Users', icon='users', blueprint=bp, visible=user_acl_check)
def index():
conn = get_conn()
conn.search(current_app.config["LDAP_BASE_USER"], '(objectclass=person)')
users = []
for i in conn.entries:
users.append(User.from_ldap(i))
return render_template('user_list.html', users=users)
@bp.route("/<int:uid>")
@bp.route("/new")
def show(uid=None):
if not uid:
user = User()
ldif = '<none yet>'
else:
conn = get_conn()
conn.search(current_app.config["LDAP_BASE_USER"], '(&(objectclass=person)(uidNumber={}))'.format((escape_filter_chars(uid))))
assert len(conn.entries) == 1
user = User.from_ldap(conn.entries[0])
ldif = conn.entries[0].entry_to_ldif()
return render_template('user.html', user=user, user_ldif=ldif, roles=Role.query.all())
@bp.route("/<int:uid>/update", methods=['POST'])
@bp.route("/new", methods=['POST'])
@csrf_protect(blueprint=bp)
def update(uid=False):
conn = get_conn()
is_newuser = bool(not uid)
if is_newuser:
user = User()
if not user.set_loginname(request.form['loginname']):
flash('Login name does not meet requirements')
return redirect(url_for('user.show'))
else:
conn.search(current_app.config["LDAP_BASE_USER"], '(&(objectclass=person)(uidNumber={}))'.format((escape_filter_chars(uid))))
assert len(conn.entries) == 1
user = User.from_ldap(conn.entries[0])
if not user.set_mail(request.form['mail']):
flash('Mail is invalide.')
return redirect(url_for('user.show', uid=uid))
new_displayname = request.form['displayname'] if request.form['displayname'] else request.form['loginname']
if not user.set_displayname(new_displayname):
flash('Display name does not meet requirements')
return redirect(url_for('user.show', uid=uid))
new_password = request.form.get('password')
if new_password and not is_newuser:
user.set_password(new_password)
session = db.session
roles = Role.query.all()
for role in roles:
role_member_dns = role.member_dns()
if request.values.get('role-{}'.format(role.id), False) or role.name in current_app.config["ROLES_BASEROLES"]:
if user.dn in role_member_dns:
continue
role.add_member(user)
elif user.dn in role_member_dns:
role.del_member(user)
if user.to_ldap(new=is_newuser):
if is_newuser:
send_passwordreset(user.loginname)
flash('User created. We sent the user a password reset link by mail')
else:
flash('User updated')
recalculate_user_groups(user)
if not user.to_ldap():
flash('updating group membership for user {} failed'.format(user.loginname))
session.commit()
else:
flash('Error updating user: {}'.format(conn.result['message']))
session.rollback()
return redirect(url_for('user.show', uid=user.uid))
@bp.route("/<int:uid>/del")
@csrf_protect(blueprint=bp)
def delete(uid):
conn = get_conn()
conn.search(current_app.config["LDAP_BASE_USER"], '(&(objectclass=person)(uidNumber={}))'.format((escape_filter_chars(uid))))
assert len(conn.entries) == 1
user = User.from_ldap(conn.entries[0])
session = db.session
for role in Role.get_for_user(user).all():
if user.dn in role.member_dns():
role.del_member(user)
if conn.delete(conn.entries[0].entry_dn):
flash('Deleted user')
session.commit()
else:
flash('Could not delete user: {}'.format(conn.result['message']))
session.rollback()
return redirect(url_for('user.index'))
@bp.route("/csv", methods=['POST'])
@csrf_protect(blueprint=bp)
def csvimport():
csvdata = request.values.get('csv')
if not csvdata:
flash('No data for csv import!')
return redirect(url_for('user.index'))
roles = Role.query.all()
usersadded = 0
with io.StringIO(initial_value=csvdata) as csvfile:
csvreader = csv.reader(csvfile)
for row in csvreader:
if not len(row) == 3:
flash("invalid line, ignored : {}".format(row))
continue
newuser = User()
if not newuser.set_loginname(row[0]) or not newuser.set_displayname(row[0]):
flash("invalid login name, skipped : {}".format(row))
continue
if not newuser.set_mail(row[1]):
flash("invalid mail address, skipped : {}".format(row))
continue
session = db.session
for role in roles:
role_member_dns = role.member_dns()
if (str(role.id) in row[2].split(';')) or role.name in current_app.config["ROLES_BASEROLES"]:
if newuser.dn in role_member_dns:
continue
role.add_member(newuser)
result = newuser.to_ldap(new=True)
print(result)
if result:
send_passwordreset(newuser.loginname)
usergroups = set()
for role in Role.get_for_user(newuser).all():
usergroups.update(role.group_dns())
newuser.replace_group_dns(usergroups)
session.commit()
usersadded += 1
else:
flash('Error adding user {}'.format(row[0]))
session.rollback()
continue
flash('Added {} new users'.format(usersadded))
return redirect(url_for('user.index'))
import secrets
import math
import base64
def token_with_alphabet(alphabet, nbytes=None):
'''Return random text token that consists of characters from `alphabet`'''
if nbytes is None:
nbytes = max(secrets.DEFAULT_ENTROPY, 32)
nbytes_per_char = math.log(len(alphabet), 256)
nchars = math.ceil(nbytes / nbytes_per_char)
return ''.join([secrets.choice(alphabet) for _ in range(nchars)])
def token_typeable(nbytes=None):
'''Return random text token that is easy to type (on mobile)'''
alphabet = '123456789abcdefghkmnopqrstuvwx' # No '0ijlyz'
return token_with_alphabet(alphabet, nbytes=nbytes)
def token_urlfriendly(nbytes=None):
'''Return random text token that is urlsafe and works around common parsing bugs'''
alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
return token_with_alphabet(alphabet, nbytes=nbytes)
def nopad_b32decode(value):
if isinstance(value, bytes):
value = value.decode()
return base64.b32decode(value + ('=' * (-len(value) % 8)))
def nopad_b32encode(value):
return base64.b32encode(value).rstrip(b'=')
def nopad_urlsafe_b64decode(value):
if isinstance(value, bytes):
value = value.decode()
return base64.urlsafe_b64decode(value + ('=' * (-len(value) % 4)))
def nopad_urlsafe_b64encode(value):
return base64.urlsafe_b64encode(value).rstrip(b'=')
from flask import redirect, url_for, request, render_template
from werkzeug.exceptions import Forbidden
from uffd.secure_redirect import secure_local_redirect
from . import session, selfservice, signup, oauth2, user, group, service, role, invite, api, mail, rolemod
def init_app(app):
@app.errorhandler(403)
def handle_403(error):
return render_template('403.html', description=error.description if error.description != Forbidden.description else None), 403
@app.route("/")
def index(): #pylint: disable=unused-variable
if app.config['DEFAULT_PAGE_SERVICES']:
return redirect(url_for('service.overview'))
return redirect(url_for('selfservice.index'))
@app.route('/lang', methods=['POST'])
def setlang(): #pylint: disable=unused-variable
resp = secure_local_redirect(request.values.get('ref', '/'))
if 'lang' in request.values:
resp.set_cookie('language', request.values['lang'])
return resp
app.register_blueprint(session.bp)
app.register_blueprint(selfservice.bp)
app.register_blueprint(signup.bp)
app.register_blueprint(oauth2.bp)
app.register_blueprint(user.bp)
app.register_blueprint(group.bp)
app.register_blueprint(service.bp)
app.register_blueprint(role.bp)
app.register_blueprint(invite.bp)
app.register_blueprint(api.bp)
app.register_blueprint(mail.bp)
app.register_blueprint(rolemod.bp)
app.add_url_rule("/metrics", view_func=api.prometheus_metrics)
import functools
from flask import Blueprint, jsonify, request, abort, Response
from uffd.database import db
from uffd.models import (
User, ServiceUser, Group, Mail, MailReceiveAddress, MailDestinationAddress, APIClient,
RecoveryCodeMethod, TOTPMethod, WebauthnMethod, Invite, Role, Service )
from .session import login_ratelimit
bp = Blueprint('api', __name__, template_folder='templates', url_prefix='/api/v1/')
def apikey_required(permission=None):
# pylint: disable=too-many-return-statements
if permission is not None:
assert APIClient.permission_exists(permission)
def wrapper(func):
@functools.wraps(func)
def decorator(*args, **kwargs):
if not request.authorization or not request.authorization.password:
return 'Unauthorized', 401, {'WWW-Authenticate': ['Basic realm="api"']}
client = APIClient.query.filter_by(auth_username=request.authorization.username).first()
if not client:
return 'Unauthorized', 401, {'WWW-Authenticate': ['Basic realm="api"']}
if not client.auth_password.verify(request.authorization.password):
return 'Unauthorized', 401, {'WWW-Authenticate': ['Basic realm="api"']}
if client.auth_password.needs_rehash:
client.auth_password = request.authorization.password
db.session.commit()
if permission is not None and not client.has_permission(permission):
return 'Forbidden', 403
request.api_client = client
return func(*args, **kwargs)
return decorator
return wrapper
def generate_group_dict(group):
return {
'id': group.unix_gid,
'name': group.name,
'members': [
user.loginname
for user in group.members
if not user.is_deactivated or not request.api_client.service.hide_deactivated_users
]
}
@bp.route('/getgroups', methods=['GET', 'POST'])
@apikey_required('users')
def getgroups():
if len(request.values) > 1:
abort(400)
key = (list(request.values.keys()) or [None])[0]
values = request.values.getlist(key)
query = Group.query
if key is None:
pass
elif key == 'id' and len(values) == 1:
query = query.filter(Group.unix_gid == values[0])
elif key == 'name' and len(values) == 1:
query = query.filter(Group.name == values[0])
elif key == 'member' and len(values) == 1:
query = query.join(Group.members).filter(User.loginname == values[0])
if request.api_client.service.hide_deactivated_users:
query = query.filter(db.not_(User.is_deactivated))
else:
abort(400)
# Single-result queries perform better without eager loading
if key is None or key == 'member':
query = query.options(db.selectinload(Group.members))
return jsonify([generate_group_dict(group) for group in query])
def generate_user_dict(service_user):
return {
'id': service_user.user.unix_uid,
'loginname': service_user.user.loginname,
'email': service_user.email,
'displayname': service_user.user.displayname,
'groups': [group.name for group in service_user.user.groups]
}
@bp.route('/getusers', methods=['GET', 'POST'])
@apikey_required('users')
def getusers():
if len(request.values) > 1:
abort(400)
key = (list(request.values.keys()) or [None])[0]
values = request.values.getlist(key)
query = ServiceUser.query.filter_by(service=request.api_client.service).join(ServiceUser.user)
if request.api_client.service.hide_deactivated_users:
query = query.filter(db.not_(User.is_deactivated))
if key is None:
pass
elif key == 'id' and len(values) == 1:
query = query.filter(User.unix_uid == values[0])
elif key == 'loginname' and len(values) == 1:
query = query.filter(User.loginname == values[0])
elif key == 'email' and len(values) == 1:
query = ServiceUser.filter_query_by_email(query, values[0])
elif key == 'group' and len(values) == 1:
query = query.join(User.groups).filter(Group.name == values[0])
else:
abort(400)
# Single-result queries perform better without eager loading
if key is None or key == 'group':
# pylint: disable=no-member
query = query.options(db.joinedload(ServiceUser.user).selectinload(User.groups))
query = query.options(db.joinedload(ServiceUser.user).joinedload(User.primary_email))
return jsonify([generate_user_dict(user) for user in query])
@bp.route('/checkpassword', methods=['POST'])
@apikey_required('checkpassword')
def checkpassword():
if set(request.values.keys()) != {'loginname', 'password'}:
abort(400)
username = request.form['loginname'].lower()
password = request.form['password']
login_delay = login_ratelimit.get_delay(username)
if login_delay:
return 'Too Many Requests', 429, {'Retry-After': '%d'%login_delay}
service_user = ServiceUser.query.join(User).filter(
ServiceUser.service == request.api_client.service,
User.loginname == username,
).one_or_none()
if service_user is None or not service_user.user.password.verify(password):
login_ratelimit.log(username)
return jsonify(None)
if service_user.user.is_deactivated:
return jsonify(None)
if service_user.user.password.needs_rehash:
service_user.user.password = password
db.session.commit()
return jsonify(generate_user_dict(service_user))
def generate_mail_dict(mail):
return {
'name': mail.uid,
'receive_addresses': list(mail.receivers),
'destination_addresses': list(mail.destinations)
}
@bp.route('/getmails', methods=['GET', 'POST'])
@apikey_required('mail_aliases')
def getmails():
if len(request.values) > 1:
abort(400)
key = (list(request.values.keys()) or [None])[0]
values = request.values.getlist(key)
query = Mail.query
if key is None:
pass
elif key == 'name' and len(values) == 1:
query = query.filter_by(uid=values[0])
elif key == 'receive_address' and len(values) == 1:
query = query.filter(Mail.receivers.any(MailReceiveAddress.address==values[0].lower()))
elif key == 'destination_address' and len(values) == 1:
query = query.filter(Mail.destinations.any(MailDestinationAddress.address==values[0]))
else:
abort(400)
return jsonify([generate_mail_dict(mail) for mail in query])
@bp.route('/resolve-remailer', methods=['GET', 'POST'])
@apikey_required('remailer')
def resolve_remailer():
if list(request.values.keys()) != ['orig_address']:
abort(400)
values = request.values.getlist('orig_address')
if len(values) != 1:
abort(400)
service_user = ServiceUser.get_by_remailer_email(values[0])
if not service_user:
return jsonify(address=None)
return jsonify(address=service_user.real_email)
@bp.route('/metrics_prometheus', methods=['GET'])
@apikey_required('metrics')
def prometheus_metrics():
import pkg_resources #pylint: disable=import-outside-toplevel
from prometheus_client.core import CollectorRegistry, CounterMetricFamily, InfoMetricFamily #pylint: disable=import-outside-toplevel
from prometheus_client import PLATFORM_COLLECTOR, generate_latest, CONTENT_TYPE_LATEST #pylint: disable=import-outside-toplevel
class UffdCollector():
def collect(self):
try:
uffd_version = str(pkg_resources.get_distribution('uffd').version)
except pkg_resources.DistributionNotFound:
uffd_version = "unknown"
yield InfoMetricFamily('uffd_version', 'Various version infos', value={"version": uffd_version})
user_metric = CounterMetricFamily('uffd_users_total', 'Number of users', labels=['user_type'])
user_metric.add_metric(['regular'], value=User.query.filter_by(is_service_user=False).count())
user_metric.add_metric(['service'], User.query.filter_by(is_service_user=True).count())
yield user_metric
mfa_auth_metric = CounterMetricFamily('uffd_users_auth_mfa_total', 'mfa stats', labels=['mfa_type'])
mfa_auth_metric.add_metric(['recoverycode'], value=RecoveryCodeMethod.query.count())
mfa_auth_metric.add_metric(['totp'], value=TOTPMethod.query.count())
mfa_auth_metric.add_metric(['webauthn'], value=WebauthnMethod.query.count())
yield mfa_auth_metric
yield CounterMetricFamily('uffd_roles_total', 'Number of roles', value=Role.query.count())
role_members_metric = CounterMetricFamily('uffd_role_members_total', 'Members of a role', labels=['role_name'])
for role in Role.query.all():
role_members_metric.add_metric([role.name], value=len(role.members))
yield role_members_metric
group_metric = CounterMetricFamily('uffd_groups_total', 'Total number of groups', value=Group.query.count())
yield group_metric
invite_metric = CounterMetricFamily('uffd_invites_total', 'Number of invites', labels=['invite_state'])
invite_metric.add_metric(['used'], value=Invite.query.filter_by(used=True).count())
invite_metric.add_metric(['expired'], value=Invite.query.filter_by(expired=True).count())
invite_metric.add_metric(['disabled'], value=Invite.query.filter_by(disabled=True).count())
invite_metric.add_metric(['voided'], value=Invite.query.filter_by(voided=True).count())
invite_metric.add_metric([], value=Invite.query.count())
yield invite_metric
yield CounterMetricFamily('uffd_services_total', 'Number of services', value=Service.query.count())
registry = CollectorRegistry(auto_describe=True)
registry.register(PLATFORM_COLLECTOR)
registry.register(UffdCollector())
return Response(response=generate_latest(registry=registry),content_type=CONTENT_TYPE_LATEST)
from flask import Blueprint, render_template, current_app, request, flash, redirect, url_for
from flask_babel import lazy_gettext, gettext as _
import sqlalchemy
from uffd.navbar import register_navbar
from uffd.csrf import csrf_protect
from uffd.database import db
from uffd.models import Group
from .session import login_required
bp = Blueprint("group", __name__, template_folder='templates', url_prefix='/group/')
def group_acl_check():
return request.user and request.user.is_in_group(current_app.config['ACL_ADMIN_GROUP'])
@bp.before_request
@login_required(group_acl_check)
def group_acl():
pass
@bp.route("/")
@register_navbar(lazy_gettext('Groups'), icon='layer-group', blueprint=bp, visible=group_acl_check)
def index():
return render_template('group/list.html', groups=Group.query.all())
@bp.route("/<int:id>")
@bp.route("/new")
def show(id=None):
group = Group() if id is None else Group.query.get_or_404(id)
return render_template('group/show.html', group=group)
@bp.route("/<int:id>/update", methods=['POST'])
@bp.route("/new", methods=['POST'])
@csrf_protect(blueprint=bp)
def update(id=None):
if id is None:
group = Group()
if request.form['unix_gid']:
try:
group.unix_gid = int(request.form['unix_gid'])
except ValueError:
flash(_('GID is already in use or was used in the past'))
return render_template('group/show.html', group=group), 400
if not group.set_name(request.form['name']):
flash(_('Invalid name'))
return render_template('group/show.html', group=group), 400
else:
group = Group.query.get_or_404(id)
group.description = request.form['description']
db.session.add(group)
if id is None:
try:
db.session.commit()
except sqlalchemy.exc.IntegrityError:
db.session.rollback()
flash(_('Group with this name or id already exists'))
return render_template('group/show.html', group=group), 400
else:
db.session.commit()
if id is None:
flash(_('Group created'))
else:
flash(_('Group updated'))
return redirect(url_for('group.show', id=group.id))
@bp.route("/<int:id>/delete")
@csrf_protect(blueprint=bp)
def delete(id):
group = Group.query.get_or_404(id)
db.session.delete(group)
db.session.commit()
flash(_('Deleted group'))
return redirect(url_for('group.index'))
import datetime
import secrets
from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, jsonify, abort
from flask_babel import gettext as _, lazy_gettext, to_utc
import sqlalchemy
from uffd.csrf import csrf_protect
from uffd.sendmail import sendmail
from uffd.navbar import register_navbar
from uffd.database import db
from uffd.models import Role, User, Group, Invite, InviteSignup, InviteGrant, host_ratelimit, format_delay
from .session import login_required
from .signup import signup_ratelimit
from .selfservice import selfservice_acl_check
bp = Blueprint('invite', __name__, template_folder='templates', url_prefix='/invite/')
def invite_acl_check():
if not request.user:
return False
if request.user.is_in_group(current_app.config['ACL_ADMIN_GROUP']):
return True
if request.user.is_in_group(current_app.config['ACL_SIGNUP_GROUP']):
return True
if Role.query.join(Role.moderator_group).join(Group.members).filter(User.id==request.user.id).count():
return True
return False
def view_acl_filter(user):
if user.is_in_group(current_app.config['ACL_ADMIN_GROUP']):
return sqlalchemy.true()
creator_filter = (Invite.creator == user)
rolemod_filter = Invite.roles.any(Role.moderator_group.has(Group.id.in_([group.id for group in user.groups])))
return creator_filter | rolemod_filter
def reset_acl_filter(user):
if user.is_in_group(current_app.config['ACL_ADMIN_GROUP']):
return sqlalchemy.true()
return Invite.creator == user
@bp.route('/')
@register_navbar(lazy_gettext('Invites'), icon='link', blueprint=bp, visible=invite_acl_check)
@login_required(invite_acl_check)
def index():
invites = Invite.query.filter(view_acl_filter(request.user)).all()
return render_template('invite/list.html', invites=invites)
@bp.route('/new')
@login_required(invite_acl_check)
def new():
if request.user.is_in_group(current_app.config['ACL_ADMIN_GROUP']):
allow_signup = True
roles = Role.query.all()
else:
allow_signup = request.user.is_in_group(current_app.config['ACL_SIGNUP_GROUP'])
roles = Role.query.join(Role.moderator_group).join(Group.members).filter(User.id==request.user.id).all()
return render_template('invite/new.html', roles=roles, allow_signup=allow_signup)
def parse_datetime_local_input(value):
return to_utc(datetime.datetime.fromisoformat(value))
@bp.route('/new', methods=['POST'])
@login_required(invite_acl_check)
@csrf_protect(blueprint=bp)
def new_submit():
invite = Invite(creator=request.user,
single_use=(request.values['single-use'] == '1'),
valid_until=parse_datetime_local_input(request.values['valid-until']),
allow_signup=(request.values.get('allow-signup', '0') == '1'))
for key, value in request.values.items():
if key.startswith('role-') and value == '1':
invite.roles.append(Role.query.get(key[5:]))
if invite.valid_until > datetime.datetime.utcnow() + datetime.timedelta(days=current_app.config['INVITE_MAX_VALID_DAYS']):
flash(_('The "Expires After" date is too far in the future'))
return new()
if not invite.permitted:
flash(_('You are not allowed to create invite links with these permissions'))
return new()
if not invite.allow_signup and not invite.roles:
flash(_('Invite link must either allow signup or grant at least one role'))
return new()
db.session.add(invite)
db.session.commit()
return redirect(url_for('invite.index'))
@bp.route('/<int:invite_id>/disable', methods=['POST'])
@login_required(invite_acl_check)
@csrf_protect(blueprint=bp)
def disable(invite_id):
invite = Invite.query.filter(view_acl_filter(request.user)).filter_by(id=invite_id).first_or_404()
invite.disable()
db.session.commit()
return redirect(url_for('.index'))
@bp.route('/<int:invite_id>/reset', methods=['POST'])
@login_required(invite_acl_check)
@csrf_protect(blueprint=bp)
def reset(invite_id):
invite = Invite.query.filter(reset_acl_filter(request.user)).filter_by(id=invite_id).first_or_404()
invite.reset()
db.session.commit()
return redirect(url_for('.index'))
@bp.route('/<int:invite_id>/<token>')
def use(invite_id, token):
invite = Invite.query.get(invite_id)
if not invite or not secrets.compare_digest(invite.token, token):
abort(404)
if not invite.active:
flash(_('Invalid invite link'))
return redirect('/')
return render_template('invite/use.html', invite=invite)
@bp.route('/<int:invite_id>/<token>/grant', methods=['POST'])
@login_required(selfservice_acl_check)
@csrf_protect(blueprint=bp)
def grant(invite_id, token):
invite = Invite.query.get(invite_id)
if not invite or not secrets.compare_digest(invite.token, token):
abort(404)
invite_grant = InviteGrant(invite=invite, user=request.user)
db.session.add(invite_grant)
success, msg = invite_grant.apply()
if not success:
flash(msg)
return redirect(url_for('selfservice.index'))
db.session.commit()
flash(_('Roles successfully updated'))
return redirect(url_for('selfservice.index'))
@bp.url_defaults
def inject_invite_token(endpoint, values):
if endpoint in ['invite.signup_submit', 'invite.signup_check']:
if 'invite_id' in request.view_args:
values['invite_id'] = request.view_args['invite_id']
if 'token' in request.view_args:
values['token'] = request.view_args['token']
@bp.route('/<int:invite_id>/<token>/signup')
def signup_start(invite_id, token):
invite = Invite.query.get(invite_id)
if not invite or not secrets.compare_digest(invite.token, token):
abort(404)
if not invite.active:
flash(_('Invalid invite link'))
return redirect('/')
if not invite.allow_signup:
flash(_('Invite link does not allow signup'))
return redirect('/')
return render_template('signup/start.html')
@bp.route('/<int:invite_id>/<token>/signupcheck', methods=['POST'])
def signup_check(invite_id, token):
if host_ratelimit.get_delay():
return jsonify({'status': 'ratelimited'})
host_ratelimit.log()
invite = Invite.query.get(invite_id)
if not invite or not secrets.compare_digest(invite.token, token):
abort(404)
if not invite.active or not invite.allow_signup:
return jsonify({'status': 'error'}), 403
if not User().set_loginname(request.form['loginname']):
return jsonify({'status': 'invalid'})
if User.query.filter_by(loginname=request.form['loginname']).all():
return jsonify({'status': 'exists'})
return jsonify({'status': 'ok'})
@bp.route('/<int:invite_id>/<token>/signup', methods=['POST'])
def signup_submit(invite_id, token):
invite = Invite.query.get(invite_id)
if not invite or not secrets.compare_digest(invite.token, token):
abort(404)
if request.form['password1'] != request.form['password2']:
flash(_('Passwords do not match'), 'error')
return render_template('signup/start.html')
signup_delay = signup_ratelimit.get_delay(request.form['mail'])
host_delay = host_ratelimit.get_delay()
if signup_delay and signup_delay > host_delay:
flash(_('Too many signup requests with this mail address! Please wait %(delay)s.',
delay=format_delay(signup_delay)), 'error')
return render_template('signup/start.html')
if host_delay:
flash(_('Too many requests! Please wait %(delay)s.', delay=format_delay(host_delay)), 'error')
return render_template('signup/start.html')
host_ratelimit.log()
signup = InviteSignup(invite=invite, loginname=request.form['loginname'],
displayname=request.form['displayname'],
mail=request.form['mail'],
password=request.form['password1'])
valid, msg = signup.validate()
if not valid:
flash(msg, 'error')
return render_template('signup/start.html')
db.session.add(signup)
db.session.commit()
sent = sendmail(signup.mail, 'Confirm your mail address', 'signup/mail.txt', signup=signup)
if not sent:
flash(_('Could not send mail'), 'error')
return render_template('signup/start.html')
signup_ratelimit.log(request.form['mail'])
return render_template('signup/submitted.html', signup=signup)