Changeset - d2319cb2ba9b
[Not reviewed]
Merge default
0 5 0
Mads Kiilerich (mads) - 6 years ago 2019-12-19 20:50:33
mads@kiilerich.com
Merge stable
5 files changed with 12 insertions and 7 deletions:
0 comments (0 inline, 0 general)
kallithea/bin/ldap_sync.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
"""
 
kallithea.bin.ldap_sync
 
~~~~~~~~~~~~~~~~~~~~~~~
 

	
 
LDAP sync script
 

	
 
This file was forked by the Kallithea project in July 2014.
 
Original author and date, and relevant copyright and licensing information is below:
 
:created_on: Mar 06, 2013
 
:author: marcink
 
:copyright: (c) 2013 RhodeCode GmbH, and others.
 
:license: GPLv3, see LICENSE.md for more details.
 
"""
 

	
 
from __future__ import print_function
 

	
 
import urllib2
 
import uuid
 
from ConfigParser import ConfigParser
 

	
 
import ldap
 

	
 
from kallithea.lib.compat import json
 

	
 

	
 
config = ConfigParser()
 
config.read('ldap_sync.conf')
 

	
 

	
 
class InvalidResponseIDError(Exception):
 
    """ Request and response don't have the same UUID. """
 

	
 

	
 
class ResponseError(Exception):
 
    """ Response has an error, something went wrong with request execution. """
 

	
 

	
 
class UserAlreadyInGroupError(Exception):
 
    """ User is already a member of the target group. """
 

	
 

	
 
class UserNotInGroupError(Exception):
 
    """ User is not a member of the target group. """
 

	
 

	
 
class API(object):
 

	
 
    def __init__(self, url, key):
 
        self.url = url
 
        self.key = key
 

	
 
    def get_api_data(self, uid, method, args):
 
        """Prepare dict for API post."""
 
        return {
 
            "id": uid,
 
            "api_key": self.key,
 
            "method": method,
 
            "args": args
 
        }
 

	
 
    def post(self, method, args):
 
        """Send a generic API post to Kallithea.
 

	
 
        This will generate the UUID for validation check after the
 
        response is returned. Handle errors and get the result back.
 
        """
 
        uid = str(uuid.uuid1())
 
        data = self.get_api_data(uid, method, args)
 

	
 
        data = json.dumps(data)
 
        headers = {'content-type': 'text/plain'}
 
        req = urllib2.Request(self.url, data, headers)
 

	
 
        response = urllib2.urlopen(req)
 
        response = json.load(response)
 

	
 
        if uid != response["id"]:
 
            raise InvalidResponseIDError("UUID does not match.")
 

	
 
        if response["error"] is not None:
 
            raise ResponseError(response["error"])
 

	
 
        return response["result"]
 

	
 
    def create_group(self, name, active=True):
 
        """Create the Kallithea user group."""
 
        args = {
 
            "group_name": name,
 
            "active": str(active)
 
        }
 
        self.post("create_user_group", args)
 

	
 
    def add_membership(self, group, username):
 
        """Add specific user to a group."""
 
        args = {
 
            "usersgroupid": group,
 
            "userid": username
 
        }
 
        result = self.post("add_user_to_user_group", args)
 
        if not result["success"]:
 
            raise UserAlreadyInGroupError("User %s already in group %s." %
 
                                          (username, group))
 

	
 
    def remove_membership(self, group, username):
 
        """Remove specific user from a group."""
 
        args = {
 
            "usersgroupid": group,
 
            "userid": username
 
        }
 
        result = self.post("remove_user_from_user_group", args)
 
        if not result["success"]:
 
            raise UserNotInGroupError("User %s not in group %s." %
 
                                      (username, group))
 

	
 
    def get_group_members(self, name):
 
        """Get the list of member usernames from a user group."""
 
        args = {"usersgroupid": name}
 
        members = self.post("get_user_group", args)['members']
 
        member_list = []
 
        for member in members:
 
            member_list.append(member["username"])
 
        return member_list
 

	
 
    def get_group(self, name):
 
        """Return group info."""
 
        args = {"usersgroupid": name}
 
        return self.post("get_user_group", args)
 

	
 
    def get_user(self, username):
 
        """Return user info."""
 
        args = {"userid": username}
 
        return self.post("get_user", args)
 

	
 

	
 
class LdapClient(object):
 

	
 
    def __init__(self, uri, user, key, base_dn):
 
        self.client = ldap.initialize(uri, trace_level=0)
 
        self.client.set_option(ldap.OPT_REFERRALS, 0)
 
        self.client.simple_bind(user, key)
 
        self.base_dn = base_dn
 

	
 
    def close(self):
 
        self.client.unbind()
 

	
 
    def get_groups(self):
 
        """Get all the groups in form of dict {group_name: group_info,...}."""
 
        searchFilter = "objectClass=groupOfUniqueNames"
 
        result = self.client.search_s(self.base_dn, ldap.SCOPE_SUBTREE,
 
                                      searchFilter)
 

	
 
        groups = {}
 
        for group in result:
 
            groups[group[1]['cn'][0]] = group[1]
 

	
 
        return groups
 

	
 
    def get_group_users(self, groups, group):
 
        """Returns all the users belonging to a single group.
 

	
 
        Based on the list of groups and memberships, returns all the
 
        users belonging to a single group, searching recursively.
 
        """
 
        users = []
 
        for member in groups[group]["uniqueMember"]:
 
            member = self.parse_member_string(member)
 
            if member[0] == "uid":
 
                users.append(member[1])
 
            elif member[0] == "cn":
 
                users += self.get_group_users(groups, member[1])
 

	
 
        return users
 

	
 
    def parse_member_string(self, member):
 
        """Parses the member string and returns a touple of type and name.
 

	
 
        Unique member can be either user or group. Users will have 'uid' as
 
        prefix while groups will have 'cn'.
 
        """
 
        member = member.split(",")[0]
 
        return member.split('=')
 

	
 

	
 
class LdapSync(object):
 

	
 
    def __init__(self):
 
        self.ldap_client = LdapClient(config.get("default", "ldap_uri"),
 
                                      config.get("default", "ldap_user"),
 
                                      config.get("default", "ldap_key"),
 
                                      config.get("default", "base_dn"))
 
        self.kallithea_api = API(config.get("default", "api_url"),
 
                                 config.get("default", "api_key"))
 

	
 
    def update_groups_from_ldap(self):
 
        """Add all the groups from LDAP to Kallithea."""
 
        added = existing = 0
 
        groups = self.ldap_client.get_groups()
 
        for group in groups:
 
            try:
 
                self.kallithea_api.create_group(group)
 
                added += 1
 
            except Exception:
 
                existing += 1
 

	
 
        return added, existing
 

	
 
    def update_memberships_from_ldap(self, group):
 
        """Update memberships based on the LDAP groups."""
 
        groups = self.ldap_client.get_groups()
 
        group_users = self.ldap_client.get_group_users(groups, group)
 

	
 
        # Delete memberships first from each group which are not part
 
        # of the group any more.
 
        members = self.kallithea_api.get_group_members(group)
 
        for member in members:
 
            if member not in group_users:
 
                try:
 
                    self.kallithea_api.remove_membership(group,
 
                                                         member)
 
                except UserNotInGroupError:
 
                    pass
 

	
 
        # Add memberships.
 
        for member in group_users:
 
            try:
 
                self.kallithea_api.add_membership(group, member)
 
            except UserAlreadyInGroupError:
 
                # TODO: handle somehow maybe..
 
                pass
 

	
 
    def close(self):
 
        self.ldap_client.close()
 

	
 

	
 
if __name__ == '__main__':
 
    sync = LdapSync()
 
    print(sync.update_groups_from_ldap())
 

	
 
    for gr in sync.ldap_client.get_groups():
 
        # TODO: exception when user does not exist during add membership...
 
        # How should we handle this.. Either sync users as well at this step,
 
        # or just ignore those who don't exist. If we want the second case,
 
        # we need to find a way to recognize the right exception (we always get
 
        # ResponseError with no error code so maybe by return msg (?)
 
        sync.update_memberships_from_ldap(gr)
 

	
 
    sync.close()
kallithea/i18n/ru/LC_MESSAGES/kallithea.po
Show inline comments
 
# Copyright (C) 2014 RhodeCode GmbH, and others.
 
# This file is distributed under the same license as the Kallithea project.
 

	
 
msgid ""
 
msgstr ""
 
"Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
 
"Language: ru\n"
 
"MIME-Version: 1.0\n"
 
"Content-Type: text/plain; charset=UTF-8\n"
 
"Content-Transfer-Encoding: 8bit\n"
 
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
 
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
 

	
 
msgid "There are no changesets yet"
 
msgstr "Ещё не было изменений"
 

	
 
msgid "None"
 
msgstr "Ничего"
 

	
 
msgid "(closed)"
 
msgstr "(закрыто)"
 

	
 
msgid "Show whitespace"
 
msgstr "Отображать пробелы"
 

	
 
msgid "Ignore whitespace"
 
msgstr "Игнорировать пробелы"
 

	
 
msgid "Increase diff context to %(num)s lines"
 
msgstr "Увеличить контекст до %(num)s строк"
 

	
 
msgid "Such revision does not exist for this repository"
 
msgstr "Нет такой ревизии в этом репозитории"
 

	
 
msgid "Cannot compare repositories without using common ancestor"
 
msgstr "Невозможно сравнивать репозитории без общего предка"
 

	
 
msgid "No response"
 
msgstr "Нет ответа"
 

	
 
msgid "Unknown error"
 
msgstr "Неизвестная ошибка"
 

	
 
msgid ""
 
"The request could not be understood by the server due to malformed syntax."
 
msgstr "Запрос не распознан сервером из-за неправильного синтаксиса."
 

	
 
msgid "Unauthorized access to resource"
 
msgstr "Несанкционированный доступ к ресурсу"
 

	
 
msgid "You don't have permission to view this page"
 
msgstr "У вас нет прав для просмотра этой страницы"
 

	
 
msgid "The resource could not be found"
 
msgstr "Ресурс не найден"
 

	
 
msgid ""
 
"The server encountered an unexpected condition which prevented it from "
 
"fulfilling the request."
 
msgstr ""
 
"Сервер не может выполнить запрос из-за неправильного условия в запросе."
 

	
 
msgid "%s committed on %s"
 
msgstr "%s выполнил коммит в %s"
 

	
 
msgid "Changeset was too big and was cut off..."
 
msgstr "Изменения оказались слишком большими и были вырезаны..."
 

	
 
msgid "%s %s feed"
 
msgstr "Лента новостей %s %s"
 

	
 
msgid "Changes on %s repository"
 
msgstr "Изменения в репозитории %s"
 

	
 
msgid "Click here to add new file"
 
msgstr "Нажмите чтобы добавить новый файл"
 

	
 
msgid "%s at %s"
 
msgstr "%s (%s)"
 

	
 
msgid "You can only delete files with revision being a valid branch"
 
msgstr ""
 
"Вы можете удалять файлы только в ревизии, связанной с существующей веткой "
 
"Вы можете удалять файлы только в ревизии, являющейся корректной веткой"
 

	
 
msgid "Deleted file %s via Kallithea"
 
msgstr "Файл %s удалён с помощью Kallithea"
 

	
 
msgid "Successfully deleted file %s"
 
msgstr "Файл %s удалён"
 

	
 
msgid "Error occurred during commit"
 
msgstr "Во время коммита произошла ошибка"
 

	
 
msgid "You can only edit files with revision being a valid branch"
 
msgstr ""
 
"Вы можете редактировать файлы только в ревизии, связанной с существующей "
 
"веткой "
 
"веткой"
 

	
 
msgid "Edited file %s via Kallithea"
 
msgstr "Файл %s отредактирован с помощью Kallithea"
 

	
 
msgid "No changes"
 
msgstr "Без изменений"
 

	
 
msgid "Successfully committed to %s"
 
msgstr "Изменения применены в %s"
 

	
 
msgid "Added file via Kallithea"
 
msgstr "Файл добавлен с помощью Kallithea"
 

	
 
msgid "No content"
 
msgstr "Пусто"
 

	
 
msgid "No filename"
 
msgstr "Безымянный"
 

	
 
msgid "Location must be relative path and must not contain .. in path"
 
msgstr ""
 
"Расположение должно быть относительным путем, и не должно содержать \".."
 
"\" в пути"
 

	
 
msgid "Downloads disabled"
 
msgstr "Возможность скачивать отключена"
 

	
 
msgid "Unknown revision %s"
 
msgstr "Неизвестная ревизия %s"
 

	
 
msgid "Empty repository"
 
msgstr "Пустой репозиторий"
 

	
 
msgid "Unknown archive type"
 
msgstr "Неизвестный тип архива"
 

	
 
msgid "Changesets"
 
msgstr "Набор изменений"
 

	
 
msgid "Branches"
 
msgstr "Ветки"
 

	
 
msgid "Tags"
 
msgstr "Метки"
 

	
 
msgid "An error occurred during repository forking %s"
 
msgstr "Произошла ошибка во время создания форка репозитория %s"
 

	
 
msgid "Groups"
 
msgstr "Группы"
 

	
 
msgid "Repositories"
 
msgstr "Репозитории"
 

	
 
msgid "Branch"
 
msgstr "Ветка"
 

	
 
msgid "Closed Branches"
 
msgstr "Закрытые ветки"
 

	
 
msgid "Tag"
 
msgstr "Тэги"
 

	
 
msgid "Bookmark"
 
msgstr "Закладки"
 

	
 
msgid "Public Journal"
 
msgstr "Публичный журнал"
 

	
 
msgid "Journal"
 
msgstr "Журнал"
 

	
 
msgid "Bad captcha"
 
msgstr "Неверная капча"
 

	
 
msgid "You have successfully registered with %s"
 
msgstr "Регистрация в %s прошла успешно"
 

	
 
msgid "A password reset confirmation code has been sent"
 
msgstr "Код для сброса пароля отправлена"
 

	
 
msgid "Invalid password reset token"
 
msgstr "Неверный код сброса пароля"
 

	
 
msgid "Successfully updated password"
 
msgstr "Пароль обновлён"
 

	
 
msgid "%s (closed)"
 
msgstr "%s (закрыта)"
 

	
 
msgid "Changeset"
 
msgstr "Изменения"
 

	
 
msgid "Special"
 
msgstr "Специальный"
 

	
 
msgid "Peer branches"
 
msgstr "Ветки участника"
 

	
 
msgid "Bookmarks"
 
msgstr "Закладки"
 

	
 
msgid "Error creating pull request: %s"
 
msgstr "Ошибка при создании pull-запроса: %s"
 

	
 
msgid "Error occurred while creating pull request"
 
msgstr "Произошла ошибка при создании pull-запроса"
 

	
 
msgid "Successfully opened new pull request"
 
msgstr "Pull-запрос создан успешно"
 

	
 
msgid "No description"
 
msgstr "Нет описания"
 

	
 
msgid "Pull request updated"
 
msgstr "Pull-запрос обновлён"
 

	
 
msgid "Successfully deleted pull request"
 
msgstr "Pull-запрос успешно удалён"
 

	
 
msgid "This pull request has already been merged to %s."
 
msgstr "Этот pull-запрос уже принят на ветку %s."
 

	
 
msgid "This pull request has been closed and can not be updated."
 
msgstr "Этот pull-запрос был закрыт и не может быть обновлён."
 

	
 
msgid "Note: Branch %s has another head: %s."
 
msgstr "Внимание: Ветка %s имеет ещё одну верхушку: %s."
 

	
 
msgid "Invalid search query. Try quoting it."
 
msgstr "Недопустимый поисковый запрос. Попробуйте заключить его в кавычки."
 

	
 
msgid "An error occurred during search operation."
 
msgstr "Произошла ошибка при выполнении этого поиска."
 

	
 
msgid "No data ready yet"
 
msgstr "Нет данных"
 

	
 
msgid "Statistics are disabled for this repository"
 
msgstr "Статистические данные отключены для этого репозитария"
 

	
 
msgid "Auth settings updated successfully"
 
msgstr "Настройки авторизации успешно обновлены"
 

	
 
msgid "error occurred during update of auth settings"
 
msgstr "произошла ошибка при обновлении настроек авторизации"
 

	
 
msgid "Default settings updated successfully"
 
msgstr "Стандартные настройки успешно обновлены"
 

	
 
msgid "Error occurred during update of defaults"
 
msgstr "Произошла ошибка при обновлении стандартных настроек"
 

	
 
msgid "5 minutes"
 
msgstr "5 минут"
 

	
 
msgid "1 hour"
 
msgstr "1 час"
 

	
 
msgid "1 day"
 
msgstr "1 день"
 

	
 
msgid "1 month"
 
msgstr "1 месяц"
 

	
 
msgid "Lifetime"
 
msgstr "Срок"
 

	
 
msgid "Error occurred during gist creation"
 
msgstr "Произошла ошибка во время создания gist-записи"
 

	
 
msgid "Deleted gist %s"
 
msgstr "Gist-запись %s удалена"
 

	
 
msgid "Unmodified"
 
msgstr "Неизменный"
 

	
 
msgid "Successfully updated gist data"
 
msgstr "Данные gist-записи обновлены"
 

	
 
msgid "Error occurred during update of gist %s"
 
msgstr "Произошла ошибка при обновлении gist-записи %s"
 

	
 
msgid "You can't edit this user since it's crucial for entire application"
 
msgstr ""
 
"Вы не можете изменить данные этого пользователя, поскольку он важен для "
 
"работы всего приложения"
 

	
 
msgid "Your account was updated successfully"
 
msgstr "Ваша учетная запись успешно обновлена"
 

	
 
msgid "Error occurred during update of user %s"
 
msgstr "Произошла ошибка при обновлении пользователя %s"
 

	
 
msgid "Error occurred during update of user password"
 
msgstr "Ошибка при обновлении пароля"
 

	
 
msgid "Added email %s to user"
 
msgstr "Пользователю добавлен e-mail %s"
 

	
 
msgid "An error occurred during email saving"
 
msgstr "Произошла ошибка при сохранении e-mail"
 

	
 
msgid "Removed email from user"
 
msgstr "E-mail пользователя удалён"
 

	
 
msgid "API key successfully created"
 
msgstr "API-ключ успешно создан"
 

	
 
msgid "API key successfully reset"
 
msgstr "API-ключ успешно сброшен"
 

	
 
msgid "API key successfully deleted"
 
msgstr "API-ключ успешно удалён"
 

	
 
msgid "Read"
 
msgstr "Чтение"
 

	
 
msgid "Write"
 
msgstr "Запись"
 

	
 
msgid "Admin"
 
msgstr "Администратор"
 

	
 
msgid "Disabled"
 
msgstr "Отключено"
 

	
 
msgid "Allowed with manual account activation"
 
msgstr "Разрешена, с ручной активацией учётной записи"
 

	
 
msgid "Allowed with automatic account activation"
 
msgstr "Разрешена, с автоматической активацией учётной записи"
 

	
 
msgid "Manual activation of external account"
 
msgstr "Ручная активация внешней учетной записи"
 

	
 
msgid "Automatic activation of external account"
 
msgstr "Автоматическая активация внешней учетной записи"
 

	
 
msgid "Enabled"
 
msgstr "Включено"
 

	
 
msgid "Global permissions updated successfully"
 
msgstr "Глобальные привилегии успешно обновлены"
 

	
 
msgid "Error occurred during update of permissions"
 
msgstr "Произошла ошибка во время обновления привилегий"
 

	
 
msgid "Error occurred during creation of repository group %s"
 
msgstr "Произошла ошибка при создании группы репозиториев %s"
 

	
 
msgid "Created repository group %s"
 
msgstr "Создана новая группа репозиториев %s"
 

	
 
msgid "Updated repository group %s"
 
msgstr "Группа репозиториев %s обновлена"
 

	
 
msgid "Error occurred during update of repository group %s"
 
msgstr "Произошла ошибка при обновлении группы репозиториев %s"
 

	
 
msgid "This group contains %s repositories and cannot be deleted"
 
msgstr "Данная группа содержит %s репозитариев и не может быть удалена"
 

	
 
msgid "This group contains %s subgroups and cannot be deleted"
 
msgstr "Группа содержит в себе %s подгрупп и не может быть удалён"
 

	
 
msgid "Removed repository group %s"
 
msgstr "Группа репозиториев %s удалена"
 

	
 
msgid "Error occurred during deletion of repository group %s"
 
msgstr "Произошла ошибка при удалении группы репозиториев %s"
 

	
 
msgid "Cannot revoke permission for yourself as admin"
 
msgstr "Администратор не может отозвать свои привелегии"
 

	
 
msgid "Repository group permissions updated"
 
msgstr "Привилегии группы репозиториев обновлены"
 

	
 
msgid "An error occurred during revoking of permission"
 
msgstr "Произошла ошибка при отзыве привелегии"
 

	
 
msgid "Error creating repository %s"
 
msgstr "Произошла ошибка при создании репозитория %s"
 

	
 
msgid "Created repository %s from %s"
 
msgstr "Репозиторий %s создан из %s"
 

	
 
msgid "Forked repository %s as %s"
 
msgstr "Сделан форк(копия) репозитория %s на %s"
 

	
 
msgid "Created repository %s"
 
msgstr "Репозиторий %s создан"
 

	
 
msgid "Repository %s updated successfully"
 
msgstr "Репозитарий %s успешно обновлён"
 

	
 
msgid "Error occurred during update of repository %s"
 
msgstr "Произошла ошибка во время обновления репозитория %s"
 

	
 
msgid "Detached %s forks"
 
msgstr "Форки %s отсоединены"
 

	
 
msgid "Deleted %s forks"
 
msgstr "Удалены форки репозитория %s"
 

	
 
msgid "Deleted repository %s"
 
msgstr "Репозиторий %s удалён"
 

	
 
msgid "Cannot delete repository %s which still has forks"
 
msgstr "Невозможно удалить %s, у него всё ещё есть форки"
 

	
 
msgid "An error occurred during deletion of %s"
 
msgstr "Произошла ошибка во время удаления %s"
 

	
 
msgid "Repository permissions updated"
 
msgstr "Привилегии репозитория обновлены"
 

	
 
msgid "An error occurred during removal of field"
 
msgstr "Произошла ошибка при удалении поля"
 

	
 
msgid "-- Not a fork --"
 
msgstr "-- Не форк --"
 

	
 
msgid "Updated repository visibility in public journal"
 
msgstr "Видимость репозитория в публичном журнале обновлена"
 

	
 
msgid "An error occurred during setting this repository in public journal"
 
msgstr "Произошла ошибка при установке репозитария в общедоступный журнал"
 

	
 
msgid "Nothing"
 
msgstr "Ничего"
 

	
 
msgid "Marked repository %s as fork of %s"
 
msgstr "Репозиторий %s отмечен как форк %s"
 

	
 
msgid "An error occurred during this operation"
 
msgstr "Произошла ошибка при выполнении операции"
 

	
 
msgid "Cache invalidation successful"
 
msgstr "Кэш сброшен"
 

	
 
msgid "An error occurred during cache invalidation"
 
msgstr "Произошла ошибка при очистке кэша"
 

	
 
msgid "Pulled from remote location"
 
msgstr "Внесены изменения из удалённого репозитория"
 

	
 
msgid "An error occurred during pull from remote location"
 
msgstr "Произошла ошибка при внесении изменений из удалённого репозитория"
 

	
 
msgid "An error occurred during deletion of repository stats"
 
msgstr "Произошла ошибка при удалении статистики репозитория"
 

	
 
msgid "Updated VCS settings"
 
msgstr "Обновлены настройки VCS"
 

	
 
msgid ""
 
"Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 
"missing"
 
msgstr ""
 
"Невозможно включить поддержку hgsubversion. Библиотека «hgsubversion» "
 
"отсутствует"
 

	
 
msgid "Error occurred while updating application settings"
 
msgstr "Произошла ошибка при обновлении настроек приложения"
 

	
 
msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 
msgstr "Репозитории успешно пересканированы, добавлено: %s, удалено: %s."
 

	
 
msgid "Updated application settings"
 
msgstr "Обновленные параметры настройки приложения"
 

	
 
msgid "Updated visualisation settings"
 
msgstr "Настройки визуализации обновлены"
 

	
 
msgid "Error occurred during updating visualisation settings"
 
msgstr "Произошла ошибка при обновлении настроек визуализации"
 

	
 
msgid "Please enter email address"
 
msgstr "Пожалуйста, введите адрес электронной почты"
 

	
 
msgid "Send email task created"
 
msgstr "Задача отправки Email создана"
 

	
 
msgid "Added new hook"
 
msgstr "Добавлена новая ловушка"
 

	
 
msgid "Updated hooks"
 
msgstr "Обновлённые ловушки"
 

	
 
msgid "Error occurred during hook creation"
 
msgstr "произошла ошибка при создании хука"
 

	
 
msgid "Whoosh reindex task scheduled"
 
msgstr "Запланирована переиндексация базы Whoosh"
 

	
 
msgid "Created user group %s"
 
msgstr "Создана группа пользователей %s"
 

	
 
msgid "Error occurred during creation of user group %s"
 
msgstr "Произошла ошибка при создании группы пользователей %s"
 

	
 
msgid "Updated user group %s"
 
msgstr "Группа пользователей %s обновлена"
 

	
 
msgid "Error occurred during update of user group %s"
 
msgstr "Произошла ошибка при обновлении группы пользователей %s"
 

	
 
msgid "Successfully deleted user group"
 
msgstr "Группа пользователей успешно удалена"
 

	
 
msgid "An error occurred during deletion of user group"
 
msgstr "Произошла ошибка при удалении группы пользователей"
 

	
 
msgid "Target group cannot be the same"
 
msgstr "Целевая группа не может быть такой же"
 

	
 
msgid "User group permissions updated"
 
msgstr "Привилегии группы пользователей обновлены"
 

	
 
msgid "Updated permissions"
 
msgstr "Обновлены привилегии"
 

	
 
msgid "An error occurred during permissions saving"
 
msgstr "Произошла ошибка при сохранении привилегий"
 

	
 
msgid "Created user %s"
 
msgstr "Пользователь %s создан"
 

	
 
msgid "Error occurred during creation of user %s"
 
msgstr "Произошла ошибка при создании пользователя %s"
 

	
 
msgid "User updated successfully"
 
msgstr "Пользователь успешно обновлён"
 

	
 
msgid "Successfully deleted user"
 
msgstr "Пользователь успешно удалён"
 

	
 
msgid "An error occurred during deletion of user"
 
msgstr "Произошла ошибка при удалении пользователя"
 

	
 
msgid "Added IP address %s to user whitelist"
 
msgstr "Добавлен IP %s в белый список пользователя"
 

	
 
msgid "An error occurred while adding IP address"
 
msgstr "Произошла ошибка при сохранении IP"
 

	
 
msgid "Removed IP address from user whitelist"
 
msgstr "Удален IP %s из белого списка пользователя"
 

	
 
msgid "You need to be a registered user to perform this action"
 
msgstr ""
 
"Вы должны быть зарегистрированным пользователем, чтобы выполнить это "
 
"действие"
 

	
 
msgid "You need to be signed in to view this page"
 
msgstr "Страница доступна только авторизованным пользователям"
 

	
 
msgid "Repository not found in the filesystem"
 
msgstr "Репозиторий не найден на файловой системе"
 

	
 
msgid "Binary file"
 
msgstr "Двоичный файл"
 

	
 
msgid ""
 
"Changeset was too big and was cut off, use diff menu to display this diff"
 
msgstr ""
 
"Набор изменения оказался слишком большими и был урезан, используйте меню "
 
"сравнения для показа результата сравнения"
 

	
 
msgid "No changes detected"
 
msgstr "Изменений не обнаружено"
 

	
 
msgid "Deleted branch: %s"
 
msgstr "Удалена ветка: %s"
 

	
 
msgid "Created tag: %s"
 
msgstr "Создан тег: %s"
 

	
 
msgid "Show all combined changesets %s->%s"
 
msgstr "Показать отличия вместе %s->%s"
 

	
 
msgid "and"
 
msgstr "и"
 

	
 
msgid "%s more"
 
msgstr "на %s больше"
 

	
 
msgid "revisions"
 
msgstr "версии"
 

	
 
msgid "Pull request %s"
 
msgstr "Pull-запрос %s"
 

	
 
msgid "[deleted] repository"
 
msgstr "[удален] репозиторий"
 

	
 
msgid "[created] repository"
 
msgstr "[создан] репозиторий"
 

	
 
msgid "[created] repository as fork"
 
msgstr "[создан] репозиторий как форк"
 

	
 
msgid "[forked] repository"
 
msgstr "[форкнут] репозиторий"
 

	
 
msgid "[updated] repository"
 
msgstr "[обновлён] репозиторий"
 

	
 
msgid "[downloaded] archive from repository"
 
msgstr "[загружен] архив из репозитория"
 

	
 
msgid "[delete] repository"
 
msgstr "[удален] репозиторий"
 

	
 
msgid "[created] user"
 
msgstr "[создан] пользователь"
 

	
 
msgid "[updated] user"
 
msgstr "[обновлён] пользователь"
 

	
 
msgid "[created] user group"
 
msgstr "[создана] группа пользователей"
 

	
 
msgid "[updated] user group"
 
msgstr "[обновлена] группа пользователей"
 

	
 
msgid "[commented] on revision in repository"
 
msgstr "[комментарий] к ревизии в репозитории"
 

	
 
msgid "[commented] on pull request for"
 
msgstr "[прокомментировано] в запросе на внесение изменений для"
 

	
 
msgid "[closed] pull request for"
 
msgstr "[закрыт] Pull-запрос для"
 

	
 
msgid "[pushed] into"
 
msgstr "[отправлено] в"
 

	
 
msgid "[committed via Kallithea] into repository"
 
msgstr "[внесены изменения с помощью Kallithea] в репозитории"
 

	
 
msgid "[pulled from remote] into repository"
 
msgstr "[внесены изменения из удалённого репозитория] в репозиторий"
 

	
 
msgid "[pulled] from"
 
msgstr "[внесены изменения] из"
 

	
 
msgid "[started following] repository"
 
msgstr "[добавлен в наблюдения] репозиторий"
 

	
 
msgid "[stopped following] repository"
 
msgstr "[удалён из наблюдения] репозиторий"
 

	
 
msgid " and %s more"
 
msgstr " и на %s больше"
 

	
 
msgid "No files"
 
msgstr "Нет файлов"
 

	
 
msgid "new file"
 
msgstr "новый файл"
 

	
 
msgid "mod"
 
msgstr "изменён"
 

	
 
msgid "del"
 
msgstr "удалён"
 

	
 
msgid "rename"
 
msgstr "переименован"
 

	
 
msgid "chmod"
 
msgstr "chmod"
 

	
 
msgid ""
 
"%s repository is not mapped to db perhaps it was created or renamed from "
 
"the filesystem please run the application again in order to rescan "
 
"repositories"
 
msgstr ""
 
"Репозиторий %s отсутствует в базе данных; возможно, он был создан или "
 
"переименован из файловой системы. Пожалуйста, перезапустите приложение "
 
"для сканирования репозиториев"
 

	
 
msgid "%d year"
 
msgid_plural "%d years"
 
msgstr[0] "%d год"
 
msgstr[1] "%d лет"
 
msgstr[2] "%d года"
 

	
 
msgid "%d month"
 
msgid_plural "%d months"
 
msgstr[0] "%d месяц"
 
msgstr[1] "%d месяца"
 
msgstr[2] "%d месяцев"
 

	
 
msgid "%d day"
 
msgid_plural "%d days"
 
msgstr[0] "%d день"
 
msgstr[1] "%d дня"
 
msgstr[2] "%d дней"
 

	
 
msgid "%d hour"
 
msgid_plural "%d hours"
 
msgstr[0] "%d час"
 
msgstr[1] "%d часов"
 
msgstr[2] "%d часа"
 

	
 
msgid "%d minute"
 
msgid_plural "%d minutes"
 
msgstr[0] "%d минута"
 
msgstr[1] "%d минут"
 
msgstr[2] "%d минуты"
 

	
 
msgid "%d second"
 
msgid_plural "%d seconds"
 
msgstr[0] "%d секунды"
 
msgstr[0] "%d секунда"
 
msgstr[1] "%d секунды"
 
msgstr[2] "%d секунды"
 
msgstr[2] "%d секунд"
 

	
 
msgid "in %s"
 
msgstr "в %s"
 

	
 
msgid "%s ago"
 
msgstr "%s назад"
 

	
 
msgid "in %s and %s"
 
msgstr "в %s и %s"
 

	
 
msgid "%s and %s ago"
 
msgstr "%s и %s назад"
 

	
 
msgid "just now"
 
msgstr "прямо сейчас"
 

	
 
msgid "on line %s"
 
msgstr "на строке %s"
 

	
 
msgid "[Mention]"
 
msgstr "[Упоминание]"
 

	
 
msgid "top level"
 
msgstr "верхний уровень"
 

	
 
msgid "Kallithea Administrator"
 
msgstr "Администратор Kallithea"
 

	
 
msgid "Default user has read access to new repositories"
 
msgstr "Неавторизованные пользователи имеют право чтения новых репозиториев"
 

	
 
msgid "Default user has write access to new repositories"
 
msgstr ""
 
"Неавторизованные пользователи имеют право записи в новые репозитории"
 

	
 
msgid "Only admins can create repository groups"
 
msgstr "Только администраторы могут создавать группы"
 

	
 
msgid "Registration disabled"
 
msgstr "Регистрация отключена"
 

	
 
msgid "Approved"
 
msgstr "Одобрено"
 

	
 
msgid "Please enter a login"
 
msgstr "Пожалуйста, введите логин"
 

	
 
msgid "Enter a value %(min)i characters long or more"
 
msgstr "Введите значение длиной не менее %(min)i символов"
 

	
 
msgid "Please enter a password"
 
msgstr "Пожалуйста, введите пароль"
 

	
 
msgid "Enter %(min)i characters or more"
 
msgstr "Введите не менее %(min)i символов"
 

	
 
msgid "New user %(new_username)s registered"
 
msgstr "Новый пользователь \"%(new_username)s\" зарегистрирован"
 

	
 
msgid "Closing"
 
msgstr "Закрыт"
 

	
 
msgid "latest tip"
 
msgstr "последняя версия"
 

	
 
msgid "New user registration"
 
msgstr "Регистрация нового пользователя"
 

	
 
msgid ""
 
"User \"%s\" still owns %s repositories and cannot be removed. Switch "
 
"owners or remove those repositories: %s"
 
msgstr ""
 
"Пользователь \"%s\" всё ещё является владельцем %s репозиториев и поэтому "
 
"не может быть удалён. Смените владельца или удалите эти репозитории: %s"
 

	
 
msgid ""
 
"User \"%s\" still owns %s repository groups and cannot be removed. Switch "
 
"owners or remove those repository groups: %s"
 
msgstr ""
 
"Пользователь \"%s\" всё ещё является владельцем %s групп репозиториев и "
 
"поэтому не может быть удалён. Смените владельца или удалите данные "
 
"группы: %s"
 

	
 
msgid ""
 
"User \"%s\" still owns %s user groups and cannot be removed. Switch "
 
"owners or remove those user groups: %s"
 
msgstr ""
 
"Пользователь \"%s\" всё ещё является владельцем %s групп пользователей и "
 
"поэтому не может быть удалён. Смените владельца или удалите данные "
 
"группы: %s"
 

	
 
msgid "Password reset link"
 
msgstr "Ссылка сброса пароля"
 

	
 
msgid "Value cannot be an empty list"
 
msgstr "Значение не может быть пустым списком"
 

	
 
msgid "Username \"%(username)s\" already exists"
 
msgstr "Пользователь с именем \"%(username)s\" уже существует"
 

	
 
msgid "Username %(username)s is not valid"
 
msgstr "Имя \"%(username)s\" недопустимо"
 

	
 
msgid "Invalid user group name"
 
msgstr "Неверное имя группы пользователей"
 

	
 
msgid "User group \"%(usergroup)s\" already exists"
 
msgstr "Группа пользователей \"%(usergroup)s\" уже существует"
 

	
 
msgid ""
 
"user group name may only contain alphanumeric characters underscores, "
 
"periods or dashes and must begin with alphanumeric character"
 
msgstr ""
 
"имя группы пользователей может содержать только буквы, цифры, символы "
 
"подчеркивания, точки и тире; а так же должно начинаться с буквы или цифры"
 

	
 
msgid "Cannot assign this group as parent"
 
msgstr "Невозможно использовать эту группу как родителя"
 

	
 
msgid "Group \"%(group_name)s\" already exists"
 
msgstr "Группа \"%(group_name)s\" уже существует"
 

	
 
msgid "Repository with name \"%(group_name)s\" already exists"
 
msgstr "Репозитарий с  именем \"%(group_name)s\" уже существует"
 
msgstr "Репозиторий с именем «%(group_name)s» уже существует"
 

	
 
msgid "Invalid characters (non-ascii) in password"
 
msgstr "Недопустимые символы (не ascii) в пароле"
 

	
 
msgid "Invalid old password"
 
msgstr "Неверно задан старый пароль"
 

	
 
msgid "Passwords do not match"
 
msgstr "Пароли не совпадают"
 

	
 
msgid "Repository named %(repo)s already exists"
 
msgstr "Репозитарий %(repo)s уже существует"
 

	
 
msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
 
msgstr "Репозитарий \"%(repo)s\" уже существует в группе \"%(group)s\""
 

	
 
msgid "Repository group with name \"%(repo)s\" already exists"
 
msgstr "Группа репозиториев \"%(repo)s\" уже существует"
 

	
 
msgid "Fork has to be the same type as parent"
 
msgstr "Тип форка будет совпадать с родительским"
 

	
 
msgid "You don't have permissions to create repository in this group"
 
msgstr "У вас недостаточно прав для создания репозиториев в этой группе"
 

	
 
msgid "no permission to create repository in root location"
 
msgstr "недостаточно прав для создания репозитория в корневом каталоге"
 

	
 
msgid "You don't have permissions to create a group in this location"
 
msgstr "У Вас недостаточно привилегий для создания группы в этом месте"
 

	
 
msgid "This username or user group name is not valid"
 
msgstr "Данное имя пользователя или группы пользователей недопустимо"
 

	
 
msgid "This is not a valid path"
 
msgstr "Этот путь ошибочен"
 

	
 
msgid ""
 
"The LDAP Login attribute of the CN must be specified - this is the name "
 
"of the attribute that is equivalent to \"username\""
 
msgstr ""
 
"Для входа по LDAP должно быть указано значение аттрибута CN - это "
 
"эквивалент имени пользователя"
 

	
 
msgid "Please enter a valid IPv4 or IPv6 address"
 
msgstr "Пожалуйста, введите существующий IPv4 или IPv6 адре"
 

	
 
msgid ""
 
"The network size (bits) must be within the range of 0-32 (not %(bits)r)"
 
msgstr ""
 
"Значение маски подсети должно быть в пределах от 0 до 32 (%(bits)r - "
 
"неверно)"
 

	
 
msgid "Key name can only consist of letters, underscore, dash or numbers"
 
msgstr ""
 
"Ключевое имя может только состоять из букв, символа подчеркивания, тире "
 
"или чисел"
 

	
 
msgid "Filename cannot be inside a directory"
 
msgstr "Файла нет в каталоге"
 

	
 
msgid "About"
 
msgstr "О программе"
 

	
 
msgid "Add Repository"
 
msgstr "Добавить репозиторий"
 

	
 
msgid "Add Repository Group"
 
msgstr "Добавить группу репозиториев"
 

	
 
msgid "You have admin right to this group, and can edit it"
 
msgstr ""
 
"Вы имеете администраторские права на эту группу и можете редактировать её"
 

	
 
msgid "Edit Repository Group"
 
msgstr "Изменить группу репозиториев"
 

	
 
msgid "Repository"
 
msgstr "Репозиторий"
 

	
 
msgid "Description"
 
msgstr "Описание"
 

	
 
msgid "Last Change"
 
msgstr "Последнее изменение"
 

	
 
msgid "Tip"
 
msgstr "Состояние"
 

	
 
msgid "Owner"
 
msgstr "Владелец"
 

	
 
msgid "Log In"
 
msgstr "Войти"
 

	
 
msgid "Log In to %s"
 
msgstr "Войти в %s"
 

	
 
msgid "Username"
 
msgstr "Имя пользователя"
 

	
 
msgid "Password"
 
msgstr "Пароль"
 

	
 
msgid "Forgot your password ?"
 
msgstr "Забыли пароль?"
 

	
 
msgid "Don't have an account ?"
 
msgstr "Нет аккаунта?"
 

	
 
msgid "Sign In"
 
msgstr "Войти"
 

	
 
msgid "Password Reset"
 
msgstr "Сброс пароля"
 

	
 
msgid "Reset Your Password to %s"
 
msgstr "Сброс пароля для %s"
 

	
 
msgid "Reset Your Password"
 
msgstr "Сброс пароля"
 

	
 
msgid "Email Address"
 
msgstr "Почтовый адрес"
 

	
 
msgid "Captcha"
 
msgstr "Капча"
 

	
 
msgid "Send Password Reset Email"
 
msgstr "Послать ссылку сброса пароля"
 

	
 
msgid "Sign Up"
 
msgstr "Регистрация"
 

	
 
msgid "Sign Up to %s"
 
msgstr "Регистра на %s"
 

	
 
msgid "Re-enter password"
 
msgstr "Повторите пароль"
 

	
 
msgid "First Name"
 
msgstr "Имя"
 

	
 
msgid "Last Name"
 
msgstr "Фамилия"
 

	
 
msgid "Email"
 
msgstr "E-mail"
 

	
 
msgid "Please wait for an administrator to activate your account."
 
msgstr ""
 
"Пожалуйста, подождите, пока администратор подтвердит Вашу регистрацию."
 

	
 
msgid "Admin Journal"
 
msgstr "Журнал администратора"
 

	
 
msgid "journal filter..."
 
msgstr "Фильтр журнала..."
 

	
 
msgid "Filter"
 
msgstr "Отфильтровать"
 

	
 
msgid "%s Entry"
 
msgid_plural "%s Entries"
 
msgstr[0] "%s запись"
 
msgstr[1] "%s записей"
 
msgstr[2] "%s записи"
 

	
 
msgid "Action"
 
msgstr "Действие"
 

	
 
msgid "Date"
 
msgstr "Дата"
 

	
 
msgid "From IP"
 
msgstr "С IP"
 

	
 
msgid "No actions yet"
 
msgstr "Действия ещё не производились"
 

	
 
msgid "Authentication Settings"
 
msgstr "Настройки аутентификации"
 

	
 
msgid "Authentication"
 
msgstr "Аутентификация"
 

	
 
msgid "Authentication Plugins"
 
msgstr "Плагины аутентификации"
 

	
 
msgid "Enabled Plugins"
 
msgstr "Включенные плагины"
 

	
 
msgid "Available built-in plugins"
 
msgstr "Доступные встроенные плагины"
 

	
 
msgid "Plugin"
 
msgstr "Плагин"
 

	
 
msgid "Save"
 
msgstr "Сохранить"
 

	
 
msgid "Repository Defaults"
 
msgstr "Значения по умолчанию"
 

	
 
msgid "Type"
 
msgstr "Тип"
 

	
 
msgid "Private repository"
 
msgstr "Приватный репозиторий"
 

	
 
msgid ""
 
"Private repositories are only visible to people explicitly added as "
 
"collaborators."
 
msgstr "Приватные репозитории видны только их участникам."
 

	
 
msgid "Enable statistics"
 
msgstr "Включить статистику"
 

	
 
msgid "Enable statistics window on summary page."
 
msgstr "Включить окно статистики на странице «Общие сведения»."
 

	
 
msgid "Enable downloads"
 
msgstr "Включить скачивание"
 

	
 
msgid "Enable download menu on summary page."
 
msgstr "Включить меню скачивания на странице «Общие сведения»."
 

	
 
msgid "Edit Gist"
 
msgstr "Правка gist-записи"
 

	
 
msgid "Gist description ..."
 
msgstr "Описание..."
 

	
 
msgid "Expires"
 
msgstr "Истекает"
 

	
 
msgid "Update Gist"
 
msgstr "Обновить"
 

	
 
msgid "Cancel"
 
msgstr "Отмена"
 

	
 
msgid "Private Gists for User %s"
 
msgstr "Приватная gist-запись для пользователя %s"
 

	
 
msgid "Public Gists for User %s"
 
msgstr "Публичная gist-запись для пользователя %s"
 

	
 
msgid "Public Gists"
 
msgstr "Публичные gist-записи"
 

	
 
msgid "Create New Gist"
 
msgstr "Создать новую gist-запись"
 

	
 
msgid "Created"
 
msgstr "Создано"
 

	
 
msgid "There are no gists yet"
 
msgstr "Gist-записи отсутствуют"
 

	
 
msgid "Reset"
 
msgstr "Сброс"
 

	
 
msgid "Gist"
 
msgstr "Gist"
 

	
 
msgid "URL"
 
msgstr "URL"
 

	
 
msgid "Delete"
 
msgstr "Удалить"
 

	
 
msgid "Confirm to delete this Gist"
 
msgstr "Подтвердите удаление этой gist-записи"
 

	
 
msgid "Edit"
 
msgstr "Редактировать"
 

	
 
msgid "Show as Raw"
 
msgstr "Показать только текст"
 

	
 
msgid "created"
 
msgstr "создана"
 

	
 
msgid "Show as raw"
 
msgstr "Показать только текст"
 

	
 
msgid "My Account"
 
msgstr "Мой Аккаунт"
 

	
 
msgid "Profile"
 
msgstr "Профиль"
 

	
 
msgid "API Keys"
 
msgstr "API-ключи"
 

	
 
msgid "Add"
 
msgstr "Добавить"
 

	
 
msgid "Primary"
 
msgstr "Основной"
 

	
 
msgid "Confirm to delete this email: %s"
 
msgstr "Подтвердите удаление E-mail: %s"
 

	
 
msgid "No additional emails specified."
 
msgstr "Нет дополнительных адресов e-mail."
 

	
 
msgid "New email address"
 
msgstr "Новый E-mail"
 

	
 
msgid "Change Your Account Password"
 
msgstr "Смена пароля"
 

	
 
msgid "Current password"
 
msgstr "Текущий пароль"
 

	
 
msgid "New password"
 
msgstr "Новый пароль"
 

	
 
msgid "Confirm new password"
 
msgstr "Подтвердите новый пароль"
 

	
 
msgid "Avatars are disabled"
 
msgstr "Аватары отключены"
 

	
 
msgid "Repositories You Own"
 
msgstr "Репозитории, где Вы — владелец"
 

	
 
msgid "Name"
 
msgstr "Имя"
 

	
 
msgid "Repositories You are Watching"
 
msgstr "Репозитории, за которыми Вы наблюдаете"
 

	
 
msgid "Default Permissions"
 
msgstr "Стандартные привилегии"
 

	
 
msgid "IP Whitelist"
 
msgstr "Белый список IP"
 

	
 
msgid "Anonymous access"
 
msgstr "Анонимный доступ"
 

	
 
msgid ""
 
"All default permissions on each repository will be reset to chosen "
 
"permission, note that all custom default permission on repositories will "
 
"be lost"
 
msgstr ""
 
"Выбранные привилегии будут установлены по умолчанию для каждого "
 
"репозитория. Учтите, что ранее установленные привилегии по умолчанию "
 
"будут сброшены"
 

	
 
msgid "Repository group"
 
msgstr "Группа репозиториев"
 

	
 
msgid ""
 
"All default permissions on each repository group will be reset to chosen "
 
"permission, note that all custom default permission on repository groups "
 
"will be lost"
 
msgstr ""
 
"Выбранные привилегии будут установлены по умолчанию для каждой группы "
 
"репозиториев. Учтите, что ранее установленные привилегии по умолчанию для "
 
"групп репозиториев будут сброшены"
 

	
 
msgid "User group"
 
msgstr "Группа пользователей"
 

	
 
msgid "User group creation"
 
msgstr "Создание групп пользователей"
 

	
 
msgid "Repository forking"
 
msgstr "Создание форка репозитория"
 

	
 
msgid "Registration"
 
msgstr "Регистрация"
 

	
 
msgid "External auth account activation"
 
msgstr "Активация сторонней учетной записи"
 

	
 
msgid "All IP addresses are allowed."
 
msgstr "Все IP-адреса разрешены."
 

	
 
msgid "New IP address"
kallithea/lib/ssh.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
"""
 
    kallithea.lib.ssh
 
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

	
 
    :created_on: Dec 10, 2012
 
    :author: ir4y
 
    :copyright: (C) 2012 Ilya Beda <ir4y.ix@gmail.com>
 
    :license: GPLv3, see COPYING for more details.
 
"""
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 

	
 
import binascii
 
import logging
 
import re
 

	
 
from tg.i18n import ugettext as _
 

	
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
class SshKeyParseError(Exception):
 
    """Exception raised by parse_pub_key"""
 

	
 

	
 
def parse_pub_key(ssh_key):
 
    r"""Parse SSH public key string, raise SshKeyParseError or return decoded keytype, data and comment
 

	
 
    >>> getfixture('doctest_mock_ugettext')
 
    >>> parse_pub_key('')
 
    Traceback (most recent call last):
 
    ...
 
    SshKeyParseError: SSH key is missing
 
    >>> parse_pub_key('''AAAAB3NzaC1yc2EAAAALVGhpcyBpcyBmYWtlIQ''')
 
    Traceback (most recent call last):
 
    ...
 
    SshKeyParseError: Incorrect SSH key - it must have both a key type and a base64 part
 
    >>> parse_pub_key('''abc AAAAB3NzaC1yc2EAAAALVGhpcyBpcyBmYWtlIQ''')
 
    Traceback (most recent call last):
 
    ...
 
    SshKeyParseError: Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'
 
    >>> parse_pub_key('''ssh-rsa  AAAAB3NzaC1yc2EAAAALVGhpcyBpcyBmYWtlIQ''')
 
    Traceback (most recent call last):
 
    ...
 
    SshKeyParseError: Incorrect SSH key - failed to decode base64 part 'AAAAB3NzaC1yc2EAAAALVGhpcyBpcyBmYWtlIQ'
 
    >>> parse_pub_key('''ssh-rsa  AAAAB2NzaC1yc2EAAAALVGhpcyBpcyBmYWtlIQ==''')
 
    Traceback (most recent call last):
 
    ...
 
    SshKeyParseError: Incorrect SSH key - base64 part is not 'ssh-rsa' as claimed but 'csh-rsa'
 
    >>> parse_pub_key('''ssh-rsa  AAAAB3NzaC1yc2EAAAA'LVGhpcyBpcyBmYWtlIQ''')
 
    Traceback (most recent call last):
 
    ...
 
    SshKeyParseError: Incorrect SSH key - unexpected characters in base64 part "AAAAB3NzaC1yc2EAAAA'LVGhpcyBpcyBmYWtlIQ"
 
    >>> parse_pub_key(''' ssh-rsa  AAAAB3NzaC1yc2EAAAALVGhpcyBpcyBmYWtlIQ== and a comment
 
    ... ''')
 
    ('ssh-rsa', '\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x0bThis is fake!', 'and a comment\n')
 
    >>> parse_pub_key('''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIP1NA2kBQIKe74afUXmIWD9ByDYQJqUwW44Y4gJOBRuo''')
 
    ('ssh-ed25519', '\x00\x00\x00\x0bssh-ed25519\x00\x00\x00 \xfdM\x03i\x01@\x82\x9e\xef\x86\x9fQy\x88X?A\xc86\x10&\xa50[\x8e\x18\xe2\x02N\x05\x1b\xa8', '')
 
    """
 
    if not ssh_key:
 
        raise SshKeyParseError(_("SSH key is missing"))
 

	
 
    parts = ssh_key.split(None, 2)
 
    if len(parts) < 2:
 
        raise SshKeyParseError(_("Incorrect SSH key - it must have both a key type and a base64 part"))
 

	
 
    keytype, keyvalue, comment = (parts + [''])[:3]
 
    if keytype not in ('ssh-rsa', 'ssh-dss', 'ssh-ed25519'):
 
        raise SshKeyParseError(_("Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"))
 

	
 
    if re.search(r'[^a-zA-Z0-9+/=]', keyvalue):
 
        raise SshKeyParseError(_("Incorrect SSH key - unexpected characters in base64 part %r") % keyvalue)
 

	
 
    try:
 
        decoded = keyvalue.decode('base64')
 
    except binascii.Error:
 
        raise SshKeyParseError(_("Incorrect SSH key - failed to decode base64 part %r") % keyvalue)
 

	
 
    if not decoded.startswith('\x00\x00\x00\x07' + str(keytype) + '\x00'):
 
    if not decoded.startswith('\x00\x00\x00' + chr(len(keytype)) + str(keytype) + '\x00'):
 
        raise SshKeyParseError(_("Incorrect SSH key - base64 part is not %r as claimed but %r") % (str(keytype), str(decoded[4:].split('\0', 1)[0])))
 

	
 
    return keytype, decoded, comment
 

	
 

	
 
SSH_OPTIONS = 'no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding'
 

	
 

	
 
def authorized_keys_line(kallithea_cli_path, config_file, key):
 
    """
 
    Return a line as it would appear in .authorized_keys
 

	
 
    >>> from kallithea.model.db import UserSshKeys, User
 
    >>> user = User(user_id=7, username='uu')
 
    >>> key = UserSshKeys(user_ssh_key_id=17, user=user, description='test key')
 
    >>> key.public_key='''ssh-rsa  AAAAB3NzaC1yc2EAAAALVGhpcyBpcyBmYWtlIQ== and a comment'''
 
    >>> authorized_keys_line('/srv/kallithea/venv/bin/kallithea-cli', '/srv/kallithea/my.ini', key)
 
    'no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,command="/srv/kallithea/venv/bin/kallithea-cli ssh-serve -c /srv/kallithea/my.ini 7 17" ssh-rsa AAAAB3NzaC1yc2EAAAALVGhpcyBpcyBmYWtlIQ==\\n'
 
    """
 
    try:
 
        keytype, decoded, comment = parse_pub_key(key.public_key)
 
    except SshKeyParseError:
 
        return '# Invalid Kallithea SSH key: %s %s\n' % (key.user.user_id, key.user_ssh_key_id)
 
    mimekey = decoded.encode('base64').replace('\n', '')
 
    return '%s,command="%s ssh-serve -c %s %s %s" %s %s\n' % (
 
        SSH_OPTIONS, kallithea_cli_path, config_file,
 
        key.user.user_id, key.user_ssh_key_id,
 
        keytype, mimekey)
kallithea/templates/admin/settings/settings_visual.html
Show inline comments
 
${h.form(url('admin_settings_visual'), method='post')}
 
    <div class="form">
 
            <div class="form-group">
 
                <label class="control-label">${_('General')}:</label>
 
                <div>
 
                    <div class="checkbox">
 
                        <label>
 
                            ${h.checkbox('repository_fields','True')}
 
                            ${_('Use repository extra fields')}
 
                        </label>
 
                    </div>
 
                    <span class="help-block">${_('Allows storing additional customized fields per repository.')}</span>
 

	
 
                    <div class="checkbox">
 
                        <label>
 
                            ${h.checkbox('show_version','True')}
 
                            ${_('Show Kallithea version')}
 
                        </label>
 
                    </div>
 
                    <span class="help-block">${_('Shows or hides a version number of Kallithea displayed in the footer.')}</span>
 

	
 
                    <div class="checkbox">
 
                        <label>
 
                            ${h.checkbox('use_gravatar','True')}
 
                            ${_('Show user Gravatars')}
 
                        </label>
 
                    </div>
 
                    ${h.text('gravatar_url', size=80, class_='form-control')}
 
                    <span class="help-block">${_('''Gravatar URL allows you to use another avatar server application.
 
                                                        The following variables of the URL will be replaced accordingly.
 
                                                        {scheme}    'http' or 'https' sent from running Kallithea server,
 
                                                        {email}     user email,
 
                                                        {md5email}  md5 hash of the user email (like at gravatar.com),
 
                                                        {size}      size of the image that is expected from the server application,
 
                                                        {netloc}    network location/server host of running Kallithea server''')}</span>
 
                </div>
 
            </div>
 

	
 
            <div class="form-group">
 
                <label class="control-label">${_('HTTP Clone URL')}:</label>
 
                <div>
 
                    ${h.text('clone_uri_tmpl', size=80, class_='form-control')}
 
                    <span class="help-block">${_('''Schema of clone URL construction eg. '{scheme}://{user}@{netloc}/{repo}'.
 
                                                    The following variables are available:
 
                                                    {scheme} 'http' or 'https' sent from running Kallithea server,
 
                                                    {user}   current user username,
 
                                                    {netloc} network location/server host of running Kallithea server,
 
                                                    {repo}   full repository name,
 
                                                    {repoid} ID of repository, can be used to construct clone-by-id,
 
                                                    {system_user}  name of the Kallithea system user,
 
                                                    {hostname}  server hostname
 
                                                    ''')}
 
                    </span>
 
                </div>
 
                %if c.ssh_enabled:
 
                <label class="control-label">${_('SSH Clone URL')}:</label>
 
                <div>
 
                    ${h.text('clone_ssh_tmpl', size=80, class_='form-control')}
 
                    <span class="help-block">${_('''Schema for constructing SSH clone URL, eg. 'ssh://{system_user}@{hostname}/{repo}'.''')}</span>
 
                </div>
 
                %else:
 
                ${h.hidden('clone_ssh_tmpl', size=80, class_='form-control')}
 
                %endif
 
            </div>
 

	
 
            <div class="form-group">
 
                <label class="control-label" for="dashboard_items">${_('Repository page size')}:</label>
 
                <div>
 
                    ${h.text('dashboard_items',size=5,class_='form-control')}
 
                    <span class="help-block">${_('Number of items displayed in the repository pages before pagination is shown.')}</span>
 
                </div>
 
            </div>
 

	
 
            <div class="form-group">
 
                <label class="control-label" for="admin_grid_items">${_('Admin page size')}:</label>
 
                <div>
 
                    ${h.text('admin_grid_items',size=5,class_='form-control')}
 
                    <span class="help-block">${_('Number of items displayed in the admin pages grids before pagination is shown.')}</span>
 
                </div>
 
            </div>
 

	
 
            <div class="form-group">
 
                <label class="control-label">${_('Icons')}:</label>
 
                <div>
 
                    <div class="checkbox">
 
                        <label>
 
                            ${h.checkbox('show_public_icon','True')}
 
                            ${_('Show public repository icon on repositories')}
 
                        </label>
 
                    </div>
 
                    <div class="checkbox">
 
                        <label>
 
                            ${h.checkbox('show_private_icon','True')}
 
                            ${_('Show private repository icon on repositories')}
 
                        </label>
 
                    </div>
 
                    <span class="help-block">${_('Show public/private icons next to repository names.')}</span>
 
                 </div>
 
            </div>
 

	
 
            <div class="form-group">
 
                <label class="control-label" for="stylify_metalabels">${_('Meta Tagging')}:</label>
 
                <div>
 
                    <div class="checkbox">
 
                        <label>
 
                            ${h.checkbox('stylify_metalabels','True')}
 
                            ${_('Parses meta tags from the repository description field and turns them into colored tags.')}
 
                        </label>
 
                    </div>
 
                    <div class="help-block">
 
                        ${_('Stylify recognised meta tags:')}
 
                        <ul class="list-unstyled"> <!-- Fix style here -->
 
                            <li>[featured] <span class="label label-meta" data-tag="featured">featured</span></li>
 
                            <li>[stale] <span class="label label-meta" data-tag="stale">stale</span></li>
 
                            <li>[dead] <span class="label label-meta" data-tag="dead">dead</span></li>
 
                            <li>[lang =&gt; lang] <span class="label label-meta" data-tag="lang">lang</span></li>
 
                            <li>[license =&gt; License] <span class="label label-meta" data-tag="license"><a href="http://www.opensource.org/licenses/License">License</a></span></li>
 
                            <li>[requires =&gt; Repo] <span class="label label-meta" data-tag="requires">requires =&gt; <a href="#">Repo</a></span></li>
 
                            <li>[recommends =&gt; Repo] <span class="label label-meta" data-tag="recommends">recommends =&gt; <a href="#">Repo</a></span></li>
 
                            <li>[see =&gt; URI] <span class="label label-meta" data-tag="see">see =&gt; <a href="#">URI</a> </span></li>
 
                        </ul>
 
                    </div>
 
                 </div>
 
            </div>
 

	
 
            <div class="form-group">
 
                <div class="buttons">
 
                    ${h.submit('save',_('Save Settings'),class_="btn btn-default")}
 
                    ${h.reset('reset',_('Reset'),class_="btn btn-default")}
 
                </div>
 
            </div>
 
    </div>
 
${h.end_form()}
scripts/make-release
Show inline comments
 
#!/bin/bash
 
set -e
 
set -x
 

	
 
cleanup()
 
{
 
  echo "Removing venv $venv"
 
  rm  -rf "$venv"
 
}
 

	
 
echo "Checking that you are NOT inside a virtualenv"
 
[ -z "$VIRTUAL_ENV" ]
 

	
 
venv=$(mktemp -d --tmpdir kallithea-release-XXXXX)
 
trap cleanup EXIT
 

	
 
echo "Setting up a fresh virtualenv in $venv"
 
virtualenv -p python2 "$venv"
 
. "$venv/bin/activate"
 

	
 
echo "Install/verify tools needed for building and uploading stuff"
 
pip install --upgrade -e . -r dev_requirements.txt twine
 
pip install --upgrade -e . -r dev_requirements.txt twine python-ldap python-pam
 

	
 
echo "Cleanup and update copyrights ... and clean checkout"
 
scripts/run-all-cleanup
 
scripts/update-copyrights.py
 
hg up -cr .
 

	
 
echo "Make release build from clean checkout in build/"
 
rm -rf build dist
 
hg archive build
 
cd build
 

	
 
echo "Check that each entry in MANIFEST.in match something"
 
sed -e 's/[^ ]*[ ]*\([^ ]*\).*/\1/g' MANIFEST.in | xargs ls -lad
 

	
 
echo "Build dist"
 
python2 setup.py compile_catalog
 
python2 setup.py sdist
 

	
 
echo "Verify VERSION from kallithea/__init__.py"
 
namerel=$(cd dist && echo Kallithea-*.tar.gz)
 
namerel=${namerel%.tar.gz}
 
version=${namerel#Kallithea-}
 
ls -l $(pwd)/dist/$namerel.tar.gz
 
echo "Releasing Kallithea $version in directory $namerel"
 

	
 
echo "Verify dist file content"
 
diff -u <((hg mani | grep -v '^\.hg') | LANG=C sort) <(tar tf dist/Kallithea-$version.tar.gz | sed "s|^$namerel/||" | grep . | grep -v '^kallithea/i18n/.*/LC_MESSAGES/kallithea.mo$\|^Kallithea.egg-info/\|^PKG-INFO$\|/$' | LANG=C sort)
 

	
 
echo "Verify docs build"
 
python2 setup.py build_sphinx # the results are not actually used, but we want to make sure it builds
 

	
 
echo "Shortlog for inclusion in the release announcement"
 
scripts/shortlog.py "only('.', branch('stable') & tagged() & public() & not '.')"
 

	
 
cat - << EOT
 

	
 
Now, make sure
 
* all tests are passing
 
* release note is ready
 
* announcement is ready
 
* source has been pushed to https://kallithea-scm.org/repos/kallithea
 

	
 
EOT
 

	
 
echo "Verify current revision is tagged for $version"
 
hg log -r "'$version'&." | grep .
 

	
 
echo -n "Enter \"pypi\" to upload Kallithea $version to pypi: "
 
read answer
 
[ "$answer" = "pypi" ]
 

	
 
echo "Rebuild readthedocs for docs.kallithea-scm.org"
 
xdg-open https://readthedocs.org/projects/kallithea/
 
curl -X POST http://readthedocs.org/build/kallithea
 
xdg-open https://readthedocs.org/builds/kallithea/
 
xdg-open http://docs.kallithea-scm.org/en/latest/ # or whatever the branch is
 

	
 
twine upload dist/*
 
xdg-open https://pypi.python.org/pypi/Kallithea
0 comments (0 inline, 0 general)