edubuntu-server-14.02.2/0000755000000000000000000000000012277030060011702 5ustar edubuntu-server-14.02.2/container/0000755000000000000000000000000012277030060013664 5ustar edubuntu-server-14.02.2/client/0000755000000000000000000000000012277030060013160 5ustar edubuntu-server-14.02.2/client/sbin/0000755000000000000000000000000012277030060014113 5ustar edubuntu-server-14.02.2/client/sbin/edubuntu-server-auth0000755000000000000000000002413712277013552020155 0ustar #!/usr/bin/python3 # -*- coding: utf-8 -*- # Copyright (C) 2012-2014 Stéphane Graber # Author: Stéphane Graber # 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 2 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 can find the license on Debian systems in the file # /usr/share/common-licenses/GPL-2 import argparse import configparser import gettext import os import socket import subprocess import sys _ = gettext.gettext gettext.textdomain("edubuntu-server-auth") # Constants PKG_LIST = ("libpam-sss", "libnss-sss", "libsss-sudo", "libsasl2-modules-gssapi-heimdal", "samba-common-bin") # Common functions def backup_config(path): if os.path.exists(path) and not os.path.exists("%s.pre-edubuntu" % path): os.rename(path, "%s.pre-edubuntu" % path) def get_username(args): username = "" password = "" if hasattr(args, "username"): username = args.username else: username = os.environ['USER'] if hasattr(args, "password"): if args.password == "-": password = sys.stdin.read().strip() else: password = args.password if password: return "%s%%%s" % (username, password) else: return username def restore_config(path): if os.path.exists(path): os.remove(path) if os.path.exists("%s.pre-edubuntu" % path): os.rename("%s.pre-edubuntu" % path, path) # The various actions def join_domain(args): krb5_domain = args.domain.upper() ldap_dn = "DC=%s" % ",DC=".join(args.domain.split(".")) smb_workgroup = args.domain.split(".")[0].upper() smb_hostname = socket.gethostname().upper() domain_sid = None enctypes = "rc4-hmac aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96" # Mark the packages for installation cache = apt.Cache() for pkg in PKG_LIST: if pkg in cache: cache[pkg].mark_install() else: print("Unable to find package: %s" % pkg) sys.exit(1) if cache.get_changes(): print("Installing %s needed packages." % len(cache.get_changes())) cache.update() cache.commit() # Configure kerberos backup_config("/etc/krb5.conf") backup_config("/etc/krb5.keytab") with open("/etc/krb5.conf", "w+") as fd: fd.write("""[libdefaults] default_realm = %s dns_lookup_realm = false dns_lookup_kdc = true ticket_lifetime = 24h allow_weak_crypto = true default_tgs_enctypes = %s default_tkt_enctypes = %s permitted_enctypes = %s [domain_realm] .%s = %s %s = %s """ % ( krb5_domain, enctypes, enctypes, enctypes, args.domain, krb5_domain, args.domain, krb5_domain)) os.fchmod(fd.fileno(), 0o644) os.fchown(fd.fileno(), 0, 0) # Configure samba backup_config("/etc/samba/smb.conf") with open("/etc/samba/smb.conf", "w+") as fd: fd.write("""[global] workgroup = %s realm = %s server string = %s netbios name = %s security = ads kerberos method = system keytab passdb backend = tdbsam """ % (smb_workgroup, args.domain, smb_hostname, smb_hostname)) os.fchmod(fd.fileno(), 0o644) os.fchown(fd.fileno(), 0, 0) command = ["net", "ads", "join", "-U", get_username(args)] while 1: # Join subprocess.call(command) # Get the SID net = subprocess.Popen(['net', 'getdomainsid'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) net.wait() for line in net.stdout: line = line.strip() if line.startswith("SID for domain"): domain_sid = line.split()[-1] break if not domain_sid: print("Failed to join domain. Try again.") continue break # Configure lightdm if os.path.exists("/etc/lightdm/lightdm.conf"): write_lightdm = False lightdm = configparser.ConfigParser() lightdm.read("/etc/lightdm/lightdm.conf") if not lightdm.has_section("SeatDefaults"): lightdm.add_section("SeatDefaults") if not lightdm.has_option("SeatDefaults", "greeter-show-manual-login"): lightdm.set("SeatDefaults", "greeter-show-manual-login", "true") write_lightdm = True elif lightdm.get("SeatDefaults", "greeter-show-manual-login") != "true": lightdm.set("SeatDefaults", "greeter-show-manual-login", "true") write_lightdm = True if write_lightdm: with open("/etc/lightdm/lightdm.conf", "w+") as fd: lightdm.write(fd) # Configure PAM with open("/usr/share/pam-configs/edubuntu-domain", "w+") as fd: fd.write("""Name: edubuntu-server settings Default: yes Priority: 128 Session-Type: Additional Session-Interactive-Only: yes Session: optional pam_mkhomedir.so """) os.fchmod(fd.fileno(), 0o644) os.fchown(fd.fileno(), 0, 0) with open(os.devnull, "w") as fd: subprocess.call(["pam-auth-update", "--package"], stdout=fd, stderr=fd) # Configure sssd with open(os.devnull, "w") as fd: subprocess.call(["stop", "sssd"], stdout=fd, stderr=fd) backup_config("/etc/sssd/sssd.conf") with open("/etc/sssd/sssd.conf", "w+") as fd: fd.write("""[sssd] domains = Edubuntu services = nss, pam, sudo config_file_version = 2 sbus_timeout = 30 debug_level = 0 [nss] default_shell = /bin/bash [domain/Edubuntu] enumerate = false cache_credentials = true fallback_homedir = /home/%%u id_provider = ldap auth_provider = krb5 chpass_provider = krb5 sudo_provider = ldap dns_discovery_domain = %s krb5_realm = %s ldap_idmap_default_domain_sid = %s ldap_schema = ad ldap_id_mapping = true ldap_user_gecos = displayName ldap_force_upper_case_realm = true ldap_sudo_search_base = OU=sudoers,%s ldap_sasl_mech = GSSAPI """ % (args.domain, krb5_domain, domain_sid, ldap_dn)) os.fchmod(fd.fileno(), 0o600) os.fchown(fd.fileno(), 0, 0) with open(os.devnull, "w") as fd: subprocess.call(["start", "sssd"], stdout=fd, stderr=fd) def leave_domain(args): # Unconfigure samba if os.path.exists("/etc/samba/smb.conf"): command = ["net", "ads", "leave", "-U", get_username(args)] if subprocess.call(command) != 0: print("Failed to leave the domain.") sys.exit(1) restore_config("/etc/samba/smb.conf") # Unconfigure sssd restore_config("/etc/sssd/sssd.conf") with open(os.devnull, "w") as fd: if os.path.exists("/etc/sssd/sssd.conf"): subprocess.call(["initctl", "restart", "sssd"], stdout=fd, stderr=fd) else: subprocess.call(["initctl", "stop", "sssd"], stdout=fd, stderr=fd) # Remove the Kerberos configuration restore_config("/etc/krb5.conf") restore_config("/etc/krb5.keytab") # Remove the PAM configuration restore_config("/usr/share/pam-configs/edubuntu-domain") with open(os.devnull, "w") as fd: subprocess.call(["pam-auth-update", "--package", "--remove", "edubuntu-domain"], stdout=fd, stderr=fd) # Unconfigure lightdm if os.path.exists("/etc/lightdm/lightdm.conf"): lightdm = configparser.ConfigParser() lightdm.read("/etc/lightdm/lightdm.conf") if lightdm.has_section("SeatDefaults") and \ lightdm.has_option("SeatDefaults", "greeter-show-manual-login"): lightdm.remove_option("SeatDefaults", "greeter-show-manual-login") with open("/etc/lightdm/lightdm.conf", "w+") as fd: lightdm.write(fd) # Begin parsing the command line parser = argparse.ArgumentParser( description=_("Edubuntu server authentication"), formatter_class=argparse.RawTextHelpFormatter) # Commands subparsers = parser.add_subparsers() subparser_join = subparsers.add_parser('join', help=_('Join the domain')) subparser_join.set_defaults(action=join_domain) subparser_join.add_argument(dest="domain", metavar="DOMAIN-FQDN", help=_("Fully-Qualified-Domain-Name for the domain" "(e.g. edubuntu.org)")) subparser_join.add_argument("-U", "--username", dest="username", metavar="USERNAME", help=_("Username of a Domain Administrator")) subparser_join.add_argument("-P", "--password", dest="password", metavar="PASSWORD", help=_("Password of a Domain Administrator " "('-' for stdin)")) subparser_leave = subparsers.add_parser('leave', help=_('Leave the domain')) subparser_leave.add_argument("-U", "--username", dest="username", metavar="USERNAME", help=_("Username of a Domain Administrator")) subparser_leave.add_argument("-P", "--password", dest="password", metavar="PASSWORD", help=_("Password of a Domain Administrator " "('-' for stdin)")) subparser_leave.set_defaults(action=leave_domain) args = parser.parse_args() # Basic requirements check ## Check that we have an action if not hasattr(args, "action"): parser.error(_("You must specifiy an action.")) sys.exit(1) ## The user needs to be uid 0 if not os.geteuid() == 0: parser.error(_("You must be root to run this script. " "Try running: sudo %s" % (sys.argv[0]))) sys.exit(1) # Import apt now so we don't have to build-depend on it import apt args.action(args) edubuntu-server-14.02.2/AUTHORS0000644000000000000000000000145712277013541012766 0ustar Copyright (applies if no explicit header in the file): 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 2 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Authors: Copyright (c) 2012-2014 Stéphane Graber edubuntu-server-14.02.2/COPYING0000644000000000000000000004311012207204561012736 0ustar GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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 2 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. edubuntu-server-14.02.2/SETUP0000644000000000000000000000172212110216151012520 0ustar create-domain will automatically deploy the following roles: - directory - dnsr - manager == First server, no router == - edubuntu-server-build-template - edubuntu-server-setup-network --outside eth0 --inside eth1 - edubuntu-server-create-domain edubuntu.example - edubuntu-server-auth join edubuntu.example - for role in dhcpd; do edubuntu-server-add-role $role done - edubuntu-server-manage start == First server, existing router == - edubuntu-server-build-template - edubuntu-server-setup-network --bridge eth0 - edubuntu-server-create-domain edubuntu.example - edubuntu-server-auth join edubuntu.example - for role in dhcpd; do edubuntu-server-add-role $role done - edubuntu-server-manage start == Second server == - edubuntu-server-build-template - edubuntu-server-setup-network --bridge eth0 - edubuntu-server-auth join edubuntu.example - for role in dnsr dhcpd manager directory; do edubuntu-server-add-role $role done edubuntu-server-14.02.2/debian/0000755000000000000000000000000012277030060013124 5ustar edubuntu-server-14.02.2/debian/control0000644000000000000000000000410312277027073014537 0ustar Source: edubuntu-server Section: misc Priority: optional Maintainer: Stéphane Graber Build-Depends: debhelper (>= 9), help2man, python, python3 Standards-Version: 3.9.5 Homepage: http://launchpad.net/edubuntu Vcs-Bzr: http://bazaar.launchpad.net/~edubuntu-dev/edubuntu/edubuntu-server Package: edubuntu-server-client Architecture: all Depends: python3, python3-apt, ${misc:Depends} Description: Edubuntu server (client scripts) This package is one of the building blocks of Edubuntu server. . Edubuntu server aims at providing an easy to install and use server for classroom installations and for schools. . This package contains what's needed to join an Edubuntu server domain. Package: edubuntu-server-container Architecture: all Section: metapackages Depends: edubuntu-server-client, ${misc:Depends} Description: Edubuntu server (common dependencies and scripts for containers) This package is one of the building blocks of Edubuntu server. . Edubuntu server aims at providing an easy to install and use server for classroom installations and for schools. . This package contains what's needed in an Edubuntu server container. Package: edubuntu-server-host Architecture: all Depends: lxc, python3, python3-ipaddr, python3-lxc, ${misc:Depends} Description: Edubuntu server (host dependencies and scripts) This package is one of the building blocks of Edubuntu server. . Edubuntu server aims at providing an easy to install and use server for classroom installations and for schools. . This package contains what's needed to install a physical host. Package: edubuntu-server-manager Architecture: all Depends: python, python-dnspython, python-flask, python-flaskext.wtf, python-ldap, python-wtforms, ${misc:Depends} Description: Edubuntu server (web interface) This package is one of the building blocks of Edubuntu server. . Edubuntu server aims at providing an easy to install and use server for classroom installations and for schools. . This package contains the Edubuntu Server management web interface. edubuntu-server-14.02.2/debian/edubuntu-server-manager.postrm0000644000000000000000000000173312105564307021153 0ustar #!/bin/sh # postrm script for test # # see: dh_installdeb(1) set -e # summary of how this script can be called: # * `remove' # * `purge' # * `upgrade' # * `failed-upgrade' # * `abort-install' # * `abort-install' # * `abort-upgrade' # * `disappear' # # for details, see http://www.debian.org/doc/debian-policy/ or # the debian-policy package case "$1" in purge) rm -f /etc/edubuntu-server/manager.cfg ;; remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) ;; *) echo "postrm called with unknown argument \`$1'" >&2 exit 1 ;; esac # dh_installdeb will replace this with shell code automatically # generated by other debhelper scripts. #DEBHELPER# exit 0 edubuntu-server-14.02.2/debian/changelog0000644000000000000000000000230612277030046015003 0ustar edubuntu-server (14.02.2) trusty; urgency=medium * Update for current python3-lxc. * Fix error message. * Add missing dependency on python-flaskext.wtf. * Add configuration plugin for edubuntu-server-manager. * Cleanup resolvconf config from template. -- Stéphane Graber Wed, 12 Feb 2014 21:38:28 -0500 edubuntu-server (14.02.1) trusty; urgency=medium * Change default AD password to EdubuntuServer2014 * Update for current samba package (moving from samba4) * Update copyright * Include URL_PREFIX in example config * Don't use hardcoded paths in web UI -- Stéphane Graber Wed, 12 Feb 2014 19:53:50 -0500 edubuntu-server (13.10.1) trusty; urgency=low * edubuntu-server-manager: Bind on IPv6 too * directory setup: Fix domain creation parameters to properly configure bind9 to use samba4. * edubuntu-server-deploy: Fix invalid LXC config * edubuntu-server-manage: Show help message when called without argument. -- Stéphane Graber Mon, 21 Oct 2013 11:21:48 -0400 edubuntu-server (13.08.1) saucy; urgency=low * Initial release -- Stéphane Graber Tue, 27 Aug 2013 16:20:40 -0400 edubuntu-server-14.02.2/debian/compat0000644000000000000000000000000212001613100014305 0ustar 9 edubuntu-server-14.02.2/debian/edubuntu-server-manager.dirs0000644000000000000000000000002512105555241020556 0ustar /etc/edubuntu-server edubuntu-server-14.02.2/debian/edubuntu-server-host.lintian-overrides0000644000000000000000000000031612207201060022607 0ustar postrm-does-not-call-updaterc.d-for-init.d-script etc/init.d/edubuntu-server init.d-script-not-included-in-package etc/init.d/edubuntu-server init.d-script-not-marked-as-conffile etc/init.d/edubuntu-server edubuntu-server-14.02.2/debian/edubuntu-server-manager.examples0000644000000000000000000000003412105535340021431 0ustar manager/manager.cfg.example edubuntu-server-14.02.2/debian/edubuntu-server-manager.install0000644000000000000000000000036312277027073021277 0ustar manager/edubuntu-server-manager usr/bin/ manager/libs usr/share/edubuntu-server-manager/ manager/plugins usr/share/edubuntu-server-manager/ manager/static usr/share/edubuntu-server-manager/ manager/templates usr/share/edubuntu-server-manager/ edubuntu-server-14.02.2/debian/edubuntu-server-host.install0000644000000000000000000000007612277027073020643 0ustar host/sbin/* usr/sbin/ host/share/* usr/share/edubuntu-server/ edubuntu-server-14.02.2/debian/edubuntu-server-manager.lintian-overrides0000644000000000000000000000034612207201074023254 0ustar postrm-does-not-call-updaterc.d-for-init.d-script etc/init.d/edubuntu-server-manager init.d-script-not-included-in-package etc/init.d/edubuntu-server-manager init.d-script-not-marked-as-conffile etc/init.d/edubuntu-server-manager edubuntu-server-14.02.2/debian/rules0000755000000000000000000000374512207201615014214 0ustar #!/usr/bin/make -f # -*- makefile -*- # Sample debian/rules that uses debhelper. # This file was originally written by Joey Hess and Craig Small. # As a special exception, when this file is copied by dh-make into a # dh-make output file, you may use that output file without restriction. # This special exception was added by Craig Small in version 0.37 of dh-make. # Uncomment this to turn on verbose mode. #export DH_VERBOSE=1 VERSION=$(shell head -n1 debian/changelog |sed -e 's/.*(\(.*\)).*/\1/') %: dh $@ override_dh_installinit: dh_installinit -pedubuntu-server-host --name edubuntu-server --no-restart-on-upgrade dh_installinit -pedubuntu-server-manager --name edubuntu-server-manager override_dh_installman: mkdir -p $(CURDIR)/debian/edubuntu-server-client/usr/share/man/man1/ help2man --name="Edubuntu server client" --version-string=$(VERSION) -N $(CURDIR)/client/sbin/edubuntu-server-auth > $(CURDIR)/debian/edubuntu-server-client/usr/share/man/man1/edubuntu-server-auth.1 mkdir -p $(CURDIR)/debian/edubuntu-server-host/usr/share/man/man1/ help2man --name="Edubuntu server template builder" --version-string=$(VERSION) -N $(CURDIR)/host/sbin/edubuntu-server-build-template > $(CURDIR)/debian/edubuntu-server-host/usr/share/man/man1/edubuntu-server-build-template.1 help2man --name="Edubuntu server deployment script" --version-string=$(VERSION) -N $(CURDIR)/host/sbin/edubuntu-server-deploy > $(CURDIR)/debian/edubuntu-server-host/usr/share/man/man1/edubuntu-server-deploy.1 help2man --name="Edubuntu server management script" --version-string=$(VERSION) -N $(CURDIR)/host/sbin/edubuntu-server-manage > $(CURDIR)/debian/edubuntu-server-host/usr/share/man/man1/edubuntu-server-manage.1 mkdir -p $(CURDIR)/debian/edubuntu-server-manager/usr/share/man/man1/ help2man --name="Edubuntu server management web interface" --version-string=$(VERSION) -N $(CURDIR)/manager/edubuntu-server-manager > $(CURDIR)/debian/edubuntu-server-manager/usr/share/man/man1/edubuntu-server-manager.1 dh_installman edubuntu-server-14.02.2/debian/edubuntu-server-client.install0000644000000000000000000000003012277027073021132 0ustar client/sbin/* usr/sbin/ edubuntu-server-14.02.2/debian/edubuntu-server-host.edubuntu-server.upstart0000644000000000000000000000101012105603774024020 0ustar # edubuntu-server - Setup the network and start Edubuntu Server # # This job is started at boot time and manages all of the # Edubuntu server containers and the network description "Edubuntu Server" author "Stéphane Graber " start on filesystem or runlevel [2345] stop on runlevel [!2345] pre-start script [ -f /etc/edubuntu-server/edubuntu-server.conf ] || stop exec edubuntu-server-manage start end script post-stop script exec edubuntu-server-manage stop end script edubuntu-server-14.02.2/debian/edubuntu-server-manager.edubuntu-server-manager.upstart0000644000000000000000000000067512105564032026076 0ustar # edubuntu-server-manager - Management web interface # # This job is started at boot time and starts the management # web interface for Edubuntu Server. description "Edubuntu Server" author "Stéphane Graber " start on net-static-up stop on runlevel [!2345] respawn env PYTHONDONTWRITEBYTECODE=1 pre-start script [ -f /etc/edubuntu-server/manager.cfg ] || stop end script exec edubuntu-server-manager edubuntu-server-14.02.2/debian/copyright0000644000000000000000000000171412277027073015074 0ustar This package was debianized by Stéphane Graber. Authors: Stéphane Graber Copyright: Stéphane Graber Copyright (c) 2012-2014 Stéphane Graber License: 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 2 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 can find the license on Debian systems in the file /usr/share/common-licenses/GPL-2 The Debian packaging is Copyright 2010-2012, Stéphane Graber and is licensed under the GPL, see `/usr/share/common-licenses/GPL-2'. edubuntu-server-14.02.2/debian/source/0000755000000000000000000000000012277030060014424 5ustar edubuntu-server-14.02.2/debian/source/format0000644000000000000000000000001511741524522015641 0ustar 3.0 (native) edubuntu-server-14.02.2/manager/0000755000000000000000000000000012277030060013314 5ustar edubuntu-server-14.02.2/manager/libs/0000755000000000000000000000000012277030060014245 5ustar edubuntu-server-14.02.2/manager/libs/__init__.py0000644000000000000000000000000012277013527016355 0ustar edubuntu-server-14.02.2/manager/libs/common.py0000644000000000000000000000265412277013527016127 0ustar # -*- coding: utf-8 -*- # Copyright (C) 2012-2014 Stéphane Graber # Author: Stéphane Graber # 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 2 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 can find the license on Debian systems in the file # /usr/share/common-licenses/GPL-2 from flask import url_for from werkzeug.urls import uri_to_iri class ReverseProxied(object): def __init__(self, app, prefix): self.app = app if prefix[0] != "/": self.prefix = "/%s" % prefix else: self.prefix = prefix def __call__(self, environ, start_response): script_name = self.prefix environ['SCRIPT_NAME'] = script_name path_info = environ['PATH_INFO'] if path_info.startswith(script_name): environ['PATH_INFO'] = path_info[len(script_name):] return self.app(environ, start_response) def iri_for(endpoint, **values): """ Wrapper to url_for for utf-8 URLs. """ return uri_to_iri(url_for(endpoint, **values)) edubuntu-server-14.02.2/manager/libs/ldap_func.py0000644000000000000000000003545712277013527016601 0ustar # -*- coding: utf-8 -*- # Copyright (C) 2012-2014 Stéphane Graber # Author: Stéphane Graber # 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 2 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 can find the license on Debian systems in the file # /usr/share/common-licenses/GPL-2 from flask import request, Response, g, session from functools import wraps import ldap import struct import uuid LDAP_SCOPES = {"base": ldap.SCOPE_BASE, "onelevel": ldap.SCOPE_ONELEVEL, "subtree": ldap.SCOPE_SUBTREE} LDAP_AD_GROUPTYPE_VALUES = {1: ('System', False), 2: ('Global', True), 4: ('Domain local', True), 8: ('Universal', True), 16: ('APP_BASIC', False), 32: ('APP_QUERY', False)} LDAP_AD_USERACCOUNTCONTROL_VALUES = {2: ("Account is disabled", True), 64: ("User can't change password", False), 512: ("Normal user account", False), 4096: ("Machine trust account", False), 8192: ("Server trust account", False), 65536: ("Password never expires", True), 8388608: ("Password expired", False)} LDAP_AD_BOOL_ATTRIBUTES = ['showInAdvancedViewOnly'] LDAP_AD_GUID_ATTRIBUTES = ['objectGUID'] LDAP_AD_MULTIVALUE_ATTRIBUTES = ['member', 'memberOf', 'objectClass', 'servicePrincipalName', 'sshPublicKey'] LDAP_AD_SID_ATTRIBUTES = ['objectSid'] LDAP_AD_UINT_ATTRIBUTES = ['userAccountControl', 'groupType'] def ldap_change_password(old_password, new_password, username=None): """ Change the password of the user. """ if not 'connection' in g.ldap: return False connection = g.ldap['connection'] user = ldap_get_user(username) if not user: return False old_password_u16 = ('"%s"' % old_password).encode("utf-16-le") new_password_u16 = ('"%s"' % new_password).encode("utf-16-le") if old_password: # User password change attributes = [(ldap.MOD_DELETE, 'unicodePwd', old_password_u16), (ldap.MOD_ADD, 'unicodePwd', new_password_u16)] else: # Admin password change attributes = [(ldap.MOD_REPLACE, 'unicodePwd', new_password_u16), (ldap.MOD_REPLACE, 'unicodePwd', new_password_u16)] connection.modify_s(user['distinguishedName'], attributes) def ldap_create_entry(dn, attributes): """ Create a new entry and set the attributes passed as parameter. """ if not 'connection' in g.ldap: return False connection = g.ldap['connection'] dn = dn.encode('utf-8') attr = [] for key, value in attributes.items(): if isinstance(value, list): for entry in value: attr.append((key.encode('utf-8'), entry.encode('utf-8'))) else: attr.append((key.encode('utf-8'), value.encode('utf-8'))) connection.add_s(dn, attr) return True def ldap_delete_entry(dn): """ Delete an entry as identified by its distinguishedName. """ if not 'connection' in g.ldap: return False connection = g.ldap['connection'] dn = dn.encode('utf-8') connection.delete_s(dn) return True def ldap_get_user(username=None, key="sAMAccountName"): """ Return the attributes for the user or None if it doesn't exist. """ if not username: username = g.ldap['username'] return ldap_get_entry_simple({'objectClass': 'user', key: username}) def ldap_get_group(groupname, key="sAMAccountName"): """ Return the attributes for the group or None if it doesn't exist. """ return ldap_get_entry_simple({'objectClass': 'group', key: groupname}) def ldap_get_entry_simple(filter_dict): """ Return the attributes for the entry matching the filter. The filter is a key/value dictionary. The entry that matches all the values will be returned. """ if not filter_dict or not isinstance(filter_dict, dict): return False for entry in g.ldap_cache.values(): for key, value in filter_dict.items(): if not key in entry: break if isinstance(entry[key], list): if value not in entry[key]: break continue if entry[key] != value: break else: # We've got a match! return entry ldap_filter = "" if len(filter_dict) == 1: ldap_filter = "%s=%s" % filter_dict.items()[0] else: fields = "" for key, value in filter_dict.items(): fields += "(%s=%s)" % (key, value) ldap_filter = "(&%s)" % fields return ldap_get_entry(ldap_filter) def ldap_get_entry(ldap_filter): """ Return the attributes for a single entry or None if it doesn't exist or if the filter matches multiple entries and False on errors. """ entries = ldap_get_entries(ldap_filter) # Only allow a single entry if len(entries) != 1: return None return entries[0] def ldap_get_entries(ldap_filter, base=None, scope=None): """ Return the attributes for an entry or None if it doesn't exist and False on errors. """ if not 'connection' in g.ldap: return False if not base: base = g.ldap['dn'] if scope: if scope in LDAP_SCOPES: scope = LDAP_SCOPES[scope] else: return False else: scope = ldap.SCOPE_SUBTREE connection = g.ldap['connection'] # Grab the LDAP entry result = connection.search_s(base, scope, ldap_filter, []) # Check that we at least have something if not result or not result[0] or not result[0][0]: return [] entries = [] for entry in result: # Simplify the list by only keeping the attributes we known can contain # multiple values as list and decode everything to unicode. if not entry[0]: continue attributes = {} for key, value in entry[1].items(): attributes[key] = _ldap_decode_attribute(key, value) # Expand some attributes if 'primaryGroupID' in attributes: # Retrieve primary group for user group = ldap_get_group('%s-%s' % (g.ldap['domain_sid'], attributes['primaryGroupID']), 'objectSid') attributes['__primaryGroup'] = group['distinguishedName'] # Cache or refresh the entry g.ldap_cache[attributes['objectGUID']] = attributes entries.append(attributes) return entries def ldap_get_members(name=None): """ Return the list of all groups the entry is a memberOf. """ entry = ldap_get_group(name) if not entry: return None members = [] # Start with all the simple members if 'member' in entry: members += entry['member'] # Add all the members that have the group as primaryGroup members += [member['distinguishedName'] for member in ldap_get_entries( "primaryGroupID=%s" % entry['objectSid'].split("-")[-1])] return members def ldap_get_membership(name=None): """ Return the list of all groups the entry is a memberOf. """ entry = ldap_get_entry_simple({'sAMAccountName': name}) if not entry: return None groups = [] # Always start by the primary group if present if '__primaryGroup' in entry: groups.append(entry['__primaryGroup']) # Retrieve secondary groups for user if 'memberOf' in entry: groups += entry['memberOf'] return groups def ldap_in_group(groupname, username=None): """ Checks whether a user is a member of a given group name. """ if not username: username = g.ldap['username'] group = ldap_get_group(groupname) groups = ldap_get_membership(username) # Start by looking at direct membership if group['distinguishedName'] in groups: return True # Recurse through all the groups to_check = set(groups) checked = set() while to_check != checked: for entry in to_check - checked: attr = ldap_get_group(entry, "distinguishedName") if 'memberOf' in attr: if group['distinguishedName'] in attr['memberOf']: return True to_check.update(attr['memberOf']) checked.add(entry) return group['distinguishedName'] in checked def ldap_update_attribute(dn, attribute, value, objectclass=None): """ Set/Update a given attribute. """ if not 'connection' in g.ldap: return False connection = g.ldap['connection'] dn = dn.encode('utf-8') attribute = attribute.encode('utf-8') current_entry = ldap_get_entry_simple({'distinguishedName': dn}) if not current_entry: return None changes = [] if dn.lower().startswith("%s=" % attribute.lower()): # It's a rename, not an attribute update connection.rename_s(dn, "%s=%s" % (attribute, value.encode('utf-8'))) return True if objectclass and objectclass not in current_entry['objectClass']: connection.modify_s(dn, [(ldap.MOD_ADD, "objectClass", objectclass.encode('utf-8'))]) if isinstance(value, list): # Flush all entries and re-add everything if attribute in current_entry: changes.append((ldap.MOD_DELETE, attribute, None)) for entry in value: if entry: changes.append((ldap.MOD_ADD, attribute, entry.encode('utf-8'))) elif not value and attribute in current_entry: # Drop current attribute changes.append((ldap.MOD_DELETE, attribute, None)) elif attribute in current_entry: # Update current attribute changes.append((ldap.MOD_REPLACE, attribute, value.encode('utf-8'))) elif value: # Add the attribute changes.append((ldap.MOD_ADD, attribute, value.encode('utf-8'))) if not changes: return True connection.modify_s(dn, changes) return True def ldap_user_exists(username=None): """ Return True if the user exists. False otherwise. """ if ldap_get_user(username): return True return False def ldap_group_exists(groupname=None): """ Return True if the group exists. False otherwise. """ if ldap_get_group(groupname): return True return False # Private def _ldap_authenticate(): """Sends a 401 response that enables basic auth""" return Response('Could not verify your access level for that URL.\n' 'You have to login with proper credentials', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'}) def _ldap_connect(username, password): # Already connected if 'connection' in g.ldap: return True ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) ldap.set_option(ldap.OPT_REFERRALS, 0) ldap.set_option(ldap.OPT_PROTOCOL_VERSION, 3) if isinstance(g.ldap['server'], list): servers = g.ldap['server'] else: servers = [g.ldap['server']] for server in servers: connection = ldap.initialize("ldaps://%s:636" % server) try: connection.simple_bind_s("%s@%s" % (username, g.ldap['domain']), password) g.ldap['connection'] = connection g.ldap['server'] = server g.ldap['username'] = username # Get domain SID # Can't go through ldap_get_entry as it requires domain_sid be set. result = connection.search_s(g.ldap['dn'], ldap.SCOPE_BASE) g.ldap['domain_sid'] = _ldap_decode_attribute("objectSid", result[0][1] ['objectSid']) return True except ldap.INVALID_CREDENTIALS: return False except: continue raise Exception("No server reachable at this point.") def _ldap_sid2str(sid): srl = ord(sid[0]) number_sub_id = ord(sid[1]) iav = struct.unpack('!Q', '\x00\x00' + sid[2:8])[0] sub_ids = [struct.unpack(' 1: raise Exception("Unknown multiple value field: %s" % key) else: value = value[0] # Decode SID values if key in LDAP_AD_SID_ATTRIBUTES: return _ldap_sid2str(value).decode('utf-8') # Decode GUIID values if key in LDAP_AD_GUID_ATTRIBUTES: return str(uuid.UUID(bytes_le=value)).decode('utf-8') # Decode Unsigned Integer values if key in LDAP_AD_UINT_ATTRIBUTES: return struct.unpack("I", struct.pack("i", int(value)))[0] # Decode boolean values if key in LDAP_AD_BOOL_ATTRIBUTES: return value == "TRUE" # Decode the rest to unicode try: return value.decode('utf-8') except: raise Exception("Unknown type: %s" % key) # Decorators def ldap_auth(group=None): def _my_decorator(view_func): def _decorator(*args, **kwargs): auth = request.authorization if 'logout' in session: session.pop('logout') return _ldap_authenticate() if not auth or not _ldap_connect(auth.username, auth.password): return _ldap_authenticate() if group and not ldap_in_group(group): return _ldap_authenticate() return view_func(*args, **kwargs) return wraps(view_func)(_decorator) return _my_decorator edubuntu-server-14.02.2/manager/manager.cfg.example0000644000000000000000000000024412267067434017057 0ustar SECRET_KEY = "INSERT-SECRET-KEY-HERE" LDAP_DOMAIN = "edubuntu.example" #LDAP_SERVER = "some-specific-server.edubuntu.example" #DEBUG = True #URL_PREFIX = "/domain" edubuntu-server-14.02.2/manager/edubuntu-server-manager0000755000000000000000000001020312277013527020016 0ustar #!/usr/bin/python # -*- coding: utf-8 -*- # Copyright (C) 2012-2014 Stéphane Graber # Author: Stéphane Graber # 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 2 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 can find the license on Debian systems in the file # /usr/share/common-licenses/GPL-2 import argparse import os config_file = "/etc/edubuntu-server/manager.cfg" app_prefix = "/usr/share/edubuntu-server-manager/" # Check if running from bzr for path in ('manager.cfg', 'libs', 'plugins', 'static', 'templates'): if not os.path.exists(path): break else: config_file = "%s/manager.cfg" % os.getcwd() app_prefix = "." parser = argparse.ArgumentParser(description="Edubuntu Server Manager") parser.add_argument("--config", metavar="CONFIG", default=config_file) args = parser.parse_args() if not os.path.exists(args.config): raise Exception("Missing configuration file: %s" % args.config) if not os.path.exists(app_prefix): raise Exception("Missing app dir: %s" % app_prefix) # Import the rest of the stuff we need from flask import Flask, g import glob import importlib # Look at the right place import sys sys.path.append(app_prefix) # Import our modules from libs.common import ReverseProxied from libs.common import iri_for as url_for # Prepare the web server app = Flask(__name__, static_folder="%s/static" % app_prefix, template_folder="%s/templates" % app_prefix) app.config.from_pyfile(args.config) app.jinja_env.globals['url_for'] = url_for if 'URL_PREFIX' in app.config: app.wsgi_app = ReverseProxied(app.wsgi_app, app.config['URL_PREFIX']) # Check for mandatory configuration for key in ("LDAP_DOMAIN", "SECRET_KEY"): if not key in app.config: raise KeyError("Missing mandatory %s option in configuration." % key) # LDAP configuration if "LDAP_DN" not in app.config: app.config['LDAP_DN'] = "DC=%s" % ",DC=".join( app.config['LDAP_DOMAIN'].split(".")) if "LDAP_SERVER" not in app.config: import dns.resolver import dns.rdatatype import operator record = "_ldap._tcp.%s." % app.config['LDAP_DOMAIN'] answers = [] # Query the DNS try: for answer in dns.resolver.query(record, dns.rdatatype.SRV): address = (answer.target.to_text()[:-1], answer.port) answers.append((address, answer.priority, answer.weight)) except: # Ignore exceptions, an empty list will trigger an exception anyway pass # Order by priority and weight servers = [entry[0][0] for entry in sorted(answers, key=operator.itemgetter(1, 2))] if not servers: raise Exception("No LDAP server in domain '%s'." % app.config['LDAP_DOMAIN']) if len(servers) == 1: app.config['LDAP_SERVER'] = servers[0] else: app.config['LDAP_SERVER'] = servers # Load the plugins for plugin_file in glob.glob("%s/plugins/*.py" % app_prefix): plugin_name = plugin_file.split('/')[-1].replace('.py', '') if plugin_name == "__init__": continue plugin = importlib.import_module("plugins.%s" % plugin_name) plugin.init(app) @app.before_request def pre_request(): """ Setup any of the global variables before the request is processed. """ g.menu = [] g.menu.append((url_for("core_index"), "My account")) g.menu.append((url_for("tree_base"), "Tree")) g.menu.append((url_for("core_logout"), "Log out")) # LDAP connection settings g.ldap = {} g.ldap['domain'] = app.config['LDAP_DOMAIN'] g.ldap['dn'] = app.config['LDAP_DN'] g.ldap['server'] = app.config['LDAP_SERVER'] # The various caches g.ldap_cache = {} if __name__ == '__main__': app.run(host='::', port=8080) edubuntu-server-14.02.2/manager/static/0000755000000000000000000000000012277030060014603 5ustar edubuntu-server-14.02.2/manager/static/img/0000755000000000000000000000000012277030060015357 5ustar edubuntu-server-14.02.2/manager/static/img/bg_dots.png0000644000000000000000000000022312067305464017515 0ustar PNG  IHDR& )bKGD pHYs  tIME   a IDATctrr:_ b```b@(|cIENDB`edubuntu-server-14.02.2/manager/static/img/delete.png0000644000000000000000000000041011745222666017340 0ustar PNG  IHDR(-S9PLTE; tRNS&plftIDATxeO[ vځA\)hb'~S<+ .Gz*R5bh"hĦ4.#t6k_\'Gm7$+!! ,w Ie9*, (*(B5[1 ZIah!GexzJ0e6@V|U4Dm%$͛p \Gx }@+| =+ 1- Ea5l)+!! ,y )䨞'AKڍ,E\(l&;5 5D03a0--ÃpH4V % i p[R"| #  6iZwcw*!! ,y )䨞,K*0 a;׋аY8b`4n ¨Bbbx,( Ƚ  % >  2*i* /:+$v*!! ,u )䨞l[$ Jq[q 3`Q[5:IX!0rAD8 CvHPfiiQAP@pC %D PQ46  iciNj0w )#!! ,y ). q ,G Jr(J8 C*B,&< h W~-`, ,>; 8RN<, <1T] c' qk$ @)#!;edubuntu-server-14.02.2/manager/static/img/header_tail.png0000644000000000000000000000017611745222666020350 0ustar PNG  IHDR 0 PLTE,,u[tRNS+N"IDATxM CB N=,ImIENDB`edubuntu-server-14.02.2/manager/static/img/edit.png0000644000000000000000000000045411745222666017033 0ustar PNG  IHDRڭWPLTE'tRNS$8H"8\?D2 r PT}:<.Ìm^x6':Zh)w8SP+A`tS1II%mT")x>$)_RQR .: _1ny 2{ZV/Az•hthF!@fR# ~:v#|Z_\ !fIc$͕§g6zQXZ+,dVNEuIO:/%#x ,#djtL3{$gF?yI `.TVhCjf&֮-[(޴yD>X3gEID h?j~hJwU;FORr%;]2 <l2Fj:p/p'p.x*c \0]qcl 33=qƌ!؁Efv( trZ oYU#ppJ%fVӔׁEJJZi$HŒ%rJZ*)BMb+x43/(k~Aґ8r7I?upc Q,ij ~7kˊ3_ SrA YY{AN *ڞ zCU:ZmD1-bW-6X%sBtSy- vs7SNE`( Cҗd3k{If=M^g餲}eelFme%fۗ.]=k5gr<7~e!?+W L1R]!gKZafy/Xdf{=wv )G5]`9<<t$zcMlZ?Fơ˅6CY.QvN1|*GC^a83>y`u_TD#pIVv/Ԙٺ&?w?{N͞J:(!c7rMȢ; 4!3/;sf6G bȍHJ^F(j5I={?m:ShJI75G$e4U 2BuW|h7jc1o&0YIV ̞IÁ|I/\lwceO\CR6vȔ0ph:ܳu>kf YS8)L&퀟^ >4.~G%-6QP`Hʖ;t)TƑQ+wJ]ot xV] br; Yk+) 斟(ˁ}"?|,Eg#o [:`5.#Ύem{Y@kg>Y.8*gE6LoHI7ݝ=~'_Co7@4?rjs!\ODǝt[-)rgAJE$H"k _X IENDB`edubuntu-server-14.02.2/manager/static/img/maybe.png0000644000000000000000000000064711745222666017207 0ustar PNG  IHDR(PLTE J J!K"L!K!K"M&P'T.Z8a9b/^2c3a4e5f6f7d7g8h:i;j>i@nBpGqHtJvMuMxOwR|T}U~YgmtuƁNj˦׳ݵ}٥ tRNSB~2yIDATx]@CLQP`Dsww9IHc0.%g)tXyk7**0M8g}D6$ $Em$crbvlulɠT90 zk m޽ Z7brIENDB`edubuntu-server-14.02.2/manager/static/img/inline_add.png0000644000000000000000000000030411745222666020166 0ustar PNG  IHDR ex!PLTECHIHHHHHptRNSN򯫬JIDATx] #aÄp`n vR˕?Id0@IT_u~o~ScIENDB`edubuntu-server-14.02.2/manager/static/img/header_stripes.png0000644000000000000000000000051011745222666021100 0ustar PNG  IHDR9/nI1{PLTEHIIJJKKKLLMMNNOOPPPQQ R R!S"T$V%W'X)Z*[,\.^0_1`3b5c7e8f:gtRNS;@Y\jnirIDATxڍ ECA V?Մ Re#] !Z=:Jh1>S+{`LDg]Q6l^\O?(ٻM$UV`iPSM4´0TYkol~s%VBm0 a"v;6Գa,Y9e׸NAQ`t7Di&+-0+O`Y, fC>hv h/IH-I:B|q:D܇ 1L1]좣t$-y:J%aVBP{3F@ePstJёBAMAQދt^sv E4`DGCpa`0ᝥ% %< EOSBH)(zJ+M /@Q %q[=EtCQ7EgPD~@Q"bXR (g"^@BC)$($* HPBT-PH|r Pʢ@bjQT)'CAGP 0HA70IA4[5u(:Ee?xS@Jgk! ͚|ZY Ӥ UTu& #Js9QDhƭ0-A/Mq4ԗ݃"~+'K}h\~וB9jkHM~b'95eLюC1ԓ? T_A뉟!W>ҾSː,/SGXjO|5j^> ŨeH,YB)`|u,yi>ygz 1V1Ƃy 5 &9'푽Yf3t} #4pFfI!`* W%i6`l۸ΧF{L 5y3Z߇U}lS]u7Q]r/Aeuzú1UMhtXY~Uuj>=oek>!NEOXYT}IG{P7N%PIAݎ<.9ߡ T7N$J7F3U$`U4kGa=d_mӏT)fB?;X;n&b,tD)bLMͨܘJEY܀p/u{-ZlX9w Edubuntu Server - {% block title %}{% endblock %} {% block css %} {% endblock %} {% if self.header() %} {% endif %}

{{ self.title() }}

{% if g.menu %} {% endif %}
{% if get_flashed_messages(with_categories=true) %}
    {% for category, msg in get_flashed_messages(with_categories=true) %}
  • {{ msg }}
  • {% endfor %}
{% endif %} {% block content %}{% endblock %}
edubuntu-server-14.02.2/manager/templates/forms/0000755000000000000000000000000012277030060016440 5ustar edubuntu-server-14.02.2/manager/templates/forms/basicform.html0000644000000000000000000000132112101373535021273 0ustar {% extends "base-modal.html" %} {% block title %}{{ title }}{% endblock %} {% block content %} {% from "forms/macros.html" import render_field %} {% if request.query_string %}
{% else %} {% endif %} {{ form.csrf_token }}
    {% for field in form.visible_fields %} {{ render_field(field) }} {% endfor %}
{% if parent %} Cancel {% endif %}
{% endblock %} edubuntu-server-14.02.2/manager/templates/forms/macros.html0000644000000000000000000000066512101102741020611 0ustar {% macro render_field(field) %} {% if field.errors %}
  • {% else %}
  • {% endif %} {{ field(**kwargs)|safe }} {% if field.errors %} {{ field.errors|join("
    ") }}
    {% endif %}
  • {% endmacro %} edubuntu-server-14.02.2/manager/templates/base-modal.html0000644000000000000000000000347712072404662020225 0ustar Edubuntu Server - {% block title %}{% endblock %} {% block css %} {% endblock %} {% if self.header() %} {% endif %}

    {{ self.title() }}

    {% if get_flashed_messages(with_categories=true) %}
      {% for category, msg in get_flashed_messages(with_categories=true) %}
    • {{ msg }}
    • {% endfor %}
    {% endif %} {% block content %}{% endblock %}
    edubuntu-server-14.02.2/manager/templates/pages/0000755000000000000000000000000012277030060016411 5ustar edubuntu-server-14.02.2/manager/templates/pages/group_delmember.html0000644000000000000000000000124712101131305022441 0ustar {% extends "base-modal.html" %} {% block title %}{{ title }}{% endblock %} {% block content %} {% from "forms/macros.html" import render_field %}
    {{ form.csrf_token }}

    Are you sure you want to remove '{{ member }}' from '{{ group }}'?

      {% for field in form.visible_fields %} {{ render_field(field) }} {% endfor %}
    {% if parent %} Cancel {% endif %}
    {% endblock %} edubuntu-server-14.02.2/manager/templates/pages/tree_base.html0000644000000000000000000000240512101371526021232 0ustar {% extends "base.html" %} {% block title %}{{ base }}{% endblock %} {% block content %}

    {% if parent %} Parent {% endif %} {% if admin %} Add group Add user Create container {% endif %}

    Entries

    {% for key, title in entry_fields %} {% endfor %} {% for entry in entries %} {% for key, title in entry_fields %} {% if key == "name" and key in entry %} {% elif key == "__type" and key in entry %} {% elif key in entry %} {% else %} {% endif %} {% endfor %} {% endfor %}
    {{ title }}
    {{ entry[key] }}{{ entry[key] }}{{ entry[key]|truncate(70) }} 
    {% endblock %} edubuntu-server-14.02.2/manager/templates/pages/group_delete.html0000644000000000000000000000132712077641775022003 0ustar {% extends "base-modal.html" %} {% block title %}{{ title }}{% endblock %} {% block content %} {% from "forms/macros.html" import render_field %}
    {{ form.csrf_token }}

    Continuing will irreversably destroy group '{{ groupname }}'.
    Are you sure you want to continue?

      {% for field in form.visible_fields %} {{ render_field(field) }} {% endfor %}
    {% if parent %} Cancel {% endif %}
    {% endblock %} edubuntu-server-14.02.2/manager/templates/pages/user_overview.html0000644000000000000000000000564112101101646022204 0ustar {% extends "base.html" %} {% block title %}{{ title }}{% endblock %} {% block content %} {% if 'sshPublicKey' in user or admin %}
    {% if admin %}
    {% if 'sshPublicKey' in user %}

    Edit SSH keys

    {% else %}

    Add SSH keys

    {% endif %}
    {% endif %}

    Machine access

    {% for key in user['sshPublicKey']: %} {% endfor %}
    SSH key: {{ key.split(' ')[-1] }} ({{ key.split(' ')[0] }})
    {% endif %} {% if groups %}

    Group membership

    {% for key, title in group_fields %} {% endfor %} {% for entry in groups %} {% for key, title in group_fields %} {% if key == "sAMAccountName" and key in entry %} {% elif key in entry %} {% else %} {% endif %} {% endfor %} {% endfor %}
    {{ title }}
    {{ entry[key] }}{{ entry[key] }} 
    {% endif %} {% endblock %} edubuntu-server-14.02.2/manager/templates/pages/user_delete.html0000644000000000000000000000132512077114527021611 0ustar {% extends "base-modal.html" %} {% block title %}{{ title }}{% endblock %} {% block content %} {% from "forms/macros.html" import render_field %}
    {{ form.csrf_token }}

    Continuing will irreversably destroy user '{{ username }}'.
    Are you sure you want to continue?

      {% for field in form.visible_fields %} {{ render_field(field) }} {% endfor %}
    {% if parent %} Cancel {% endif %}
    {% endblock %} edubuntu-server-14.02.2/manager/templates/pages/group_overview.html0000644000000000000000000000704712101132473022366 0ustar {% extends "base.html" %} {% block title %}{{ title }}{% endblock %} {% block content %}
    {% if admin %}

    Edit group

    Delete group

    Add members

    {% endif %}

    Properties

    {% for key, title in identity_fields %} {% if key in group %} {% else %} {% endif %} {% endif %} {% endfor %} {% if 'groupType' in group %} {% if group['groupType'].__and__(2147483648) %} {% else %} {% endif %} {% endif %}
    {{ title }} {% if key == "__groupScope" %}
      {% for scope in group[key] %}
    • {{ scope }}
    • {% endfor %}
    {{ group[key] }}
    TypeSecurity groupDistribution list
    Group flags
      {% for key, value in grouptype_values.items() %} {% if group['groupType'].__and__(key) %}
    • {{ value[0] }}
    • {% endif %} {% endfor %}
    {% if groups %}

    Group membership

    {% for key, title in group_fields %} {% endfor %} {% for entry in groups %} {% for key, title in group_fields %} {% if key == "sAMAccountName" and key in entry %} {% elif key in entry %} {% else %} {% endif %} {% endfor %} {% endfor %}
    {{ title }}
    {{ entry[key] }}{{ entry[key] }} 
    {% endif %}

    Members

    {% for key, title in group_fields %} {% endfor %} {% for entry in members %} {% for key, title in group_fields %} {% if key == "sAMAccountName" and key in entry %} {% if "user" in entry['objectClass'] %} {% else %} {% endif %} {% elif key == "description" and not entry[key] and 'displayName' in entry %} {% elif key in entry %} {% else %} {% endif %} {% endfor %} {% if '__primaryGroup' in entry and entry['__primaryGroup'] == group['distinguishedName'] %} {% else %} {% if admin %} {% else %} {% endif %} {% endif %} {% endfor %}
    {{ title }}Type
    {{ entry[key] }}{{ entry[key] }}{{ entry['displayName'] }}{{ entry[key] }} Primary groupMemberMember
    {% endblock %} edubuntu-server-14.02.2/manager/plugins/0000755000000000000000000000000012277030060014775 5ustar edubuntu-server-14.02.2/manager/plugins/group.py0000644000000000000000000003276412277013527016530 0ustar # -*- coding: utf-8 -*- # Copyright (C) 2012-2014 Stéphane Graber # Author: Stéphane Graber # 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 2 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 can find the license on Debian systems in the file # /usr/share/common-licenses/GPL-2 from libs.common import iri_for as url_for from flask import abort, flash, g, render_template, redirect, request from flask.ext.wtf import Form, RadioField, Required, TextAreaField, TextField from libs.ldap_func import ldap_auth, ldap_create_entry, ldap_delete_entry, \ ldap_get_entry_simple, ldap_get_members, ldap_get_membership, \ ldap_get_group, ldap_in_group, ldap_update_attribute, ldap_group_exists, \ LDAP_AD_GROUPTYPE_VALUES import ldap import struct class GroupAddMembers(Form): new_members = TextAreaField('New members') class GroupEdit(Form): name = TextField('Name', [Required()]) description = TextField('Description') group_type = RadioField('Type', choices=[(2147483648, 'Security group'), (0, 'Distribution list')], coerce=int) group_flags = RadioField('Scope', coerce=int) def init(app): @app.route('/groups/+add', methods=['GET', 'POST']) @ldap_auth("Domain Admins") def group_add(): title = "Add group" base = request.args.get('base') if not base: base = "OU=People,%s" % g.ldap['dn'] form = GroupEdit(request.form) field_mapping = [('sAMAccountName', form.name), ('description', form.description), (None, form.group_type), ('groupType', form.group_flags)] form.visible_fields = [field[1] for field in field_mapping] form.group_flags.choices = [(key, value[0]) for key, value in LDAP_AD_GROUPTYPE_VALUES.items() if value[1]] if form.validate_on_submit(): try: # Default attributes attributes = {'objectClass': "group"} for attribute, field in field_mapping: if attribute == "groupType": group_type = int(form.group_type.data) + \ int(form.group_flags.data) attributes[attribute] = str( struct.unpack("i", struct.pack("I", int(group_type)))[0]) elif attribute and field.data: attributes[attribute] = field.data ldap_create_entry("cn=%s,%s" % (form.name.data, base), attributes) flash("Group successfully created.", "success") return redirect(url_for('group_overview', groupname=form.name.data)) except ldap.LDAPError as e: error = e.message['info'].split(":", 2)[-1].strip() error = str(error[0].upper() + error[1:]) flash(error, "error") elif form.errors: flash("Some fields failed validation.", "error") if not form.is_submitted(): form.group_type.data = 2147483648 form.group_flags.data = 2 return render_template("forms/basicform.html", form=form, title=title, action="Add group", parent=url_for('group_add')) @app.route('/group/') @ldap_auth("Domain Users") def group_overview(groupname): title = "Group details - %s" % groupname if not ldap_group_exists(groupname=groupname): abort(404) identity_fields = [('sAMAccountName', "Name"), ('description', "Description")] group_fields = [('sAMAccountName', "Name"), ('description', "Description")] group = ldap_get_group(groupname=groupname) admin = ldap_in_group("Domain Admins") and not group['groupType'] & 1 group_details = [ldap_get_group(entry, 'distinguishedName') for entry in ldap_get_membership(groupname)] groups = sorted(group_details, key=lambda entry: entry['sAMAccountName']) member_list = [] for entry in ldap_get_members(groupname): member = ldap_get_entry_simple({'distinguishedName': entry}) if 'sAMAccountName' not in member: continue member_list.append(member) members = sorted(member_list, key=lambda entry: entry['sAMAccountName']) return render_template("pages/group_overview.html", g=g, title=title, group=group, identity_fields=identity_fields, group_fields=group_fields, admin=admin, groups=groups, members=members, grouptype_values=LDAP_AD_GROUPTYPE_VALUES) @app.route('/group//+delete', methods=['GET', 'POST']) @ldap_auth("Domain Admins") def group_delete(groupname): title = "Delete group" if not ldap_group_exists(groupname): abort(404) form = Form(request.form) if form.validate_on_submit(): try: group = ldap_get_group(groupname=groupname) ldap_delete_entry(group['distinguishedName']) flash("Group successfuly deleted.", "success") return redirect(url_for('core_index')) except ldap.LDAPError as e: error = e.message['info'].split(":", 2)[-1].strip() error = str(error[0].upper() + error[1:]) flash(error, "error") elif form.errors: flash("Some fields failed validation.", "error") return render_template("pages/group_delete.html", title=title, action="Delete group", form=form, groupname=groupname, parent=url_for('group_overview', groupname=groupname)) @app.route('/group//+edit', methods=['GET', 'POST']) @ldap_auth("Domain Admins") def group_edit(groupname): title = "Edit group" if not ldap_group_exists(groupname): abort(404) group = ldap_get_group(groupname) # We can't edit system groups if group['groupType'] & 1: abort(401) form = GroupEdit(request.form) field_mapping = [('sAMAccountName', form.name), ('description', form.description), (None, form.group_type), ('groupType', form.group_flags)] form.visible_fields = [field[1] for field in field_mapping] form.group_flags.choices = [(key, value[0]) for key, value in LDAP_AD_GROUPTYPE_VALUES.items() if value[1]] if form.validate_on_submit(): try: for attribute, field in field_mapping: value = field.data if value != group.get(attribute): if attribute == 'sAMAccountName': # Rename the account ldap_update_attribute(group['distinguishedName'], "sAMAccountName", value) # Finish by renaming the whole record ldap_update_attribute(group['distinguishedName'], "cn", value) group = ldap_get_group(value) elif attribute == "groupType": group_type = int(form.group_type.data) + \ int(form.group_flags.data) ldap_update_attribute( group['distinguishedName'], attribute, str( struct.unpack( "i", struct.pack( "I", int(group_type)))[0])) elif attribute: ldap_update_attribute(group['distinguishedName'], attribute, value) flash("Group successfully updated.", "success") return redirect(url_for('group_overview', groupname=form.name.data)) except ldap.LDAPError as e: error = e.message['info'].split(":", 2)[-1].strip() error = str(error[0].upper() + error[1:]) flash(error, "error") elif form.errors: flash("Some fields failed validation.", "error") if not form.is_submitted(): form.name.data = group.get('sAMAccountName') form.description.data = group.get('description') form.group_type.data = group['groupType'] & 2147483648 form.group_flags.data = 0 for key, flag in LDAP_AD_GROUPTYPE_VALUES.items(): if flag[1] and group['groupType'] & key: form.group_flags.data += key return render_template("forms/basicform.html", form=form, title=title, action="Save changes", parent=url_for('group_overview', groupname=groupname)) @app.route('/group//+add-members', methods=['GET', 'POST']) @ldap_auth("Domain Admins") def group_addmembers(groupname): title = "Add members" if not ldap_group_exists(groupname): abort(404) form = GroupAddMembers(request.form) form.visible_fields = [form.new_members] if form.validate_on_submit(): group = ldap_get_group(groupname) if 'member' in group: entries = set(group['member']) else: entries = set() for line in form.new_members.data.split("\n"): entry = ldap_get_entry_simple({'sAMAccountName': line.strip()}) if not entry: error = "Invalid username: %s" % line flash(error, "error") break entries.add(entry['distinguishedName']) else: try: ldap_update_attribute(group['distinguishedName'], "member", list(entries)) flash("Members added.", "success") return redirect(url_for('group_overview', groupname=groupname)) except ldap.LDAPError as e: error = e.message['info'].split(":", 2)[-1].strip() error = str(error[0].upper() + error[1:]) flash(error, "error") elif form.errors: flash("Some fields failed validation.", "error") return render_template("forms/basicform.html", form=form, title=title, action="Add members", parent=url_for('group_overview', groupname=groupname)) @app.route('/group//+del-member/', methods=['GET', 'POST']) @ldap_auth("Domain Admins") def group_delmember(groupname, member): title = "Remove group member" group = ldap_get_group(groupname) if not group or 'member' not in group: abort(404) member = ldap_get_entry_simple({'sAMAccountName': member}) if not member: abort(404) if not member['distinguishedName'] in group['member']: abort(404) form = Form(request.form) if form.validate_on_submit(): try: members = group['member'] members.remove(member['distinguishedName']) ldap_update_attribute(group['distinguishedName'], "member", members) flash("Member removed.", "success") return redirect(url_for('group_overview', groupname=groupname)) except ldap.LDAPError as e: error = e.message['info'].split(":", 2)[-1].strip() error = str(error[0].upper() + error[1:]) flash(error, "error") elif form.errors: flash("Some fields failed validation.", "error") return render_template("pages/group_delmember.html", title=title, action="Remove group member", form=form, member=member['sAMAccountName'], group=group['sAMAccountName'], parent=url_for('group_overview', groupname=groupname)) edubuntu-server-14.02.2/manager/plugins/__init__.py0000644000000000000000000000000012277013527017105 0ustar edubuntu-server-14.02.2/manager/plugins/core.py0000644000000000000000000000223012277013527016305 0ustar # -*- coding: utf-8 -*- # Copyright (C) 2012-2014 Stéphane Graber # Author: Stéphane Graber # 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 2 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 can find the license on Debian systems in the file # /usr/share/common-licenses/GPL-2 from libs.common import iri_for as url_for from flask import g, redirect, session from libs.ldap_func import ldap_auth def init(app): @app.route('/') @app.route('/user') @ldap_auth("Domain Users") def core_index(): return redirect(url_for('user_overview', username=g.ldap['username'])) @app.route('/+logout') @ldap_auth("Domain Users") def core_logout(): session['logout'] = 1 return redirect(url_for('core_index')) edubuntu-server-14.02.2/manager/plugins/tree.py0000644000000000000000000000662612277013527016331 0ustar # -*- coding: utf-8 -*- # Copyright (C) 2012-2014 Stéphane Graber # Author: Stéphane Graber # 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 2 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 can find the license on Debian systems in the file # /usr/share/common-licenses/GPL-2 from libs.common import iri_for as url_for from flask import g, render_template from libs.ldap_func import ldap_auth, ldap_get_entries, ldap_in_group TREE_BLACKLIST = ["CN=ForeignSecurityPrincipals", "OU=sudoers"] def init(app): @app.route('/tree') @app.route('/tree/') @ldap_auth("Domain Users") def tree_base(base=None): if not base: base = g.ldap['dn'] elif not base.lower().endswith(g.ldap['dn'].lower()): base += ",%s" % g.ldap['dn'] admin = ldap_in_group("Domain Admins") entry_fields = [('name', "Name"), ('__description', "Description"), ('__type', "Type")] entries = [] for entry in sorted(ldap_get_entries("objectClass=top", base, "onelevel"), key=lambda entry: entry['name']): if not 'description' in entry: if 'displayName' in entry: entry['__description'] = entry['displayName'] else: entry['__description'] = entry['description'] entry['__target'] = url_for('tree_base', base=entry['distinguishedName']) if 'user' in entry['objectClass']: entry['__type'] = "User" entry['__target'] = url_for('user_overview', username=entry['sAMAccountName']) elif 'group' in entry['objectClass']: entry['__type'] = "Group" entry['__target'] = url_for('group_overview', groupname=entry['sAMAccountName']) elif 'organizationalUnit' in entry['objectClass']: entry['__type'] = "Organizational Unit" elif 'container' in entry['objectClass']: entry['__type'] = "Container" elif 'builtinDomain' in entry['objectClass']: entry['__type'] = "Built-in" else: entry['__type'] = "Unknown" if 'showInAdvancedViewOnly' in entry \ and entry['showInAdvancedViewOnly']: continue for blacklist in TREE_BLACKLIST: if entry['distinguishedName'].startswith(blacklist): break else: entries.append(entry) parent = None base_split = base.split(',') if not base_split[0].lower().startswith("dc"): parent = ",".join(base_split[1:]) return render_template("pages/tree_base.html", parent=parent, admin=admin, base=base, entries=entries, entry_fields=entry_fields) edubuntu-server-14.02.2/manager/plugins/user.py0000644000000000000000000003626112277013527016346 0ustar # -*- coding: utf-8 -*- # Copyright (C) 2012-2014 Stéphane Graber # Author: Stéphane Graber # 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 2 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 can find the license on Debian systems in the file # /usr/share/common-licenses/GPL-2 from libs.common import iri_for as url_for from flask import abort, flash, g, render_template, redirect, request from flask.ext.wtf import EqualTo, Form, PasswordField, Required, \ SelectMultipleField, TextAreaField, TextField from libs.ldap_func import ldap_auth, ldap_change_password, \ ldap_create_entry, ldap_delete_entry, ldap_get_user, \ ldap_get_membership, ldap_get_group, ldap_in_group, \ ldap_update_attribute, ldap_user_exists, LDAP_AD_USERACCOUNTCONTROL_VALUES import ldap class UserSSHEdit(Form): ssh_keys = TextAreaField('SSH keys') class UserProfileEdit(Form): first_name = TextField('First name') last_name = TextField('Last name') display_name = TextField('Display name') user_name = TextField('Username', [Required()]) mail = TextField('E-mail address') uac_flags = SelectMultipleField('User flags', coerce=int) class UserAdd(UserProfileEdit): password = PasswordField('Password', [Required()]) password_confirm = PasswordField('Repeat password', [Required(), EqualTo('password', message='Passwords must match')]) class PasswordChange(Form): password = PasswordField('New password', [Required()]) password_confirm = PasswordField('Repeat new password', [Required(), EqualTo('password', message='Passwords must match')]) class PasswordChangeUser(PasswordChange): oldpassword = PasswordField('Current password', [Required()]) def init(app): @app.route('/users/+add', methods=['GET', 'POST']) @ldap_auth("Domain Admins") def user_add(): title = "Add user" base = request.args.get('base') if not base: base = "OU=People,%s" % g.ldap['dn'] form = UserAdd(request.form) field_mapping = [('givenName', form.first_name), ('sn', form.last_name), ('displayName', form.display_name), ('sAMAccountName', form.user_name), ('mail', form.mail), (None, form.password), (None, form.password_confirm), ('userAccountControl', form.uac_flags)] form.visible_fields = [field[1] for field in field_mapping] form.uac_flags.choices = [(key, value[0]) for key, value in LDAP_AD_USERACCOUNTCONTROL_VALUES.items() if value[1]] if form.validate_on_submit(): try: # Default attributes upn = "%s@%s" % (form.user_name.data, g.ldap['domain']) attributes = {'objectClass': "user", 'UserPrincipalName': upn, 'accountExpires': "0", 'lockoutTime': "0"} for attribute, field in field_mapping: if attribute == 'userAccountControl': current_uac = 512 for key, flag in (LDAP_AD_USERACCOUNTCONTROL_VALUES .items()): if flag[1] and key in field.data: current_uac += key attributes[attribute] = str(current_uac) elif attribute and field.data: attributes[attribute] = field.data ldap_create_entry("cn=%s,%s" % (form.user_name.data, base), attributes) ldap_change_password(None, form.password.data, form.user_name.data) flash("User successfully created.", "success") return redirect(url_for('user_overview', username=form.user_name.data)) except ldap.LDAPError as e: error = e.message['info'].split(":", 2)[-1].strip() error = str(error[0].upper() + error[1:]) flash(error, "error") elif form.errors: flash("Some fields failed validation.", "error") return render_template("forms/basicform.html", form=form, title=title, action="Add user", parent=url_for('user_add')) @app.route('/user/') @ldap_auth("Domain Users") def user_overview(username): title = "User details - %s" % username if not ldap_user_exists(username=username): abort(404) identity_fields = [('givenName', "First name"), ('sn', "Last name"), ('displayName', "Display name"), ('sAMAccountName', "User name"), ('mail', "E-mail address"), ('___primary_group', "Primary group")] group_fields = [('sAMAccountName', "Name"), ('description', "Description")] admin = ldap_in_group("Domain Admins") user = ldap_get_user(username=username) group_details = [ldap_get_group(group, 'distinguishedName') for group in ldap_get_membership(username)] user['___primary_group'] = group_details[0]['sAMAccountName'] groups = sorted(group_details, key=lambda entry: entry['sAMAccountName']) return render_template("pages/user_overview.html", g=g, title=title, user=user, identity_fields=identity_fields, group_fields=group_fields, admin=admin, groups=groups, uac_values=LDAP_AD_USERACCOUNTCONTROL_VALUES) @app.route('/user//+changepw', methods=['GET', 'POST']) @ldap_auth("Domain Users") def user_changepw(username): title = "Change password" if not ldap_user_exists(username=username): abort(404) admin = ldap_in_group("Domain Admins") if username != g.ldap['username'] and admin: form = PasswordChange(request.form) form.visible_fields = [] else: form = PasswordChangeUser(request.form) form.visible_fields = [form.oldpassword] form.visible_fields += [form.password, form.password_confirm] if form.validate_on_submit(): try: if username != g.ldap['username'] and admin: ldap_change_password(None, form.password.data, username=username) else: ldap_change_password(form.oldpassword.data, form.password.data, username=username) flash("Password changed successfuly.", "success") return redirect(url_for('user_overview', username=username)) except ldap.LDAPError as e: error = e.message['info'].split(":", 2)[-1].strip() error = str(error[0].upper() + error[1:]) flash(error, "error") elif form.errors: flash("Some fields failed validation.", "error") return render_template("forms/basicform.html", form=form, title=title, action="Change password", parent=url_for('user_overview', username=username)) @app.route('/user//+delete', methods=['GET', 'POST']) @ldap_auth("Domain Admins") def user_delete(username): title = "Delete user" if not ldap_user_exists(username=username): abort(404) form = Form(request.form) if form.validate_on_submit(): try: user = ldap_get_user(username=username) ldap_delete_entry(user['distinguishedName']) flash("User successfuly deleted.", "success") return redirect(url_for('core_index')) except ldap.LDAPError as e: error = e.message['info'].split(":", 2)[-1].strip() error = str(error[0].upper() + error[1:]) flash(error, "error") elif form.errors: flash("Some fields failed validation.", "error") return render_template("pages/user_delete.html", title=title, action="Delete user", form=form, username=username, parent=url_for('user_overview', username=username)) @app.route('/user//+edit-profile', methods=['GET', 'POST']) @ldap_auth("Domain Admins") def user_edit_profile(username): title = "Edit user" if not ldap_user_exists(username=username): abort(404) user = ldap_get_user(username=username) form = UserProfileEdit(request.form) field_mapping = [('givenName', form.first_name), ('sn', form.last_name), ('displayName', form.display_name), ('sAMAccountName', form.user_name), ('mail', form.mail), ('userAccountControl', form.uac_flags)] form.uac_flags.choices = [(key, value[0]) for key, value in LDAP_AD_USERACCOUNTCONTROL_VALUES.items() if value[1]] form.visible_fields = [field[1] for field in field_mapping] if form.validate_on_submit(): try: for attribute, field in field_mapping: value = field.data if value != user.get(attribute): if attribute == 'sAMAccountName': # Rename the account ldap_update_attribute(user['distinguishedName'], "sAMAccountName", value) ldap_update_attribute(user['distinguishedName'], "userPrincipalName", "%s@%s" % (value, g.ldap['domain'])) # Finish by renaming the whole record ldap_update_attribute(user['distinguishedName'], "cn", value) user = ldap_get_user(value) elif attribute == 'userAccountControl': current_uac = user['userAccountControl'] for key, flag in (LDAP_AD_USERACCOUNTCONTROL_VALUES .items()): if not flag[1]: continue if key in value: if not current_uac & key: current_uac += key else: if current_uac & key: current_uac -= key ldap_update_attribute(user['distinguishedName'], attribute, str(current_uac)) else: ldap_update_attribute(user['distinguishedName'], attribute, value) flash("Profile successfully updated.", "success") return redirect(url_for('user_overview', username=form.user_name.data)) except ldap.LDAPError as e: error = e.message['info'].split(":", 2)[-1].strip() error = str(error[0].upper() + error[1:]) flash(error, "error") elif form.errors: flash("Some fields failed validation.", "error") if not form.is_submitted(): form.first_name.data = user.get('givenName') form.last_name.data = user.get('sn') form.display_name.data = user.get('displayName') form.user_name.data = user.get('sAMAccountName') form.mail.data = user.get('mail') form.uac_flags.data = [key for key, flag in LDAP_AD_USERACCOUNTCONTROL_VALUES.items() if (flag[1] and user['userAccountControl'] & key)] return render_template("forms/basicform.html", form=form, title=title, action="Save changes", parent=url_for('user_overview', username=username)) @app.route('/user//+edit-ssh', methods=['GET', 'POST']) @ldap_auth("Domain Admins") def user_edit_ssh(username): title = "Edit SSH keys" if not ldap_user_exists(username=username): abort(404) user = ldap_get_user(username=username) form = UserSSHEdit(request.form) form.visible_fields = [form.ssh_keys] if form.validate_on_submit(): new_entries = [entry.strip() for entry in form.ssh_keys.data.split("\n")] try: ldap_update_attribute(user['distinguishedName'], 'sshPublicKey', new_entries, 'ldapPublicKey') flash("SSH keys successfuly updated.", "success") return redirect(url_for('user_overview', username=username)) except ldap.LDAPError as e: error = e.message['info'].split(":", 2)[-1].strip() error = str(error[0].upper() + error[1:]) flash(error, "error") elif form.errors: flash("Some fields failed validation.", "error") if not form.is_submitted(): if 'sshPublicKey' in user: form.ssh_keys.data = "\n".join(user['sshPublicKey']) return render_template("forms/basicform.html", form=form, title=title, action="Save changes", parent=url_for('user_overview', username=username)) edubuntu-server-14.02.2/host/0000755000000000000000000000000012277030060012657 5ustar edubuntu-server-14.02.2/host/share/0000755000000000000000000000000012277030060013761 5ustar edubuntu-server-14.02.2/host/share/services/0000755000000000000000000000000012277030060015604 5ustar edubuntu-server-14.02.2/host/share/services/dhcp/0000755000000000000000000000000012277030060016522 5ustar edubuntu-server-14.02.2/host/share/services/dhcp/configure0000755000000000000000000000124312016563702020436 0ustar #!/usr/bin/python3 import ipaddr import os subnet = ipaddr.IPv4Network(os.environ['network_subnet']) environ = {} environ['dhcp_network'] = "%s" % subnet.network environ['dhcp_netmask'] = "%s" % subnet.netmask environ['dhcp_range_start'] = os.environ['network_clients_start'] environ['dhcp_range_end'] = "%s" % subnet[-2] environ['dhcp_domain'] = os.environ['domain_fqdn'] environ['dhcp_dns_servers'] = os.environ['services_rdns'] environ['dhcp_wins_servers'] = os.environ['services_directory'] environ['dhcp_broadcast'] = "%s" % subnet.broadcast environ['dhcp_gateway'] = os.environ['network_gateway'] for key, value in environ.items(): print("%s=%s" % (key, value)) edubuntu-server-14.02.2/host/share/services/dhcp/setup0000755000000000000000000000150612016563746017627 0ustar #!/bin/sh # Prepare environment mount -t proc proc /proc mount -t sysfs sysfs /sys cat /etc/hostname > /proc/sys/kernel/hostname cat > /usr/sbin/policy-rc.d << EOF #!/bin/sh exit 101 EOF chmod +x /usr/sbin/policy-rc.d export DEBIAN_FRONTEND=noninteractive #Setup apt-get update apt-get dist-upgrade -y apt-get install -y isc-dhcp-server ( cat << EOF # # Edubuntu-server generated configuration. # authoritative; subnet $dhcp_network netmask $dhcp_netmask { range $dhcp_range_start $dhcp_range_end; option domain-name "$dhcp_domain"; option domain-name-servers $dhcp_dns_servers; option netbios-name-servers $dhcp_wins_servers; option broadcast-address $dhcp_broadcast; option routers $dhcp_gateway; option subnet-mask $dhcp_netmask; } EOF ) > /etc/dhcp/dhcpd.conf # Cleanup rm /usr/sbin/policy-rc.d exit 0 edubuntu-server-14.02.2/host/share/services/ltsp/0000755000000000000000000000000012277030060016566 5ustar edubuntu-server-14.02.2/host/share/services/ltsp/setup0000755000000000000000000000063012033443100017644 0ustar #!/bin/sh # Prepare environment mount -t proc proc /proc mount -t sysfs sysfs /sys cat /etc/hostname > /proc/sys/kernel/hostname cat > /usr/sbin/policy-rc.d << EOF #!/bin/sh exit 101 EOF chmod +x /usr/sbin/policy-rc.d export DEBIAN_FRONTEND=noninteractive #Setup apt-get update apt-get dist-upgrade -y apt-get install -y ltsp-server ltsp-build-client --arch i386 # Cleanup rm /usr/sbin/policy-rc.d exit 0 edubuntu-server-14.02.2/host/share/services/directory/0000755000000000000000000000000012277030060017610 5ustar edubuntu-server-14.02.2/host/share/services/directory/configure0000755000000000000000000000035712016537522021532 0ustar #!/usr/bin/python3 import os environ = {} fqdn = os.environ['domain_fqdn'] environ['directory_fqdn'] = fqdn environ['directory_workgroup'] = fqdn.split('.')[0].upper() for key, value in environ.items(): print("%s=%s" % (key, value)) edubuntu-server-14.02.2/host/share/services/directory/schemas/0000755000000000000000000000000012277030060021233 5ustar edubuntu-server-14.02.2/host/share/services/directory/schemas/sudo-1.ldif0000644000000000000000000001320412077124472023214 0ustar dn: CN=sudoUser,CN=Schema,CN=Configuration,DC=X changetype: add objectClass: top objectClass: attributeSchema cn: sudoUser distinguishedName: CN=sudoUser,CN=Schema,CN=Configuration,DC=X instanceType: 4 attributeID: 1.3.6.1.4.1.15953.9.1.1 attributeSyntax: 2.5.5.5 isSingleValued: FALSE showInAdvancedViewOnly: TRUE adminDisplayName: sudoUser adminDescription: User(s) who may run sudo oMSyntax: 22 searchFlags: 1 lDAPDisplayName: sudoUser name: sudoUser schemaIDGUID:: JrGcaKpnoU+0s+HgeFjAbg== objectCategory: CN=Attribute-Schema,CN=Schema,CN=Configuration,DC=X dn: CN=sudoHost,CN=Schema,CN=Configuration,DC=X changetype: add objectClass: top objectClass: attributeSchema cn: sudoHost distinguishedName: CN=sudoHost,CN=Schema,CN=Configuration,DC=X instanceType: 4 attributeID: 1.3.6.1.4.1.15953.9.1.2 attributeSyntax: 2.5.5.5 isSingleValued: FALSE showInAdvancedViewOnly: TRUE adminDisplayName: sudoHost adminDescription: Host(s) who may run sudo oMSyntax: 22 lDAPDisplayName: sudoHost name: sudoHost schemaIDGUID:: d0TTjg+Y6U28g/Y+ns2k4w== objectCategory: CN=Attribute-Schema,CN=Schema,CN=Configuration,DC=X dn: CN=sudoCommand,CN=Schema,CN=Configuration,DC=X changetype: add objectClass: top objectClass: attributeSchema cn: sudoCommand distinguishedName: CN=sudoCommand,CN=Schema,CN=Configuration,DC=X instanceType: 4 attributeID: 1.3.6.1.4.1.15953.9.1.3 attributeSyntax: 2.5.5.5 isSingleValued: FALSE showInAdvancedViewOnly: TRUE adminDisplayName: sudoCommand adminDescription: Command(s) to be executed by sudo oMSyntax: 22 lDAPDisplayName: sudoCommand name: sudoCommand schemaIDGUID:: D6QR4P5UyUen3RGYJCHCPg== objectCategory: CN=Attribute-Schema,CN=Schema,CN=Configuration,DC=X dn: CN=sudoRunAs,CN=Schema,CN=Configuration,DC=X changetype: add objectClass: top objectClass: attributeSchema cn: sudoRunAs distinguishedName: CN=sudoRunAs,CN=Schema,CN=Configuration,DC=X instanceType: 4 attributeID: 1.3.6.1.4.1.15953.9.1.4 attributeSyntax: 2.5.5.5 isSingleValued: FALSE showInAdvancedViewOnly: TRUE adminDisplayName: sudoRunAs adminDescription: User(s) impersonated by sudo (deprecated) oMSyntax: 22 lDAPDisplayName: sudoRunAs name: sudoRunAs schemaIDGUID:: CP98mCQTyUKKxGrQeM80hQ== objectCategory: CN=Attribute-Schema,CN=Schema,CN=Configuration,DC=X dn: CN=sudoOption,CN=Schema,CN=Configuration,DC=X changetype: add objectClass: top objectClass: attributeSchema cn: sudoOption distinguishedName: CN=sudoOption,CN=Schema,CN=Configuration,DC=X instanceType: 4 attributeID: 1.3.6.1.4.1.15953.9.1.5 attributeSyntax: 2.5.5.5 isSingleValued: FALSE showInAdvancedViewOnly: TRUE adminDisplayName: sudoOption adminDescription: Option(s) followed by sudo oMSyntax: 22 lDAPDisplayName: sudoOption name: sudoOption schemaIDGUID:: ojaPzBBlAEmsvrHxQctLnA== objectCategory: CN=Attribute-Schema,CN=Schema,CN=Configuration,DC=X dn: CN=sudoRunAsUser,CN=Schema,CN=Configuration,DC=X changetype: add objectClass: top objectClass: attributeSchema cn: sudoRunAsUser distinguishedName: CN=sudoRunAsUser,CN=Schema,CN=Configuration,DC=X instanceType: 4 attributeID: 1.3.6.1.4.1.15953.9.1.6 attributeSyntax: 2.5.5.5 isSingleValued: FALSE showInAdvancedViewOnly: TRUE adminDisplayName: sudoRunAsUser adminDescription: User(s) impersonated by sudo oMSyntax: 22 lDAPDisplayName: sudoRunAsUser name: sudoRunAsUser schemaIDGUID:: 9C52yPYd3RG3jMR2VtiVkw== objectCategory: CN=Attribute-Schema,CN=Schema,CN=Configuration,DC=X dn: CN=sudoRunAsGroup,CN=Schema,CN=Configuration,DC=X changetype: add objectClass: top objectClass: attributeSchema cn: sudoRunAsGroup distinguishedName: CN=sudoRunAsGroup,CN=Schema,CN=Configuration,DC=X instanceType: 4 attributeID: 1.3.6.1.4.1.15953.9.1.7 attributeSyntax: 2.5.5.5 isSingleValued: FALSE showInAdvancedViewOnly: TRUE adminDisplayName: sudoRunAsGroup adminDescription: Groups(s) impersonated by sudo oMSyntax: 22 lDAPDisplayName: sudoRunAsGroup name: sudoRunAsGroup schemaIDGUID:: xJhSt/Yd3RGJPTB1VtiVkw== objectCategory: CN=Attribute-Schema,CN=Schema,CN=Configuration,DC=X dn: CN=sudoNotBefore,CN=Schema,CN=Configuration,DC=X changetype: add objectClass: top objectClass: attributeSchema cn: sudoNotBefore distinguishedName: CN=sudoNotBefore,CN=Schema,CN=Configuration,DC=X instanceType: 4 attributeID: 1.3.6.1.4.1.15953.9.1.8 attributeSyntax: 2.5.5.11 isSingleValued: TRUE showInAdvancedViewOnly: TRUE adminDisplayName: sudoNotBefore adminDescription: Start of time interval for which the entry is valid oMSyntax: 24 lDAPDisplayName: sudoNotBefore name: sudoNotBefore schemaIDGUID:: dm1HnRfY4RGf4gopYYhwmw== objectCategory: CN=Attribute-Schema,CN=Schema,CN=Configuration,DC=X dn: CN=sudoNotAfter,CN=Schema,CN=Configuration,DC=X changetype: add objectClass: top objectClass: attributeSchema cn: sudoNotAfter distinguishedName: CN=sudoNotAfter,CN=Schema,CN=Configuration,DC=X instanceType: 4 attributeID: 1.3.6.1.4.1.15953.9.1.9 attributeSyntax: 2.5.5.11 isSingleValued: TRUE showInAdvancedViewOnly: TRUE adminDisplayName: sudoNotAfter adminDescription: End of time interval for which the entry is valid oMSyntax: 24 lDAPDisplayName: sudoNotAfter name: sudoNotAfter schemaIDGUID:: OAr/pBfY4RG9dBIpYYhwmw== objectCategory: CN=Attribute-Schema,CN=Schema,CN=Configuration,DC=X dn: CN=sudoOrder,CN=Schema,CN=Configuration,DC=X changetype: add objectClass: top objectClass: attributeSchema cn: sudoOrder distinguishedName: CN=sudoOrder,CN=Schema,CN=Configuration,DC=X instanceType: 4 attributeID: 1.3.6.1.4.1.15953.9.1.10 attributeSyntax: 2.5.5.9 isSingleValued: TRUE showInAdvancedViewOnly: TRUE adminDisplayName: sudoOrder adminDescription: an integer to order the sudoRole entries oMSyntax: 2 lDAPDisplayName: sudoOrder name: sudoOrder schemaIDGUID:: 0J8yrRfY4RGIYBUpYYhwmw== objectCategory: CN=Attribute-Schema,CN=Schema,CN=Configuration,DC=X edubuntu-server-14.02.2/host/share/services/directory/schemas/install.sh0000755000000000000000000000043312277013247023250 0ustar #!/bin/sh [ -z "$1" ] && echo "Usage: $0 " && exit 1 stop samba-ad-dc for schema in *.ldif; do echo "Loading: $schema" cat $schema | sed "s/DC=X/$1/g" | ldbmodify -H /var/lib/samba/private/sam.ldb - --option="dsdb:schema update allowed"=true done start samba-ad-dc edubuntu-server-14.02.2/host/share/services/directory/schemas/add-sudoers-ou.ldif0000644000000000000000000000011612077125065024734 0ustar dn: OU=sudoers,DC=X objectClass: organizationalUnit ou: sudoers name: sudoers edubuntu-server-14.02.2/host/share/services/directory/schemas/add-people-ou.ldif0000644000000000000000000000011312077125072024527 0ustar dn: OU=People,DC=X objectClass: organizationalUnit ou: People name: People edubuntu-server-14.02.2/host/share/services/directory/schemas/sshPubKey-1.ldif0000644000000000000000000000052412077124465024162 0ustar dn: CN=sshPublicKey,CN=Schema,CN=Configuration,DC=X objectClass: top objectClass: attributeSchema attributeID: 1.3.6.1.4.1.24552.500.1.1.1.13 schemaIdGuid:: LFDQkjNCgjmQ2w62qi2KtA== cn: sshPublicKey name: sshPublicKey lDAPDisplayName: sshPublicKey description: OpenSSH Public key attributeSyntax: 2.5.5.10 oMSyntax: 4 isSingleValued: FALSE edubuntu-server-14.02.2/host/share/services/directory/schemas/sshPubKey-2.ldif0000644000000000000000000000064312077124470024161 0ustar dn: CN=ldapPublicKey,CN=Schema,CN=Configuration,DC=X objectClass: top objectClass: classSchema governsID: 1.3.6.1.4.1.24552.500.1.1.2.0 schemaIdGuid:: vOa/58HpkydeJ/WgOd1nTw== cn: ldapPublicKey name: ldapPublicKey lDAPDisplayName: ldapPublicKey subClassOf: top objectClassCategory: 3 description: OpenSSH LPK objectclass mayContain: sshPublicKey defaultObjectCategory: CN=ldapPublicKey,CN=Schema,CN=Configuration,DC=X edubuntu-server-14.02.2/host/share/services/directory/schemas/sudo-2.ldif0000644000000000000000000000167012077124476023225 0ustar dn: changetype: modify add: schemaUpdateNow schemaUpdateNow: 1 - dn: CN=sudoRole,CN=Schema,CN=Configuration,DC=X changetype: add objectClass: top objectClass: classSchema cn: sudoRole distinguishedName: CN=sudoRole,CN=Schema,CN=Configuration,DC=X instanceType: 4 possSuperiors: container possSuperiors: top subClassOf: top governsID: 1.3.6.1.4.1.15953.9.2.1 mayContain: sudoCommand mayContain: sudoHost mayContain: sudoOption mayContain: sudoRunAs mayContain: sudoRunAsUser mayContain: sudoRunAsGroup mayContain: sudoUser mayContain: sudoNotBefore mayContain: sudoNotAfter mayContain: sudoOrder rDNAttID: cn showInAdvancedViewOnly: FALSE adminDisplayName: sudoRole adminDescription: Sudoer Entries objectClassCategory: 1 lDAPDisplayName: sudoRole name: sudoRole schemaIDGUID:: SQn432lnZ0+ukbdh3+gN3w== systemOnly: FALSE objectCategory: CN=Class-Schema,CN=Schema,CN=Configuration,DC=X defaultObjectCategory: CN=sudoRole,CN=Schema,CN=Configuration,DC=X edubuntu-server-14.02.2/host/share/services/directory/setup0000755000000000000000000000231212277013315020700 0ustar #!/bin/sh # Prepare environment mount -t proc proc /proc mount -t sysfs sysfs /sys cat > /usr/sbin/policy-rc.d << EOF #!/bin/sh exit 101 EOF chmod +x /usr/sbin/policy-rc.d export DEBIAN_FRONTEND=noninteractive # Setup apt-get update apt-get dist-upgrade -y apt-get install -y samba tdb-tools bind9 # First-boot script ( cat << EOF start on starting samba-ad-dc task script rm -f /etc/samba/smb.conf hostname $(cat /etc/hostname | sed "s/edubuntu-server-//") samba-tool domain provision --domain=$directory_workgroup \\ --adminpass=EdubuntuServer1404 --realm=$directory_fqdn \\ --use-ntvfs --dns-backend=BIND9_DLZ sed "s/^};$/ tkey-gssapi-keytab \"\/var\/lib\/samba\/private\/dns.keytab\";\n};/g" \ -i /etc/bind/named.conf.options chgrp bind /var/lib/samba/private/dns.keytab chmod g+r /var/lib/samba/private/dns.keytab cp /var/lib/samba/private/krb5.conf /etc/krb5.conf echo 'include "/var/lib/samba/private/named.conf";' >> \ /etc/bind/named.conf.local hostname $(cat /etc/hostname) rm -f /etc/init/first-boot.conf /etc/init.d/bind9 restart || true end script EOF ) > /etc/init/first-boot.conf # Cleanup rm /usr/sbin/policy-rc.d exit 0 edubuntu-server-14.02.2/host/share/services/rdns/0000755000000000000000000000000012277030060016552 5ustar edubuntu-server-14.02.2/host/share/services/rdns/configure0000755000000000000000000000044112016526124020462 0ustar #!/usr/bin/python3 import os environ = {} environ['rdns_domain_name'] = os.environ['domain_fqdn'] environ['rdns_domain_servers'] = os.environ['services_directory'] environ['rdns_subnet'] = os.environ['network_subnet'] for key, value in environ.items(): print("%s=%s" % (key, value)) edubuntu-server-14.02.2/host/share/services/rdns/setup0000755000000000000000000000146512016551601017646 0ustar #!/bin/sh # Prepare environment mount -t proc proc /proc mount -t sysfs sysfs /sys cat /etc/hostname > /proc/sys/kernel/hostname cat > /usr/sbin/policy-rc.d << EOF #!/bin/sh exit 101 EOF chmod +x /usr/sbin/policy-rc.d export DEBIAN_FRONTEND=noninteractive #Setup apt-get update apt-get dist-upgrade -y apt-get install -y unbound sed -i "s/RESOLVCONF=true/RESOLVCONF=false/" /etc/default/unbound sed -i "s/RESOLVCONF_FORWARDERS=true/RESOLVCONF_FORWARDERS=false/" \ /etc/default/unbound ( cat << EOF # # Edubuntu-server generated configuration. # server: interface: 0.0.0.0 interface-automatic: yes access-control: $rdns_subnet allow forward-zone: name: "$rdns_domain_name" forward-addr: $rdns_domain_servers EOF ) > /etc/unbound/unbound.conf # Cleanup rm /usr/sbin/policy-rc.d exit 0 edubuntu-server-14.02.2/host/share/services/manager/0000755000000000000000000000000012277030060017216 5ustar edubuntu-server-14.02.2/host/share/services/manager/configure0000755000000000000000000000025412277027416021141 0ustar #!/usr/bin/python3 import os environ = {} environ['manager_domain_name'] = os.environ['domain_fqdn'] for key, value in environ.items(): print("%s=%s" % (key, value)) edubuntu-server-14.02.2/host/share/services/manager/setup0000755000000000000000000000101012277027553020311 0ustar #!/bin/sh # Prepare environment mount -t proc proc /proc mount -t sysfs sysfs /sys cat /etc/hostname > /proc/sys/kernel/hostname cat > /usr/sbin/policy-rc.d << EOF #!/bin/sh exit 101 EOF chmod +x /usr/sbin/policy-rc.d export DEBIAN_FRONTEND=noninteractive #Setup apt-get update apt-get dist-upgrade -y apt-get install -y edubuntu-server-manager uuid-runtime ( cat << EOF SECRET_KEY = "$(uuidgen)" LDAP_DOMAIN = "$manager_domain_name" EOF ) > /etc/edubuntu-server/manager.cfg # Cleanup rm /usr/sbin/policy-rc.d exit 0 edubuntu-server-14.02.2/host/share/template-configure0000755000000000000000000000103712277027355017517 0ustar #!/bin/sh # Prepare environment mount -t proc proc /proc mount -t sysfs sysfs /sys cat /etc/hostname > /proc/sys/kernel/hostname cat > /usr/sbin/policy-rc.d << EOF #!/bin/sh exit 101 EOF chmod +x /usr/sbin/policy-rc.d export DEBIAN_FRONTEND=noninteractive # Setup apt-get install -y language-pack-en perl-modules apt-get remove --purge openssh-server -y apt-get autoremove --purge -y deluser --remove-home ubuntu # Cleanup rm /usr/sbin/policy-rc.d rm -f /etc/resolvconf/resolv.conf.d/tail rm -f /etc/resolvconf/resolv.conf.d/original exit 0 edubuntu-server-14.02.2/host/sbin/0000755000000000000000000000000012277030060013612 5ustar edubuntu-server-14.02.2/host/sbin/edubuntu-server-build-template0000755000000000000000000000624412277013527021624 0ustar #!/usr/bin/python3 # -*- coding: utf-8 -*- # Copyright (C) 2012-2014 Stéphane Graber # Author: Stéphane Graber # 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 2 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 can find the license on Debian systems in the file # /usr/share/common-licenses/GPL-2 # NOTE: To remove once the API is stabilized import warnings warnings.filterwarnings("ignore", "The python-lxc API isn't yet stable") import argparse import gettext import glob import os import shutil import subprocess import sys _ = gettext.gettext gettext.textdomain("edubuntu-server-build-template") # Some constants LXC_LIB = "/var/lib/lxc/" LXC_CACHE = "/var/cache/lxc/" LXC_TEMPLATE = "tpl-edubuntu-server" LXC_CONFIGURE = "/usr/share/edubuntu-server/template-configure" # Begin parsing the command line parser = argparse.ArgumentParser( description=_("Edubuntu server template builder"), formatter_class=argparse.RawTextHelpFormatter) # Optional arguments parser.add_argument("--cleanup", action="store_true", help=_("Remove an existing template and start over.")) args = parser.parse_args() # Basic requirements check ## The user needs to be uid 0 if not os.geteuid() == 0: print(_("You must be root to run this script. Try running: sudo %s" % (sys.argv[0]))) sys.exit(1) # Only import lxc now so we don't need to build-depend on it for help2man import lxc for path in (LXC_LIB, LXC_CACHE): if not os.path.exists(path): print(_("%s doesn't exist." % path)) sys.exit(1) # In cleanup mode, wipe the LXC cache and any existing template if args.cleanup: if os.path.exists("%s/%s" % (LXC_LIB, LXC_TEMPLATE)): shutil.rmtree("%s/%s" % (LXC_LIB, LXC_TEMPLATE)) for entry in glob.glob("%s/*" % LXC_CACHE): shutil.rmtree(entry) # Check that the container doesn't exist if os.path.exists("%s/%s" % (LXC_LIB, LXC_TEMPLATE)): print(_("The template already exists.")) sys.exit(1) # Create a basic container template = lxc.Container(LXC_TEMPLATE) if not template.create("ubuntu"): print(_("Failed to create the base system.")) sys.exit(1) # Run the configuration script shutil.copy(LXC_CONFIGURE, "%s/%s/rootfs/configure" % (LXC_LIB, LXC_TEMPLATE)) with open(os.devnull, "w") as fd: configure = subprocess.Popen(["lxc-unshare", "-s", "PID|MOUNT|IPC|UTSNAME", "chroot", "%s/%s/rootfs/" % (LXC_LIB, LXC_TEMPLATE), "--", "/configure"], stdout=fd, stderr=fd) if configure.wait() != 0: print(_("Failed to configure the template.")) sys.exit(1) os.remove("%s/%s/rootfs/configure" % (LXC_LIB, LXC_TEMPLATE)) edubuntu-server-14.02.2/host/sbin/edubuntu-server-manage0000755000000000000000000001171312277013527020141 0ustar #!/usr/bin/python3 # -*- coding: utf-8 -*- # Copyright (C) 2012-2014 Stéphane Graber # Author: Stéphane Graber # 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 2 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 can find the license on Debian systems in the file # /usr/share/common-licenses/GPL-2 # NOTE: To remove once the API is stabilized import warnings warnings.filterwarnings("ignore", "The python-lxc API isn't yet stable") import argparse import configparser import gettext import os import subprocess import sys _ = gettext.gettext gettext.textdomain("edubuntu-server-manage") # Some constants EDUBUNTU_GLOBAL_CONF = "/etc/edubuntu-server/edubuntu-server.conf" EDUBUNTU_CONTAINERS_CONF = "/etc/edubuntu-server/containers.conf" EDUBUNTU_RUN = "/run/edubuntu-server.state" LXC_LIB = "/var/lib/lxc/" LXC_CACHE = "/var/cache/lxc/" # The various functions def start(): # Load the config global_config = configparser.ConfigParser() global_config.read(EDUBUNTU_GLOBAL_CONF) containers_config = configparser.ConfigParser() containers_config.read(EDUBUNTU_CONTAINERS_CONF) # Setup the bridge bridge_name = global_config.get("network", "bridge_name") bridge_interface = global_config.get("network", "bridge_interface", fallback=None) gateway = ipaddr.IPv4Address(global_config.get("network", "gateway")) network = ipaddr.IPv4Network(global_config.get("network", "subnet")) subprocess.call(['brctl', 'addbr', bridge_name]) if bridge_interface: subprocess.call(['ip', 'link', 'set', 'dev', bridge_interface, 'up']) subprocess.call(['brctl', 'addif', bridge_name, bridge_interface]) subprocess.call(['ip', '-4', 'addr', 'add', 'dev', bridge_name, "%s/%s" % (gateway, network.prefixlen)]) subprocess.call(['ip', 'link', 'set', 'dev', bridge_name, 'up']) # Setup the NAT subprocess.call(['iptables', '-t', 'nat', '-A', 'POSTROUTING', '-s', str(network), '!', '-d', str(network), '-j', 'MASQUERADE']) # Spawn the containers for entry in containers_config.sections(): if containers_config.getboolean(entry, 'autostart'): container = lxc.Container(entry) # Skip any running container if container.running: continue container.start() if not container.wait("RUNNING", 30): print(_("Failed to start container: %s" % entry)) def stop(): # Load the config global_config = configparser.ConfigParser() global_config.read(EDUBUNTU_GLOBAL_CONF) containers_config = configparser.ConfigParser() containers_config.read(EDUBUNTU_CONTAINERS_CONF) # Stop the containers for entry in containers_config.sections(): if containers_config.getboolean(entry, 'autostart'): container = lxc.Container(entry) # Skip any stopped container if not container.running: continue container.stop() if not container.wait("STOPPED", 30): print(_("Failed to stop container: %s" % entry)) # Teardown the NAT network = ipaddr.IPv4Network(global_config.get("network", "subnet")) subprocess.call(['iptables', '-t', 'nat', '-D', 'POSTROUTING', '-s', str(network), '!', '-d', str(network), '-j', 'MASQUERADE']) # Destroy the bridge bridge_name = global_config.get("network", "bridge_name") subprocess.call(['ip', 'link', 'set', 'dev', bridge_name, 'down']) subprocess.call(['brctl', 'delbr', bridge_name]) def status(): pass # Begin parsing the command line parser = argparse.ArgumentParser(description=_("Edubuntu server manager"), formatter_class=argparse.RawTextHelpFormatter) # Required argument sp = parser.add_subparsers() sp_start = sp.add_parser("start", help=_("Start Edubuntu server")) sp_start.set_defaults(func=start) sp_stop = sp.add_parser("stop", help=_("Stop Edubuntu server")) sp_stop.set_defaults(func=stop) sp_status = sp.add_parser("status", help=_("Query the status of " "Edubuntu server")) sp_status.set_defaults(func=status) args = parser.parse_args() if not os.geteuid() == 0: print(_("You must be root to run this script. Try running: sudo %s" % (sys.argv[0]))) sys.exit(1) # Only import ipaddr and lxc now to avoid having to build-depend on them import ipaddr import lxc if not hasattr(args, "func"): parser.print_help() else: args.func() edubuntu-server-14.02.2/host/sbin/edubuntu-server-deploy0000755000000000000000000002601012277022076020200 0ustar #!/usr/bin/python3 # -*- coding: utf-8 -*- # Copyright (C) 2012-2014 Stéphane Graber # Author: Stéphane Graber # 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 2 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 can find the license on Debian systems in the file # /usr/share/common-licenses/GPL-2 # NOTE: To remove once the API is stabilized import warnings warnings.filterwarnings("ignore", "The python-lxc API isn't yet stable") import argparse import configparser import gettext import glob import os import re import shutil import subprocess import sys _ = gettext.gettext gettext.textdomain("edubuntu-server-deploy") # Some constants EDUBUNTU_CONF = "/etc/edubuntu-server" EDUBUNTU_GLOBAL_CONF = "/etc/edubuntu-server/edubuntu-server.conf" EDUBUNTU_CONTAINERS_CONF = "/etc/edubuntu-server/containers.conf" ALL_SERVICES = ["manager", "directory", "dhcp", "rdns", "ltsp"] LAN_SERVICES = ["dhcp"] MANDATORY_SERVICES = ["manager", "directory", "rdns"] UPSTART_JOB = "edubuntu-server" MIN_CLIENTS = 30 LXC_TEMPLATE = "tpl-edubuntu-server" LXC_LIB = "/var/lib/lxc/" LXC_CACHE = "/var/cache/lxc/" # Main functions def isValidDomain(domain): if len(domain) > 255: return False allowed = re.compile("(?!-)[A-Z\d-]{1,63}(? Main configuration file (domain, network, ...) └── containers.conf => List of containers (auto-start flags, IPs, ...) == Configuration == === edubuntu-server.conf === This file is the main configuration file, it typically contains information on the subnet, domain and other information that aren't specific to a container. It's completely generated by edubuntu-server-deploy and used by most scripts. Updating the file will mostly likely break the edubuntu server setup as changes would need to be manually applied to all the containers too. Example containing all the valid keys: [network] bridge_name = edubr0 bridge_interface = eth1 subnet = 10.0.128.0/24 gateway = 10.0.128.1 [domain] fqdn = edubuntu.test workgroup = edubuntu