Commit edde348d authored by ale's avatar ale
Browse files

initial commit

parents
git-ssh-acl
===========
A simple and lightweight access control checker for accessing Git
repositories over SSH, without creating actual system accounts for
users. The configuration consists of a JSON file (rather than a
separate Git repository like gitolite). The tool doesn't implement
many features besides read-only/read-write access lists for each
repository. Access control is only checked at the repository level (no
ACLs for branches etc).
All configuration is stored below the `/etc/git-ssh-acl` directory.
## Installation
To build from source:
$ go build -o git-ssh-acl main.go
Then copy the resulting binary to a directory in your PATH, like
`/usr/local/bin`.
## Usage
Let's assume that the system user for Git repositories is `git`, and
that you want to control access to a bare Git repository named
`test.git` to two separate users, one allowed to commit (`user`) and
the other limited to read-only access (`deploy`).
Store the SSH public key for *user* in
`/etc/git-ssh-acl/users/user.key`, and the one for *deploy* in
`/etc/git-ssh-acl/users/deploy.key`.
Write the configuration to `/etc/git-ssh-acl/config.json` in JSON
format. It should contain a list of repositories:
[
{
"path": "test.git",
"rw": ["user"],
"ro": ["deploy"]
}
]
The `rw` entry in the repository information object defines the list
of users that are allowed to commit to the repository, while the `ro`
entry contains the list of users with read-only access. Any other user
will be denied access.
Finally, update the `authorized_keys` file for the system-wide Git
user:
$ sudo -u git git-ssh-acl --update ~git/.ssh/authorized_keys
package main
import (
"bufio"
"encoding/json"
"errors"
"flag"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
)
var (
configPath = flag.String("config", "/etc/git-ssh-acl", "config directory")
doUpdateKeys = flag.Bool("update", false, "update authorized_keys file")
)
type RepoAuth struct {
Path string `json:"path"`
Writers []string `json:"rw,omitempty"`
Readers []string `json:"ro,omitempty"`
}
func (r *RepoAuth) CheckACL(cmd, user string) bool {
switch cmd {
case "git-receive-pack", "git receive-pack":
return contains(r.Writers, user)
case "git-upload-pack", "git upload-pack", "git-upload-archive", "git upload-archive":
return contains(r.Writers, user) || contains(r.Readers, user)
}
return false
}
func contains(l []string, s string) bool {
for _, elem := range l {
if elem == s {
return true
}
}
return false
}
func readConfig(path string) ([]*RepoAuth, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var conf []*RepoAuth
if err := json.NewDecoder(f).Decode(&conf); err != nil {
return nil, err
}
return conf, nil
}
func parseCommand(s string) (string, string, error) {
parts := strings.SplitN(s, " ", 2)
if len(parts) < 2 {
return "", "", errors.New("not enough arguments")
}
action := parts[0]
arg := parts[1]
if action == "git" {
subparts := strings.SplitN(arg, " ", 2)
if len(subparts) < 2 {
return "", "", errors.New("not enough arguments")
}
action = fmt.Sprintf("%s %s", action, subparts[0])
arg = subparts[1]
}
return action, arg, nil
}
var pathRx = regexp.MustCompile(`^'/*([a-zA-Z0-9][a-zA-Z0-9._-]*(/[a-zA-Z0-9][a-zA-Z0-9._-]*)*)'$`)
func runGit(s, user string, conf []*RepoAuth) error {
cmd, arg, err := parseCommand(s)
if err != nil {
return err
}
matches := pathRx.FindStringSubmatch(arg)
if matches == nil || matches[1] == "" {
return errors.New("bad argument")
}
path := matches[1]
found := false
for _, repo := range conf {
if repo.Path == path {
if !repo.CheckACL(cmd, user) {
return errors.New("unauthorized")
}
found = true
break
}
}
if !found {
return errors.New("unknown repository")
}
gitCmd := exec.Command("git", "shell", "-c", fmt.Sprintf("%s %s", cmd, arg))
gitCmd.Stdout = os.Stdout
gitCmd.Stderr = os.Stderr
gitCmd.Stdin = os.Stdin
return gitCmd.Run()
}
func readSshKeys(path string) ([]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var keys []string
for s := bufio.NewScanner(f); s.Scan(); {
line := s.Text()
if strings.HasPrefix(line, "ssh-") {
keys = append(keys, line)
}
}
return keys, nil
}
func updateAuthorizedKeys(outputFile string) error {
filenames, err := filepath.Glob(fmt.Sprintf("%s/users/*.key", *configPath))
if err != nil {
return err
}
w, err := os.OpenFile(outputFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer w.Close()
for _, f := range filenames {
keys, err := readSshKeys(f)
if err != nil {
log.Printf("%v", err)
continue
}
user := strings.TrimSuffix(filepath.Base(f), ".key")
for _, k := range keys {
fmt.Fprintf(w, "command=\"%s %s\" %s\n", os.Args[0], user, k)
}
}
return nil
}
func main() {
log.SetFlags(0)
flag.Parse()
if *doUpdateKeys {
if flag.NArg() != 1 {
log.Fatal("Usage: git-ssh-acl --update <AUTHORIZED_KEYS_FILE>")
}
updateAuthorizedKeys(flag.Arg(0))
} else {
if flag.NArg() != 1 {
log.Fatal("Usage: git-ssh-acl <USER>")
}
user := flag.Arg(0)
conf, err := readConfig(filepath.Join(*configPath, "config.json"))
if err != nil {
log.Fatal(err)
}
cmd := os.Getenv("SSH_ORIGINAL_COMMAND")
if cmd == "" {
log.Fatal("SSH_ORIGINAL_COMMAND is unset")
}
if err := runGit(cmd, user, conf); err != nil {
log.Fatal(err)
}
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment