Gitfed
bastien-mrq/gitfed / internal / adminrpc / server.go
package adminrpc

import (
	"encoding/json"
	"fmt"
	"log"
	"net"
	"os"

	"git.neuromancer.ovh/bastien-mrq/gitfed/internal/admin"
)

type Server struct {
	ops        *admin.Admin
	socketPath string
}

func NewServer(ops *admin.Admin, socketPath string) *Server {
	return &Server{ops: ops, socketPath: socketPath}
}

// ListenAndServe serves admin RPC requests on the Unix socket until it
// errors. Only the owning user can connect (socket mode 0600) since this
// grants full admin control of the instance.
func (s *Server) ListenAndServe() error {
	_ = os.Remove(s.socketPath) // safe: bbolt's exclusive lock means we're the only server for this data dir

	l, err := net.Listen("unix", s.socketPath)
	if err != nil {
		return fmt.Errorf("adminrpc: listen %s: %w", s.socketPath, err)
	}
	if err := os.Chmod(s.socketPath, 0600); err != nil {
		return fmt.Errorf("adminrpc: chmod %s: %w", s.socketPath, err)
	}
	log.Printf("gitfed admin socket listening on %s", s.socketPath)

	for {
		conn, err := l.Accept()
		if err != nil {
			return err
		}
		go s.handleConn(conn)
	}
}

func (s *Server) handleConn(conn net.Conn) {
	defer conn.Close()

	var req wireRequest
	if err := json.NewDecoder(conn).Decode(&req); err != nil {
		return
	}

	result, err := s.dispatch(req)
	resp := response{Result: result}
	if err != nil {
		resp.Error = err.Error()
	}
	_ = json.NewEncoder(conn).Encode(resp)
}

type wireRequest struct {
	Method string          `json:"method"`
	Args   json.RawMessage `json:"args,omitempty"`
}

func (s *Server) dispatch(req wireRequest) (any, error) {
	switch req.Method {
	case methodListUsers:
		users, err := s.ops.ListUsers()
		return listUsersResult{Users: users}, err

	case methodCreateUser:
		var a userKeyArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return nil, s.ops.CreateUser(a.Username, a.PubKey)

	case methodAddUserKey:
		var a userKeyArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return nil, s.ops.AddUserKey(a.Username, a.PubKey)

	case methodRemoveUserKey:
		var a userKeyArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return nil, s.ops.RemoveUserKey(a.Username, a.PubKey)

	case methodDeleteUser:
		var a nameArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return nil, s.ops.DeleteUser(a.Name)

	case methodListRepos:
		repos, err := s.ops.ListRepos()
		return listReposResult{Repos: repos}, err

	case methodGetRepo:
		var a nameArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return s.ops.GetRepo(a.Name)

	case methodCreateRepo:
		var a createRepoArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return nil, s.ops.CreateRepo(a.Name, a.Owner)

	case methodImportRepo:
		var a importRepoArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return nil, s.ops.ImportRepo(a.Name, a.SourceURL, a.Owner)

	case methodDeleteRepo:
		var a nameArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return nil, s.ops.DeleteRepo(a.Name)

	case methodGetACL:
		var a nameArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return s.ops.GetACL(a.Name)

	case methodGrantCollaborator:
		var a collaboratorArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return nil, s.ops.GrantCollaborator(a.Repo, a.Principal, a.Actor, a.Role)

	case methodRevokeCollaborator:
		var a collaboratorArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return nil, s.ops.RevokeCollaborator(a.Repo, a.Principal)

	case methodListTrustedCAs:
		trust, err := s.ops.ListTrustedCAs()
		return listTrustResult{Trust: trust}, err

	case methodCountPendingTrust:
		count, err := s.ops.CountPendingTrust()
		return countResult{Count: count}, err

	case methodApproveDomain:
		var a domainArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return nil, s.ops.ApproveDomain(a.Domain)

	case methodListAudit:
		var a limitArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		events, err := s.ops.ListAudit(a.Limit)
		return listAuditResult{Events: events}, err

	case methodSetRepoPublic:
		var a setPublicArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return nil, s.ops.SetRepoPublic(a.Name, a.Public)

	case methodSetRepoTopics:
		var a setTopicsArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return nil, s.ops.SetRepoTopics(a.Name, a.Topics)

	case methodSetRepoDescription:
		var a setDescriptionArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return nil, s.ops.SetRepoDescription(a.Name, a.Description)

	case methodGetRepoReadme:
		var a nameArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		content, found, err := s.ops.GetRepoReadme(a.Name)
		return readmeResult{Content: content, Found: found}, err

	case methodListReadmeLangs:
		var a nameArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		langs, err := s.ops.ListRepoReadmeLanguages(a.Name)
		return readmeLangsResult{Langs: langs}, err

	case methodGetRepoReadmeLang:
		var a nameLangArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		content, found, err := s.ops.GetRepoReadmeLang(a.Name, a.Lang)
		return readmeResult{Content: content, Found: found}, err

	case methodGetRepoLicense:
		var a nameArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		content, filename, found, err := s.ops.GetRepoLicense(a.Name)
		return licenseResult{Content: content, Filename: filename, Found: found}, err

	case methodListRepoTags:
		var a nameArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		tags, err := s.ops.ListRepoTags(a.Name)
		return listTagsResult{Tags: tags}, err

	case methodGetRepoArchive:
		var a archiveArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		data, err := s.ops.GetRepoArchive(a.Name, a.Ref, a.Format)
		return archiveResult{Data: data}, err

	case methodListRepoTree:
		var a pathArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		entries, found, err := s.ops.ListRepoTree(a.Name, a.Path)
		return listTreeResult{Entries: entries, Found: found}, err

	case methodListRepoTreeAtRef:
		var a pathRefArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		entries, found, err := s.ops.ListRepoTreeAtRef(a.Name, a.Ref, a.Path)
		return listTreeResult{Entries: entries, Found: found}, err

	case methodGetRepoFile:
		var a pathArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		content, found, err := s.ops.GetRepoFile(a.Name, a.Path)
		return fileResult{Content: content, Found: found}, err

	case methodGetRepoFileAtRef:
		var a pathRefArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		content, found, err := s.ops.GetRepoFileAtRef(a.Name, a.Ref, a.Path)
		return fileResult{Content: content, Found: found}, err

	case methodGetRepoFileRaw:
		var a pathArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		data, found, err := s.ops.GetRepoFileRaw(a.Name, a.Path)
		return fileRawResult{Data: data, Found: found}, err

	case methodGetRepoFileRawAtRef:
		var a pathRefArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		data, found, err := s.ops.GetRepoFileRawAtRef(a.Name, a.Ref, a.Path)
		return fileRawResult{Data: data, Found: found}, err

	case methodGetRepoBranch:
		var a nameArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		branch, found, err := s.ops.GetRepoBranch(a.Name)
		return branchResult{Branch: branch, Found: found}, err

	case methodCountCommits:
		var a nameArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		count, found, err := s.ops.CountCommits(a.Name)
		return countFoundResult{Count: count, Found: found}, err

	case methodCountContributors:
		var a nameArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		count, found, err := s.ops.CountContributors(a.Name)
		return countFoundResult{Count: count, Found: found}, err

	case methodDominantLanguage:
		var a nameArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		lang, found, err := s.ops.DominantLanguage(a.Name)
		return languageResult{Language: lang, Found: found}, err

	case methodRepoDiskUsage:
		var a nameArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		bytes, err := s.ops.RepoDiskUsage(a.Name)
		return diskUsageResult{Bytes: bytes}, err

	case methodGetUser:
		var a nameArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		user, err := s.ops.GetUser(a.Name)
		return userResult{User: user}, err

	case methodSetUserAdmin:
		var a setAdminArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return nil, s.ops.SetUserAdmin(a.Username, a.IsAdmin)

	case methodSetUserBio:
		var a setBioArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return nil, s.ops.SetUserBio(a.Username, a.Bio)

	case methodSetPassword:
		var a setPasswordArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return nil, s.ops.SetPassword(a.Username, a.Password)

	case methodVerifyPassword:
		var a setPasswordArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		isAdmin, ok, err := s.ops.VerifyPassword(a.Username, a.Password)
		return verifyPasswordResult{IsAdmin: isAdmin, OK: ok}, err

	case methodCreateSession:
		var a createSessionArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		token, err := s.ops.CreateSession(a.Principal, a.Username, a.IsAdmin)
		return createSessionResult{Token: token}, err

	case methodGetSession:
		var a tokenArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		sess, err := s.ops.GetSession(a.Token)
		return sessionResult{Session: sess}, err

	case methodDeleteSession:
		var a tokenArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return nil, s.ops.DeleteSession(a.Token)

	case methodCheckAccess:
		var a checkAccessArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		role, ok, err := s.ops.CheckAccess(a.Repo, a.Principal, a.Want)
		return checkAccessResult{Role: role, OK: ok}, err

	case methodListCommits:
		var a nameLimitArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		commits, found, err := s.ops.ListCommits(a.Name, a.Limit)
		return listCommitsResult{Commits: commits, Found: found}, err

	case methodListCommitsPage:
		var a nameLimitOffsetArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		commits, found, err := s.ops.ListCommitsPage(a.Name, a.Limit, a.Offset)
		return listCommitsResult{Commits: commits, Found: found}, err

	case methodShowCommit:
		var a nameHashArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		detail, found, err := s.ops.ShowCommit(a.Name, a.Hash)
		return showCommitResult{Detail: detail, Found: found}, err

	case methodCommitDiff:
		var a nameHashArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		diff, truncated, err := s.ops.CommitDiff(a.Name, a.Hash)
		return commitDiffResult{Diff: diff, Truncated: truncated}, err

	case methodListBranches:
		var a nameArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		branches, err := s.ops.ListBranches(a.Name)
		return listBranchesResult{Branches: branches}, err

	case methodCreateMergeRequest:
		var a createMRArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		mr, err := s.ops.CreateMergeRequest(a.Repo, a.Title, a.Description, a.Author, a.SourceBranch, a.TargetBranch)
		return mrResult{MR: mr}, err

	case methodListMergeRequests:
		var a nameArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		mrs, err := s.ops.ListMergeRequests(a.Name)
		return listMRResult{MRs: mrs}, err

	case methodGetMergeRequest:
		var a mrArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		mr, err := s.ops.GetMergeRequest(a.Repo, a.Number)
		return mrResult{MR: mr}, err

	case methodMergeRequestDiff:
		var a mrArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		files, diff, truncated, found, err := s.ops.MergeRequestDiff(a.Repo, a.Number)
		return mrDiffResult{Files: files, Diff: diff, Truncated: truncated, Found: found}, err

	case methodCheckMergeRequestMergeable:
		var a mrArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		result, err := s.ops.CheckMergeRequestMergeable(a.Repo, a.Number)
		return mergeResultRPC{Result: result}, err

	case methodMergeMergeRequest:
		var a mergeMRArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		mr, result, err := s.ops.MergeMergeRequest(a.Repo, a.Number, a.Actor)
		return mergeMRResult{MR: mr, Result: result}, err

	case methodCloseMergeRequest:
		var a mrArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return nil, s.ops.CloseMergeRequest(a.Repo, a.Number)

	case methodListMRComments:
		var a mrArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		comments, err := s.ops.ListMRComments(a.Repo, a.Number)
		return listMRCommentsResult{Comments: comments}, err

	case methodAddMRComment:
		var a mrCommentArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		comment, err := s.ops.AddMRComment(a.Repo, a.Number, a.Author, a.Body)
		return addMRCommentResult{Comment: comment}, err

	case methodPinRepo:
		var a pinArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return nil, s.ops.PinRepo(a.Principal, a.Domain, a.Repo, a.Label)

	case methodUnpinRepo:
		var a pinArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return nil, s.ops.UnpinRepo(a.Principal, a.Domain, a.Repo)

	case methodListPinnedRepos:
		var a principalArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		pins, err := s.ops.ListPinnedRepos(a.Principal)
		return listPinsResult{Pins: pins}, err

	case methodListNotifications:
		var a principalArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		notifs, err := s.ops.ListNotifications(a.Principal)
		return listNotifsResult{Notifications: notifs}, err

	case methodCountPendingNotifs:
		var a principalArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		count, err := s.ops.CountPendingNotifications(a.Principal)
		return countResult{Count: count}, err

	case methodAcceptNotification:
		var a notificationIDArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return nil, s.ops.AcceptNotification(a.Principal, a.ID)

	case methodDismissNotification:
		var a notificationIDArgs
		if err := json.Unmarshal(req.Args, &a); err != nil {
			return nil, err
		}
		return nil, s.ops.DismissNotification(a.Principal, a.ID)

	default:
		return nil, fmt.Errorf("adminrpc: unknown method %q", req.Method)
	}
}