Userplex ![GoDoc](https://godoc.org/go.mozilla.org/userplex?status.svg)
Propagate users from LDAP to Puppet, AWS, Github, Datadog, ...
![Build Status](https://travis-ci.org/mozilla-services/userplex.svg)
Installation
If you don't yet have your GOPATH setup, follow these steps (an possibly add the
env variables to your .bashrc).
$ go version
go version go1.5 linux/amd64
$ export GOPATH="$HOME/go"
$ mkdir $GOPATH
$ export GO15VENDOREXPERIMENT=1
$ export PATH=$GOPATH/bin:$PATH
To download and install userplex into $GOPATH/bin/userplex
, run this command:
$ go get go.mozilla.org/userplex
(Optional) Install Glide for dependency management. All dependencies for userplex should be versioned in the vendor
directory, and any new dependencies must be accounted for via glide. glide update
to update dependencies, glide install
to install.
Configuration
Ldap
How to connect to the LDAP source.
uri
is the connection string that uses the format
<protocol>://<hostname>(:<port>)/<basedn>
username
the DN of the bind user
password
the password of the bind user
insecure
disables cert verification (chain of trust and name mismatch) when
using TLS or StartTLS
starttls
enables starttls when the protocol is ldap
(if the protocol is
ldaps
, then regular tls is used).
If a client cert is needed, put the PEM of the cert in tlscert
and the PEM
of the key in tlskey
.
Example:
ldap:
uri: "ldap://myldap.example.net/dc=example,dc=net"
username: "uid=bind-user,ou=logins,dc=example,dc=net"
password: "ohg81w0yofhd0193tyedlgh279eoqhsd"
insecure: false
starttls: true
tlscert: |
-----BEGIN CERTIFICATE-----
MIIEHzCCAwegAwIBAgIBBzANBgkqhkiG9w0BAQUFADByMQswCQYDVQQGEwJVUzET
CCpw
-----END CERTIFICATE-----
tlskey: |
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAs3OJduktoyACixovmcLZFryq36WOCkesjrBO9+ElGg3BtpoJ
Mgg==
-----END RSA PRIVATE KEY-----
Modules
Each module is documented in its own modules/ directory.
The base module configuration uses the following parameters:
name
is a module name that must map to a module in modules/<modulename>
ldapgroups
is a list of ldap group DNs
create
indicates whether user create is enabled
delete
indicates whether user deletion is enabled
reset
indicates whether user reset is enabled
uidmap
is a mapping of UIDs as described in the next section
credentials
contains module-specific credentials
parameters
contains module-specific parameters
modules:
- name: aws
ldapgroups:
- mysysadmins
- thedevelopers
create: true
delete: true
reset: true
uidmap: *CUSTOMMAP1
credentials:
accesskey: AKIAbbbb
secretkey: YOLOMAN
parameters:
assignroles:
- ldap_managed
One-off Reset
User accounts can be reset by passing their comma separated LDAP uids to the -reset
userplex
command line flag.
Example one-off command specification:
userplex -reset foo,bar
UID Maps
By default, userplex
will create users based on their LDAP uid
attribute. In
some instance, you may want to use a different login name, which is where UID
Maps come in handy.
A UID map is just a mapping between a LDAP UID ldapuid
and an effective UID
localuid
. The map is defined with a custom name and references in modules.
mycustomawsuidmap1: &CUSTOMMAP1
- ldapuid: bkelso
localuid: bob
- ldapuid: tanderson
localuid: neo
modules:
- name: authorizedkeys
uidmap: *CUSTOMMAP1
...
The map above will translate the ldap uids bkelso
and tanderson
into bob
and neo
, and then create the authorizedkeys files with the translated uids.
Notifications
Userplex provides a simple way for modules to send notifications to their users
when accounts are created, deleted, or reset.
Modules only need to send a notification in a channel provided by the main
userplex program, and don't need to know how to speak SMTP or other notification
protocol. Notifications are aggregated per user, such that N notifications to a
given user will only result in a single notification being sent, to reduce noise.
Userplex can also encrypt notifications with the user's public PGP key. This
requires two things:
-
the user must have a public PGP fingerprint in LDAP (see
mozldap.GetUserPGPFingerprint()
).
-
the module must set modules.Notifications.MustEncrypt=true
when publishing
the notification in the channel.
When aggregating notifications, Userplex will PGP encrypt the notification body
with the user's public key if at least one notification requires encryption.
The AWS module is an example of requiring encryption, because it sends user
credentials upon creation of an account.
Writing modules
A module must import go.mozilla.org/userplex/modules
.
A module registers itself at runtime via its init()
function which must
call modules.Register
with a module name and an instance implementing
modules.Moduler
.
package datadog
func init() {
modules.Register("datadog", new(module))
}
type module struct {
}
A module must have a unique name. A good practice is to use the same name for
the module name as for the Go package name. However, it is possible for a
single Go package to implement multiple modules, simply by registering
different Modulers with different names.
Configuration
Modules get their configuration from the modules.Configuration
struct passed
by the main program. The configuration contains a LDAP handler that is already
connected. Module-specific parameters go under the Parameters
interface, and
module-specific credentials under the Credentials
interface.
Example with the aws
module:
package aws
import (
"go.mozilla.org/userplex/modules"
//... other imports
)
func init() {
modules.Register("aws", new(module))
}
type module struct {
}
func (m *module) NewRun(c modules.Configuration) modules.Runner {
r := new(run)
r.Conf = c
return r
}
type run struct {
Conf modules.Configuration
p parameters
c credentials
cli *iam.IAM
}
// parameters are specific to the aws module and need to be accessed
// used r.Conf.GetParameters(¶ms) as show in Run()
type parameters struct {
IamGroups []string
NotifyNewUsers bool
SmtpRelay string
SmtpFrom string
AccountName string
}
// same logic as for the parameters
type credentials struct {
AccessKey string
SecretKey string
}
func (r *run) Run() (err error) {
err = r.Conf.GetParameters(&r.p)
if err != nil {
return
}
err = r.Conf.GetCredentials(&r.c)
if err != nil {
return
}
// We are setup, start doing work
// connect an IAM client using the credentials
var awsconf aws.Config
if r.c.AccessKey != "" && r.c.SecretKey != "" {
awscreds := awscred.NewStaticCredentials(r.c.AccessKey, r.c.SecretKey, "")
awsconf.Credentials = awscreds
}
r.cli = iam.New(&awsconf)
if r.cli == nil {
return fmt.Errorf("failed to connect to aws using access key %q", r.c.AccessKey)
}
// Retrieve a list of ldap users from the groups configured
ldapers = make(map[string]bool)
users, err := r.Conf.LdapCli.GetUsersInGroups(r.Conf.LdapGroups)
if err != nil {
return
}
for _, user := range users {
shortdn := strings.Split(user, ",")[0]
uid, err := r.Conf.LdapCli.GetUserId(shortdn)
if err != nil {
log.Printf("[warning] aws: ldap query failed with error %v", err)
continue
}
// apply the uid map: only store the translated uid in the ldapuid
for _, mapping := range r.Conf.UidMap {
if mapping.LdapUid == uid {
uid = mapping.LocalUID
}
}
ldapers[uid] = true
}
// create or add the users to groups.
for uid, _ := range ldapers {
resp, err := r.cli.GetUser(&iam.GetUserInput{
UserName: aws.String(uid),
})
if err != nil || resp == nil {
log.Printf("[info] user %q not found, needs to be created", uid)
r.createIamUser(uid)
// send a notification to the user
r.Conf.Notify.Channel <- modules.Notification{
Module: "aws",
Recipient: usermail,
Mode: r.Conf.Notify.Mode,
MustEncrypt: true,
Body: []byte(fmt.Sprintf(`New AWS account:
login: %s
pass: %s (change at first login)
url: %s`,
uid, password,
fmt.Sprintf("https://%s.signin.aws.amazon.com/console", r.p.AccountName))),
}
} else {
r.updateUserGroups(uid)
}
}
// etc...
return
}
Sending notifications
Modules receives a notification channel in run.Conf.Notify.Channel
that
accepts messages of type modules.Notification
. Sending notifications to users
is just a matter of publishing into that channel, and Userplex does the rest.
rcpt := r.Conf.Notify.Recipient
if rcpt == "{ldap:mail}" {
rcpt = myuseremail
}
r.Conf.Notify.Channel <- modules.Notification{
Module: "mymodule",
Recipient: rcpt,
Mode: r.Conf.Notify.Mode,
MustEncrypt: true,
Body: []byte(fmt.Sprintf(`New MyModule account:
login: %s
pass: %s (change at first login)
url: %s`, uid, password, url)),
}
License
Mozilla Public License 2.0
Authors
Julien Vehent ulfr@mozilla.com