Commit 1bbe8161 authored by Cool Fire's avatar Cool Fire

First production test release

parent 158d6397
tmp/hieradata
tmp/requests.yaml
config.yaml
---
stages:
- testing
- deployment
ruby_syntax:
stage: testing
image: ruby:2.5
script:
- ruby -v
- find . -name '*\.rb' -exec bash -c 'echo -ne "{}\t\t\t" && ruby -c {}' \;
ruby_lint:
stage: testing
image: ruby:2.5
script:
- gem install rubocop --no-ri --no-rdoc
- rubocop -P -f s
yaml_lint:
stage: testing
image: python:3.6
script:
- pip3 install yamllint
- yamllint -v
- yamllint -s .
ruby_bundler:
stage: testing
image: ruby:2.5
script:
- gem install bundler -no-ri --no-rdoc
- bundle install
deploy:
stage: deployment
script:
- echo "Do nothing yet."
# - apt-get update -qq
# - 'which ssh-agent || (apt-get install -y -qq openssh-client)'
# - eval $(ssh-agent -s)
# - ssh-add <(echo "$SSH_PRIVATE_KEY")
# - mkdir -p ~/.ssh
# - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
# - bash deploy.sh
# - bash populate_config.sh
image: ubuntu:18.04
only:
- master
---
Metrics/LineLength:
Max: 120
# frozen_string_literal: true
source 'https://rubygems.org'
gem 'git'
gem 'mysql2'
gem 'net-ldap'
gem 'pony'
gem 'sinatra'
gem 'sinatra-contrib'
group :test, :development do
gem 'rack'
gem 'rubocop'
end
GEM
remote: https://rubygems.org/
specs:
activesupport (5.2.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
ast (2.4.0)
backports (3.11.4)
concurrent-ruby (1.0.5)
git (1.5.0)
i18n (1.1.0)
concurrent-ruby (~> 1.0)
jaro_winkler (1.5.1)
mail (2.7.0)
mini_mime (>= 0.1.1)
mini_mime (1.0.1)
minitest (5.11.3)
multi_json (1.13.1)
mustermann (1.0.3)
mysql2 (0.5.2)
net-ldap (0.16.1)
parallel (1.12.1)
parser (2.5.1.2)
ast (~> 2.4.0)
pony (1.12)
mail (>= 2.0)
powerpack (0.1.2)
rack (2.0.5)
rack-protection (2.0.3)
rack
rainbow (3.0.0)
rubocop (0.58.2)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
parser (>= 2.5, != 2.5.1.1)
powerpack (~> 0.1)
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
ruby-progressbar (1.10.0)
sinatra (2.0.3)
mustermann (~> 1.0)
rack (~> 2.0)
rack-protection (= 2.0.3)
tilt (~> 2.0)
sinatra-contrib (2.0.3)
activesupport (>= 4.0.0)
backports (>= 2.8.2)
multi_json
mustermann (~> 1.0)
rack-protection (= 2.0.3)
sinatra (= 2.0.3)
tilt (>= 1.3, < 3)
thread_safe (0.3.6)
tilt (2.0.8)
tzinfo (1.2.5)
thread_safe (~> 0.1)
unicode-display_width (1.4.0)
PLATFORMS
ruby
DEPENDENCIES
git
mysql2
net-ldap
pony
rack
rubocop
sinatra
sinatra-contrib
BUNDLED WITH
1.16.4
API to allow limited access to puppet configuration and LDAP
## Quick function reference
### Function: `POST /v1/apache/vhost/list`:
List configured vhosts
- Requires API key in `key` parameter to access
### Function: `POST /v1/apache/vhost/request`:
Request a new vhost
- Requires API key in `key` parameter to access
- Requires additional parameter to be set: `domain`
- Allows optional parameter to be set: `comment`
- Allows optional parameter to be set: `documentroot`
- Allows optional parameter to be set: `strongtls`
### Function: `POST /v1/apache/vhost/disable`:
Disable a vhost
- Requires API key in `key` parameter to access
- Requires additional parameter to be set: `domain`
### Function: `POST /v1/apache/vhost/enable`:
Enable a vhost
- Requires API key in `key` parameter to access
- Requires additional parameter to be set: `domain`
### Function: `POST /v1/apache/vhost/new`:
Create a new vhost
- Requires API key in `key` parameter to access
- Requires administrative privileges
- Requires additional parameters to be set: `domain`, `username`
- Allows optional parameter to be set: `strongtls`
- Allows optional parameter to be set: `documentroot`
### Function: `POST /v1/limit/list`:
List configured limites
- Requires API key in `key` parameter to access
### Function: `POST /v1/limit/disk/request`:
Request new disk quota
- Requires API key in `key` parameter to access
- Requires additional parameter to be set: `limit`
- Allows optional parameter to be set: `comment`
### Function: `POST /v1/limit/disk/set`:
Set a users disk quota
- Requires API key in `key` parameter to access
- Requires administrative privileges
- Requires additional parameters to be set: `username`, `limit`
### Function: `POST /v1/limit/disk/get`:
Get a users current disk quota
- Requires API key in `key` parameter to access
- Requires administrative privileges
- Requires additional parameter to be set: `username`
### Function: `POST /v1/limit/process/request`:
Request higher process count limit
- Requires API key in `key` parameter to access
- Allows optional parameter to be set: `comment`
### Function: `POST /v1/limit/process/set`:
Set higher process count limit
- Requires API key in `key` parameter to access
- Requires administrative privileges
- Requires additional parameter to be set: `username`
### Function: `POST /v1/limit/cpu/request`:
Request new cpu shares cap
- Requires API key in `key` parameter to access
- Requires additional parameter to be set: `limit`
- Allows optional parameter to be set: `comment`
### Function: `POST /v1/limit/cpu/set`:
Set a users cpu shares cap
- Requires API key in `key` parameter to access
- Requires administrative privileges
- Requires additional parameters to be set: `username`, `limit`
### Function: `POST /v1/limit/cpu/get`:
Get a users cpu shares cap
- Requires API key in `key` parameter to access
- Requires administrative privileges
- Requires additional parameter to be set: `username`
### Function: `POST /v1/limit/memory/request`:
Request new memory usage cap
- Requires API key in `key` parameter to access
- Requires additional parameter to be set: `limit`
- Allows optional parameter to be set: `comment`
### Function: `POST /v1/limit/memory/set`:
Set a users memory usage cap
- Requires API key in `key` parameter to access
- Requires administrative privileges
- Requires additional parameters to be set: `username`, `limit`
### Function: `POST /v1/limit/memory/get`:
Get a users memory usage cap
- Requires API key in `key` parameter to access
- Requires administrative privileges
- Requires additional parameter to be set: `username`
### Function: `POST /v1/mysql/list`:
List configured databases
- Requires API key in `key` parameter to access
### Function: `POST /v1/mysql/request`:
Request a database
- Requires API key in `key` parameter to access
- Requires additional parameter to be set: `dbname`
- Allows optional parameter to be set: `comment`
### Function: `POST /v1/mysql/resetpassword`:
Generate a new password for MySQL database
- Requires API key in `key` parameter to access
- Requires additional parameter to be set: `dbname`
### Function: `POST /v1/mysql/new`:
Add a new database
- Requires API key in `key` parameter to access
- Requires administrative privileges
- Requires additional parameters to be set: `username`, `dbname`
### Function: `POST /v1/package/request`:
Request package install
- Requires API key in `key` parameter to access
- Requires additional parameter to be set: `package`
- Allows optional parameter to be set: `comment`
- Allows optional parameter to be set: `backports`
### Function: `POST /v1/package/new`:
Install a package
- Requires API key in `key` parameter to access
- Requires administrative privileges
- Requires additional parameter to be set: `package`
### Function: `POST /v1/request/delete`:
Delete a pending request
- Requires API key in `key` parameter to access
- Requires administrative privileges
- Requires additional parameter to be set: `id`
### Function: `POST /v1/request/list`:
List all pending requests
- Requires API key in `key` parameter to access
- Requires administrative privileges
### Function: `POST /v1/notify/irc`:
Send notification via IRC
- Requires API key in `key` parameter to access
- Requires administrative privileges
- Requires additional parameter to be set: `user`
### Function: `POST /v1/notify/email`:
Send notification via email
- Requires API key in `key` parameter to access
- Requires administrative privileges
- Requires additional parameter to be set: `user`
### Function: `GET /v1/test`:
Function to help people test if their implemntation is working
### Function: `POST /v1/test/key`:
Function to help people test if their implemntation is working
- Requires API key in `key` parameter to access
### Function: `POST /v1/user/admin`:
Check if API key can do admin-only commands
- Requires API key in `key` parameter to access
- Requires administrative privileges
### Function: `POST /v1/user/new`:
Create new user account
- Requires API key in `key` parameter to access
- Requires administrative privileges
- Requires additional parameter to be set: `username`
### Function: `POST /v1/user/remove`:
Mark user account as removed
- Requires API key in `key` parameter to access
- Requires administrative privileges
- Requires additional parameter to be set: `username`
### Function: `POST /v1/user/delete`:
Delete user account
- Requires API key in `key` parameter to access
- Requires administrative privileges
- Requires additional parameter to be set: `username`
### Function: `POST /v1/user/shell/get`:
Get currently set shell from LDAP
- Requires API key in `key` parameter to access
### Function: `POST /v1/user/shell/set`:
Set shell in LDAP
- Requires API key in `key` parameter to access
- Requires additional parameter to be set: `shell`
### Function: `POST /v1/user/apikey/reset`:
Reset user API key
- Requires API key in `key` parameter to access
- Requires administrative privileges
- Requires additional parameter to be set: `username`
### Function: `POST /v1/user/password/reset`:
Reset user password
- Requires API key in `key` parameter to access
- Requires administrative privileges
- Requires additional parameter to be set: `username`
### Function: `POST /v1/user/requests/list`:
List pending requests
- Requires API key in `key` parameter to access
### Function: `POST /v1/user/email/get`:
Retrieve configured contact email address
- Requires API key in `key` parameter to access
### Function: `POST /v1/user/email/set`:
Set a contact email address
- Requires API key in `key` parameter to access
- Requires additional parameter to be set: `email`
### Function: `POST /v1/user/invite/new`:
Send an invite
- Requires API key in `key` parameter to access
- Requires additional parameter to be set: `email`
---
repo:
location: 'tmp/hieradata'
name: 'hieradata'
base: 'tmp/'
uri: 'ssh://git@git.insomnia247.nl:33/puppet/hieradata.git'
yamlfile: 'tmp/hieradata/node/lydia6.insomnia247.nl.yaml'
requests:
yamlfile: 'tmp/requests.yaml'
ldap:
server: ldap.insomnia247.nl
base: dc=insomnia247,dc=nl
oup: ou=People
oug: ou=Group
user: cn=ldapsuperusername
pass: placeholderpassword
shells:
- /bin/sh
- /bin/dash
- /bin/bash
- /bin/rbash
- /usr/bin/fish
- /bin/ksh93
- /bin/rksh93
- /usr/bin/screen
- /bin/tcsh
- /usr/bin/tcsh
- /usr/bin/tmux
- /bin/zsh
- /usr/bin/zsh
mysql:
server: 127.0.0.1
port: 3306
database: database_name
username: database_user
password: secret
timeout: 10
notify:
irc:
channel: '#shells'
admins:
- Cool_Fire
- cFire
endpoint: http://nanoapi.insomnia247.nl/v1/message
key: secret_stuff
mail:
server: mail.insomnia247.nl
from: no-reply@insomnia247.nl
# frozen_string_literal: true
require File.expand_path 'manager2_api.rb', __dir__
run Manager2Api.new
# frozen_string_literal: true
files = Dir.glob('routes/**/*\.rb').sort
lastline = ''
files.each do |file|
content = File.read(file)
content.each_line do |line|
if line =~ /(get|post) '(.+)' do/i
puts "\n### Function: `#{Regexp.last_match(1).upcase} #{Regexp.last_match(2)}`:"
unless lastline.match?(/rubocop/)
puts Regexp.last_match(1) if lastline =~ /^\s*#\s+(.+)$/
end
elsif line.match?(/check_key\(params\)/)
puts ' - Requires API key in `key` parameter to access'
elsif line.match?(/check_key_admin\(params\)/)
puts " - Requires API key in `key` parameter to access\n - Requires administrative privileges"
elsif line =~ /check_param\(params\,\s*'(.+)'\)/
puts " - Requires additional parameter to be set: `#{Regexp.last_match(1)}`"
elsif line =~ /check_params\(params\,\s*%w\[(.+)\]\)/
puts " - Requires additional parameters to be set: `#{Regexp.last_match(1).split.join('`, `')}`"
elsif line =~ /params\.key\?[\s(]*'(.+?)'/
puts " - Allows optional parameter to be set: `#{Regexp.last_match(1)}`"
end
lastline = line
end
end
# frozen_string_literal: true
# rubocop:disable Metrics/ClassLength
# Internal helper functions
class Manager2Api < Sinatra::Base
before do
@ldap = ldap_connect
end
# rubocop:disable Metrics/BlockLength
helpers do
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
# Connect to LDAP server and return LDAP connection object
def ldap_connect
connection = Net::LDAP.new(
host: settings.config['ldap']['server'],
port: 389,
auth: {
method: :simple,
username: "#{settings.config['ldap']['user']},#{settings.config['ldap']['base']}",
password: settings.config['ldap']['pass']
}
)
begin
connection.bind
rescue Net::LDAP::Error => e
log_error "Cannot bind to LDAP server: #{e}"
ldap_error false
end
connection
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength
# Throw generic ldap error response
def ldap_error(objready = true)
log_error @ldap.get_operation_result if objready
halt erb :error, locals: {
code: 500,
message: 'Internal error: Contact administrator'
}
end
# Query ldap for all ids of a certain type
def ldap_get_all_ids(ouname, filter)
@ldap.search(
base: "#{ouname},#{settings.config['ldap']['base']}",
filter: "#{filter}=*",
attributes: [filter]
).map { |e| e[filter].first.to_i }
end
# Get first free id from a list
def ldap_get_next_free_id(idlist, newid)
newid += 1 while idlist.include? newid
newid
end
# Get next free UID
def ldap_get_next_uid(startuid = 1000)
uids = ldap_get_all_ids settings.config['ldap']['oup'], 'uidnumber'
ldap_get_next_free_id uids, startuid
end
# Get next free GID
def ldap_get_next_gid(startgid = 1000)
gids = ldap_get_all_ids settings.config['ldap']['oug'], 'gidnumber'
ldap_get_next_free_id gids, startgid
end
# rubocop:disable Metrics/MethodLength
# Create a group
def ldap_create_group(groupname)
groupdn = "cn=#{groupname},#{settings.config['ldap']['oug']},#{settings.config['ldap']['base']}"
newgid = ldap_get_next_gid
grpattrs = {
cn: groupname,
objectclass: %w[posixGroup top],
userpassword: '{crypt}x',
gidnumber: newgid.to_s
}
# Add to LDAP server
unless @ldap.add(dn: groupdn, attributes: grpattrs)
log_error "Failed to add LDAP object: #{groupdn}"
ldap_error
end
log_info "Added LDAP object: #{groupdn}"
newgid
end
# rubocop:enable Metrics/MethodLength
# Remove a group
def ldap_delete_group(groupname)
groupdn = "cn=#{groupname},#{settings.config['ldap']['oug']},#{settings.config['ldap']['base']}"
# Remove from LDAP server
unless @ldap.delete(dn: groupdn)
log_error "Failed to delete LDAP object: #{groupdn}"
ldap_error
end
log_info "Deleted LDAP object: #{groupdn}"
end
# Check if a user is part of a group
def ldap_check_user_in_group(username, groupname)
@ldap.search(
base: "#{settings.config['ldap']['oug']},#{settings.config['ldap']['base']}",
filter: "cn=#{groupname}",
attributes: ['memberuid'],
return_result: true
).first['memberuid'].include? username
end
# rubocop:disable Metrics/MethodLength
# Remove user from extra group
def ldap_remove_user_from_group(username, groupname)
groupdn = "cn=#{groupname},#{settings.config['ldap']['oug']},#{settings.config['ldap']['base']}"
unless @ldap.modify(
dn: groupdn,
operations: [
[:delete, :memberuid, username]
]
)
log_error "Failed to modify LDAP object: #{groupdn}"
ldap_error
end
log_info "Modified LDAP object: #{groupdn}"
end
# rubocop:enable Metrics/MethodLength
# rubocop:disable Metrics/MethodLength
# Add user to extra group
def ldap_add_user_to_group(username, groupname)
groupdn = "cn=#{groupname},#{settings.config['ldap']['oug']},#{settings.config['ldap']['base']}"
unless @ldap.modify(
dn: groupdn,
operations: [
[:add, :memberuid, username]
]
)
log_error "Failed to modify LDAP object: #{groupdn}"
ldap_error
end
log_info "Modified LDAP object: #{groupdn}"
end
# rubocop:enable Metrics/MethodLength
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
# Create a user
def ldap_create_user(username, groupid, homedir, nologin = false)
password = generate_password
pwhash = generate_password_hash(password, nologin)
newuid = ldap_get_next_uid
userdn = "uid=#{username},#{settings.config['ldap']['oup']},#{settings.config['ldap']['base']}"
userattrs = {
cn: username,
gidnumber: groupid.to_s,
homedirectory: homedir,
loginshell: '/bin/bash',
objectclass: %w[account posixAccount top shadowAccount],
shadowlastchange: '0',
shadowmax: '99999',
shadowwarning: '7',
uid: username,
uidnumber: newuid.to_s,
userpassword: pwhash
}
# Add object to LDAP server
unless @ldap.add(dn: userdn, attributes: userattrs)
log_error "Failed to add LDAP object: #{userdn}"
ldap_error
end
log_info "Added LDAP object: #{userdn}"
password unless nologin
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength
# Remove a user
def ldap_delete_user(username)
userdn = "uid=#{username},#{settings.config['ldap']['oup']},#{settings.config['ldap']['base']}"
# Remove from LDAP server
unless @ldap.delete(dn: userdn)
log_error "Failed to delete LDAP object: #{userdn}"
ldap_error
end
log_info "Deleted LDAP object: #{userdn}"
end
# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/AbcSize
# Reset a password
def ldap_password_reset(username, notify = true)
password = generate_password
pwhash = generate_password_hash(password, false)
userdn = "uid=#{username},#{settings.config['ldap']['oup']},#{settings.config['ldap']['base']}"
unless @ldap.modify(
dn: userdn,
operations: [
[:replace, :userPassword, pwhash],
[:replace, :shadowLastChange, '0']
]
)
log_error "Failed to modify LDAP object: #{userdn}"
ldap_error
end
# Notify user of new password
if notify
notify_mail_user(
username,
'Password reset',
(erb :'email/newuser_password', locals: { password: password })
)
end
log_info "Modified LDAP object: #{userdn}"
password
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength
# Functions to configure login shell in LDAP server
def ldap_get_shell(username)
@ldap.search(
base: "#{settings.config['ldap']['oup']},#{settings.config['ldap']['base']}",
filter: "uid=#{username}",
attributes: ['loginShell'],
return_result: true
).first['loginShell'].first
end
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
def ldap_set_shell(username, shell)
return false unless settings.config['ldap']['shells'].include? shell
userdn = "uid=#{username},#{settings.config['ldap']['oup']},#{settings.config['ldap']['base']}"
unless @ldap.modify(
dn: userdn,
operations: [
[:replace, :loginShell, shell]
]
)
log_error "Failed to modify LDAP object: #{userdn}"
ldap_error
end
log_info "Modified LDAP object: #{userdn}"
log_info "Set shell to: #{shell}"
true
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength
end
# rubocop:enable Metrics/BlockLength
end
# rubocop:enable Metrics/ClassLength
# frozen_string_literal: true
# Helper functions for manipulating limits
class Manager2Api < Sinatra::Base
# rubocop:disable Metrics/BlockLength
helpers do
# Disk quotas
def limit_disk_get(username)
data = git_get
return 'No disk quota set, defaults apply' unless data[username]['properties'].key? 'quota'
data[username]['properties']['quota']
end
def limit_disk_set(username, limit)