Skip to content

Commit

Permalink
feat(backend): collabs & admins interface
Browse files Browse the repository at this point in the history
  • Loading branch information
aymanbagabas committed May 2, 2023
1 parent c1cbb40 commit 4f07abf
Show file tree
Hide file tree
Showing 5 changed files with 300 additions and 74 deletions.
54 changes: 4 additions & 50 deletions server/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,56 +9,10 @@ import (
// Backend is an interface that handles repositories management and any
// non-Git related operations.
type Backend interface {
// ServerName returns the server's name.
ServerName() string
// SetServerName sets the server's name.
SetServerName(name string) error
// ServerHost returns the server's host.
ServerHost() string
// SetServerHost sets the server's host.
SetServerHost(host string) error
// ServerPort returns the server's port.
ServerPort() string
// SetServerPort sets the server's port.
SetServerPort(port string) error

// AnonAccess returns the access level for anonymous users.
AnonAccess() AccessLevel
// SetAnonAccess sets the access level for anonymous users.
SetAnonAccess(level AccessLevel) error
// AllowKeyless returns true if keyless access is allowed.
AllowKeyless() bool
// SetAllowKeyless sets whether or not keyless access is allowed.
SetAllowKeyless(allow bool) error

// Repository finds the given repository.
Repository(repo string) (Repository, error)
// Repositories returns a list of all repositories.
Repositories() ([]Repository, error)
// CreateRepository creates a new repository.
CreateRepository(name string, private bool) (Repository, error)
// DeleteRepository deletes a repository.
DeleteRepository(name string) error
// RenameRepository renames a repository.
RenameRepository(oldName, newName string) error

// Description returns the repo's description.
Description(repo string) string
// SetDescription sets the repo's description.
SetDescription(repo, desc string) error
// IsPrivate returns true if the repository is private.
IsPrivate(repo string) bool
// SetPrivate sets the repository's private status.
SetPrivate(repo string, priv bool) error

// IsCollaborator returns true if the authorized key is a collaborator on the repository.
IsCollaborator(pk ssh.PublicKey, repo string) bool
// AddCollaborator adds the authorized key as a collaborator on the repository.
AddCollaborator(pk ssh.PublicKey, repo string) error
// IsAdmin returns true if the authorized key is an admin.
IsAdmin(pk ssh.PublicKey) bool
// AddAdmin adds the authorized key as an admin.
AddAdmin(pk ssh.PublicKey) error
ServerBackend
RepositoryStore
RepositoryMetadata
RepositoryAccess
}

// ParseAuthorizedKey parses an authorized key string into a public key.
Expand Down
219 changes: 198 additions & 21 deletions server/backend/file/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func (fb *FileBackend) adminsPath() string {
}

func (fb *FileBackend) collabsPath(repo string) string {
return filepath.Join(fb.reposPath(), repo, collabs)
return filepath.Join(fb.path, collabs, repo)
}

func sanatizeRepo(repo string) string {
Expand Down Expand Up @@ -117,10 +117,16 @@ func readAll(path string) (string, error) {
return string(bts), err
}

// exists returns true if the given path exists.
func exists(path string) bool {
_, err := os.Stat(path)
return err == nil
}

// NewFileBackend creates a new FileBackend.
func NewFileBackend(path string) (*FileBackend, error) {
fb := &FileBackend{path: path}
for _, dir := range []string{repos, settings} {
for _, dir := range []string{repos, settings, collabs} {
if err := os.MkdirAll(filepath.Join(path, dir), 0755); err != nil {
return nil, err
}
Expand Down Expand Up @@ -181,10 +187,10 @@ func (fb *FileBackend) AccessLevel(repo string, pk gossh.PublicKey) backend.Acce
// AddAdmin adds a public key to the list of server admins.
//
// It implements backend.Backend.
func (fb *FileBackend) AddAdmin(pk gossh.PublicKey) error {
func (fb *FileBackend) AddAdmin(pk gossh.PublicKey, memo string) error {
// Skip if the key already exists.
if fb.IsAdmin(pk) {
return nil
return fmt.Errorf("key already exists")
}

ak := backend.MarshalAuthorizedKey(pk)
Expand All @@ -195,32 +201,206 @@ func (fb *FileBackend) AddAdmin(pk gossh.PublicKey) error {
}

defer f.Close() //nolint:errcheck
_, err = fmt.Fprintln(f, ak)
if memo != "" {
memo = " " + memo
}
_, err = fmt.Fprintf(f, "%s%s\n", ak, memo)
return err
}

// AddCollaborator adds a public key to the list of collaborators for the given repo.
//
// It implements backend.Backend.
func (fb *FileBackend) AddCollaborator(pk gossh.PublicKey, repo string) error {
func (fb *FileBackend) AddCollaborator(pk gossh.PublicKey, memo string, name string) error {
// Check if repo exists
if !exists(filepath.Join(fb.reposPath(), sanatizeRepo(name)+".git")) {
return fmt.Errorf("repository %s does not exist", name)
}

// Skip if the key already exists.
if fb.IsCollaborator(pk, repo) {
return nil
if fb.IsCollaborator(pk, name) {
return fmt.Errorf("key already exists")
}

ak := backend.MarshalAuthorizedKey(pk)
repo = sanatizeRepo(repo) + ".git"
f, err := os.OpenFile(fb.collabsPath(repo), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
name = sanatizeRepo(name)
if err := os.MkdirAll(filepath.Dir(fb.collabsPath(name)), 0755); err != nil {
logger.Debug("failed to create collaborators directory",
"err", err, "path", filepath.Dir(fb.collabsPath(name)))
return err
}

f, err := os.OpenFile(fb.collabsPath(name), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
logger.Debug("failed to open collaborators file", "err", err, "path", fb.collabsPath(repo))
logger.Debug("failed to open collaborators file", "err", err, "path", fb.collabsPath(name))
return err
}

defer f.Close() //nolint:errcheck
_, err = fmt.Fprintln(f, ak)
if memo != "" {
memo = " " + memo
}
_, err = fmt.Fprintf(f, "%s%s\n", ak, memo)
return err
}

// Admins returns a list of public keys that are admins.
//
// It implements backend.Backend.
func (fb *FileBackend) Admins() ([]string, error) {
admins := make([]string, 0)
f, err := os.Open(fb.adminsPath())
if err != nil {
logger.Debug("failed to open admin keys file", "err", err, "path", fb.adminsPath())
return nil, err
}

defer f.Close() //nolint:errcheck
s := bufio.NewScanner(f)
for s.Scan() {
admins = append(admins, s.Text())
}

return admins, s.Err()
}

// Collaborators returns a list of public keys that are collaborators for the given repo.
//
// It implements backend.Backend.
func (fb *FileBackend) Collaborators(repo string) ([]string, error) {
// Check if repo exists
if !exists(filepath.Join(fb.reposPath(), sanatizeRepo(repo)+".git")) {
return nil, fmt.Errorf("repository %s does not exist", repo)
}

collabs := make([]string, 0)
f, err := os.Open(fb.collabsPath(repo))
if err != nil && errors.Is(err, os.ErrNotExist) {
return collabs, nil
}
if err != nil {
logger.Debug("failed to open collaborators file", "err", err, "path", fb.collabsPath(repo))
return nil, err
}

defer f.Close() //nolint:errcheck
s := bufio.NewScanner(f)
for s.Scan() {
collabs = append(collabs, s.Text())
}

return collabs, s.Err()
}

// RemoveAdmin implements backend.Backend
func (fb *FileBackend) RemoveAdmin(pk gossh.PublicKey) error {
f, err := os.OpenFile(fb.adminsPath(), os.O_RDWR, 0644)
if err != nil {
logger.Debug("failed to open admin keys file", "err", err, "path", fb.adminsPath())
return err
}

defer f.Close() //nolint:errcheck
s := bufio.NewScanner(f)
lines := make([]string, 0)
for s.Scan() {
apk, _, err := backend.ParseAuthorizedKey(s.Text())
if err != nil {
logger.Debug("failed to parse admin key", "err", err, "path", fb.adminsPath())
continue
}

if !ssh.KeysEqual(apk, pk) {
lines = append(lines, s.Text())
}
}

if err := s.Err(); err != nil {
logger.Debug("failed to scan admin keys file", "err", err, "path", fb.adminsPath())
return err
}

if err := f.Truncate(0); err != nil {
logger.Debug("failed to truncate admin keys file", "err", err, "path", fb.adminsPath())
return err
}

if _, err := f.Seek(0, 0); err != nil {
logger.Debug("failed to seek admin keys file", "err", err, "path", fb.adminsPath())
return err
}

w := bufio.NewWriter(f)
for _, line := range lines {
if _, err := fmt.Fprintln(w, line); err != nil {
logger.Debug("failed to write admin keys file", "err", err, "path", fb.adminsPath())
return err
}
}

return w.Flush()
}

// RemoveCollaborator removes a public key from the list of collaborators for the given repo.
//
// It implements backend.Backend.
func (fb *FileBackend) RemoveCollaborator(pk gossh.PublicKey, repo string) error {
// Check if repo exists
if !exists(filepath.Join(fb.reposPath(), sanatizeRepo(repo)+".git")) {
return fmt.Errorf("repository %s does not exist", repo)
}

f, err := os.OpenFile(fb.collabsPath(repo), os.O_RDWR, 0644)
if err != nil && errors.Is(err, os.ErrNotExist) {
return nil
}

if err != nil {
logger.Debug("failed to open collaborators file", "err", err, "path", fb.collabsPath(repo))
return err
}

defer f.Close() //nolint:errcheck
s := bufio.NewScanner(f)
lines := make([]string, 0)
for s.Scan() {
apk, _, err := backend.ParseAuthorizedKey(s.Text())
if err != nil {
logger.Debug("failed to parse collaborator key", "err", err, "path", fb.collabsPath(repo))
continue
}

if !ssh.KeysEqual(apk, pk) {
lines = append(lines, s.Text())
}
}

if err := s.Err(); err != nil {
logger.Debug("failed to scan collaborators file", "err", err, "path", fb.collabsPath(repo))
return err
}

if err := f.Truncate(0); err != nil {
logger.Debug("failed to truncate collaborators file", "err", err, "path", fb.collabsPath(repo))
return err
}

if _, err := f.Seek(0, 0); err != nil {
logger.Debug("failed to seek collaborators file", "err", err, "path", fb.collabsPath(repo))
return err
}

w := bufio.NewWriter(f)
for _, line := range lines {
if _, err := fmt.Fprintln(w, line); err != nil {
logger.Debug("failed to write collaborators file", "err", err, "path", fb.collabsPath(repo))
return err
}
}

return w.Flush()
}

// AllowKeyless returns true if keyless access is allowed.
//
// It implements backend.Backend.
Expand Down Expand Up @@ -304,19 +484,16 @@ func (fb *FileBackend) IsAdmin(pk gossh.PublicKey) bool {
// given repo.
//
// It implements backend.Backend.
func (fb *FileBackend) IsCollaborator(pk gossh.PublicKey, repo string) bool {
repo = sanatizeRepo(repo) + ".git"
_, err := os.Stat(filepath.Join(fb.reposPath(), repo))
if errors.Is(err, os.ErrNotExist) {
func (fb *FileBackend) IsCollaborator(pk gossh.PublicKey, name string) bool {
name = sanatizeRepo(name)
_, err := os.Stat(fb.collabsPath(name))
if err != nil {
return false
}

f, err := os.Open(fb.collabsPath(repo))
if err != nil && errors.Is(err, os.ErrNotExist) {
return false
}
f, err := os.Open(fb.collabsPath(name))
if err != nil {
logger.Debug("failed to open collaborators file", "err", err, "path", fb.collabsPath(repo))
logger.Debug("failed to open collaborators file", "err", err, "path", fb.collabsPath(name))
return false
}

Expand Down
24 changes: 22 additions & 2 deletions server/backend/noop/noop.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,38 @@ type Noop struct {
Port string
}

// Admins implements backend.Backend
func (*Noop) Admins() ([]string, error) {
return nil, nil
}

// Collaborators implements backend.Backend
func (*Noop) Collaborators(repo string) ([]string, error) {
return nil, nil
}

// RemoveAdmin implements backend.Backend
func (*Noop) RemoveAdmin(pk ssh.PublicKey) error {
return nil
}

// RemoveCollaborator implements backend.Backend
func (*Noop) RemoveCollaborator(pk ssh.PublicKey, repo string) error {
return nil
}

// AccessLevel implements backend.AccessMethod
func (*Noop) AccessLevel(repo string, pk ssh.PublicKey) backend.AccessLevel {
return backend.AdminAccess
}

// AddAdmin implements backend.Backend
func (*Noop) AddAdmin(pk ssh.PublicKey) error {
func (*Noop) AddAdmin(pk ssh.PublicKey, memo string) error {
return ErrNotImpl
}

// AddCollaborator implements backend.Backend
func (*Noop) AddCollaborator(pk ssh.PublicKey, repo string) error {
func (*Noop) AddCollaborator(pk ssh.PublicKey, memo string, repo string) error {
return ErrNotImpl
}

Expand Down

0 comments on commit 4f07abf

Please sign in to comment.