diff --git a/README.md b/README.md index b7acab4..404ff03 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ A configuration tool is available at https://github.com/linuxmint/lightdm-settin - This greeter supports HiDPI. - Sessions are validated. If a default/chosen session isn't present on the system, the greeter scans for known sessions in /usr/share/xsessions and replaces the invalid session choice with a valid session. - You can take a screenshot by pressing PrintScrn. The screenshot is saved in /var/lib/lightdm/Screenshot.png. +- A **banner/acceptance dialog** can be shown before login. When enabled, the user must read and accept a policy notice before proceeding. The dialog reads its text from `/etc/issue` by default, or from a custom file specified in the configuration. # Credit @@ -62,4 +63,6 @@ Configuration file format for /etc/lightdm/slick-greeter.conf # only-on-monitor=Sets the monitor on which to show the login window, -1 means "follow the mouse" # stretch-background-across-monitors=Whether to stretch the background across multiple monitors (false by default) # clock-format=What clock format to use (e.g., %H:%M or %l:%M %p) + # show-banner=Whether to show a banner/acceptance dialog before login (true or false) + # banner-file=Path to the file whose contents are shown in the banner dialog (defaults to /etc/issue) diff --git a/data/x.dm.slick-greeter.gschema.xml b/data/x.dm.slick-greeter.gschema.xml index 4f9a1c5..7e89884 100644 --- a/data/x.dm.slick-greeter.gschema.xml +++ b/data/x.dm.slick-greeter.gschema.xml @@ -160,5 +160,14 @@ 'left' Alignment of the main content + + false + Whether to show a usage-policy banner from /etc/issue before login + When enabled the greeter reads the file specified by banner-file (default /etc/issue) and displays its contents in a dialog. The user must click "I Accept" before the login UI is enabled. + + + '' + Path to the banner file shown before login (default: /etc/issue) + diff --git a/debian/install b/debian/install index 931288b..e8cc93d 100644 --- a/debian/install +++ b/debian/install @@ -1 +1,2 @@ debian/90-slick-greeter.conf /usr/share/lightdm/lightdm.conf.d/ +debian/slick-greeter.conf /etc/lightdm/ diff --git a/debian/slick-greeter.conf b/debian/slick-greeter.conf new file mode 100644 index 0000000..b6a8ea5 --- /dev/null +++ b/debian/slick-greeter.conf @@ -0,0 +1,118 @@ +[Greeter] +# Background image path (leave blank for none) +#background= + +# Background color when no image is set +#background-color=#000000 + +# Stretch background across all monitors +#stretch-background-across-monitors=false + +# Draw per-user backgrounds +#draw-user-backgrounds=true + +# Draw the dot-grid overlay +#draw-grid=false + +# Show the hostname in the menubar +#show-hostname=true + +# Show the keyboard layout selector +#show-keyboard=true + +# Show the accessibility menu +#show-a11y=true + +# Show the power menu +#show-power=true + +# Show the clock +#show-clock=true + +# Show the quit/session-end button +#show-quit=true + +# Path to a logo image shown in the greeter +#logo= + +# Path to a logo image shown on secondary monitors +#other-monitors-logo= + +# GTK theme name +#theme-name=Adwaita + +# Icon theme name +#icon-theme-name=gnome + +# Cursor theme name +#cursor-theme-name=default + +# Cursor theme size +#cursor-theme-size=24 + +# Font name and size +#font-name=Ubuntu 11 + +# Enable font antialiasing +#xft-antialias=true + +# Font DPI +#xft-dpi=96 + +# Font hint style (none, slight, medium, full) +#xft-hintstyle= + +# Font subpixel rendering (none, rgb, bgr, vrgb, vbgr) +#xft-rgba= + +# Enable the on-screen keyboard by default +#onscreen-keyboard=false + +# Path to the on-screen keyboard layout file +#onscreen-keyboard-layout=/usr/share/onboard/layouts/Small.onboard + +# Enable high-contrast mode by default +#high-contrast=false + +# Enable the screen reader by default +#screen-reader=false + +# Path to a sound file to play when the greeter is ready (leave blank for none) +#play-ready-sound= + +# Space-separated list of usernames to hide from the user list +#hidden-users= + +# Only show users belonging to this group (leave blank for all users) +#group-filter= + +# HiDPI mode: auto, on, or off +#enable-hidpi=auto + +# Activate Num Lock on startup +#activate-numlock=false + +# Only show the greeter on this monitor (e.g. "0", "HDMI-1"); "auto" uses the primary +#only-on-monitor=auto + +# Clock format string (strftime syntax) +#clock-format=%H:%M + +# Horizontal alignment of the login panel: left, center, right +#content-align= + +# ── System Use Notification (login banner) ────────────────────────────────── +# +# When show-banner is true the greeter displays the contents of banner-file +# in a dialog before the login UI is shown. The user must click "I Accept" +# to proceed. This satisfies the DoD / NIST SP 800-53 AC-8 requirement for +# a system-use notification at login time. +# +# To enable: +# 1. Set show-banner to true. +# 2. Set banner-file to the path of your notification text (plain text). +# Defaults to /etc/issue when left blank. +# 3. Edit that file with your approved system-use notification wording. +# +#show-banner=false +#banner-file=/etc/issue diff --git a/po/POTFILES.in b/po/POTFILES.in index 4b4afbe..486c3b8 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -2,6 +2,8 @@ # Please keep this file sorted alphabetically. src/animate-timer.vala src/background.vala +src/banner-dialog.vala +src/banner-dialog.vala src/cached-image.vala src/dash-box.vala src/dash-button.vala diff --git a/slick-greeter.pot b/slick-greeter.pot index a8bdc98..df5c347 100644 --- a/slick-greeter.pot +++ b/slick-greeter.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-06-01 11:07+0100\n" +"POT-Creation-Date: 2026-02-27 10:00-0600\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,88 +17,105 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: src/greeter-list.vala:303 +#: src/banner-dialog.vala:81 +msgid "System Notice" +msgstr "" + +#: src/banner-dialog.vala:90 +msgid "You must read and accept the following policy before logging in:" +msgstr "" + +#: src/banner-dialog.vala:142 +msgid "I Accept" +msgstr "" + +#. Translators: shown when the banner file cannot be read +#: src/banner-dialog.vala:175 +msgid "(Could not read the login banner file.)" +msgstr "" + +#: src/greeter-list.vala:307 #, c-format msgid "Enter password for %s" msgstr "" -#: src/greeter-list.vala:305 +#: src/greeter-list.vala:309 msgid "Enter your username" msgstr "" -#: src/greeter-list.vala:815 src/user-list.vala:720 +#: src/greeter-list.vala:819 src/user-list.vala:720 msgid "Password:" msgstr "" -#: src/greeter-list.vala:817 src/user-list.vala:714 +#: src/greeter-list.vala:821 src/user-list.vala:714 msgid "Username:" msgstr "" -#: src/greeter-list.vala:872 +#: src/greeter-list.vala:877 msgid "Invalid password, please try again" msgstr "" -#: src/greeter-list.vala:883 +#: src/greeter-list.vala:888 msgid "Failed to authenticate" msgstr "" -#: src/greeter-list.vala:929 +#: src/greeter-list.vala:934 msgid "Failed to start session" msgstr "" -#: src/greeter-list.vala:943 +#: src/greeter-list.vala:948 msgid "Logging in..." msgstr "" -#: src/main-window.vala:52 +#: src/main-window.vala:54 msgid "Login Screen" msgstr "" -#: src/main-window.vala:113 +#: src/main-window.vala:143 msgid "Back" msgstr "" -#: src/menubar.vala:324 +#: src/menubar.vala:372 msgid "Power:" msgstr "" -#: src/menubar.vala:374 +#: src/menubar.vala:437 msgid "Accessibility" msgstr "" -#: src/menubar.vala:383 +#: src/menubar.vala:446 msgid "Onscreen keyboard" msgstr "" -#: src/menubar.vala:388 +#: src/menubar.vala:451 msgid "High Contrast" msgstr "" -#: src/menubar.vala:394 +#: src/menubar.vala:457 msgid "Screen Reader" msgstr "" -#: src/menubar.vala:406 src/menubar.vala:453 +#: src/menubar.vala:469 src/menubar.vala:516 msgid "Quit..." msgstr "" -#: src/menubar.vala:419 src/shutdown-dialog.vala:155 +#: src/menubar.vala:482 src/shutdown-dialog.vala:155 msgid "Suspend" msgstr "" -#: src/menubar.vala:437 src/shutdown-dialog.vala:172 +#: src/menubar.vala:500 src/shutdown-dialog.vala:172 msgid "Hibernate" msgstr "" -#: src/menubar.vala:476 src/menubar.vala:520 src/menubar.vala:544 +#: src/menubar.vala:539 src/menubar.vala:583 src/menubar.vala:606 msgid "Keyboard layout:" msgstr "" -#: src/menubar.vala:532 +#: src/menubar.vala:595 msgid "More layouts..." msgstr "" -#: src/prompt-box.vala:215 +#: src/prompt-box.vala:250 msgid "Session Options" msgstr "" @@ -119,6 +136,7 @@ msgid "Are you sure you want to shut down the computer?" msgstr "" #: src/shutdown-dialog.vala:137 +#, c-format msgid "" "Other users are currently logged in to this computer, shutting down now will " "also close these other sessions." @@ -128,24 +146,25 @@ msgstr "" msgid "Restart" msgstr "" -#: src/slick-greeter.vala:749 +#. Translators: %s is a session name like KDE or Ubuntu +#: src/toggle-box.vala:109 +#, c-format +msgid "%s (Default)" +msgstr "" + +#: src/slick-greeter.vala:770 msgid "Show release version" msgstr "" -#: src/slick-greeter.vala:752 +#: src/slick-greeter.vala:773 msgid "Run in test mode" msgstr "" -#: src/slick-greeter.vala:768 +#: src/slick-greeter.vala:789 #, c-format msgid "Run '%s --help' to see a full list of available command line options." msgstr "" -#: src/toggle-box.vala:109 -#, c-format -msgid "%s (Default)" -msgstr "" - #: src/user-list.vala:45 msgid "Guest Session" msgstr "" diff --git a/src/banner-dialog.vala b/src/banner-dialog.vala new file mode 100644 index 0000000..17b16fa --- /dev/null +++ b/src/banner-dialog.vala @@ -0,0 +1,282 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2011,2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * BannerDialog — shows the contents of /etc/issue (or a configurable file) + * and requires the user to accept before logging in. + * + * The widget is a Gtk.Fixed overlay that sits on top of the Background widget, + * following the same design pattern as ShutdownDialog. + */ +public class BannerDialog : Gtk.Fixed +{ + /** Emitted when the user clicks "I Accept". */ + public signal void accepted (); + + private Monitor monitor; + private weak Background background; + + private Gtk.EventBox overlay_events; + private Gtk.EventBox vbox_events; + private Gtk.Box vbox; + + private const int DIALOG_WIDTH = 640; + private const int PADDING = 20; + + public BannerDialog (Background bg) + { + background = bg; + + /* --- Full-screen transparent overlay (blocks all background clicks) --- */ + overlay_events = new Gtk.EventBox (); + overlay_events.visible = true; + overlay_events.set_visible_window (false); + overlay_events.events |= Gdk.EventMask.BUTTON_PRESS_MASK; + overlay_events.button_press_event.connect (() => { return true; }); + add (overlay_events); + + /* --- Dialog panel --- */ + vbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 10); + vbox.visible = true; + vbox.margin = PADDING; + + vbox_events = new Gtk.EventBox (); + vbox_events.visible = true; + vbox_events.set_visible_window (true); + vbox_events.events |= Gdk.EventMask.BUTTON_PRESS_MASK; + vbox_events.button_press_event.connect (() => { return true; }); + vbox_events.add (vbox); + overlay_events.add (vbox_events); + + /* Panel background style */ + try + { + var style = new Gtk.CssProvider (); + style.load_from_data ( + "* { background-color: rgba(20, 20, 20, 0.92); " + + " border-radius: 4px; }", -1); + vbox_events.get_style_context ().add_provider ( + style, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + } + catch (Error e) + { + debug ("BannerDialog panel style error: %s", e.message); + } + + /* --- Title --- */ + var title_label = new Gtk.Label (_("System Notice")); + title_label.visible = true; + title_label.override_font (Pango.FontDescription.from_string ("Ubuntu Light 18")); + title_label.override_color (Gtk.StateFlags.NORMAL, { 1.0f, 1.0f, 1.0f, 1.0f }); + title_label.set_alignment (0.5f, 0.5f); + vbox.pack_start (title_label, false, false, 0); + + /* --- Subtitle --- */ + var desc_label = new Gtk.Label ( + _("You must read and accept the following policy before logging in:")); + desc_label.visible = true; + desc_label.set_line_wrap (true); + desc_label.override_font (Pango.FontDescription.from_string ("Ubuntu 10")); + desc_label.override_color (Gtk.StateFlags.NORMAL, { 0.80f, 0.80f, 0.80f, 1.0f }); + desc_label.set_alignment (0.0f, 0.5f); + vbox.pack_start (desc_label, false, false, 0); + + /* --- Separator --- */ + var sep = new Gtk.Separator (Gtk.Orientation.HORIZONTAL); + sep.visible = true; + vbox.pack_start (sep, false, false, 0); + + /* --- Text view with full banner content (no scroll) --- */ + var text_view = new Gtk.TextView (); + text_view.visible = true; + text_view.editable = false; + text_view.cursor_visible = false; + text_view.wrap_mode = Gtk.WrapMode.WORD_CHAR; + text_view.left_margin = 8; + text_view.right_margin = 8; + text_view.top_margin = 6; + text_view.bottom_margin = 6; + text_view.buffer.text = read_banner_text (); + text_view.override_font (Pango.FontDescription.from_string ("Monospace 10")); + text_view.override_color (Gtk.StateFlags.NORMAL, { 0.90f, 0.90f, 0.90f, 1.0f }); + + try + { + var style = new Gtk.CssProvider (); + style.load_from_data ("* { background-color: rgba(8, 8, 8, 0.85); }", -1); + text_view.get_style_context ().add_provider ( + style, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + } + catch (Error e) + { + debug ("BannerDialog text_view style error: %s", e.message); + } + + vbox.pack_start (text_view, true, true, 0); + + /* --- Second separator --- */ + var sep2 = new Gtk.Separator (Gtk.Orientation.HORIZONTAL); + sep2.visible = true; + vbox.pack_start (sep2, false, false, 0); + + /* --- Button row --- */ + var button_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); + button_box.visible = true; + button_box.halign = Gtk.Align.END; + vbox.pack_start (button_box, false, false, 0); + + var accept_button = new Gtk.Button.with_label (_("I Accept")); + accept_button.visible = true; + accept_button.get_style_context ().add_class ("suggested-action"); + accept_button.clicked.connect (() => { accepted (); }); + button_box.pack_start (accept_button, false, false, 0); + + /* Give focus to the accept button when the widget is shown */ + show.connect (() => { accept_button.grab_focus (); }); + } + + /* ------------------------------------------------------------------ */ + /* Helpers */ + /* ------------------------------------------------------------------ */ + + /** + * Read the banner file, strip ANSI escape sequences, and expand the + * /etc/issue escape codes that are most commonly found in the wild. + */ + private string read_banner_text () + { + var banner_file = UGSettings.get_string (UGSettings.KEY_BANNER_FILE); + if (banner_file == "") + banner_file = "/etc/issue"; + + string contents = ""; + try + { + FileUtils.get_contents (banner_file, out contents); + } + catch (Error e) + { + warning ("BannerDialog: could not read '%s': %s", banner_file, e.message); + /* Translators: shown when the banner file cannot be read */ + return _("(Could not read the login banner file.)"); + } + + /* Strip ANSI/VT100 escape sequences: ESC [ … */ + try + { + var ansi_re = new Regex ("\x1b\\[[0-9;]*[a-zA-Z]"); + contents = ansi_re.replace (contents, -1, 0, ""); + } + catch (RegexError e) + { + debug ("BannerDialog: ANSI regex error: %s", e.message); + } + + /* Expand common /etc/issue escape codes */ + try + { + /* \n or \N → hostname */ + var hostname = GLib.Environment.get_host_name (); + var re = new Regex ("\\\\[nN]"); + contents = re.replace (contents, -1, 0, hostname); + + /* \s → kernel/OS name */ + re = new Regex ("\\\\s"); + contents = re.replace (contents, -1, 0, "Linux"); + + /* \r → kernel release */ + re = new Regex ("\\\\r"); + contents = re.replace (contents, -1, 0, ""); + + /* \v → kernel version */ + re = new Regex ("\\\\v"); + contents = re.replace (contents, -1, 0, ""); + + /* \m → machine arch */ + re = new Regex ("\\\\m"); + contents = re.replace (contents, -1, 0, ""); + + /* \l → virtual console / tty */ + re = new Regex ("\\\\l"); + contents = re.replace (contents, -1, 0, ""); + + /* \o → domain name */ + re = new Regex ("\\\\o"); + contents = re.replace (contents, -1, 0, ""); + + /* \d → date, \t → time (drop them) */ + re = new Regex ("\\\\[dt]"); + contents = re.replace (contents, -1, 0, ""); + + /* Drop any remaining backslash-letter sequences */ + re = new Regex ("\\\\[a-zA-Z]"); + contents = re.replace (contents, -1, 0, ""); + } + catch (RegexError e) + { + debug ("BannerDialog: issue-escape regex error: %s", e.message); + } + + return contents.strip (); + } + + /* ------------------------------------------------------------------ */ + /* Monitor management (mirrors ShutdownDialog) */ + /* ------------------------------------------------------------------ */ + + public void set_active_monitor (Monitor m) + { + if (monitor != null && m.equals (monitor)) + return; + + monitor = m; + set_size_request (monitor.width, monitor.height); + queue_resize (); + } + + public override void size_allocate (Gtk.Allocation allocation) + { + base.size_allocate (allocation); + + /* Overlay covers the entire monitor */ + overlay_events.size_allocate (allocation); + + /* Dialog box is centered */ + var dialog_alloc = Gtk.Allocation (); + int min_w, nat_w, min_h, nat_h; + vbox_events.get_preferred_width (out min_w, out nat_w); + vbox_events.get_preferred_height_for_width (nat_w, out min_h, out nat_h); + + var dlg_w = int.min (nat_w, allocation.width - 80); + var dlg_h = int.min (nat_h, allocation.height - 120); + + dialog_alloc.x = allocation.x + (allocation.width - dlg_w) / 2; + dialog_alloc.y = allocation.y + (allocation.height - dlg_h) / 2; + dialog_alloc.width = dlg_w; + dialog_alloc.height = dlg_h; + + vbox_events.size_allocate (dialog_alloc); + } + + /* Darken the greeter background while the dialog is up */ + public override bool draw (Cairo.Context c) + { + c.set_source_rgba (0.0, 0.0, 0.0, 0.55); + c.paint (); + return base.draw (c); + } +} diff --git a/src/main-window.vala b/src/main-window.vala index b275ff7..81ed30b 100644 --- a/src/main-window.vala +++ b/src/main-window.vala @@ -33,6 +33,7 @@ public class MainWindow : Gtk.Window private Gtk.Box content_box; private Gtk.Button back_button; private ShutdownDialog? shutdown_dialog = null; + private BannerDialog? banner_dialog = null; private bool do_resize; public ListStack stack; @@ -352,6 +353,12 @@ public class MainWindow : Gtk.Window shutdown_dialog.set_active_monitor (monitor); background.move (shutdown_dialog, monitor.x, monitor.y); } + + if (banner_dialog != null) + { + banner_dialog.set_active_monitor (monitor); + background.move (banner_dialog, monitor.x, monitor.y); + } } private void add_user_list () @@ -494,4 +501,37 @@ public class MainWindow : Gtk.Window login_box.sensitive = true; } + + /** Show the /etc/issue acceptance banner; disables the login box until accepted. */ + public void show_banner_dialog () + { + if (banner_dialog != null) + return; + + /* Block login input while banner is displayed */ + login_box.sensitive = false; + + banner_dialog = new BannerDialog (background); + banner_dialog.set_active_monitor (active_monitor); + + banner_dialog.accepted.connect (() => + { + close_banner_dialog (); + }); + + background.add (banner_dialog); + move_to_monitor (active_monitor); + banner_dialog.visible = true; + } + + private void close_banner_dialog () + { + if (banner_dialog == null) + return; + + banner_dialog.destroy (); + banner_dialog = null; + + login_box.sensitive = true; + } } diff --git a/src/meson.build b/src/meson.build index 8bca6c2..a523370 100644 --- a/src/meson.build +++ b/src/meson.build @@ -4,6 +4,7 @@ slick_greeter_sources = files( 'xsync.vapi', 'animate-timer.vala', 'background.vala', + 'banner-dialog.vala', 'cached-image.vala', 'cairo-utils.vala', 'email-autocompleter.vala', diff --git a/src/settings.vala b/src/settings.vala index ab8a838..3ba5d0e 100644 --- a/src/settings.vala +++ b/src/settings.vala @@ -54,6 +54,8 @@ public class UGSettings public const string KEY_CLOCK_FORMAT = "clock-format"; public const string KEY_ONSCREEN_KEYBOARD_LAYOUT = "onscreen-keyboard-layout"; public const string KEY_CONTENT_ALIGN = "content-align"; + public const string KEY_SHOW_BANNER = "show-banner"; + public const string KEY_BANNER_FILE = "banner-file"; public static bool get_boolean (string key) { @@ -152,6 +154,9 @@ public class UGSettings bool_keys.append (KEY_SHOW_QUIT); bool_keys.append (KEY_XFT_ANTIALIAS); bool_keys.append (KEY_ACTIVATE_NUMLOCK); + bool_keys.append (KEY_SHOW_BANNER); + + string_keys.append (KEY_BANNER_FILE); var int_keys = new List (); int_keys.append (KEY_XFT_DPI); diff --git a/src/slick-greeter.vala b/src/slick-greeter.vala index 5db59cd..4dfeb5e 100644 --- a/src/slick-greeter.vala +++ b/src/slick-greeter.vala @@ -317,6 +317,19 @@ public class SlickGreeter main_window.show (); main_window.get_window ().focus (Gdk.CURRENT_TIME); main_window.set_keyboard_state (); + + /* Show the /etc/issue acceptance banner if enabled */ + if (UGSettings.get_boolean (UGSettings.KEY_SHOW_BANNER)) + { + var banner_file = UGSettings.get_string (UGSettings.KEY_BANNER_FILE); + if (banner_file == "") + banner_file = "/etc/issue"; + + if (FileUtils.test (banner_file, FileTest.EXISTS)) + main_window.show_banner_dialog (); + else + debug ("show-banner is enabled but '%s' does not exist; skipping banner", banner_file); + } } public bool is_authenticated ()