Skip to content
This repository has been archived by the owner on Apr 26, 2021. It is now read-only.

Commit

Permalink
Merge pull request #154 from scorphus/namespaces
Browse files Browse the repository at this point in the history
Support namespaces for repository
  • Loading branch information
andrewsmedina committed Aug 26, 2014
2 parents 2f9c0bd + 3e050fb commit 2cb2e32
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 46 deletions.
30 changes: 30 additions & 0 deletions api/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,36 @@ func (s *S) TestMaxMemoryValueDontResetMaxMemory(c *gocheck.C) {
c.Assert(maxMemoryValue(), gocheck.Equals, uint(359))
}

func (s *S) TestAccessParametersShouldReturnErrorWhenInvalidJSONInput(c *gocheck.C) {
b := bufferCloser{bytes.NewBufferString(``)}
_, _, err := accessParameters(b)
c.Assert(err, gocheck.ErrorMatches, `^Could not parse json: .+$`)
b = bufferCloser{bytes.NewBufferString(`{`)}
_, _, err = accessParameters(b)
c.Assert(err, gocheck.ErrorMatches, `^Could not parse json: .+$`)
b = bufferCloser{bytes.NewBufferString(`bang`)}
_, _, err = accessParameters(b)
c.Assert(err, gocheck.ErrorMatches, `^Could not parse json: .+$`)
b = bufferCloser{bytes.NewBufferString(` `)}
_, _, err = accessParameters(b)
c.Assert(err, gocheck.ErrorMatches, `^Could not parse json: .+$`)
}

func (s *S) TestAccessParametersShouldReturnErrorWhenNoUserListProvided(c *gocheck.C) {
b := bufferCloser{bytes.NewBufferString(`{"users": "oneuser"}`)}
_, _, err := accessParameters(b)
c.Assert(err, gocheck.ErrorMatches, `^Could not parse json: json: cannot unmarshal string into Go value of type \[\]string$`)
b = bufferCloser{bytes.NewBufferString(`{"repositories": ["barad-dur"]}`)}
_, _, err = accessParameters(b)
c.Assert(err, gocheck.ErrorMatches, `^It is need a user list$`)
}

func (s *S) TestAccessParametersShouldReturnErrorWhenNoRepositoryListProvided(c *gocheck.C) {
b := bufferCloser{bytes.NewBufferString(`{"users": ["nazgul"]}`)}
_, _, err := accessParameters(b)
c.Assert(err, gocheck.ErrorMatches, `^It is need a repository list$`)
}

func (s *S) TestNewUser(c *gocheck.C) {
b := strings.NewReader(fmt.Sprintf(`{"name": "brain", "keys": {"keyname": %q}}`, rawKey))
recorder, request := post("/user", b, c)
Expand Down
42 changes: 14 additions & 28 deletions bin/gandalf.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func action() string {
// and gets the repository from the database based on the info
// obtained by the SSH_ORIGINAL_COMMAND parse.
func requestedRepository() (repository.Repository, error) {
repoName, err := requestedRepositoryName()
_, repoName, err := parseGitCommand()
if err != nil {
return repository.Repository{}, err
}
Expand All @@ -79,30 +79,19 @@ func requestedRepository() (repository.Repository, error) {
return repo, nil
}

func requestedRepositoryName() (string, error) {
r, err := regexp.Compile(`[\w-]+ '/?([\w-]+)\.git'`)
if err != nil {
panic(err)
}
m := r.FindStringSubmatch(os.Getenv("SSH_ORIGINAL_COMMAND"))
if len(m) < 2 {
return "", errors.New("Cannot deduce repository name from command. You are probably trying to do something nasty")
}
return m[1], nil
}

// Checks whether a command is a valid git command
// The following format is allowed:
// git-([\w-]+) '([\w-]+)\.git'
func validateCmd() error {
r, err := regexp.Compile(`git-([\w-]+) '/?([\w-]+)\.git'`)
// (git-[a-z-]+) '/?([\w-+@][\w-+.@]*/)?([\w-]+)\.git'
func parseGitCommand() (command, name string, err error) {
r, err := regexp.Compile(`(git-[a-z-]+) '/?([\w-+@][\w-+.@]*/)?([\w-]+)\.git'`)
if err != nil {
panic(err)
}
if m := r.FindStringSubmatch(os.Getenv("SSH_ORIGINAL_COMMAND")); len(m) < 3 {
return errors.New("You've tried to execute some weird command, I'm deliberately denying you to do that, get over it.")
m := r.FindStringSubmatch(os.Getenv("SSH_ORIGINAL_COMMAND"))
if len(m) != 4 {
return "", "", errors.New("You've tried to execute some weird command, I'm deliberately denying you to do that, get over it.")
}
return nil
return m[1], m[2] + m[3], nil
}

// Executes the SSH_ORIGINAL_COMMAND based on the condition
Expand Down Expand Up @@ -161,21 +150,18 @@ func formatCommand() ([]string, error) {
log.Err(err.Error())
return []string{}, err
}
repoName, err := requestedRepositoryName()
_, repoName, err := parseGitCommand()
if err != nil {
log.Err(err.Error())
return []string{}, err
}
repoName += ".git"
cmdList := strings.Split(os.Getenv("SSH_ORIGINAL_COMMAND"), " ")
for i, c := range cmdList {
c = strings.Trim(c, "'")
c = strings.Trim(c, "/")
if c == repoName {
cmdList[i] = path.Join(p, repoName)
break
}
if len(cmdList) != 2 {
log.Err("Malformed git command")
return []string{}, fmt.Errorf("Malformed git command")
}
cmdList[1] = path.Join(p, repoName)
return cmdList, nil
}

Expand All @@ -192,7 +178,7 @@ func main() {
fmt.Fprintln(os.Stderr, err.Error())
return
}
err = validateCmd()
_, _, err = parseGitCommand()
if err != nil {
log.Err(err.Error())
fmt.Fprintln(os.Stderr, err.Error())
Expand Down
51 changes: 35 additions & 16 deletions bin/gandalf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,12 @@ func (s *S) TestRequestedRepositoryShouldReturnErrorWhenCommandDoesNotPassesWhat
os.Setenv("SSH_ORIGINAL_COMMAND", "rm -rf /")
defer os.Setenv("SSH_ORIGINAL_COMMAND", "")
_, err := requestedRepository()
c.Assert(err, gocheck.ErrorMatches, "^Cannot deduce repository name from command. You are probably trying to do something nasty$")
c.Assert(err, gocheck.ErrorMatches, "^You've tried to execute some weird command, I'm deliberately denying you to do that, get over it.$")
}

func (s *S) TestRequestedRepositoryShouldReturnErrorWhenThereIsNoCommandPassedToSSH_ORIGINAL_COMMAND(c *gocheck.C) {
_, err := requestedRepository()
c.Assert(err, gocheck.ErrorMatches, "^Cannot deduce repository name from command. You are probably trying to do something nasty$")
c.Assert(err, gocheck.ErrorMatches, "^You've tried to execute some weird command, I'm deliberately denying you to do that, get over it.$")
}

func (s *S) TestRequestedRepositoryShouldReturnFormatedErrorWhenRepositoryDoesNotExists(c *gocheck.C) {
Expand All @@ -163,48 +163,56 @@ func (s *S) TestRequestedRepositoryShouldReturnEmptyRepositoryStructOnError(c *g
c.Assert(repo.Name, gocheck.Equals, "")
}

func (s *S) TestRequestedRepositoryName(c *gocheck.C) {
func (s *S) TestParseGitCommand(c *gocheck.C) {
os.Setenv("SSH_ORIGINAL_COMMAND", "git-receive-pack 'foobar.git'")
defer os.Setenv("SSH_ORIGINAL_COMMAND", "")
name, err := requestedRepositoryName()
_, name, err := parseGitCommand()
c.Assert(err, gocheck.IsNil)
c.Assert(name, gocheck.Equals, "foobar")
}

func (s *S) TestRequestedRepositoryNameWithSlash(c *gocheck.C) {
func (s *S) TestParseGitCommandWithSlash(c *gocheck.C) {
os.Setenv("SSH_ORIGINAL_COMMAND", "git-receive-pack '/foobar.git'")
defer os.Setenv("SSH_ORIGINAL_COMMAND", "")
name, err := requestedRepositoryName()
_, name, err := parseGitCommand()
c.Assert(err, gocheck.IsNil)
c.Assert(name, gocheck.Equals, "foobar")
}

func (s *S) TestrequestedRepositoryNameShouldReturnErrorWhenTheresNoMatch(c *gocheck.C) {
os.Setenv("SSH_ORIGINAL_COMMAND", "git-receive-pack foobar")
func (s *S) TestParseGitCommandShouldReturnErrorWhenTheresNoMatch(c *gocheck.C) {
defer os.Setenv("SSH_ORIGINAL_COMMAND", "")
name, err := requestedRepositoryName()
c.Assert(err, gocheck.ErrorMatches, "Cannot deduce repository name from command. You are probably trying to do something nasty")
os.Setenv("SSH_ORIGINAL_COMMAND", "git-receive-pack foobar")
_, name, err := parseGitCommand()
c.Assert(err, gocheck.ErrorMatches, "You've tried to execute some weird command, I'm deliberately denying you to do that, get over it.")
c.Assert(name, gocheck.Equals, "")
os.Setenv("SSH_ORIGINAL_COMMAND", "git-receive-pack ../foobar")
_, name, err = parseGitCommand()
c.Assert(err, gocheck.ErrorMatches, "You've tried to execute some weird command, I'm deliberately denying you to do that, get over it.")
c.Assert(name, gocheck.Equals, "")
os.Setenv("SSH_ORIGINAL_COMMAND", "git-receive-pack /etc")
_, name, err = parseGitCommand()
c.Assert(err, gocheck.ErrorMatches, "You've tried to execute some weird command, I'm deliberately denying you to do that, get over it.")
c.Assert(name, gocheck.Equals, "")
}

func (s *S) TestValidateCmdReturnsErrorWhenSSH_ORIGINAL_COMMANDIsNotAGitCommand(c *gocheck.C) {
func (s *S) TestParseGitCommandReturnsErrorWhenSSH_ORIGINAL_COMMANDIsNotAGitCommand(c *gocheck.C) {
os.Setenv("SSH_ORIGINAL_COMMAND", "rm -rf /")
defer os.Setenv("SSH_ORIGINAL_COMMAND", "")
err := validateCmd()
_, _, err := parseGitCommand()
c.Assert(err, gocheck.ErrorMatches, "^You've tried to execute some weird command, I'm deliberately denying you to do that, get over it.$")
}

func (s *S) TestValidateCmdDoNotReturnsErrorWhenSSH_ORIGINAL_COMMANDIsAValidGitCommand(c *gocheck.C) {
func (s *S) TestParseGitCommandDoNotReturnsErrorWhenSSH_ORIGINAL_COMMANDIsAValidGitCommand(c *gocheck.C) {
os.Setenv("SSH_ORIGINAL_COMMAND", "git-receive-pack 'my-repo.git'")
defer os.Setenv("SSH_ORIGINAL_COMMAND", "")
err := validateCmd()
_, _, err := parseGitCommand()
c.Assert(err, gocheck.IsNil)
}

func (s *S) TestValidateCmdDoNotReturnsErrorWhenSSH_ORIGINAL_COMMANDIsAValidGitCommandWithDashInName(c *gocheck.C) {
func (s *S) TestParseGitCommandDoNotReturnsErrorWhenSSH_ORIGINAL_COMMANDIsAValidGitCommandWithDashInName(c *gocheck.C) {
os.Setenv("SSH_ORIGINAL_COMMAND", "git-receive-pack '/my-repo.git'")
defer os.Setenv("SSH_ORIGINAL_COMMAND", "")
err := validateCmd()
_, _, err := parseGitCommand()
c.Assert(err, gocheck.IsNil)
}

Expand Down Expand Up @@ -270,6 +278,17 @@ func (s *S) TestFormatCommandShouldReceiveAGitCommandAndCanonizalizeTheRepositor
c.Assert(cmd, gocheck.DeepEquals, []string{"git-receive-pack", expected})
}

func (s *S) TestFormatCommandShouldReceiveAGitCommandAndCanonizalizeTheRepositoryPathWithNamespace(c *gocheck.C) {
os.Setenv("SSH_ORIGINAL_COMMAND", "git-receive-pack 'me/myproject.git'")
defer os.Setenv("SSH_ORIGINAL_COMMAND", "")
cmd, err := formatCommand()
c.Assert(err, gocheck.IsNil)
p, err := config.GetString("git:bare:location")
c.Assert(err, gocheck.IsNil)
expected := path.Join(p, "me/myproject.git")
c.Assert(cmd, gocheck.DeepEquals, []string{"git-receive-pack", expected})
}

func (s *S) TestFormatCommandShouldReceiveAGitCommandProjectWithDash(c *gocheck.C) {
os.Setenv("SSH_ORIGINAL_COMMAND", "git-receive-pack '/myproject.git'")
defer os.Setenv("SSH_ORIGINAL_COMMAND", "")
Expand Down
31 changes: 31 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -340,3 +340,34 @@ Example result::
}],
next: "1267b5de5943632e47cb6f8bf5b2147bc0be5cf123"
}

Namespaces
----------

Gandalf supports namespaces for repositories and must be informed in the name of the repository followed by a single slash and the actual name of the repository, i.e. `mynamespace/myrepository`. Examples of usage:

* Creates a repository in a namespace:

* Method: POST
* URI: /repository
* Format: JSON

Example URL (http://gandalf-server omitted for clarity)::

$ curl -XPOST /repository \
-d '{"name": "mynamespace/myrepository", \
"users": ["myuser"]}'

* Returns a list of all the branches of the specified `mynamespace/myrepository`.

* Method: GET
* URI: //repository/`:name`/branches
* Format: JSON

Where:

* `:name` is the name of the repository.

Example URL (http://gandalf-server omitted for clarity)::

$ curl /repository/mynamespace/myrepository/branches # gets list of branches
13 changes: 11 additions & 2 deletions repository/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"mime/multipart"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
)
Expand Down Expand Up @@ -253,17 +254,25 @@ func (r *Repository) ReadOnlyURL() string {
}

// Validates a repository
// A valid repository must have:
// A valid repository MUST have:
// - a name without any special chars only alphanumeric and underlines are allowed.
// - at least one user in users array
// A valid repository MAY have one namespace since:
// - one slash (/) separates namespace and name
// - a namespace does not start with period
// - a namespace contains ony alphanumeric, underlines @, - and period
func (r *Repository) isValid() (bool, error) {
m, e := regexp.Match(`^[\w-]+$`, []byte(r.Name))
m, e := regexp.Match(`^([\w-+@][\w-+.@]*/)?[\w-]+$`, []byte(r.Name))
if e != nil {
panic(e)
}
if !m {
return false, errors.New("Validation Error: repository name is not valid")
}
absPath, err := filepath.Abs(barePath(r.Name))
if err != nil || !strings.HasPrefix(absPath, bare) {
return false, errors.New("Validation Error: repository name is not valid")
}
if len(r.Users) == 0 {
return false, errors.New("Validation Error: repository should have at least one user")
}
Expand Down
Loading

0 comments on commit 2cb2e32

Please sign in to comment.