Skip to content

Instantly share code, notes, and snippets.

@snrbrnjna
Last active May 10, 2022 01:56
Show Gist options
  • Save snrbrnjna/422a4b7e017192c284b3 to your computer and use it in GitHub Desktop.
Save snrbrnjna/422a4b7e017192c284b3 to your computer and use it in GitHub Desktop.
Jekyll Auth Plugin - to manage http basic auth for jekyll generated pages and directories
#### Jekyll Layout: /_layouts/.htaccess
# Apache Configuration File
{% if ((page.auth_dir.users != empty) or (page.auth_dir.groups != empty)) %}
AuthName "Privater Bereich"
AuthType Basic
# => mehrere Require Blocks werden geodert: http://d43.me/blog/1157
AuthzUserAuthoritative Off
AuthUserFile {{ page.auth_remote_user_file }}
AuthGroupFile {{ page.auth_remote_group_file }}
{% if page.auth_dir.valid_user %}Require valid-user{% endif %}
{% if page.auth_dir.users != empty %}Require user {{ page.auth_dir.users | join:' ' }}{% endif %}
{% if page.auth_dir.groups != empty %}Require group {{ page.auth_dir.groups | join:' ' }}{% endif %}
<Files "favicon.ico">
Satisfy Any
Allow from all
</Files>
<Files "404.html">
Satisfy Any
Allow from all
</Files>
{% endif %}
{% if page.auth_dir.dir == '/' %}{% include auth/root_htaccess %}{% endif %}

Jekyll Auth Plugin

What

Plugin to manage http basic auth for jekyll generated pages and directories.

How

You specify users, groups and resources which should only be accessible for the specified users or groups. This can be done globally in the _config.yml file or on post/page level in the YAML Front Matter.

The plugin creates user-, group- and .htaccess files for the http basic auth stuff to work properly. The .htaccess files are merged into the other jekyll generated pages.
The user and group auth files have to get deployed on their own. They should not get deployed into the web root for security reasons. So specify the location of these files in the config.

For further details see the inline docs in jekyll_auth.rb.

#### Jekyll Configuration File: /_config.yml
# Authentication stuff
auth_local_user_file: /tmp/jekyll/.htusers
auth_local_group_file: /tmp/jekyll/.htgroups
auth_remote_user_file: /var/www/virtual/username/.htusers
auth_remote_group_file: /var/www/virtual/username/.htgroups
auth_dirs:
'/':
auth_groups: admin
'/assets':
auth_valid_user: true
'/css':
auth_valid_user: true
'/js':
auth_valid_user: true
auth_groups:
admin: hans buenaventura
auth_users:
hans: meier
buenaventura: durruti
# Jekyll plugin: /_plugins/jekyll_auth.rb
# DEBUG
# require 'debugger'
module Jekyll
# Jekyll Auth Plugin - Plugin to manage http basic auth for jekyll generated pages and directories.
#
# Configuration:
#
# - ``auth_local_user_file``:
# full qualified path to the locally generated user auth file;
# default: ``/tmp/.jekyll_user_file``.
# - ``auth_remote_user_file``:
# full qualified path to the user auth file, when it is deployed. is written
# into the .htaccess files; no default value.
# - ``auth_local_group_file``:
# full qualified path to the locally generated group auth file;
# default: ``/tmp/.jekyll_group_file``.
# - ``auth_remote_group_file``:
# full qualified path to the group auth file, when it is deployed. is written
# into the .htaccess files; no default value.
#
#
class AuthGenerator < Generator
priority :high
def generate site
puts; puts "#### Jekyll::Auth"
# Read in data
auth = Auth.new site
# Validate
auth.validate!
# Write user file
size = auth.users.size
puts "#{size} User#{size>1||size==0 ? 's' : ''} found#{size>0 ? ':' : ''}" # Logging Users
auth.generate_user_file
# Write group file
size = auth.groups.size
puts "#{size} Group#{size>1||size==0 ? 's' : ''} found#{size>0 ? ':' : ''}" # Logging Groups
auth.groups.values.each do |group|
puts " - #{group.groupname}: #{group.users.join(' ')}"
end
auth.generate_group_file
# Logging Resources
size = auth.directories.size
puts "#{size} Director#{size>1||size==0 ? 'ies' : 'y'} with restricted access found#{size>0 ? ':' : ''}"
auth.directories.values.each do |dir|
puts " - #{dir.dir}"
end
size = auth.files.size
puts "#{size} File#{size>1||size==0 ? 's' : ''} with restricted access (" +
"not supported yet".red + ") found#{size>0 ? ':' : ''}"
auth.files.values.each do |file|
puts " - #{file.path}"
end
# Write directives into .htaccess files
auth.write_directives(site)
end
end
class Auth
attr_reader :users, :groups, :directories, :files,
:remote_user_file, :remote_group_file
def initialize site
@users = {}; @groups = {}
@directories = {}; @files = {}
read_config site
read_posts site
# TODO: validate!!
end
def read_config site
@local_user_file = site.config['auth_local_user_file'] || '/tmp/.jekyll_user_file'
@remote_user_file = site.config['auth_remote_user_file']
@local_group_file = site.config['auth_local_group_file'] || '/tmp/.jekyll_group_file'
@remote_group_file = site.config['auth_remote_group_file']
add_users site.config['auth_users'] if(site.config['auth_users'])
add_groups site.config['auth_groups'] if(site.config['auth_groups'])
add_dirs site.config['auth_dirs'] if (site.config['auth_dirs'])
end
def read_posts site
# collect all posts and pages with auth info
payload = site.site_payload
auth_pp = payload['site']['posts'].concat(payload['site']['pages']).select do |post|
post.data.has_key?('auth_users') || post.data.has_key?('auth_groups')
end
# read auth info from every page or post
auth_pp.each do |p|
p_users = p.data['auth_users'] ? add_users(p.data['auth_users']) : []
p_groups = p.data['auth_groups'] ? add_groups(p.data['auth_groups']) : []
if (p.data['layout'] == 'set')
p.data['auth_dir'] = add_dir(p.url, p.data['auth_users'], p.data['auth_groups'], p.data['auth_valid_user'])
end
# TODO AuthFiles!
end
end
def validate!
unless (invalid_users = @users.values.select{|user| !user.valid?}).empty?
raise Exception.new "Invalid Auth-Users found in your config/posts: #{invalid_users.map(&:username).join(', ')}"
end
unless (invalid_groups = @groups.values.select{|group| !group.valid?}).empty?
raise Exception.new "Invalid Auth-Groups found in your config/posts: #{invalid_groups.map(&:groupname).join(', ')}"
end
end
def generate_user_file
# create user_file directory if not existant yet
user_dir = File.dirname(@local_user_file)
FileUtils.mkdir_p(user_dir) unless File.exists?(user_dir)
# generate user entries in user file
@users.values.each_with_index do |user, i|
if i == 0
user.generate_user_file @local_user_file
else
user.update_user_file @local_user_file
end
end
end
def generate_group_file
# create group_file directory if not existant yet
group_dir = File.dirname(@local_group_file)
FileUtils.mkdir_p(group_dir) unless File.exists?(group_dir)
# generate group entries in group file
File.open(@local_group_file, 'w') do |groupfile|
@groups.values.each do |auth_group|
groupfile.puts "#{auth_group.groupname}: #{auth_group.users.join(' ')}"
end
end
end
def write_directives site
create_htaccess_files site
end
def create_htaccess_files site
directories.values.each do |auth_dir|
site.pages << HtaccessFile.new(site, site.source, auth_dir, self)
end
end
# Hash with username => password mapping or String with space seperated
# usernames
# Returns Array of AuthUser objects
def add_users hash_or_string
new_users = []
if hash_or_string.is_a?(String)
hash_or_string.split(' ').each do |uname|
new_users << add_user(uname)
end
elsif hash_or_string.is_a?(Hash)
hash_or_string.each do |uname, pword|
new_users << add_user(uname, pword)
end
end
new_users
end
def add_user username, password=nil
if !@users[username]
@users[username] = AuthUser.new(username, password)
elsif password
if !@users[username].password
@users[username].password = password
elsif password != @users[username].password
raise Exception.new "User '#{username}' specified with different passwords!"
end
end
@users[username]
end
def add_groups hash_or_string
new_groups = []
if hash_or_string.is_a?(String)
hash_or_string.split(' ').each do |gname|
new_groups << add_group(gname)
end
elsif hash_or_string.is_a?(Hash)
hash_or_string.each do |gname, users|
new_groups << add_group(gname, users)
end
end
new_groups
end
def add_group name, users=''
if !@groups[name]
@groups[name] = AuthGroup.new(name, users.split(' '))
elsif !users.empty?
@groups[name].users += users.split(' ')
end
@groups[name]
end
# dirs is a Hash:
# with dirnames as keys and
# a Hash with auth_users, auth_groups, auth_valid_user as its values
def add_dirs dirs
new_dirs = []
dirs.each do |dir_name, dir|
new_dirs << add_dir(dir_name, dir['auth_users'], dir['auth_groups'], dir['auth_valid_user'])
end
new_dirs
end
def add_dir dir_name, users, groups, valid_user
# add users
new_users = users ? add_users(users) : []
# add groups
new_groups = groups ? add_groups(groups) : []
# add dirs
if !@directories[dir_name]
@directories[dir_name] = AuthDirectory.new(dir_name, new_users, new_groups, valid_user)
else
@directories[dir_name].users += new_users
@directories[dir_name].groups += new_groups
end
@directories[dir_name]
end
end # end Auth
class HtaccessFile < Page
# override Page#dir: which returns / or the path as described with #permalink
attr_accessor :dir
def initialize(site, base, auth_dir, auth)
@site = site
@base = base
@dir = auth_dir.dir
@name = '.htaccess'
self.process(@name)
self.read_yaml(File.join(base, '_layouts'), '.htaccess')
# pass data to the 'page'
self.data['auth_dir'] = auth_dir
self.data['auth_remote_user_file'] = auth.remote_user_file
self.data['auth_remote_group_file'] = auth.remote_group_file
end
end
class AuthResource
attr_reader :dir
attr_accessor :users, :groups
# users, groups are Arrays of AuthUser and AuthGroup objects
# valid_user is a boolean
def initialize dir, users, groups, valid_user
@users = users;
@valid_user = valid_user
@groups = groups;
@dir = dir
end
def to_liquid
{
'users' => @users.map(&:username).uniq,
'groups' => @groups.map(&:groupname).uniq,
'valid_user' => @valid_user,
'dir' => @dir
}
end
end
class AuthDirectory < AuthResource
end
# TODO: wird noch nicht geschrieben
class AuthFile < AuthResource
attr_reader :file, :path
def initialize path, users, groups, valid_user
super File.dirname(path), users, groups, valid_user
@path = path
@file = File.basename(path)
end
def to_liquid
super.merge({
'file' => @file
})
end
end
class AuthUser
attr_reader :username
attr_accessor :password
def initialize username, password
@username = username
@password = password
end
# username and password not empty, no spaces
def valid?
!username.nil? && !password.nil? && !username.empty? && !password.empty? &&
username.index(/\s/).nil? && password.index(/\s/).nil?
end
def generate_user_file user_file_path
`htpasswd -cb #{user_file_path} #{@username} #{@password}`
end
def update_user_file user_file_path
`htpasswd -b #{user_file_path} #{@username} #{@password}`
end
end
class AuthGroup
attr_reader :groupname
attr_accessor :users
# users: Array of usernames
def initialize groupname, users=[]
@groupname = groupname
@users = users
end
# groupname not empty, no spaces, users.any?
def valid?
!groupname.nil? && !groupname.empty? && groupname.index(/\s/).nil? && users.any?
end
end
end
# Jekyll include snippet: /_includes/auth/root_htaccess
# Here you place the global root htaccess file with all the apache conf stuff you need.
# i.e. something like that: https://github.com/h5bp/server-configs-apache
@apeltzer
Copy link

Well I've been trying this out, and apparently it seems to build something, too - but I still can access the stuff I'm supposed not to be accessing without a password prompt.

Jekyll::Auth

2 Users found:
Adding password for user hans
Adding password for user buenaventura
1 Group found:

  • admin: hans buenaventura
    5 Directories with restricted access found:
  • /
  • /assets
  • /css
  • /js
  • /projects
    0 Files with restricted access (not supported yet) found
    Generating Github feed from rss using feedzirra with github_feed.rb
    done.
    Server address: http://127.0.0.1:4000/
    Server running... press ctrl-c to stop.

That means everything at 127.0.0.1:4000/projects for example should require a password, right? Am I missing something here? Does this only work when using Apache2 and my WebRick based testing environment is just not working for this?

@swizca
Copy link

swizca commented Jan 7, 2017

Could you add something into your readme that explains ... so someone logs in to a directory and contributes something ... it would seem that that something would go into the live site. How does this something get back into the jekyll source dir to survive a jekyll build? Thx.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment