22. User management¶
If you’ve gone through the Installation Guide, you might have noticed that we had to create a Cog user before you could run any commands. The reason we need a full user object and don’t just ask for a Slack username is central to Cog’s design; we need users so Cog can check permissions when running a command. And, continuing along those lines, we also keep users and chat handles separate so changing your Slack username or switching chat providers doesn’t force you to rework any of your assigned groups or roles.
While this design allows Cog to implement permisisons and groups in a sane way, it might be tedious to manage by hand, especially if you’re trying to keep it in sync with another service that holds your organization’s user data. So why not automate syncing your organizations users with Cog users? Below I’ll walk you through three different ways to automate user management: enabling self registration, writing a script with cogctl, and using Cog’s API directly.
22.1. Self Registration¶
The easiest way to avoid manually creating Cog users for each new member
of your team, is to enable “Self Registration” by setting the
environment variable COG_ALLOW_SELF_REGISTRATION=1
. Once enabled, if
Cog is contacted by a new user it does not know about, a user will
automatically be created for them with information populated from their
chat user and a randomly generated password. However, this new user will
have no permissions as it hasn’t been added to any groups yet. An admin
will have to manually assign them to a group to give them any
permissions. If you need more automation around setting specific user
details or want to also automatically assign them to specific groups,
writing a script around cogctl commands is the next best option.
22.2. User Management via Cogctl¶
You’ve probably already used cogctl to bootstrap Cog, but it has also been designed for use in shell scripts. Let’s walk through a quick example of importing users from a CSV file with a simple shell script.
First, let’s go over the cogctl commands we’ll be using.
cogctl user create
- Creates a Cog usercogctl chat-handle create
- Creates a chat handle for a given
usercogctl group add
- Adds a user to a given groupOur CSV file will look like this:
users.csv.
email,password,slack_username,groups
mark@operable.io,secr3t,imbriaco,"admin,raleigh"
kevin@operable.io,passw0rd,kevsmith,"admin,raleigh"
patrick@operable.io,k3yphrase,vanstee,"admin,atlanta"
We can pretty easily loop through each row, to create a user, add a chat handle and add them them to the set of groups.
import.sh.
#!/usr/bin/env bash
OLDIFS=$IFS
IFS=,
cat $1 | tail +2 | while read email password username groups; do
./cogctl user create $username \
--email $email \
--password $password < /dev/null
./cogctl chat-handle create $username slack $username < /dev/null
groups=$(echo "$groups" | sed -e 's/"//g')
read -ra groups <<< "$groups"
for group in "${groups[@]}"; do
./cogctl group add $group $username < /dev/null
done
done
IFS=$OLDIFS
There are a few caveat though. cogctl reads from STDIN, so you’ll
noticed I’ve added < /dev/null
to the end to avoid the while loop
from terminating after the first item. Also if you need to do any output
parsing it can be difficult since we emit structured text rather than
something easy to parse, like JSON. If you’re looking to do something
fancier, like an idempotent user sync based on email address,
interacting directly with the API is your best bet.
22.3. User Management via Cog’s API¶
While we created cogctl as an easy way to complete simple tasks, you can always use Cog’s HTTP API to handle more advanced work or cases where it’s not convenient to shell out to cogctl. In fact, it’s the same API used by cogctl, so the cogctl codebase can be a good place to start if you want to do something similar to an existing command.
If you’ve dealt with a modern HTTP API you’ll find Cog’s API to be pretty common. It (mostly) uses Restful URIs, it accepts and responds with JSON, and uses an Authorization header containing a token to authenticate a specific user. Here’s a quick example in Ruby similar to the one above that will handle cases like changing your username and removing someone from a group as well.
sync.rb.
#!/usr/bin/env ruby
require 'csv'
require 'httparty'
require 'json'
$username = ENV['COG_USERNAME']
$password = ENV['COG_PASSWORD']
$host = ENV['COG_HOST']
$port = ENV['COG_PORT']
$secure = ENV['COG_HTTPS'] == '1'
$address = "#{$secure ? 'https' : 'http'}://#{$host}:#{$port}"
def create_token
params = {username: $username, password: $password}
response = HTTParty.post("#{$address}/v1/token", body: params)
response.parsed_response["token"]["value"]
end
def find_user(email)
headers = {"Authorization" => "token #{create_token}"}
response = HTTParty.get("#{$address}/v1/users", headers: headers)
users = response.parsed_response["users"]
users.find { |u| u["email_address"] == email }
end
def create_user(user)
headers = {"Authorization" => "token #{create_token}"}
params = {user: user}
response = HTTParty.post("#{$address}/v1/users", headers: headers, body: params)
user = response.parsed_response["user"]
puts "Created user #{user["email_address"]}"
user
end
def update_user(user_id, username, password)
headers = {"Authorization" => "token #{create_token}"}
params = {user: {user_id: user_id, username: username, password: password}}
response = HTTParty.patch("#{$address}/v1/users/#{user_id}", headers: headers, body: params)
user = response.parsed_response["user"]
puts "Updated user #{user["email_address"]}"
user
end
def find_group(name)
headers = {"Authorization" => "token #{create_token}"}
response = HTTParty.get("#{$address}/v1/groups", headers: headers)
groups = response.parsed_response["groups"]
groups.find { |g| g["name"] == name }
end
def update_chat_handle(user, username)
headers = {"Authorization" => "token #{create_token}"}
params = {chat_handle: username}
response = HTTParty.post("#{$address}/v1/users/#{user["id"]}/chat_handles", headers: headers, body: params)
chat_handle = response.parsed_response["chat_handle"]
puts "Updated chat handle for user #{user["email_address"]}"
chat_handle
end
def manage_group(user, group_id, action)
headers = {"Authorization" => "token #{create_token}"}
params = {users: {action => [user["username"]]}}
response = HTTParty.post("#{$address}/v1/groups/#{group_id}/users", headers: headers, body: params)
group = response.parsed_response["group"]
adding = action == :add
puts "#{adding ? "Added" : "Removed"} user #{user["email_address"]} #{adding ? "to" : "from"} group #{group_id}"
group
end
CSV.foreach(ARGV.first, headers: true) do |csv_user|
if cog_user = find_user(csv_user["email_address"])
update_user(cog_user["id"], csv_user["username"], csv_user["password"])
else
cog_user = create_user(csv_user)
end
update_chat_handle(cog_user, csv_user["username"])
csv_groups = csv_user["groups"].split(",").map { |g| find_group(g) }.map { |g| g["id"] }
cog_groups = cog_user["groups"].map { |g| g["id"] }
groups_to_add = csv_groups - cog_groups
groups_to_remove = cog_groups - csv_groups
groups_to_add.each { |g| manage_group(cog_user, g, :add) }
groups_to_remove.each { |g| manage_group(cog_user, g, :remove) }
end