5545 lines
163 KiB
Go
5545 lines
163 KiB
Go
package handlers
|
|
|
|
import "crypto/rand"
|
|
import "database/sql"
|
|
import "encoding/hex"
|
|
import "errors"
|
|
import "fmt"
|
|
import "io"
|
|
import "mime/multipart"
|
|
import "net/http"
|
|
import "os"
|
|
import "path/filepath"
|
|
import "strconv"
|
|
import "strings"
|
|
import "time"
|
|
import "unicode"
|
|
|
|
import "codit/internal/auth"
|
|
import "codit/internal/config"
|
|
import "codit/internal/db"
|
|
import "codit/internal/docker"
|
|
import "codit/internal/git"
|
|
import "codit/internal/middleware"
|
|
import "codit/internal/models"
|
|
import "codit/internal/rpm"
|
|
import "codit/internal/storage"
|
|
import "codit/internal/util"
|
|
|
|
type API struct {
|
|
Cfg config.Config
|
|
Store *db.Store
|
|
Repos git.RepoManager
|
|
RpmBase string
|
|
RpmMeta *rpm.MetaManager
|
|
RpmMirror *rpm.MirrorManager
|
|
DockerBase string
|
|
Uploads storage.FileStore
|
|
Logger *util.Logger
|
|
OnTLSListenersChanged func()
|
|
OnTLSListenerRuntimeStatus func() map[string]int
|
|
}
|
|
|
|
type loginRequest struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
type createUserRequest struct {
|
|
Username string `json:"username"`
|
|
DisplayName string `json:"display_name"`
|
|
Email string `json:"email"`
|
|
Password string `json:"password"`
|
|
IsAdmin bool `json:"is_admin"`
|
|
}
|
|
|
|
type updateMeRequest struct {
|
|
DisplayName string `json:"display_name"`
|
|
Email string `json:"email"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
type updateAuthSettingsRequest struct {
|
|
AuthMode string `json:"auth_mode"`
|
|
OIDCEnabled bool `json:"oidc_enabled"`
|
|
LDAPURL string `json:"ldap_url"`
|
|
LDAPBindDN string `json:"ldap_bind_dn"`
|
|
LDAPBindPassword string `json:"ldap_bind_password"`
|
|
LDAPUserBaseDN string `json:"ldap_user_base_dn"`
|
|
LDAPUserFilter string `json:"ldap_user_filter"`
|
|
LDAPTLSInsecureSkipVerify bool `json:"ldap_tls_insecure_skip_verify"`
|
|
OIDCClientID string `json:"oidc_client_id"`
|
|
OIDCClientSecret string `json:"oidc_client_secret"`
|
|
OIDCAuthorizeURL string `json:"oidc_authorize_url"`
|
|
OIDCTokenURL string `json:"oidc_token_url"`
|
|
OIDCUserInfoURL string `json:"oidc_userinfo_url"`
|
|
OIDCRedirectURL string `json:"oidc_redirect_url"`
|
|
OIDCScopes string `json:"oidc_scopes"`
|
|
OIDCTLSInsecureSkipVerify bool `json:"oidc_tls_insecure_skip_verify"`
|
|
}
|
|
|
|
type testLDAPSettingsRequest struct {
|
|
AuthMode string `json:"auth_mode"`
|
|
LDAPURL string `json:"ldap_url"`
|
|
LDAPBindDN string `json:"ldap_bind_dn"`
|
|
LDAPBindPassword string `json:"ldap_bind_password"`
|
|
LDAPUserBaseDN string `json:"ldap_user_base_dn"`
|
|
LDAPUserFilter string `json:"ldap_user_filter"`
|
|
LDAPTLSInsecureSkipVerify *bool `json:"ldap_tls_insecure_skip_verify"`
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
type updateTLSSettingsRequest struct {
|
|
HTTPAddrs []string `json:"http_addrs"`
|
|
HTTPSAddrs []string `json:"https_addrs"`
|
|
TLSServerCertSource string `json:"tls_server_cert_source"`
|
|
TLSCertFile string `json:"tls_cert_file"`
|
|
TLSKeyFile string `json:"tls_key_file"`
|
|
TLSPKIServerCertID string `json:"tls_pki_server_cert_id"`
|
|
TLSClientAuth string `json:"tls_client_auth"`
|
|
TLSClientCAFile string `json:"tls_client_ca_file"`
|
|
TLSPKIClientCAID string `json:"tls_pki_client_ca_id"`
|
|
TLSMinVersion string `json:"tls_min_version"`
|
|
}
|
|
|
|
type tlsListenerRequest struct {
|
|
Name string `json:"name"`
|
|
Enabled bool `json:"enabled"`
|
|
HTTPAddrs []string `json:"http_addrs"`
|
|
HTTPSAddrs []string `json:"https_addrs"`
|
|
AuthPolicy string `json:"auth_policy"`
|
|
ApplyPolicyAPI bool `json:"apply_policy_api"`
|
|
ApplyPolicyGit bool `json:"apply_policy_git"`
|
|
ApplyPolicyRPM bool `json:"apply_policy_rpm"`
|
|
ApplyPolicyV2 bool `json:"apply_policy_v2"`
|
|
ClientCertAllowlist []string `json:"client_cert_allowlist"`
|
|
TLSServerCertSource string `json:"tls_server_cert_source"`
|
|
TLSCertFile string `json:"tls_cert_file"`
|
|
TLSKeyFile string `json:"tls_key_file"`
|
|
TLSPKIServerCertID string `json:"tls_pki_server_cert_id"`
|
|
TLSClientAuth string `json:"tls_client_auth"`
|
|
TLSClientCAFile string `json:"tls_client_ca_file"`
|
|
TLSPKIClientCAID string `json:"tls_pki_client_ca_id"`
|
|
TLSMinVersion string `json:"tls_min_version"`
|
|
}
|
|
|
|
type servicePrincipalRequest struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
IsAdmin bool `json:"is_admin"`
|
|
Disabled bool `json:"disabled"`
|
|
}
|
|
|
|
type certPrincipalBindingRequest struct {
|
|
Fingerprint string `json:"fingerprint"`
|
|
PrincipalID string `json:"principal_id"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
type principalProjectRoleRequest struct {
|
|
ProjectID string `json:"project_id"`
|
|
Role string `json:"role"`
|
|
}
|
|
|
|
type createProjectRequest struct {
|
|
Slug string `json:"slug"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
HomePage string `json:"home_page"`
|
|
}
|
|
|
|
type createRepoRequest struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
type updateRepoRequest struct {
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type attachRepoRequest struct {
|
|
RepoID string `json:"repo_id"`
|
|
}
|
|
|
|
type repoResponse struct {
|
|
models.Repo
|
|
CloneURL string `json:"clone_url"`
|
|
RPMURL string `json:"rpm_url"`
|
|
DockerURL string `json:"docker_url"`
|
|
}
|
|
|
|
type repoListItem struct {
|
|
models.Repo
|
|
CloneURL string `json:"clone_url"`
|
|
RPMURL string `json:"rpm_url"`
|
|
DockerURL string `json:"docker_url"`
|
|
OwnerProject string `json:"owner_project"`
|
|
OwnerSlug string `json:"owner_slug"`
|
|
}
|
|
|
|
type availableRepoItem struct {
|
|
models.Repo
|
|
OwnerProject string `json:"owner_project"`
|
|
OwnerSlug string `json:"owner_slug"`
|
|
}
|
|
|
|
type repoStatsResponse struct {
|
|
RepoID string `json:"repo_id"`
|
|
Branches int `json:"branches"`
|
|
DefaultRef string `json:"default_ref"`
|
|
LastCommit string `json:"last_commit"`
|
|
LastAuthor string `json:"last_author"`
|
|
LastWhen string `json:"last_when"`
|
|
LastMessage string `json:"last_message"`
|
|
CloneURL string `json:"clone_url"`
|
|
ProjectSlug string `json:"project_slug"`
|
|
Repository string `json:"repository"`
|
|
}
|
|
|
|
type repoBranchesInfo struct {
|
|
Default string `json:"default"`
|
|
Branches []git.BranchInfo `json:"branches"`
|
|
}
|
|
|
|
type repoTypeItem struct {
|
|
Value string `json:"value"`
|
|
Label string `json:"label"`
|
|
}
|
|
|
|
type repoBranchRequest struct {
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type repoBranchRenameRequest struct {
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
}
|
|
|
|
type repoBranchCreateRequest struct {
|
|
Name string `json:"name"`
|
|
From string `json:"from"`
|
|
}
|
|
|
|
type repoRPMCreateRequest struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Parent string `json:"parent"`
|
|
Mode string `json:"mode"`
|
|
AllowDelete bool `json:"allow_delete"`
|
|
RemoteURL string `json:"remote_url"`
|
|
ConnectHost string `json:"connect_host"`
|
|
HostHeader string `json:"host_header"`
|
|
TLSServerName string `json:"tls_server_name"`
|
|
TLSInsecureSkipVerify bool `json:"tls_insecure_skip_verify"`
|
|
SyncIntervalSec int64 `json:"sync_interval_sec"`
|
|
}
|
|
|
|
type repoRPMUpdateRequest struct {
|
|
Path *string `json:"path"`
|
|
Name *string `json:"name"`
|
|
Mode *string `json:"mode"`
|
|
AllowDelete *bool `json:"allow_delete"`
|
|
RemoteURL *string `json:"remote_url"`
|
|
ConnectHost *string `json:"connect_host"`
|
|
HostHeader *string `json:"host_header"`
|
|
TLSServerName *string `json:"tls_server_name"`
|
|
TLSInsecureSkipVerify *bool `json:"tls_insecure_skip_verify"`
|
|
SyncIntervalSec *int64 `json:"sync_interval_sec"`
|
|
}
|
|
|
|
type createAPIKeyRequest struct {
|
|
Name string `json:"name"`
|
|
ExpiresAt int64 `json:"expires_at"`
|
|
}
|
|
|
|
type apiKeyResponse struct {
|
|
models.APIKey
|
|
Token string `json:"token,omitempty"`
|
|
}
|
|
|
|
type repoDockerRenameTagRequest struct {
|
|
Image string `json:"image"`
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
}
|
|
|
|
type repoDockerRenameImageRequest struct {
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
}
|
|
|
|
type repoRPMUploadResponse struct {
|
|
Filename string `json:"filename"`
|
|
Size int64 `json:"size"`
|
|
}
|
|
|
|
type createIssueRequest struct {
|
|
Title string `json:"title"`
|
|
Body string `json:"body"`
|
|
AssigneeID string `json:"assignee_id"`
|
|
}
|
|
|
|
type updateIssueRequest struct {
|
|
Title string `json:"title"`
|
|
Body string `json:"body"`
|
|
Status string `json:"status"`
|
|
AssigneeID string `json:"assignee_id"`
|
|
}
|
|
|
|
type createWikiRequest struct {
|
|
Title string `json:"title"`
|
|
Slug string `json:"slug"`
|
|
Body string `json:"body"`
|
|
}
|
|
|
|
type updateWikiRequest struct {
|
|
Title string `json:"title"`
|
|
Body string `json:"body"`
|
|
}
|
|
|
|
type issueCommentRequest struct {
|
|
Body string `json:"body"`
|
|
}
|
|
|
|
type projectMemberRequest struct {
|
|
UserID string `json:"user_id"`
|
|
Role string `json:"role"`
|
|
}
|
|
|
|
func (api *API) Login(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var req loginRequest
|
|
var err error
|
|
var user models.User
|
|
var storedHash string
|
|
var ldapUser auth.LDAPUser
|
|
var newUser models.User
|
|
var created models.User
|
|
var authCfg config.Config
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
if req.Username == "" || req.Password == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "username and password required"})
|
|
return
|
|
}
|
|
|
|
user, storedHash, err = api.Store.GetUserByUsername(req.Username)
|
|
if err == nil && user.Disabled {
|
|
WriteJSON(w, http.StatusForbidden, map[string]string{"error": "user disabled"})
|
|
return
|
|
}
|
|
if err == nil && storedHash != "" && auth.ComparePassword(storedHash, req.Password) == nil {
|
|
api.issueSession(w, user)
|
|
WriteJSON(w, http.StatusOK, user)
|
|
return
|
|
}
|
|
|
|
authCfg, _ = api.effectiveAuthConfig()
|
|
if authCfg.AuthMode == "ldap" || authCfg.AuthMode == "hybrid" {
|
|
if api.Logger != nil {
|
|
api.Logger.Write("auth", util.LOG_INFO, "ldap login attempt username=%s mode=%s", req.Username, authCfg.AuthMode)
|
|
}
|
|
ldapUser, err = auth.LDAPAuthenticateContext(r.Context(), authCfg, req.Username, req.Password)
|
|
if err == nil {
|
|
if api.Logger != nil {
|
|
api.Logger.Write("auth", util.LOG_INFO, "ldap login success username=%s", req.Username)
|
|
}
|
|
if user.ID != "" && user.Disabled {
|
|
WriteJSON(w, http.StatusForbidden, map[string]string{"error": "user disabled"})
|
|
return
|
|
}
|
|
if user.ID == "" {
|
|
newUser = models.User{
|
|
Username: ldapUser.Username,
|
|
DisplayName: ldapUser.DisplayName,
|
|
Email: ldapUser.Email,
|
|
AuthSource: "ldap",
|
|
}
|
|
if newUser.Email == "" {
|
|
newUser.Email = ldapUser.Username + "@local"
|
|
}
|
|
created, err = api.Store.CreateUser(newUser, "")
|
|
if err == nil {
|
|
user = created
|
|
}
|
|
}
|
|
api.issueSession(w, user)
|
|
WriteJSON(w, http.StatusOK, user)
|
|
return
|
|
}
|
|
if api.Logger != nil {
|
|
api.Logger.Write("auth", util.LOG_WARN, "ldap login failed username=%s err=%v", req.Username, err)
|
|
}
|
|
}
|
|
|
|
WriteJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
|
|
}
|
|
|
|
func (api *API) issueSession(w http.ResponseWriter, user models.User) {
|
|
var token string
|
|
var err error
|
|
var expires time.Time
|
|
var cookie *http.Cookie
|
|
token, err = auth.NewSessionToken()
|
|
if err != nil {
|
|
return
|
|
}
|
|
expires = auth.SessionExpiry(api.Cfg)
|
|
_ = api.Store.CreateSession(user.ID, token, expires)
|
|
cookie = &http.Cookie{
|
|
Name: "codit_session",
|
|
Value: token,
|
|
HttpOnly: true,
|
|
Path: "/",
|
|
Expires: expires,
|
|
}
|
|
http.SetCookie(w, cookie)
|
|
}
|
|
|
|
func (api *API) Logout(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var cookie *http.Cookie
|
|
var err error
|
|
cookie, err = r.Cookie("codit_session")
|
|
if err == nil {
|
|
_ = api.Store.DeleteSession(cookie.Value)
|
|
}
|
|
cookie = &http.Cookie{
|
|
Name: "codit_session",
|
|
Value: "",
|
|
Path: "/",
|
|
Expires: time.Unix(0, 0),
|
|
HttpOnly: true,
|
|
}
|
|
http.SetCookie(w, cookie)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) Me(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var user models.User
|
|
var ok bool
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, user)
|
|
}
|
|
|
|
func (api *API) UpdateMe(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var ctxUser models.User
|
|
var ok bool
|
|
var user models.User
|
|
var req updateMeRequest
|
|
var err error
|
|
var hash string
|
|
var newPassword bool
|
|
ctxUser, ok = middleware.UserFromContext(r.Context())
|
|
if !ok {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
user, err = api.Store.GetUserByID(ctxUser.ID)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "user not found"})
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
if req.DisplayName != "" {
|
|
user.DisplayName = req.DisplayName
|
|
}
|
|
if req.Email != "" {
|
|
user.Email = req.Email
|
|
}
|
|
newPassword = strings.TrimSpace(req.Password) != ""
|
|
if newPassword && user.AuthSource != "db" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "password update not supported for non-db users"})
|
|
return
|
|
}
|
|
if newPassword {
|
|
hash, err = auth.HashPassword(req.Password)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to hash password"})
|
|
return
|
|
}
|
|
err = api.Store.UpdateUserWithPassword(user, hash)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
} else {
|
|
err = api.Store.UpdateUser(user)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
}
|
|
WriteJSON(w, http.StatusOK, user)
|
|
}
|
|
|
|
func (api *API) ListUsers(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var users []models.User
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
users, err = api.Store.ListUsers()
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, users)
|
|
}
|
|
|
|
func (api *API) CreateUser(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var req createUserRequest
|
|
var err error
|
|
var hash string
|
|
var user models.User
|
|
var created models.User
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
if req.Username == "" || req.Password == "" || req.DisplayName == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "username, display_name, and password required"})
|
|
return
|
|
}
|
|
hash, err = auth.HashPassword(req.Password)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to hash password"})
|
|
return
|
|
}
|
|
user = models.User{
|
|
Username: req.Username,
|
|
DisplayName: req.DisplayName,
|
|
Email: req.Email,
|
|
IsAdmin: req.IsAdmin,
|
|
AuthSource: "db",
|
|
}
|
|
created, err = api.Store.CreateUser(user, hash)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusCreated, created)
|
|
}
|
|
|
|
func (api *API) UpdateUser(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var user models.User
|
|
var err error
|
|
var payload createUserRequest
|
|
var hash string
|
|
var newPassword bool
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
user, err = api.Store.GetUserByID(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "user not found"})
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &payload)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
if payload.DisplayName != "" {
|
|
user.DisplayName = payload.DisplayName
|
|
}
|
|
if payload.Email != "" {
|
|
user.Email = payload.Email
|
|
}
|
|
user.IsAdmin = payload.IsAdmin
|
|
newPassword = payload.Password != ""
|
|
if newPassword {
|
|
hash, err = auth.HashPassword(payload.Password)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to hash password"})
|
|
return
|
|
}
|
|
err = api.Store.UpdateUserWithPassword(user, hash)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
} else {
|
|
err = api.Store.UpdateUser(user)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
}
|
|
WriteJSON(w, http.StatusOK, user)
|
|
}
|
|
|
|
func (api *API) DeleteUser(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
err = api.Store.DeleteUser(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) DisableUser(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
err = api.Store.SetUserDisabled(params["id"], true)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) EnableUser(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
err = api.Store.SetUserDisabled(params["id"], false)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) GetAuthSettings(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var settings models.AuthSettings
|
|
var err error
|
|
var user models.User
|
|
var ok bool
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if ok && api.Logger != nil {
|
|
api.Logger.Write("auth", util.LOG_INFO, "auth settings fetch requested by user=%s", user.Username)
|
|
}
|
|
settings, err = api.getMergedAuthSettings()
|
|
if err != nil {
|
|
if api.Logger != nil {
|
|
api.Logger.Write("auth", util.LOG_ERROR, "auth settings fetch failed err=%v", err)
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if api.Logger != nil {
|
|
api.Logger.Write("auth", util.LOG_INFO, "auth settings fetch success mode=%s ldap_url=%s oidc_authorize_url=%s", settings.AuthMode, settings.LDAPURL, settings.OIDCAuthorizeURL)
|
|
}
|
|
WriteJSON(w, http.StatusOK, settings)
|
|
}
|
|
|
|
func (api *API) UpdateAuthSettings(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var req updateAuthSettingsRequest
|
|
var err error
|
|
var settings models.AuthSettings
|
|
var user models.User
|
|
var ok bool
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
settings = models.AuthSettings{
|
|
AuthMode: normalizeAuthMode(req.AuthMode),
|
|
OIDCEnabled: req.OIDCEnabled,
|
|
LDAPURL: strings.TrimSpace(req.LDAPURL),
|
|
LDAPBindDN: strings.TrimSpace(req.LDAPBindDN),
|
|
LDAPBindPassword: req.LDAPBindPassword,
|
|
LDAPUserBaseDN: strings.TrimSpace(req.LDAPUserBaseDN),
|
|
LDAPUserFilter: strings.TrimSpace(req.LDAPUserFilter),
|
|
LDAPTLSInsecureSkipVerify: req.LDAPTLSInsecureSkipVerify,
|
|
OIDCClientID: strings.TrimSpace(req.OIDCClientID),
|
|
OIDCClientSecret: req.OIDCClientSecret,
|
|
OIDCAuthorizeURL: strings.TrimSpace(req.OIDCAuthorizeURL),
|
|
OIDCTokenURL: strings.TrimSpace(req.OIDCTokenURL),
|
|
OIDCUserInfoURL: strings.TrimSpace(req.OIDCUserInfoURL),
|
|
OIDCRedirectURL: strings.TrimSpace(req.OIDCRedirectURL),
|
|
OIDCScopes: strings.TrimSpace(req.OIDCScopes),
|
|
OIDCTLSInsecureSkipVerify: req.OIDCTLSInsecureSkipVerify,
|
|
}
|
|
if settings.LDAPUserFilter == "" {
|
|
settings.LDAPUserFilter = "(uid={username})"
|
|
}
|
|
if settings.OIDCScopes == "" {
|
|
settings.OIDCScopes = "openid profile email"
|
|
}
|
|
if ok && api.Logger != nil {
|
|
api.Logger.Write("auth", util.LOG_INFO, "auth settings update requested by user=%s mode=%s oidc_enabled=%t ldap_url=%s oidc_authorize_url=%s",
|
|
user.Username, settings.AuthMode, settings.OIDCEnabled, settings.LDAPURL, settings.OIDCAuthorizeURL)
|
|
}
|
|
err = api.Store.SetAuthSettings(settings)
|
|
if err != nil {
|
|
if api.Logger != nil {
|
|
api.Logger.Write("auth", util.LOG_ERROR, "auth settings update failed err=%v", err)
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if api.Logger != nil {
|
|
api.Logger.Write("auth", util.LOG_INFO, "auth settings update success mode=%s oidc_enabled=%t ldap_url=%s oidc_authorize_url=%s", settings.AuthMode, settings.OIDCEnabled, settings.LDAPURL, settings.OIDCAuthorizeURL)
|
|
}
|
|
WriteJSON(w, http.StatusOK, settings)
|
|
}
|
|
|
|
func (api *API) TestLDAPSettings(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var req testLDAPSettingsRequest
|
|
var err error
|
|
var merged models.AuthSettings
|
|
var cfg config.Config
|
|
var user auth.LDAPUser
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
merged, err = api.getMergedAuthSettings()
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.AuthMode) != "" {
|
|
merged.AuthMode = normalizeAuthMode(req.AuthMode)
|
|
}
|
|
if strings.TrimSpace(req.LDAPURL) != "" {
|
|
merged.LDAPURL = strings.TrimSpace(req.LDAPURL)
|
|
}
|
|
if strings.TrimSpace(req.LDAPBindDN) != "" {
|
|
merged.LDAPBindDN = strings.TrimSpace(req.LDAPBindDN)
|
|
}
|
|
if req.LDAPBindPassword != "" {
|
|
merged.LDAPBindPassword = req.LDAPBindPassword
|
|
}
|
|
if strings.TrimSpace(req.LDAPUserBaseDN) != "" {
|
|
merged.LDAPUserBaseDN = strings.TrimSpace(req.LDAPUserBaseDN)
|
|
}
|
|
if strings.TrimSpace(req.LDAPUserFilter) != "" {
|
|
merged.LDAPUserFilter = strings.TrimSpace(req.LDAPUserFilter)
|
|
}
|
|
if req.LDAPTLSInsecureSkipVerify != nil {
|
|
merged.LDAPTLSInsecureSkipVerify = *req.LDAPTLSInsecureSkipVerify
|
|
}
|
|
if merged.LDAPUserFilter == "" {
|
|
merged.LDAPUserFilter = "(uid={username})"
|
|
}
|
|
cfg = api.Cfg
|
|
cfg.AuthMode = merged.AuthMode
|
|
cfg.LDAPURL = merged.LDAPURL
|
|
cfg.LDAPBindDN = merged.LDAPBindDN
|
|
cfg.LDAPBindPassword = merged.LDAPBindPassword
|
|
cfg.LDAPUserBaseDN = merged.LDAPUserBaseDN
|
|
cfg.LDAPUserFilter = merged.LDAPUserFilter
|
|
err = auth.LDAPTestConnectionContext(r.Context(), cfg)
|
|
if err != nil {
|
|
if api.Logger != nil {
|
|
api.Logger.Write("auth", util.LOG_WARN, "ldap test connection failed url=%s err=%v", cfg.LDAPURL, err)
|
|
}
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if api.Logger != nil {
|
|
api.Logger.Write("auth", util.LOG_INFO, "ldap test connection success url=%s", cfg.LDAPURL)
|
|
}
|
|
if strings.TrimSpace(req.Username) != "" && strings.TrimSpace(req.Password) != "" {
|
|
user, err = auth.LDAPAuthenticateContext(r.Context(), cfg, strings.TrimSpace(req.Username), req.Password)
|
|
if err != nil {
|
|
if api.Logger != nil {
|
|
api.Logger.Write("auth", util.LOG_WARN, "ldap test bind failed username=%s err=%v", strings.TrimSpace(req.Username), err)
|
|
}
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if api.Logger != nil {
|
|
api.Logger.Write("auth", util.LOG_INFO, "ldap test bind success username=%s", user.Username)
|
|
}
|
|
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok", "user": user.Username})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (api *API) GetTLSSettings(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var settings models.TLSSettings
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
settings, err = api.getMergedTLSSettings()
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, settings)
|
|
}
|
|
|
|
func (api *API) UpdateTLSSettings(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var req updateTLSSettingsRequest
|
|
var settings models.TLSSettings
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
settings = models.TLSSettings{
|
|
HTTPAddrs: normalizeAddrList(req.HTTPAddrs),
|
|
HTTPSAddrs: normalizeAddrList(req.HTTPSAddrs),
|
|
TLSServerCertSource: "pki",
|
|
TLSCertFile: "",
|
|
TLSKeyFile: "",
|
|
TLSPKIServerCertID: strings.TrimSpace(req.TLSPKIServerCertID),
|
|
TLSClientAuth: normalizeTLSClientAuth(req.TLSClientAuth),
|
|
TLSClientCAFile: "",
|
|
TLSPKIClientCAID: strings.TrimSpace(req.TLSPKIClientCAID),
|
|
TLSMinVersion: normalizeTLSMinVersion(req.TLSMinVersion),
|
|
}
|
|
if len(settings.HTTPAddrs) == 0 && len(settings.HTTPSAddrs) == 0 {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "at least one of http_addrs or https_addrs is required"})
|
|
return
|
|
}
|
|
if len(settings.HTTPSAddrs) > 0 && strings.TrimSpace(settings.TLSPKIServerCertID) == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "tls_pki_server_cert_id is required when https_addrs is configured"})
|
|
return
|
|
}
|
|
if tlsClientAuthNeedsCA(settings.TLSClientAuth) && !tlsClientCAConfigured(settings.TLSPKIClientCAID) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "tls_pki_client_ca_id is required for selected tls_client_auth"})
|
|
return
|
|
}
|
|
err = api.Store.SetTLSSettings(settings)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, settings)
|
|
}
|
|
|
|
func (api *API) ListTLSListeners(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var items []models.TLSListener
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
items, err = api.Store.ListTLSListeners()
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, items)
|
|
}
|
|
|
|
func (api *API) CreateTLSListener(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var req tlsListenerRequest
|
|
var item models.TLSListener
|
|
var created models.TLSListener
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
item = normalizeTLSListenerRequest(req)
|
|
if item.Name == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
|
|
return
|
|
}
|
|
if len(item.HTTPAddrs) == 0 && len(item.HTTPSAddrs) == 0 {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "at least one of http_addrs or https_addrs is required"})
|
|
return
|
|
}
|
|
if len(item.HTTPSAddrs) > 0 && strings.TrimSpace(item.TLSPKIServerCertID) == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "tls_pki_server_cert_id is required when https_addrs is configured"})
|
|
return
|
|
}
|
|
if tlsClientAuthNeedsCA(item.TLSClientAuth) && !tlsClientCAConfigured(item.TLSPKIClientCAID) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "tls_pki_client_ca_id is required for selected tls_client_auth"})
|
|
return
|
|
}
|
|
created, err = api.Store.CreateTLSListener(item)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if api.OnTLSListenersChanged != nil {
|
|
api.OnTLSListenersChanged()
|
|
}
|
|
WriteJSON(w, http.StatusCreated, created)
|
|
}
|
|
|
|
func (api *API) UpdateTLSListener(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req tlsListenerRequest
|
|
var item models.TLSListener
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
item, err = api.Store.GetTLSListener(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "listener not found"})
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
item = mergeTLSListener(item, normalizeTLSListenerRequest(req))
|
|
if item.Name == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
|
|
return
|
|
}
|
|
if len(item.HTTPAddrs) == 0 && len(item.HTTPSAddrs) == 0 {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "at least one of http_addrs or https_addrs is required"})
|
|
return
|
|
}
|
|
if len(item.HTTPSAddrs) > 0 && strings.TrimSpace(item.TLSPKIServerCertID) == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "tls_pki_server_cert_id is required when https_addrs is configured"})
|
|
return
|
|
}
|
|
if tlsClientAuthNeedsCA(item.TLSClientAuth) && !tlsClientCAConfigured(item.TLSPKIClientCAID) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "tls_pki_client_ca_id is required for selected tls_client_auth"})
|
|
return
|
|
}
|
|
err = api.Store.UpdateTLSListener(item)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if api.OnTLSListenersChanged != nil {
|
|
api.OnTLSListenersChanged()
|
|
}
|
|
WriteJSON(w, http.StatusOK, item)
|
|
}
|
|
|
|
func (api *API) DeleteTLSListener(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
err = api.Store.DeleteTLSListener(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if api.OnTLSListenersChanged != nil {
|
|
api.OnTLSListenersChanged()
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) GetTLSListenerRuntimeStatus(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var status map[string]int
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
status = make(map[string]int)
|
|
if api.OnTLSListenerRuntimeStatus != nil {
|
|
status = api.OnTLSListenerRuntimeStatus()
|
|
}
|
|
WriteJSON(w, http.StatusOK, status)
|
|
}
|
|
|
|
func (api *API) ListServicePrincipals(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var items []models.ServicePrincipal
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
items, err = api.Store.ListServicePrincipals()
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, items)
|
|
}
|
|
|
|
func (api *API) CreateServicePrincipal(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var req servicePrincipalRequest
|
|
var item models.ServicePrincipal
|
|
var created models.ServicePrincipal
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
item = models.ServicePrincipal{
|
|
Name: strings.TrimSpace(req.Name),
|
|
Description: strings.TrimSpace(req.Description),
|
|
IsAdmin: req.IsAdmin,
|
|
Disabled: req.Disabled,
|
|
}
|
|
if item.Name == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
|
|
return
|
|
}
|
|
created, err = api.Store.CreateServicePrincipal(item)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusCreated, created)
|
|
}
|
|
|
|
func (api *API) UpdateServicePrincipal(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req servicePrincipalRequest
|
|
var item models.ServicePrincipal
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
item, err = api.Store.GetServicePrincipal(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "principal not found"})
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
item.Name = strings.TrimSpace(req.Name)
|
|
item.Description = strings.TrimSpace(req.Description)
|
|
item.IsAdmin = req.IsAdmin
|
|
item.Disabled = req.Disabled
|
|
if item.Name == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
|
|
return
|
|
}
|
|
err = api.Store.UpdateServicePrincipal(item)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, item)
|
|
}
|
|
|
|
func (api *API) DeleteServicePrincipal(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
err = api.Store.DeleteServicePrincipal(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) ListCertPrincipalBindings(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var items []models.CertPrincipalBinding
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
items, err = api.Store.ListCertPrincipalBindings()
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, items)
|
|
}
|
|
|
|
func (api *API) UpsertCertPrincipalBinding(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var req certPrincipalBindingRequest
|
|
var item models.CertPrincipalBinding
|
|
var updated models.CertPrincipalBinding
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
item = models.CertPrincipalBinding{
|
|
Fingerprint: strings.ToLower(strings.TrimSpace(req.Fingerprint)),
|
|
PrincipalID: strings.TrimSpace(req.PrincipalID),
|
|
Enabled: req.Enabled,
|
|
}
|
|
if item.Fingerprint == "" || item.PrincipalID == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "fingerprint and principal_id are required"})
|
|
return
|
|
}
|
|
updated, err = api.Store.UpsertCertPrincipalBinding(item)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, updated)
|
|
}
|
|
|
|
func (api *API) DeleteCertPrincipalBinding(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
err = api.Store.DeleteCertPrincipalBinding(params["fingerprint"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) ListPrincipalProjectRoles(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var items []models.PrincipalProjectRole
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
items, err = api.Store.ListPrincipalProjectRoles(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, items)
|
|
}
|
|
|
|
func (api *API) UpsertPrincipalProjectRole(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req principalProjectRoleRequest
|
|
var item models.PrincipalProjectRole
|
|
var saved models.PrincipalProjectRole
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
item = models.PrincipalProjectRole{
|
|
PrincipalID: strings.TrimSpace(params["id"]),
|
|
ProjectID: strings.TrimSpace(req.ProjectID),
|
|
Role: normalizeRole(req.Role),
|
|
}
|
|
if item.PrincipalID == "" || item.ProjectID == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "principal id and project id are required"})
|
|
return
|
|
}
|
|
saved, err = api.Store.UpsertPrincipalProjectRole(item)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, saved)
|
|
}
|
|
|
|
func (api *API) DeletePrincipalProjectRole(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
err = api.Store.DeletePrincipalProjectRole(params["id"], params["projectId"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) ListProjects(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var projects []models.Project
|
|
var err error
|
|
var limit int
|
|
var offset int
|
|
var query string
|
|
var v string
|
|
var i int
|
|
var user models.User
|
|
var ok bool
|
|
limit = 0
|
|
offset = 0
|
|
query = strings.TrimSpace(r.URL.Query().Get("q"))
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
v = r.URL.Query().Get("limit")
|
|
if v != "" {
|
|
i, err = strconv.Atoi(v)
|
|
if err == nil && i > 0 {
|
|
limit = i
|
|
}
|
|
}
|
|
v = r.URL.Query().Get("offset")
|
|
if v != "" {
|
|
i, err = strconv.Atoi(v)
|
|
if err == nil && i >= 0 {
|
|
offset = i
|
|
}
|
|
}
|
|
if user.IsAdmin {
|
|
if query != "" || limit > 0 || offset > 0 {
|
|
projects, err = api.Store.ListProjectsFiltered(limit, offset, query)
|
|
} else {
|
|
projects, err = api.Store.ListProjects()
|
|
}
|
|
} else {
|
|
if query != "" || limit > 0 || offset > 0 {
|
|
projects, err = api.Store.ListProjectsFilteredForUser(user.ID, limit, offset, query)
|
|
} else {
|
|
projects, err = api.Store.ListProjectsForUser(user.ID)
|
|
}
|
|
}
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, projects)
|
|
}
|
|
|
|
func (api *API) GetProject(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var project models.Project
|
|
var err error
|
|
project, err = api.Store.GetProject(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
|
|
return
|
|
}
|
|
if !api.requireProjectRole(w, r, project.ID, "viewer") {
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, project)
|
|
}
|
|
|
|
func (api *API) CreateProject(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var req createProjectRequest
|
|
var err error
|
|
var user models.User
|
|
var project models.Project
|
|
var created models.Project
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
if req.Slug == "" || req.Name == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "slug and name required"})
|
|
return
|
|
}
|
|
if slugHasWhitespace(req.Slug) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "slug cannot contain whitespace"})
|
|
return
|
|
}
|
|
user, _ = middleware.UserFromContext(r.Context())
|
|
project = models.Project{
|
|
Slug: req.Slug,
|
|
Name: req.Name,
|
|
Description: req.Description,
|
|
HomePage: normalizeProjectHomePage(req.HomePage),
|
|
CreatedBy: user.ID,
|
|
UpdatedBy: user.ID,
|
|
}
|
|
created, err = api.Store.CreateProject(project)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusCreated, created)
|
|
}
|
|
|
|
func (api *API) UpdateProject(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var project models.Project
|
|
var err error
|
|
var req createProjectRequest
|
|
var user models.User
|
|
if !api.requireProjectRole(w, r, params["id"], "admin") {
|
|
return
|
|
}
|
|
project, err = api.Store.GetProject(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
if req.Name != "" {
|
|
project.Name = req.Name
|
|
}
|
|
if req.Slug != "" {
|
|
if slugHasWhitespace(req.Slug) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "slug cannot contain whitespace"})
|
|
return
|
|
}
|
|
project.Slug = req.Slug
|
|
}
|
|
project.Description = req.Description
|
|
if req.HomePage != "" {
|
|
project.HomePage = normalizeProjectHomePage(req.HomePage)
|
|
}
|
|
if project.HomePage == "" {
|
|
project.HomePage = "info"
|
|
}
|
|
user, _ = middleware.UserFromContext(r.Context())
|
|
project.UpdatedBy = user.ID
|
|
project.UpdatedAt = time.Now().UTC().Unix()
|
|
err = api.Store.UpdateProject(project)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, project)
|
|
}
|
|
|
|
func (api *API) DeleteProject(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var err error
|
|
var repos []models.Repo
|
|
var running bool
|
|
var i int
|
|
var tempPaths []string
|
|
var sourcePaths []string
|
|
var temp string
|
|
if !api.requireProjectRole(w, r, params["id"], "admin") {
|
|
return
|
|
}
|
|
repos, err = api.Store.ListReposOwned(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
for i = 0; i < len(repos); i++ {
|
|
if repos[i].Type != "rpm" {
|
|
continue
|
|
}
|
|
running, err = api.Store.HasRunningRPMMirrorTask(repos[i].ID)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if running {
|
|
WriteJSON(w, http.StatusConflict, map[string]string{"error": "cannot delete project while rpm mirror sync is running", "repo_id": repos[i].ID, "repo_name": repos[i].Name})
|
|
return
|
|
}
|
|
}
|
|
tempPaths = make([]string, 0, len(repos))
|
|
sourcePaths = make([]string, 0, len(repos))
|
|
for i = 0; i < len(repos); i++ {
|
|
if repos[i].Path == "" {
|
|
continue
|
|
}
|
|
temp, err = moveToTrash(repos[i].Path)
|
|
if err != nil {
|
|
restoreFromTrash(tempPaths, sourcePaths)
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
tempPaths = append(tempPaths, temp)
|
|
sourcePaths = append(sourcePaths, repos[i].Path)
|
|
}
|
|
err = api.Store.DeleteProject(params["id"])
|
|
if err != nil {
|
|
restoreFromTrash(tempPaths, sourcePaths)
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
removeTrash(tempPaths)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) ListProjectMembers(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var members []models.ProjectMember
|
|
var err error
|
|
if !api.requireProjectRole(w, r, params["projectId"], "viewer") {
|
|
return
|
|
}
|
|
members, err = api.Store.ListProjectMembers(params["projectId"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, members)
|
|
}
|
|
|
|
func (api *API) AddProjectMember(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req projectMemberRequest
|
|
var err error
|
|
var member models.ProjectMember
|
|
if !api.requireProjectRole(w, r, params["projectId"], "admin") {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
if req.UserID == "" || req.Role == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "user_id and role required"})
|
|
return
|
|
}
|
|
member, err = api.Store.AddProjectMember(params["projectId"], req.UserID, normalizeRole(req.Role))
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusCreated, member)
|
|
}
|
|
|
|
func (api *API) UpdateProjectMember(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req projectMemberRequest
|
|
var err error
|
|
if !api.requireProjectRole(w, r, params["projectId"], "admin") {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
if req.UserID == "" || req.Role == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "user_id and role required"})
|
|
return
|
|
}
|
|
err = api.Store.UpdateProjectMemberRole(params["projectId"], req.UserID, normalizeRole(req.Role))
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) RemoveProjectMember(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var err error
|
|
if !api.requireProjectRole(w, r, params["projectId"], "admin") {
|
|
return
|
|
}
|
|
err = api.Store.RemoveProjectMember(params["projectId"], params["userId"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) ListProjectMemberCandidates(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var users []models.User
|
|
var err error
|
|
var query string
|
|
var filtered []models.User
|
|
var i int
|
|
var username string
|
|
var displayName string
|
|
var email string
|
|
if !api.requireProjectRole(w, r, params["projectId"], "admin") {
|
|
return
|
|
}
|
|
users, err = api.Store.ListUsers()
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
query = strings.ToLower(strings.TrimSpace(r.URL.Query().Get("q")))
|
|
if query == "" {
|
|
WriteJSON(w, http.StatusOK, users)
|
|
return
|
|
}
|
|
filtered = make([]models.User, 0, len(users))
|
|
for i = 0; i < len(users); i++ {
|
|
username = strings.ToLower(users[i].Username)
|
|
displayName = strings.ToLower(users[i].DisplayName)
|
|
email = strings.ToLower(users[i].Email)
|
|
if strings.Contains(username, query) || strings.Contains(displayName, query) || strings.Contains(email, query) {
|
|
filtered = append(filtered, users[i])
|
|
}
|
|
}
|
|
WriteJSON(w, http.StatusOK, filtered)
|
|
}
|
|
|
|
func (api *API) ListRepos(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var err error
|
|
var repos []models.Repo
|
|
var resp []repoListItem
|
|
var i int
|
|
var repo models.Repo
|
|
var cache map[string]models.Project
|
|
var owner models.Project
|
|
var ok bool
|
|
if !api.requireProjectRole(w, r, params["projectId"], "viewer") {
|
|
return
|
|
}
|
|
repos, err = api.Store.ListRepos(params["projectId"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
resp = make([]repoListItem, 0, len(repos))
|
|
cache = map[string]models.Project{}
|
|
for i = 0; i < len(repos); i++ {
|
|
repo = repos[i]
|
|
owner, ok = cache[repo.ProjectID]
|
|
if !ok {
|
|
owner, err = api.Store.GetProject(repo.ProjectID)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
|
|
return
|
|
}
|
|
cache[repo.ProjectID] = owner
|
|
}
|
|
var cloneURL string
|
|
var rpmURL string
|
|
var dockerURL string
|
|
if repo.Type == "git" {
|
|
cloneURL = api.cloneURL(owner.Slug, repo.Name)
|
|
}
|
|
if repo.Type == "rpm" {
|
|
rpmURL = api.rpmURL(owner.Slug, repo.Name)
|
|
}
|
|
if repo.Type == "docker" {
|
|
dockerURL = api.dockerURL(owner.Slug, repo.Name)
|
|
}
|
|
resp = append(resp, repoListItem{
|
|
Repo: repo,
|
|
CloneURL: cloneURL,
|
|
RPMURL: rpmURL,
|
|
DockerURL: dockerURL,
|
|
OwnerProject: owner.ID,
|
|
OwnerSlug: owner.Slug,
|
|
})
|
|
}
|
|
WriteJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
func (api *API) ListAllRepos(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var err error
|
|
var repos []models.Repo
|
|
var resp []repoListItem
|
|
var i int
|
|
var repo models.Repo
|
|
var cache map[string]models.Project
|
|
var owner models.Project
|
|
var ok bool
|
|
var user models.User
|
|
var projectIDs []string
|
|
var query string
|
|
var repoType string
|
|
var filtered []models.Repo
|
|
var okUser bool
|
|
user, okUser = middleware.UserFromContext(r.Context())
|
|
if !okUser {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
if user.IsAdmin {
|
|
repos, err = api.Store.ListAllRepos()
|
|
} else {
|
|
projectIDs, err = api.Store.ListProjectIDsForUser(user.ID)
|
|
if err == nil {
|
|
repos, err = api.Store.ListReposByProjectIDs(projectIDs)
|
|
}
|
|
}
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
query = strings.ToLower(strings.TrimSpace(r.URL.Query().Get("q")))
|
|
repoType = strings.ToLower(strings.TrimSpace(r.URL.Query().Get("type")))
|
|
if query != "" || (repoType != "" && repoType != "all") {
|
|
filtered = make([]models.Repo, 0, len(repos))
|
|
for i = 0; i < len(repos); i++ {
|
|
repo = repos[i]
|
|
if repoType != "" && repoType != "all" && repo.Type != repoType {
|
|
continue
|
|
}
|
|
if query != "" {
|
|
if !strings.Contains(strings.ToLower(repo.Name), query) && !strings.Contains(strings.ToLower(repo.ID), query) {
|
|
continue
|
|
}
|
|
}
|
|
filtered = append(filtered, repo)
|
|
}
|
|
repos = filtered
|
|
}
|
|
resp = make([]repoListItem, 0, len(repos))
|
|
cache = map[string]models.Project{}
|
|
for i = 0; i < len(repos); i++ {
|
|
repo = repos[i]
|
|
owner, ok = cache[repo.ProjectID]
|
|
if !ok {
|
|
owner, err = api.Store.GetProject(repo.ProjectID)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
|
|
return
|
|
}
|
|
cache[repo.ProjectID] = owner
|
|
}
|
|
var cloneURL string
|
|
var rpmURL string
|
|
var dockerURL string
|
|
if repo.Type == "git" {
|
|
cloneURL = api.cloneURL(owner.Slug, repo.Name)
|
|
}
|
|
if repo.Type == "rpm" {
|
|
rpmURL = api.rpmURL(owner.Slug, repo.Name)
|
|
}
|
|
if repo.Type == "docker" {
|
|
dockerURL = api.dockerURL(owner.Slug, repo.Name)
|
|
}
|
|
resp = append(resp, repoListItem{
|
|
Repo: repo,
|
|
CloneURL: cloneURL,
|
|
RPMURL: rpmURL,
|
|
DockerURL: dockerURL,
|
|
OwnerProject: owner.ID,
|
|
OwnerSlug: owner.Slug,
|
|
})
|
|
}
|
|
WriteJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
func (api *API) GetRepo(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var project models.Project
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
|
return
|
|
}
|
|
project, err = api.Store.GetProject(repo.ProjectID)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
|
|
return
|
|
}
|
|
var cloneURL string
|
|
var rpmURL string
|
|
var dockerURL string
|
|
if repo.Type == "git" {
|
|
cloneURL = api.cloneURL(project.Slug, repo.Name)
|
|
}
|
|
if repo.Type == "rpm" {
|
|
rpmURL = api.rpmURL(project.Slug, repo.Name)
|
|
}
|
|
if repo.Type == "docker" {
|
|
dockerURL = api.dockerURL(project.Slug, repo.Name)
|
|
}
|
|
WriteJSON(w, http.StatusOK, repoResponse{Repo: repo, CloneURL: cloneURL, RPMURL: rpmURL, DockerURL: dockerURL})
|
|
}
|
|
|
|
func (api *API) CreateRepo(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req createRepoRequest
|
|
var err error
|
|
var user models.User
|
|
var repoID string
|
|
var project models.Project
|
|
var repoPath string
|
|
var repo models.Repo
|
|
var created models.Repo
|
|
var repoType string
|
|
var projectStorageID int64
|
|
var repoStorageID int64
|
|
var ok bool
|
|
var exists bool
|
|
if !api.requireProjectRole(w, r, params["projectId"], "writer") {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
if req.Name == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name required"})
|
|
return
|
|
}
|
|
if nameHasWhitespace(req.Name) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name cannot contain whitespace"})
|
|
return
|
|
}
|
|
repoType, ok = normalizeRepoType(req.Type)
|
|
if !ok {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "unsupported repo type"})
|
|
return
|
|
}
|
|
exists, err = api.Store.RepoNameExists(params["projectId"], req.Name, repoType)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if exists {
|
|
WriteJSON(w, http.StatusConflict, map[string]string{"error": "repository name already exists"})
|
|
return
|
|
}
|
|
user, _ = middleware.UserFromContext(r.Context())
|
|
repoID, err = util.NewID()
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to create repo"})
|
|
return
|
|
}
|
|
project, err = api.Store.GetProject(params["projectId"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
|
|
return
|
|
}
|
|
repo = models.Repo{
|
|
ID: repoID,
|
|
ProjectID: params["projectId"],
|
|
Name: req.Name,
|
|
Type: repoType,
|
|
Path: "",
|
|
CreatedBy: user.ID,
|
|
}
|
|
created, err = api.Store.CreateRepo(repo)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
projectStorageID, repoStorageID, err = api.Store.GetRepoStorageIDs(created.ID)
|
|
if err != nil {
|
|
_ = api.Store.DeleteRepo(created.ID)
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
repoPath = api.repoStoragePathByType(created.Type, projectStorageID, repoStorageID)
|
|
if created.Type == "git" {
|
|
err = os.MkdirAll(filepath.Dir(repoPath), 0o755)
|
|
if err == nil {
|
|
err = api.Repos.InitRepo(repoPath, true)
|
|
}
|
|
} else {
|
|
err = os.MkdirAll(repoPath, 0o755)
|
|
}
|
|
if err != nil {
|
|
_ = api.Store.DeleteRepo(created.ID)
|
|
_ = os.RemoveAll(repoPath)
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
created.Path = repoPath
|
|
err = api.Store.UpdateRepo(created)
|
|
if err != nil {
|
|
_ = api.Store.DeleteRepo(created.ID)
|
|
_ = os.RemoveAll(repoPath)
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
var cloneURL string
|
|
var rpmURL string
|
|
var dockerURL string
|
|
if created.Type == "git" {
|
|
cloneURL = api.cloneURL(project.Slug, created.Name)
|
|
}
|
|
if created.Type == "rpm" {
|
|
rpmURL = api.rpmURL(project.Slug, created.Name)
|
|
}
|
|
if created.Type == "docker" {
|
|
dockerURL = api.dockerURL(project.Slug, created.Name)
|
|
}
|
|
WriteJSON(w, http.StatusCreated, repoResponse{Repo: created, CloneURL: cloneURL, RPMURL: rpmURL, DockerURL: dockerURL})
|
|
}
|
|
|
|
func (api *API) ListAvailableRepos(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repos []models.Repo
|
|
var err error
|
|
var query string
|
|
var limit int
|
|
var offset int
|
|
var v string
|
|
var i int
|
|
var resp []availableRepoItem
|
|
var repo models.Repo
|
|
var cache map[string]models.Project
|
|
var owner models.Project
|
|
var ok bool
|
|
if !api.requireProjectRole(w, r, params["projectId"], "admin") {
|
|
return
|
|
}
|
|
query = strings.TrimSpace(r.URL.Query().Get("q"))
|
|
limit = 0
|
|
offset = 0
|
|
v = r.URL.Query().Get("limit")
|
|
if v != "" {
|
|
i, err = strconv.Atoi(v)
|
|
if err == nil && i > 0 {
|
|
limit = i
|
|
}
|
|
}
|
|
v = r.URL.Query().Get("offset")
|
|
if v != "" {
|
|
i, err = strconv.Atoi(v)
|
|
if err == nil && i >= 0 {
|
|
offset = i
|
|
}
|
|
}
|
|
repos, err = api.Store.ListAvailableReposForProject(params["projectId"], query, limit, offset)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
resp = make([]availableRepoItem, 0, len(repos))
|
|
cache = map[string]models.Project{}
|
|
for i = 0; i < len(repos); i++ {
|
|
repo = repos[i]
|
|
owner, ok = cache[repo.ProjectID]
|
|
if !ok {
|
|
owner, err = api.Store.GetProject(repo.ProjectID)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
|
|
return
|
|
}
|
|
cache[repo.ProjectID] = owner
|
|
}
|
|
resp = append(resp, availableRepoItem{
|
|
Repo: repo,
|
|
OwnerProject: owner.ID,
|
|
OwnerSlug: owner.Slug,
|
|
})
|
|
}
|
|
WriteJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
func (api *API) AttachForeignRepo(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req attachRepoRequest
|
|
var err error
|
|
var repo models.Repo
|
|
var project models.Project
|
|
if !api.requireProjectRole(w, r, params["projectId"], "admin") {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
if req.RepoID == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo_id required"})
|
|
return
|
|
}
|
|
repo, err = api.getRepoResolved(req.RepoID)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if repo.ProjectID == params["projectId"] {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo already belongs to this project"})
|
|
return
|
|
}
|
|
project, err = api.Store.GetProject(repo.ProjectID)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
|
|
return
|
|
}
|
|
err = api.Store.AttachRepoToProject(params["projectId"], repo.ID)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
repo.IsForeign = true
|
|
var cloneURL string
|
|
var rpmURL string
|
|
var dockerURL string
|
|
if repo.Type == "git" {
|
|
cloneURL = api.cloneURL(project.Slug, repo.Name)
|
|
}
|
|
if repo.Type == "rpm" {
|
|
rpmURL = api.rpmURL(project.Slug, repo.Name)
|
|
}
|
|
if repo.Type == "docker" {
|
|
dockerURL = api.dockerURL(project.Slug, repo.Name)
|
|
}
|
|
WriteJSON(w, http.StatusCreated, repoListItem{
|
|
Repo: repo,
|
|
CloneURL: cloneURL,
|
|
RPMURL: rpmURL,
|
|
DockerURL: dockerURL,
|
|
OwnerProject: project.ID,
|
|
OwnerSlug: project.Slug,
|
|
})
|
|
}
|
|
|
|
func (api *API) DetachForeignRepo(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
if !api.requireProjectRole(w, r, params["projectId"], "admin") {
|
|
return
|
|
}
|
|
repo, err = api.getRepoResolved(params["repoId"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if repo.ProjectID == params["projectId"] {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "cannot detach owned repo"})
|
|
return
|
|
}
|
|
err = api.Store.DetachRepoFromProject(params["projectId"], repo.ID)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) UpdateRepo(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var project models.Project
|
|
var req updateRepoRequest
|
|
var err error
|
|
var newPath string
|
|
var projectStorageID int64
|
|
var repoStorageID int64
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireProjectRole(w, r, repo.ProjectID, "writer") {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.Name) == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name required"})
|
|
return
|
|
}
|
|
if nameHasWhitespace(req.Name) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name cannot contain whitespace"})
|
|
return
|
|
}
|
|
project, err = api.Store.GetProject(repo.ProjectID)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
|
|
return
|
|
}
|
|
projectStorageID, repoStorageID, err = api.Store.GetRepoStorageIDs(repo.ID)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
newPath = api.repoStoragePathByType(repo.Type, projectStorageID, repoStorageID)
|
|
repo.Name = req.Name
|
|
repo.Path = newPath
|
|
err = api.Store.UpdateRepo(repo)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
var cloneURL string
|
|
var rpmURL string
|
|
var dockerURL string
|
|
if repo.Type == "git" {
|
|
cloneURL = api.cloneURL(project.Slug, repo.Name)
|
|
}
|
|
if repo.Type == "rpm" {
|
|
rpmURL = api.rpmURL(project.Slug, repo.Name)
|
|
}
|
|
if repo.Type == "docker" {
|
|
dockerURL = api.dockerURL(project.Slug, repo.Name)
|
|
}
|
|
WriteJSON(w, http.StatusOK, repoResponse{Repo: repo, CloneURL: cloneURL, RPMURL: rpmURL, DockerURL: dockerURL})
|
|
}
|
|
|
|
func (api *API) DeleteRepo(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var project models.Project
|
|
var running bool
|
|
var err error
|
|
var temp string
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireProjectRole(w, r, repo.ProjectID, "writer") {
|
|
return
|
|
}
|
|
if repo.Type == "rpm" {
|
|
running, err = api.Store.HasRunningRPMMirrorTask(repo.ID)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if running {
|
|
WriteJSON(w, http.StatusConflict, map[string]string{"error": "cannot delete repository while rpm mirror sync is running"})
|
|
return
|
|
}
|
|
}
|
|
project, err = api.Store.GetProject(repo.ProjectID)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
|
|
return
|
|
}
|
|
if repo.Path != "" {
|
|
temp, err = moveToTrash(repo.Path)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
}
|
|
err = api.Store.DeleteRepo(repo.ID)
|
|
if err != nil {
|
|
if temp != "" && repo.Path != "" {
|
|
_ = os.Rename(temp, repo.Path)
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if temp != "" {
|
|
_ = os.RemoveAll(temp)
|
|
}
|
|
var cloneURL string
|
|
var rpmURL string
|
|
var dockerURL string
|
|
if repo.Type == "git" {
|
|
cloneURL = api.cloneURL(project.Slug, repo.Name)
|
|
}
|
|
if repo.Type == "rpm" {
|
|
rpmURL = api.rpmURL(project.Slug, repo.Name)
|
|
}
|
|
if repo.Type == "docker" {
|
|
dockerURL = api.dockerURL(project.Slug, repo.Name)
|
|
}
|
|
WriteJSON(w, http.StatusOK, repoResponse{Repo: repo, CloneURL: cloneURL, RPMURL: rpmURL, DockerURL: dockerURL})
|
|
}
|
|
|
|
func moveToTrash(path string) (string, error) {
|
|
var dir string
|
|
var base string
|
|
var temp string
|
|
var err error
|
|
dir = filepath.Dir(path)
|
|
base = filepath.Base(path)
|
|
temp = filepath.Join(dir, base+"__deleted__"+strconv.FormatInt(time.Now().UnixNano(), 10))
|
|
err = os.Rename(path, temp)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return temp, nil
|
|
}
|
|
|
|
func restoreFromTrash(tempPaths []string, sourcePaths []string) {
|
|
var i int
|
|
if len(tempPaths) != len(sourcePaths) {
|
|
return
|
|
}
|
|
for i = len(tempPaths) - 1; i >= 0; i-- {
|
|
if tempPaths[i] == "" || sourcePaths[i] == "" {
|
|
continue
|
|
}
|
|
_ = os.Rename(tempPaths[i], sourcePaths[i])
|
|
}
|
|
}
|
|
|
|
func removeTrash(tempPaths []string) {
|
|
var i int
|
|
for i = 0; i < len(tempPaths); i++ {
|
|
if tempPaths[i] == "" {
|
|
continue
|
|
}
|
|
_ = os.RemoveAll(tempPaths[i])
|
|
}
|
|
}
|
|
|
|
func (api *API) RepoCommits(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var limit int
|
|
var offset int
|
|
var v string
|
|
var i int
|
|
var query string
|
|
var ref string
|
|
var commits []git.Commit
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
|
return
|
|
}
|
|
limit = 20
|
|
v = r.URL.Query().Get("limit")
|
|
if v != "" {
|
|
i, err = strconv.Atoi(v)
|
|
if err == nil {
|
|
limit = i
|
|
}
|
|
}
|
|
offset = 0
|
|
v = r.URL.Query().Get("offset")
|
|
if v != "" {
|
|
i, err = strconv.Atoi(v)
|
|
if err == nil {
|
|
offset = i
|
|
}
|
|
}
|
|
query = r.URL.Query().Get("q")
|
|
ref = r.URL.Query().Get("ref")
|
|
commits, err = git.ListCommits(repo.Path, ref, limit, offset, query)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, commits)
|
|
}
|
|
|
|
func (api *API) RepoBranches(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var branches []string
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
|
return
|
|
}
|
|
branches, err = git.ListBranches(repo.Path)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, branches)
|
|
}
|
|
|
|
func (api *API) RepoBranchesInfo(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var branches []git.BranchInfo
|
|
var info repoBranchesInfo
|
|
var ref string
|
|
var query string
|
|
var i int
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
|
return
|
|
}
|
|
branches, err = git.ListBranchInfos(repo.Path)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
ref, _ = git.GetDefaultBranch(repo.Path)
|
|
query = strings.ToLower(strings.TrimSpace(r.URL.Query().Get("q")))
|
|
if query != "" {
|
|
var filtered []git.BranchInfo
|
|
for i = 0; i < len(branches); i++ {
|
|
if strings.Contains(strings.ToLower(branches[i].Name), query) {
|
|
filtered = append(filtered, branches[i])
|
|
}
|
|
}
|
|
branches = filtered
|
|
}
|
|
info = repoBranchesInfo{Default: ref, Branches: branches}
|
|
WriteJSON(w, http.StatusOK, info)
|
|
}
|
|
|
|
func (api *API) RepoSetDefaultBranch(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var req repoBranchRequest
|
|
if !api.requireRepoRole(w, r, params["id"], "admin") {
|
|
return
|
|
}
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil || strings.TrimSpace(req.Name) == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "branch name required"})
|
|
return
|
|
}
|
|
err = git.SetDefaultBranch(repo.Path, strings.TrimSpace(req.Name))
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (api *API) RepoDeleteBranch(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var req repoBranchRequest
|
|
var name string
|
|
if !api.requireRepoRole(w, r, params["id"], "admin") {
|
|
return
|
|
}
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
name = strings.TrimSpace(req.Name)
|
|
if name == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "branch name required"})
|
|
return
|
|
}
|
|
err = git.DeleteBranch(repo.Path, name)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (api *API) RepoRenameBranch(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var req repoBranchRenameRequest
|
|
var from string
|
|
var to string
|
|
if !api.requireRepoRole(w, r, params["id"], "admin") {
|
|
return
|
|
}
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
from = strings.TrimSpace(req.From)
|
|
to = strings.TrimSpace(req.To)
|
|
if from == "" || to == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "from and to required"})
|
|
return
|
|
}
|
|
if from == to {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "new branch name must be different"})
|
|
return
|
|
}
|
|
err = git.RenameBranch(repo.Path, from, to)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (api *API) RepoCreateBranch(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var req repoBranchCreateRequest
|
|
if !api.requireRepoRole(w, r, params["id"], "admin") {
|
|
return
|
|
}
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.Name) == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "branch name required"})
|
|
return
|
|
}
|
|
err = git.CreateBranch(repo.Path, strings.TrimSpace(req.Name), strings.TrimSpace(req.From))
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (api *API) RepoTree(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var ref string
|
|
var path string
|
|
var entries []git.TreeEntry
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
|
return
|
|
}
|
|
ref = r.URL.Query().Get("ref")
|
|
path = r.URL.Query().Get("path")
|
|
entries, err = git.ListTree(repo.Path, ref, path)
|
|
if err != nil {
|
|
if errors.Is(err, git.ErrPathNotFound) {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "path not found"})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, entries)
|
|
}
|
|
|
|
func (api *API) RepoBlob(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var ref string
|
|
var path string
|
|
var content string
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
|
return
|
|
}
|
|
ref = r.URL.Query().Get("ref")
|
|
path = r.URL.Query().Get("path")
|
|
if path == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
|
|
return
|
|
}
|
|
content, err = git.ReadBlob(repo.Path, ref, path, 200*1024)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, map[string]string{"path": path, "content": content})
|
|
}
|
|
|
|
func (api *API) RepoBlobRaw(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var ref string
|
|
var path string
|
|
var content []byte
|
|
var contentType string
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
|
return
|
|
}
|
|
ref = r.URL.Query().Get("ref")
|
|
path = r.URL.Query().Get("path")
|
|
if path == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
|
|
return
|
|
}
|
|
content, err = git.ReadBlobBytes(repo.Path, ref, path, 5*1024*1024)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
contentType = http.DetectContentType(content)
|
|
w.Header().Set("Content-Type", contentType)
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write(content)
|
|
}
|
|
|
|
func (api *API) RepoFileHistory(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var ref string
|
|
var path string
|
|
var limit int
|
|
var v string
|
|
var i int
|
|
var commits []git.Commit
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
|
return
|
|
}
|
|
ref = r.URL.Query().Get("ref")
|
|
path = r.URL.Query().Get("path")
|
|
if path == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
|
|
return
|
|
}
|
|
limit = 20
|
|
v = r.URL.Query().Get("limit")
|
|
if v != "" {
|
|
i, err = strconv.Atoi(v)
|
|
if err == nil {
|
|
limit = i
|
|
}
|
|
}
|
|
commits, err = git.ListFileHistory(repo.Path, ref, path, limit)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, commits)
|
|
}
|
|
|
|
func (api *API) RepoFileDiff(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var ref string
|
|
var path string
|
|
var diff string
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
|
return
|
|
}
|
|
ref = r.URL.Query().Get("ref")
|
|
path = r.URL.Query().Get("path")
|
|
if path == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
|
|
return
|
|
}
|
|
diff, err = git.DiffFile(repo.Path, ref, path)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, map[string]string{"path": path, "diff": diff})
|
|
}
|
|
|
|
func (api *API) RepoCommitDetail(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var hash string
|
|
var detail git.CommitDetail
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
|
return
|
|
}
|
|
hash = r.URL.Query().Get("hash")
|
|
if hash == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "hash required"})
|
|
return
|
|
}
|
|
detail, err = git.GetCommitDetail(repo.Path, hash)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, detail)
|
|
}
|
|
|
|
func (api *API) RepoCommitDiff(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var hash string
|
|
var diff string
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
|
return
|
|
}
|
|
hash = r.URL.Query().Get("hash")
|
|
if hash == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "hash required"})
|
|
return
|
|
}
|
|
diff, err = git.CommitDiff(repo.Path, hash)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, map[string]string{"hash": hash, "diff": diff})
|
|
}
|
|
|
|
func (api *API) RepoCompare(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var base string
|
|
var head string
|
|
var limit int
|
|
var v string
|
|
var i int
|
|
var commits []git.Commit
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
|
return
|
|
}
|
|
base = r.URL.Query().Get("base")
|
|
head = r.URL.Query().Get("head")
|
|
if base == "" || head == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "base and head required"})
|
|
return
|
|
}
|
|
limit = 50
|
|
v = r.URL.Query().Get("limit")
|
|
if v != "" {
|
|
i, err = strconv.Atoi(v)
|
|
if err == nil {
|
|
limit = i
|
|
}
|
|
}
|
|
commits, err = git.ListCommitsBetween(repo.Path, base, head, limit)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, commits)
|
|
}
|
|
|
|
func (api *API) RepoStats(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var project models.Project
|
|
var err error
|
|
var branches []string
|
|
var ref string
|
|
var commits []git.Commit
|
|
var stats repoStatsResponse
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
|
return
|
|
}
|
|
project, err = api.Store.GetProject(repo.ProjectID)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
|
|
return
|
|
}
|
|
if repo.Type != "git" {
|
|
stats = repoStatsResponse{
|
|
RepoID: repo.ID,
|
|
Branches: 0,
|
|
DefaultRef: "",
|
|
ProjectSlug: project.Slug,
|
|
Repository: repo.Name,
|
|
CloneURL: "",
|
|
}
|
|
WriteJSON(w, http.StatusOK, stats)
|
|
return
|
|
}
|
|
branches, err = git.ListBranches(repo.Path)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
ref = ""
|
|
if contains(branches, "main") {
|
|
ref = "main"
|
|
} else if contains(branches, "master") {
|
|
ref = "master"
|
|
} else if len(branches) > 0 {
|
|
ref = branches[0]
|
|
}
|
|
if ref != "" {
|
|
commits, err = git.ListCommits(repo.Path, ref, 1, 0, "")
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
}
|
|
stats = repoStatsResponse{
|
|
RepoID: repo.ID,
|
|
Branches: len(branches),
|
|
DefaultRef: ref,
|
|
ProjectSlug: project.Slug,
|
|
Repository: repo.Name,
|
|
CloneURL: api.cloneURL(project.Slug, repo.Name),
|
|
}
|
|
if len(commits) > 0 {
|
|
stats.LastCommit = commits[0].Hash
|
|
stats.LastAuthor = commits[0].Author
|
|
stats.LastWhen = commits[0].When
|
|
stats.LastMessage = commits[0].Message
|
|
}
|
|
WriteJSON(w, http.StatusOK, stats)
|
|
}
|
|
|
|
func (api *API) RepoRPMPackages(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var packages []rpm.PackageSummary
|
|
var query string
|
|
var filtered []rpm.PackageSummary
|
|
var i int
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
|
return
|
|
}
|
|
if repo.Type != "rpm" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
|
|
return
|
|
}
|
|
packages, err = rpm.ListPackages(repo.Path)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
query = strings.ToLower(strings.TrimSpace(r.URL.Query().Get("q")))
|
|
if query == "" {
|
|
WriteJSON(w, http.StatusOK, packages)
|
|
return
|
|
}
|
|
filtered = make([]rpm.PackageSummary, 0, len(packages))
|
|
for i = 0; i < len(packages); i++ {
|
|
if strings.Contains(strings.ToLower(packages[i].Name), query) || strings.Contains(strings.ToLower(packages[i].Filename), query) {
|
|
filtered = append(filtered, packages[i])
|
|
}
|
|
}
|
|
WriteJSON(w, http.StatusOK, filtered)
|
|
}
|
|
|
|
func (api *API) RepoRPMPackage(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var filename string
|
|
var detail rpm.PackageDetail
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
|
return
|
|
}
|
|
if repo.Type != "rpm" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
|
|
return
|
|
}
|
|
filename = strings.TrimSpace(r.URL.Query().Get("file"))
|
|
if filename == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "file is required"})
|
|
return
|
|
}
|
|
detail, err = rpm.GetPackageDetail(repo.Path, filename)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, detail)
|
|
}
|
|
|
|
func (api *API) RepoTypes(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var types []repoTypeItem
|
|
types = []repoTypeItem{
|
|
{Value: "git", Label: "Git"},
|
|
{Value: "rpm", Label: "RPM"},
|
|
{Value: "docker", Label: "Docker"},
|
|
}
|
|
WriteJSON(w, http.StatusOK, types)
|
|
}
|
|
|
|
func (api *API) RepoRPMCreateSubdir(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var dirConfig models.RPMRepoDir
|
|
var writeBlocked bool
|
|
var writeBlockedPath string
|
|
var err error
|
|
var req repoRPMCreateRequest
|
|
var name string
|
|
var dirType string
|
|
var mode string
|
|
var parent string
|
|
var parentPath string
|
|
var fullPath string
|
|
var repodataPath string
|
|
var fullRel string
|
|
var fullRelLower string
|
|
var absParent string
|
|
var hasRepoAncestor bool
|
|
var allowDelete bool
|
|
var tlsInsecureSkipVerify bool
|
|
var syncIntervalSec int64
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "writer") {
|
|
return
|
|
}
|
|
if repo.Type != "rpm" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
name = strings.TrimSpace(req.Name)
|
|
if name == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name required"})
|
|
return
|
|
}
|
|
if strings.EqualFold(name, "repodata") {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repodata is reserved"})
|
|
return
|
|
}
|
|
if !isSafeSubdirName(name) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid subdirectory name"})
|
|
return
|
|
}
|
|
dirType = strings.ToLower(strings.TrimSpace(req.Type))
|
|
if dirType == "" {
|
|
dirType = "container"
|
|
}
|
|
if dirType != "container" && dirType != "repo" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid subdirectory type"})
|
|
return
|
|
}
|
|
parent = strings.TrimSpace(req.Parent)
|
|
if parent != "" {
|
|
if !isSafeSubdirPath(parent) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid parent path"})
|
|
return
|
|
}
|
|
parentPath = filepath.FromSlash(parent)
|
|
}
|
|
parent = filepath.ToSlash(parentPath)
|
|
if parent == "." {
|
|
parent = ""
|
|
}
|
|
writeBlocked, writeBlockedPath, err = api.isRPMWriteBlocked(repo, parent)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if writeBlocked {
|
|
WriteJSON(w, http.StatusForbidden, map[string]string{"error": "writes are disabled for mirror repo subtree", "mirror_root": writeBlockedPath})
|
|
return
|
|
}
|
|
fullRel = filepath.ToSlash(filepath.Join(parentPath, name))
|
|
fullRelLower = strings.ToLower(fullRel)
|
|
if fullRelLower == "repodata" || strings.HasPrefix(fullRelLower, "repodata/") || strings.Contains(fullRelLower, "/repodata/") {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "subdirectories under repodata are not allowed"})
|
|
return
|
|
}
|
|
if dirType == "repo" {
|
|
mode = normalizeRPMRepoDirMode(req.Mode)
|
|
allowDelete = req.AllowDelete
|
|
tlsInsecureSkipVerify = req.TLSInsecureSkipVerify
|
|
syncIntervalSec = req.SyncIntervalSec
|
|
if syncIntervalSec == 0 {
|
|
syncIntervalSec = 300
|
|
}
|
|
syncIntervalSec = normalizeRPMMirrorIntervalSec(syncIntervalSec)
|
|
absParent = filepath.Join(repo.Path, parentPath)
|
|
hasRepoAncestor, err = hasRepodataAncestor(repo.Path, absParent)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if hasRepoAncestor {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo directories cannot be created under another repo directory"})
|
|
return
|
|
}
|
|
err = validateRPMMirrorConfig(mode, strings.TrimSpace(req.RemoteURL), strings.TrimSpace(req.ConnectHost), strings.TrimSpace(req.HostHeader), strings.TrimSpace(req.TLSServerName))
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
}
|
|
fullPath = filepath.Join(repo.Path, parentPath, name)
|
|
err = os.MkdirAll(fullPath, 0o755)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if dirType == "repo" {
|
|
repodataPath = filepath.Join(fullPath, "repodata")
|
|
err = os.MkdirAll(repodataPath, 0o755)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
dirConfig = models.RPMRepoDir{
|
|
RepoID: repo.ID,
|
|
Path: fullRel,
|
|
Mode: mode,
|
|
AllowDelete: allowDelete,
|
|
RemoteURL: strings.TrimSpace(req.RemoteURL),
|
|
ConnectHost: strings.TrimSpace(req.ConnectHost),
|
|
HostHeader: strings.TrimSpace(req.HostHeader),
|
|
TLSServerName: strings.TrimSpace(req.TLSServerName),
|
|
TLSInsecureSkipVerify: tlsInsecureSkipVerify,
|
|
SyncIntervalSec: syncIntervalSec,
|
|
SyncEnabled: true,
|
|
}
|
|
err = api.Store.UpsertRPMRepoDir(dirConfig)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
}
|
|
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (api *API) RepoRPMGetSubdir(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var relPath string
|
|
var normalizedPath string
|
|
var config models.RPMRepoDir
|
|
var err error
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
|
return
|
|
}
|
|
if repo.Type != "rpm" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
|
|
return
|
|
}
|
|
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
|
|
if relPath == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
|
|
return
|
|
}
|
|
if !isSafeSubdirPath(relPath) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
|
return
|
|
}
|
|
normalizedPath = normalizeRPMPath(relPath)
|
|
config, err = api.Store.GetRPMRepoDir(repo.ID, normalizedPath)
|
|
if err == nil {
|
|
WriteJSON(w, http.StatusOK, config)
|
|
return
|
|
}
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, models.RPMRepoDir{RepoID: repo.ID, Path: normalizedPath, Mode: "local"})
|
|
}
|
|
|
|
func (api *API) RepoRPMSyncSubdir(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var relPath string
|
|
var normalizedPath string
|
|
var config models.RPMRepoDir
|
|
var err error
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "writer") {
|
|
return
|
|
}
|
|
if repo.Type != "rpm" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
|
|
return
|
|
}
|
|
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
|
|
if relPath == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
|
|
return
|
|
}
|
|
if !isSafeSubdirPath(relPath) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
|
return
|
|
}
|
|
normalizedPath = normalizeRPMPath(relPath)
|
|
config, err = api.Store.GetRPMRepoDir(repo.ID, normalizedPath)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo directory config not found"})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if normalizeRPMRepoDirMode(config.Mode) != "mirror" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "sync is only supported for mirror mode"})
|
|
return
|
|
}
|
|
if !config.SyncEnabled {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "mirror sync is suspended"})
|
|
return
|
|
}
|
|
err = api.Store.MarkRPMMirrorTaskDirty(repo.ID, normalizedPath)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, map[string]string{"status": "scheduled"})
|
|
}
|
|
|
|
func (api *API) RepoRPMSuspendSubdir(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
api.repoRPMSetSyncEnabled(w, r, params, false)
|
|
}
|
|
|
|
func (api *API) RepoRPMResumeSubdir(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
api.repoRPMSetSyncEnabled(w, r, params, true)
|
|
}
|
|
|
|
func (api *API) RepoRPMRebuildSubdirMetadata(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var relPath string
|
|
var normalizedPath string
|
|
var fullPath string
|
|
var repodataPath string
|
|
var err error
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "writer") {
|
|
return
|
|
}
|
|
if repo.Type != "rpm" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
|
|
return
|
|
}
|
|
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
|
|
if relPath == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
|
|
return
|
|
}
|
|
if !isSafeSubdirPath(relPath) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
|
return
|
|
}
|
|
normalizedPath = normalizeRPMPath(relPath)
|
|
fullPath = filepath.Join(repo.Path, filepath.FromSlash(normalizedPath))
|
|
repodataPath = filepath.Join(fullPath, "repodata")
|
|
_, err = os.Stat(repodataPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path is not a repository directory"})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if api.RpmMeta == nil {
|
|
WriteJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "metadata manager unavailable"})
|
|
return
|
|
}
|
|
api.RpmMeta.Schedule(fullPath)
|
|
WriteJSON(w, http.StatusOK, map[string]string{"status": "scheduled"})
|
|
}
|
|
|
|
func (api *API) RepoRPMCancelSubdirSync(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var relPath string
|
|
var normalizedPath string
|
|
var config models.RPMRepoDir
|
|
var canceled bool
|
|
var err error
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "writer") {
|
|
return
|
|
}
|
|
if repo.Type != "rpm" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
|
|
return
|
|
}
|
|
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
|
|
if relPath == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
|
|
return
|
|
}
|
|
if !isSafeSubdirPath(relPath) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
|
return
|
|
}
|
|
normalizedPath = normalizeRPMPath(relPath)
|
|
config, err = api.Store.GetRPMRepoDir(repo.ID, normalizedPath)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo directory config not found"})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if normalizeRPMRepoDirMode(config.Mode) != "mirror" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "sync control is only supported for mirror mode"})
|
|
return
|
|
}
|
|
if !config.SyncRunning {
|
|
WriteJSON(w, http.StatusConflict, map[string]string{"error": "sync is not running"})
|
|
return
|
|
}
|
|
if api.RpmMirror == nil {
|
|
WriteJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "mirror manager unavailable"})
|
|
return
|
|
}
|
|
canceled = api.RpmMirror.CancelTask(repo.ID, normalizedPath)
|
|
if !canceled {
|
|
WriteJSON(w, http.StatusConflict, map[string]string{"error": "sync is not running"})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, map[string]string{"status": "cancel_requested"})
|
|
}
|
|
|
|
func (api *API) repoRPMSetSyncEnabled(w http.ResponseWriter, r *http.Request, params map[string]string, enabled bool) {
|
|
var repo models.Repo
|
|
var relPath string
|
|
var normalizedPath string
|
|
var config models.RPMRepoDir
|
|
var cancelRequested bool
|
|
var err error
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "writer") {
|
|
return
|
|
}
|
|
if repo.Type != "rpm" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
|
|
return
|
|
}
|
|
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
|
|
if relPath == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
|
|
return
|
|
}
|
|
if !isSafeSubdirPath(relPath) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
|
return
|
|
}
|
|
normalizedPath = normalizeRPMPath(relPath)
|
|
config, err = api.Store.GetRPMRepoDir(repo.ID, normalizedPath)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo directory config not found"})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if normalizeRPMRepoDirMode(config.Mode) != "mirror" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "sync control is only supported for mirror mode"})
|
|
return
|
|
}
|
|
err = api.Store.SetRPMMirrorSyncEnabled(repo.ID, normalizedPath, enabled)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
cancelRequested = false
|
|
if !enabled && api.RpmMirror != nil {
|
|
cancelRequested = api.RpmMirror.CancelTask(repo.ID, normalizedPath)
|
|
}
|
|
WriteJSON(w, http.StatusOK, map[string]any{"status": "ok", "sync_enabled": enabled, "cancel_requested": cancelRequested})
|
|
}
|
|
|
|
func (api *API) RepoRPMMirrorRuns(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var relPath string
|
|
var normalizedPath string
|
|
var limit int
|
|
var runs []models.RPMMirrorRun
|
|
var err error
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
|
return
|
|
}
|
|
if repo.Type != "rpm" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
|
|
return
|
|
}
|
|
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
|
|
if relPath == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
|
|
return
|
|
}
|
|
if !isSafeSubdirPath(relPath) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
|
return
|
|
}
|
|
normalizedPath = normalizeRPMPath(relPath)
|
|
limit = 20
|
|
if strings.TrimSpace(r.URL.Query().Get("limit")) != "" {
|
|
limit, err = strconv.Atoi(strings.TrimSpace(r.URL.Query().Get("limit")))
|
|
if err != nil || limit <= 0 {
|
|
limit = 20
|
|
}
|
|
}
|
|
runs, err = api.Store.ListRPMMirrorRuns(repo.ID, normalizedPath, limit)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, runs)
|
|
}
|
|
|
|
func (api *API) RepoRPMClearMirrorRuns(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var relPath string
|
|
var normalizedPath string
|
|
var count int64
|
|
var err error
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "writer") {
|
|
return
|
|
}
|
|
if repo.Type != "rpm" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
|
|
return
|
|
}
|
|
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
|
|
if relPath == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
|
|
return
|
|
}
|
|
if !isSafeSubdirPath(relPath) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
|
return
|
|
}
|
|
normalizedPath = normalizeRPMPath(relPath)
|
|
count, err = api.Store.DeleteRPMMirrorRuns(repo.ID, normalizedPath)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, map[string]any{"status": "ok", "deleted_count": count})
|
|
}
|
|
|
|
func (api *API) RepoRPMDeleteSubdir(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var writeBlocked bool
|
|
var writeBlockedPath string
|
|
var targetConfig models.RPMRepoDir
|
|
var targetHasConfig bool
|
|
var mirrorRoot string
|
|
var allowMirrorRootDelete bool
|
|
var allowMirrorDelete bool
|
|
var busy bool
|
|
var busyPath string
|
|
var busyReason string
|
|
var err error
|
|
var relPath string
|
|
var fullPath string
|
|
var info os.FileInfo
|
|
var relPathClean string
|
|
var parentPath string
|
|
var repodataPath string
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "writer") {
|
|
return
|
|
}
|
|
if repo.Type != "rpm" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
|
|
return
|
|
}
|
|
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
|
|
if relPath == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
|
|
return
|
|
}
|
|
if !isSafeSubdirPath(relPath) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
|
return
|
|
}
|
|
if isRepodataPath(relPath) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repodata cannot be deleted directly"})
|
|
return
|
|
}
|
|
relPathClean = filepath.ToSlash(filepath.Clean(filepath.FromSlash(relPath)))
|
|
relPathClean = strings.TrimPrefix(relPathClean, "./")
|
|
busy, busyPath, busyReason, err = api.hasBusyMirrorRootUnder(repo, relPathClean)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if busy {
|
|
WriteJSON(w, http.StatusConflict, map[string]string{"error": "cannot delete directory while mirror activity is running", "mirror_root": busyPath, "reason": busyReason})
|
|
return
|
|
}
|
|
targetConfig, err = api.Store.GetRPMRepoDir(repo.ID, relPathClean)
|
|
if err == nil {
|
|
targetHasConfig = true
|
|
} else if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeBlocked, writeBlockedPath, err = api.isRPMWriteBlocked(repo, relPathClean)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if writeBlocked {
|
|
mirrorRoot = normalizeRPMPath(writeBlockedPath)
|
|
allowMirrorRootDelete = targetHasConfig &&
|
|
normalizeRPMRepoDirMode(targetConfig.Mode) == "mirror" &&
|
|
normalizeRPMPath(targetConfig.Path) == normalizeRPMPath(relPathClean) &&
|
|
normalizeRPMPath(relPathClean) == mirrorRoot
|
|
if !allowMirrorRootDelete {
|
|
allowMirrorDelete, err = api.allowRPMMirrorDelete(repo, relPathClean, true)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if !allowMirrorDelete {
|
|
WriteJSON(w, http.StatusForbidden, map[string]string{"error": "writes are disabled for mirror repo subtree", "mirror_root": writeBlockedPath})
|
|
return
|
|
}
|
|
}
|
|
if targetConfig.SyncRunning {
|
|
WriteJSON(w, http.StatusConflict, map[string]string{"error": "cannot delete mirror repo directory while sync is running", "mirror_root": writeBlockedPath})
|
|
return
|
|
}
|
|
}
|
|
fullPath = filepath.Join(repo.Path, filepath.FromSlash(relPathClean))
|
|
if writeBlocked && allowMirrorRootDelete && api.RpmMeta != nil && api.RpmMeta.IsRunning(fullPath) {
|
|
WriteJSON(w, http.StatusConflict, map[string]string{"error": "cannot delete mirror repo directory while metadata update is running", "mirror_root": writeBlockedPath})
|
|
return
|
|
}
|
|
info, err = os.Stat(fullPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "path not found"})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if info == nil || !info.IsDir() {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path is not a directory"})
|
|
return
|
|
}
|
|
if writeBlocked && !allowMirrorRootDelete && allowMirrorDelete {
|
|
repodataPath = filepath.Join(fullPath, "repodata")
|
|
_, err = os.Stat(repodataPath)
|
|
if err == nil {
|
|
WriteJSON(w, http.StatusForbidden, map[string]string{"error": "only container directories can be deleted in mirror subtree", "mirror_root": writeBlockedPath})
|
|
return
|
|
}
|
|
if err != nil && !os.IsNotExist(err) {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
}
|
|
err = os.RemoveAll(fullPath)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
err = api.Store.DeleteRPMRepoDirSubtree(repo.ID, relPathClean)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
parentPath = filepath.Dir(fullPath)
|
|
repodataPath = filepath.Join(parentPath, "repodata")
|
|
_, err = os.Stat(repodataPath)
|
|
if err == nil && api.RpmMeta != nil {
|
|
api.RpmMeta.Schedule(parentPath)
|
|
}
|
|
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (api *API) RepoRPMRenameSubdir(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var existingConfig models.RPMRepoDir
|
|
var dirConfig models.RPMRepoDir
|
|
var writeBlocked bool
|
|
var writeBlockedPath string
|
|
var newMode string
|
|
var renamed bool
|
|
var isRepoDir bool
|
|
var err error
|
|
var req repoRPMUpdateRequest
|
|
var relPath string
|
|
var relPathClean string
|
|
var newName string
|
|
var fullPath string
|
|
var info os.FileInfo
|
|
var parentRel string
|
|
var parentPath string
|
|
var newPath string
|
|
var newRelPath string
|
|
var repodataPath string
|
|
var hasAncestor bool
|
|
var absParent string
|
|
var existingConfigLoaded bool
|
|
var targetConfigExists bool
|
|
var allowDelete bool
|
|
var tlsInsecureSkipVerify bool
|
|
var syncIntervalSec int64
|
|
var modeValue string
|
|
var remoteURL string
|
|
var connectHost string
|
|
var hostHeader string
|
|
var tlsServerName string
|
|
var busy bool
|
|
var busyPath string
|
|
var busyReason string
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "writer") {
|
|
return
|
|
}
|
|
if repo.Type != "rpm" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
if req.Path == nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
|
|
return
|
|
}
|
|
relPath = strings.TrimSpace(*req.Path)
|
|
newName = ""
|
|
if req.Name != nil {
|
|
newName = strings.TrimSpace(*req.Name)
|
|
}
|
|
if relPath == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
|
|
return
|
|
}
|
|
if !isSafeSubdirPath(relPath) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
|
return
|
|
}
|
|
if isRepodataPath(relPath) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repodata cannot be renamed"})
|
|
return
|
|
}
|
|
relPathClean = filepath.ToSlash(filepath.Clean(filepath.FromSlash(relPath)))
|
|
relPathClean = strings.TrimPrefix(relPathClean, "./")
|
|
busy, busyPath, busyReason, err = api.hasBusyMirrorRootUnder(repo, relPathClean)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if busy {
|
|
WriteJSON(w, http.StatusConflict, map[string]string{"error": "cannot rename/update directory while mirror activity is running", "mirror_root": busyPath, "reason": busyReason})
|
|
return
|
|
}
|
|
if newName == "" {
|
|
newName = filepath.Base(filepath.FromSlash(relPathClean))
|
|
}
|
|
if strings.EqualFold(newName, "repodata") {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repodata is reserved"})
|
|
return
|
|
}
|
|
if !isSafeSubdirName(newName) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid name"})
|
|
return
|
|
}
|
|
writeBlocked, writeBlockedPath, err = api.isRPMWriteBlocked(repo, relPathClean)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if writeBlocked && writeBlockedPath != relPathClean {
|
|
WriteJSON(w, http.StatusForbidden, map[string]string{"error": "writes are disabled for mirror repo subtree", "mirror_root": writeBlockedPath})
|
|
return
|
|
}
|
|
fullPath = filepath.Join(repo.Path, filepath.FromSlash(relPathClean))
|
|
info, err = os.Stat(fullPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "path not found"})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if info == nil || !info.IsDir() {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path is not a directory"})
|
|
return
|
|
}
|
|
parentRel = filepath.Dir(filepath.FromSlash(relPathClean))
|
|
if parentRel == "." {
|
|
parentRel = ""
|
|
}
|
|
parentPath = filepath.FromSlash(parentRel)
|
|
newPath = filepath.Join(repo.Path, parentPath, newName)
|
|
newRelPath = filepath.ToSlash(filepath.Join(parentRel, newName))
|
|
_, err = api.Store.GetRPMRepoDir(repo.ID, newRelPath)
|
|
if err == nil {
|
|
targetConfigExists = true
|
|
} else if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
repodataPath = filepath.Join(fullPath, "repodata")
|
|
_, err = os.Stat(repodataPath)
|
|
if err == nil {
|
|
isRepoDir = true
|
|
existingConfig, err = api.Store.GetRPMRepoDir(repo.ID, relPathClean)
|
|
if err == nil {
|
|
existingConfigLoaded = true
|
|
if normalizeRPMRepoDirMode(existingConfig.Mode) == "mirror" && existingConfig.SyncRunning {
|
|
WriteJSON(w, http.StatusConflict, map[string]string{"error": "cannot rename mirror repo directory while sync is running"})
|
|
return
|
|
}
|
|
if normalizeRPMRepoDirMode(existingConfig.Mode) == "mirror" && api.RpmMeta != nil && api.RpmMeta.IsRunning(fullPath) {
|
|
WriteJSON(w, http.StatusConflict, map[string]string{"error": "cannot update mirror repo directory while metadata update is running"})
|
|
return
|
|
}
|
|
} else if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
modeValue = ""
|
|
if req.Mode != nil {
|
|
modeValue = *req.Mode
|
|
}
|
|
newMode = normalizeRPMRepoDirMode(modeValue)
|
|
if newMode == "" {
|
|
if existingConfigLoaded {
|
|
newMode = normalizeRPMRepoDirMode(existingConfig.Mode)
|
|
} else {
|
|
newMode = "local"
|
|
}
|
|
}
|
|
if req.AllowDelete != nil {
|
|
allowDelete = *req.AllowDelete
|
|
} else if existingConfigLoaded {
|
|
allowDelete = existingConfig.AllowDelete
|
|
} else {
|
|
allowDelete = false
|
|
}
|
|
if req.TLSInsecureSkipVerify != nil {
|
|
tlsInsecureSkipVerify = *req.TLSInsecureSkipVerify
|
|
} else if existingConfigLoaded {
|
|
tlsInsecureSkipVerify = existingConfig.TLSInsecureSkipVerify
|
|
} else {
|
|
tlsInsecureSkipVerify = false
|
|
}
|
|
if req.SyncIntervalSec != nil {
|
|
syncIntervalSec = *req.SyncIntervalSec
|
|
} else if existingConfigLoaded {
|
|
syncIntervalSec = existingConfig.SyncIntervalSec
|
|
} else {
|
|
syncIntervalSec = 300
|
|
}
|
|
syncIntervalSec = normalizeRPMMirrorIntervalSec(syncIntervalSec)
|
|
if req.RemoteURL != nil {
|
|
remoteURL = strings.TrimSpace(*req.RemoteURL)
|
|
} else if existingConfigLoaded {
|
|
remoteURL = existingConfig.RemoteURL
|
|
} else {
|
|
remoteURL = ""
|
|
}
|
|
if req.ConnectHost != nil {
|
|
connectHost = strings.TrimSpace(*req.ConnectHost)
|
|
} else if existingConfigLoaded {
|
|
connectHost = existingConfig.ConnectHost
|
|
} else {
|
|
connectHost = ""
|
|
}
|
|
if req.HostHeader != nil {
|
|
hostHeader = strings.TrimSpace(*req.HostHeader)
|
|
} else if existingConfigLoaded {
|
|
hostHeader = existingConfig.HostHeader
|
|
} else {
|
|
hostHeader = ""
|
|
}
|
|
if req.TLSServerName != nil {
|
|
tlsServerName = strings.TrimSpace(*req.TLSServerName)
|
|
} else if existingConfigLoaded {
|
|
tlsServerName = existingConfig.TLSServerName
|
|
} else {
|
|
tlsServerName = ""
|
|
}
|
|
err = validateRPMMirrorConfig(newMode, remoteURL, connectHost, hostHeader, tlsServerName)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
absParent = filepath.Join(repo.Path, parentPath)
|
|
hasAncestor, err = hasRepodataAncestor(repo.Path, absParent)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if hasAncestor {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo directories cannot be renamed under another repo directory"})
|
|
return
|
|
}
|
|
}
|
|
renamed = newPath != fullPath
|
|
if renamed {
|
|
if targetConfigExists {
|
|
WriteJSON(w, http.StatusConflict, map[string]string{"error": "target repo directory config already exists"})
|
|
return
|
|
}
|
|
_, err = os.Stat(newPath)
|
|
if err == nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "target already exists"})
|
|
return
|
|
}
|
|
if err != nil && !os.IsNotExist(err) {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
err = os.Rename(fullPath, newPath)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
err = api.Store.MoveRPMRepoDir(repo.ID, relPathClean, newRelPath)
|
|
if err != nil {
|
|
_ = os.Rename(newPath, fullPath)
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
}
|
|
if isRepoDir {
|
|
dirConfig = models.RPMRepoDir{
|
|
RepoID: repo.ID,
|
|
Path: newRelPath,
|
|
Mode: newMode,
|
|
AllowDelete: allowDelete,
|
|
RemoteURL: remoteURL,
|
|
ConnectHost: connectHost,
|
|
HostHeader: hostHeader,
|
|
TLSServerName: tlsServerName,
|
|
TLSInsecureSkipVerify: tlsInsecureSkipVerify,
|
|
SyncIntervalSec: syncIntervalSec,
|
|
}
|
|
if existingConfigLoaded {
|
|
dirConfig.SyncEnabled = existingConfig.SyncEnabled
|
|
} else if errors.Is(err, sql.ErrNoRows) {
|
|
dirConfig.SyncEnabled = true
|
|
} else {
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
dirConfig.SyncEnabled = true
|
|
}
|
|
err = api.Store.UpsertRPMRepoDir(dirConfig)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
}
|
|
repodataPath = filepath.Join(filepath.Join(repo.Path, parentPath), "repodata")
|
|
_, err = os.Stat(repodataPath)
|
|
if err == nil && api.RpmMeta != nil {
|
|
api.RpmMeta.Schedule(filepath.Join(repo.Path, parentPath))
|
|
}
|
|
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (api *API) RepoRPMDeleteFile(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var writeBlocked bool
|
|
var writeBlockedPath string
|
|
var allowMirrorDelete bool
|
|
var err error
|
|
var relPath string
|
|
var relPathClean string
|
|
var fullPath string
|
|
var info os.FileInfo
|
|
var parentPath string
|
|
var repodataPath string
|
|
var lower string
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "writer") {
|
|
return
|
|
}
|
|
if repo.Type != "rpm" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
|
|
return
|
|
}
|
|
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
|
|
if relPath == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
|
|
return
|
|
}
|
|
if !isSafeSubdirPath(relPath) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
|
return
|
|
}
|
|
if isRepodataPath(relPath) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repodata files cannot be deleted"})
|
|
return
|
|
}
|
|
relPathClean = filepath.ToSlash(filepath.Clean(filepath.FromSlash(relPath)))
|
|
relPathClean = strings.TrimPrefix(relPathClean, "./")
|
|
writeBlocked, writeBlockedPath, err = api.isRPMWriteBlocked(repo, relPathClean)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if writeBlocked {
|
|
allowMirrorDelete, err = api.allowRPMMirrorDelete(repo, relPathClean, false)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if !allowMirrorDelete {
|
|
WriteJSON(w, http.StatusForbidden, map[string]string{"error": "writes are disabled for mirror repo subtree", "mirror_root": writeBlockedPath})
|
|
return
|
|
}
|
|
}
|
|
lower = strings.ToLower(relPathClean)
|
|
if !strings.HasSuffix(lower, ".rpm") {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "only rpm files can be deleted"})
|
|
return
|
|
}
|
|
fullPath = filepath.Join(repo.Path, filepath.FromSlash(relPathClean))
|
|
info, err = os.Stat(fullPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "path not found"})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if info == nil || info.IsDir() {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path is not a file"})
|
|
return
|
|
}
|
|
err = os.Remove(fullPath)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
parentPath = filepath.Dir(fullPath)
|
|
repodataPath = filepath.Join(parentPath, "repodata")
|
|
_, err = os.Stat(repodataPath)
|
|
if err == nil && api.RpmMeta != nil {
|
|
api.RpmMeta.Schedule(parentPath)
|
|
}
|
|
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (api *API) RepoRPMFile(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var relPath string
|
|
var cleanPath string
|
|
var fullPath string
|
|
var lower string
|
|
var allowed bool
|
|
var file *os.File
|
|
var size int64
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
|
return
|
|
}
|
|
if repo.Type != "rpm" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
|
|
return
|
|
}
|
|
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
|
|
if relPath == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path required"})
|
|
return
|
|
}
|
|
if !isSafeSubdirPath(relPath) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
|
return
|
|
}
|
|
cleanPath = filepath.ToSlash(relPath)
|
|
cleanPath = strings.TrimPrefix(cleanPath, "./")
|
|
lower = strings.ToLower(cleanPath)
|
|
allowed = strings.HasSuffix(lower, "repodata/repomd.xml") ||
|
|
strings.HasSuffix(lower, "repodata/primary.xml.gz") ||
|
|
strings.HasSuffix(lower, "repodata/other.xml.gz") ||
|
|
strings.HasSuffix(lower, "repodata/filelists.xml.gz")
|
|
if !allowed {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "file not allowed"})
|
|
return
|
|
}
|
|
fullPath = filepath.Join(repo.Path, filepath.FromSlash(cleanPath))
|
|
file, err = os.Open(fullPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "file not found"})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
defer file.Close()
|
|
if strings.HasSuffix(lower, ".gz") {
|
|
w.Header().Set("Content-Type", "application/gzip")
|
|
} else {
|
|
w.Header().Set("Content-Type", "application/xml")
|
|
}
|
|
size, err = io.Copy(w, file)
|
|
_ = size
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
}
|
|
|
|
func (api *API) RepoRPMTree(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var relPath string
|
|
var entries []rpm.TreeEntry
|
|
var repoDirs []models.RPMRepoDir
|
|
var modeByPath map[string]string
|
|
var i int
|
|
var entryPath string
|
|
var repodataPath string
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
|
return
|
|
}
|
|
if repo.Type != "rpm" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
|
|
return
|
|
}
|
|
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
|
|
if strings.Contains(relPath, "..") {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
|
return
|
|
}
|
|
entries, err = rpm.ListTree(repo.Path, relPath)
|
|
if err != nil {
|
|
if errors.Is(err, rpm.ErrPathNotFound) {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "path not found"})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
repoDirs, err = api.Store.ListRPMRepoDirs(repo.ID)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
modeByPath = make(map[string]string)
|
|
for i = 0; i < len(repoDirs); i++ {
|
|
modeByPath[normalizeRPMPath(repoDirs[i].Path)] = normalizeRPMRepoDirMode(repoDirs[i].Mode)
|
|
}
|
|
for i = 0; i < len(entries); i++ {
|
|
if entries[i].Type != "dir" {
|
|
continue
|
|
}
|
|
entryPath = normalizeRPMPath(entries[i].Path)
|
|
if modeByPath[entryPath] != "" {
|
|
entries[i].IsRepoDir = true
|
|
entries[i].RepoMode = modeByPath[entryPath]
|
|
continue
|
|
}
|
|
repodataPath = filepath.Join(repo.Path, filepath.FromSlash(entryPath), "repodata")
|
|
_, err = os.Stat(repodataPath)
|
|
if err == nil {
|
|
entries[i].IsRepoDir = true
|
|
entries[i].RepoMode = "local"
|
|
continue
|
|
}
|
|
if err != nil && !os.IsNotExist(err) {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
}
|
|
WriteJSON(w, http.StatusOK, entries)
|
|
}
|
|
|
|
func (api *API) RepoRPMUpload(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var writeBlocked bool
|
|
var writeBlockedPath string
|
|
var err error
|
|
var relPath string
|
|
var relPathClean string
|
|
var dirPath string
|
|
var repodataDir string
|
|
var file multipart.File
|
|
var header *multipart.FileHeader
|
|
var filename string
|
|
var lower string
|
|
var fullPath string
|
|
var tempPath string
|
|
var out *os.File
|
|
var size int64
|
|
var detail rpm.PackageDetail
|
|
var overwriteParam string
|
|
var overwrite bool
|
|
var info os.FileInfo
|
|
var oldTemp string
|
|
var ts string
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "writer") {
|
|
return
|
|
}
|
|
if repo.Type != "rpm" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not rpm"})
|
|
return
|
|
}
|
|
relPath = strings.TrimSpace(r.URL.Query().Get("path"))
|
|
overwriteParam = strings.TrimSpace(r.URL.Query().Get("overwrite"))
|
|
overwriteParam = strings.ToLower(overwriteParam)
|
|
overwrite = overwriteParam == "1" || overwriteParam == "true" || overwriteParam == "yes"
|
|
if relPath != "" && !isSafeSubdirPath(relPath) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
|
return
|
|
}
|
|
relPathClean = filepath.ToSlash(filepath.Clean(filepath.FromSlash(relPath)))
|
|
relPathClean = strings.TrimPrefix(relPathClean, "./")
|
|
if relPath == "" {
|
|
relPathClean = ""
|
|
}
|
|
if isRepodataPath(relPathClean) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "uploads are not allowed in repodata"})
|
|
return
|
|
}
|
|
writeBlocked, writeBlockedPath, err = api.isRPMWriteBlocked(repo, relPathClean)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if writeBlocked {
|
|
WriteJSON(w, http.StatusForbidden, map[string]string{"error": "writes are disabled for mirror repo subtree", "mirror_root": writeBlockedPath})
|
|
return
|
|
}
|
|
dirPath = filepath.Join(repo.Path, filepath.FromSlash(relPathClean))
|
|
repodataDir, err = nearestRepodataDir(repo.Path, dirPath)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if repodataDir == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repodata not found in ancestor directories"})
|
|
return
|
|
}
|
|
file, header, err = r.FormFile("file")
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "file required"})
|
|
return
|
|
}
|
|
defer file.Close()
|
|
filename = sanitizeFilename(header.Filename)
|
|
filename = filepath.Base(filename)
|
|
if filename == "" || filename == "." || filename == string(filepath.Separator) {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid filename"})
|
|
return
|
|
}
|
|
lower = strings.ToLower(filename)
|
|
if !strings.HasSuffix(lower, ".rpm") {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "only rpm files are allowed"})
|
|
return
|
|
}
|
|
fullPath = filepath.Join(dirPath, filename)
|
|
info, err = os.Stat(fullPath)
|
|
if err == nil {
|
|
if !overwrite {
|
|
WriteJSON(w, http.StatusConflict, map[string]string{"error": "file already exists"})
|
|
return
|
|
}
|
|
if info != nil && info.IsDir() {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "path is a directory"})
|
|
return
|
|
}
|
|
oldTemp, err = moveToTrash(fullPath)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
}
|
|
if err != nil && !os.IsNotExist(err) {
|
|
if oldTemp != "" {
|
|
_ = os.Rename(oldTemp, fullPath)
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
ts = strconv.FormatInt(time.Now().UnixNano(), 10)
|
|
tempPath = fullPath + ".uploading-" + ts
|
|
out, err = os.Create(tempPath)
|
|
if err != nil {
|
|
if oldTemp != "" {
|
|
_ = os.Rename(oldTemp, fullPath)
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
defer out.Close()
|
|
size, err = io.Copy(out, file)
|
|
if err != nil {
|
|
_ = os.Remove(tempPath)
|
|
if oldTemp != "" {
|
|
_ = os.Rename(oldTemp, fullPath)
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
err = os.Rename(tempPath, fullPath)
|
|
if err != nil {
|
|
_ = os.Remove(tempPath)
|
|
if oldTemp != "" {
|
|
_ = os.Rename(oldTemp, fullPath)
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
detail, err = rpm.GetPackageDetail(repo.Path, filepath.ToSlash(filepath.Join(relPathClean, filename)))
|
|
if err != nil {
|
|
_ = os.Remove(fullPath)
|
|
if oldTemp != "" {
|
|
_ = os.Rename(oldTemp, fullPath)
|
|
}
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid rpm file"})
|
|
return
|
|
}
|
|
_ = detail
|
|
if oldTemp != "" {
|
|
_ = os.RemoveAll(oldTemp)
|
|
}
|
|
if api.RpmMeta != nil {
|
|
api.RpmMeta.Schedule(repodataDir)
|
|
}
|
|
WriteJSON(w, http.StatusOK, repoRPMUploadResponse{Filename: filename, Size: size})
|
|
}
|
|
|
|
func (api *API) RepoDockerTags(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var tags []docker.TagInfo
|
|
var image string
|
|
var imagePath string
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
|
return
|
|
}
|
|
if repo.Type != "docker" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
|
|
return
|
|
}
|
|
image = strings.TrimSpace(r.URL.Query().Get("image"))
|
|
imagePath = docker.ImagePath(repo.Path, image)
|
|
tags, err = docker.ListTags(imagePath)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, tags)
|
|
}
|
|
|
|
func (api *API) RepoDockerManifest(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var ref string
|
|
var detail docker.ManifestDetail
|
|
var image string
|
|
var imagePath string
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
|
return
|
|
}
|
|
if repo.Type != "docker" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
|
|
return
|
|
}
|
|
ref = strings.TrimSpace(r.URL.Query().Get("ref"))
|
|
if ref == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "ref required"})
|
|
return
|
|
}
|
|
image = strings.TrimSpace(r.URL.Query().Get("image"))
|
|
imagePath = docker.ImagePath(repo.Path, image)
|
|
detail, err = docker.GetManifestDetail(imagePath, ref)
|
|
if err != nil {
|
|
if err == docker.ErrNotFound {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "manifest not found"})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, detail)
|
|
}
|
|
|
|
func (api *API) RepoDockerImages(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var images []string
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
|
return
|
|
}
|
|
if repo.Type != "docker" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
|
|
return
|
|
}
|
|
images, err = docker.ListImages(repo.Path)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, images)
|
|
}
|
|
|
|
func (api *API) ListAPIKeys(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var user models.User
|
|
var ok bool
|
|
var keys []models.APIKey
|
|
var err error
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
keys, err = api.Store.ListAPIKeys(user.ID)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, keys)
|
|
}
|
|
|
|
func (api *API) CreateAPIKey(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var user models.User
|
|
var ok bool
|
|
var req createAPIKeyRequest
|
|
var err error
|
|
var name string
|
|
var buf []byte
|
|
var token string
|
|
var prefix string
|
|
var hash string
|
|
var key models.APIKey
|
|
var nowUnix int64
|
|
var expiresAt int64
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
name = strings.TrimSpace(req.Name)
|
|
if name == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name required"})
|
|
return
|
|
}
|
|
expiresAt = req.ExpiresAt
|
|
if expiresAt < 0 {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "expires_at must be 0 or a future unix timestamp"})
|
|
return
|
|
}
|
|
if expiresAt > 0 {
|
|
nowUnix = time.Now().UTC().Unix()
|
|
if expiresAt <= nowUnix {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "expires_at must be in the future"})
|
|
return
|
|
}
|
|
}
|
|
buf = make([]byte, 32)
|
|
_, err = rand.Read(buf)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to generate token"})
|
|
return
|
|
}
|
|
token = "ck_" + hex.EncodeToString(buf)
|
|
if len(token) > 8 {
|
|
prefix = token[:8]
|
|
} else {
|
|
prefix = token
|
|
}
|
|
hash = util.HashToken(token)
|
|
key, err = api.Store.CreateAPIKey(user.ID, name, hash, prefix, expiresAt)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusCreated, apiKeyResponse{APIKey: key, Token: token})
|
|
}
|
|
|
|
func (api *API) DeleteAPIKey(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var user models.User
|
|
var ok bool
|
|
var err error
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
err = api.Store.DeleteAPIKey(user.ID, params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) DisableAPIKey(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var user models.User
|
|
var ok bool
|
|
var err error
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
err = api.Store.SetAPIKeyDisabled(user.ID, params["id"], true)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) EnableAPIKey(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var user models.User
|
|
var ok bool
|
|
var err error
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
err = api.Store.SetAPIKeyDisabled(user.ID, params["id"], false)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) ListAdminAPIKeys(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var keys []models.AdminAPIKey
|
|
var err error
|
|
var userID string
|
|
var query string
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
userID = strings.TrimSpace(r.URL.Query().Get("user_id"))
|
|
query = strings.TrimSpace(r.URL.Query().Get("q"))
|
|
keys, err = api.Store.ListAPIKeysAdmin(userID, query)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, keys)
|
|
}
|
|
|
|
func (api *API) DeleteAdminAPIKey(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
err = api.Store.DeleteAPIKeyByID(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) DisableAdminAPIKey(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
err = api.Store.SetAPIKeyDisabledByID(params["id"], true)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) EnableAdminAPIKey(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
err = api.Store.SetAPIKeyDisabledByID(params["id"], false)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (api *API) RepoDockerDeleteTag(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var image string
|
|
var tag string
|
|
var imagePath string
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "writer") {
|
|
return
|
|
}
|
|
if repo.Type != "docker" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
|
|
return
|
|
}
|
|
image = strings.TrimSpace(r.URL.Query().Get("image"))
|
|
tag = strings.TrimSpace(r.URL.Query().Get("tag"))
|
|
if tag == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "tag required"})
|
|
return
|
|
}
|
|
imagePath = docker.ImagePath(repo.Path, image)
|
|
err = docker.DeleteTag(imagePath, tag)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (api *API) RepoDockerDeleteImage(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var image string
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "writer") {
|
|
return
|
|
}
|
|
if repo.Type != "docker" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
|
|
return
|
|
}
|
|
image = strings.TrimSpace(r.URL.Query().Get("image"))
|
|
err = docker.DeleteImage(repo.Path, image)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (api *API) RepoDockerRenameTag(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var req repoDockerRenameTagRequest
|
|
var image string
|
|
var from string
|
|
var to string
|
|
var imagePath string
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "writer") {
|
|
return
|
|
}
|
|
if repo.Type != "docker" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
image = strings.TrimSpace(req.Image)
|
|
from = strings.TrimSpace(req.From)
|
|
to = strings.TrimSpace(req.To)
|
|
if from == "" || to == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "from and to required"})
|
|
return
|
|
}
|
|
imagePath = docker.ImagePath(repo.Path, image)
|
|
err = docker.RenameTag(imagePath, from, to)
|
|
if err != nil {
|
|
if err == docker.ErrNotFound {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "tag not found"})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (api *API) RepoDockerRenameImage(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var repo models.Repo
|
|
var err error
|
|
var req repoDockerRenameImageRequest
|
|
var from string
|
|
var to string
|
|
repo, err = api.getRepoResolved(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
|
return
|
|
}
|
|
if !api.requireRepoRole(w, r, repo.ID, "writer") {
|
|
return
|
|
}
|
|
if repo.Type != "docker" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
from = strings.TrimSpace(req.From)
|
|
to = strings.TrimSpace(req.To)
|
|
if from == "" && to == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "from or to required"})
|
|
return
|
|
}
|
|
err = docker.RenameImage(repo.Path, from, to)
|
|
if err != nil {
|
|
if err == docker.ErrNotFound {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "image not found"})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (api *API) ListIssues(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var issues []models.Issue
|
|
var err error
|
|
if !api.requireProjectRole(w, r, params["projectId"], "viewer") {
|
|
return
|
|
}
|
|
issues, err = api.Store.ListIssues(params["projectId"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, issues)
|
|
}
|
|
|
|
func (api *API) CreateIssue(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req createIssueRequest
|
|
var err error
|
|
var user models.User
|
|
var issue models.Issue
|
|
var created models.Issue
|
|
if !api.requireProjectRole(w, r, params["projectId"], "writer") {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
if req.Title == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "title required"})
|
|
return
|
|
}
|
|
user, _ = middleware.UserFromContext(r.Context())
|
|
issue = models.Issue{
|
|
ProjectID: params["projectId"],
|
|
Title: req.Title,
|
|
Body: req.Body,
|
|
AssigneeID: req.AssigneeID,
|
|
CreatedBy: user.ID,
|
|
}
|
|
created, err = api.Store.CreateIssue(issue)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusCreated, created)
|
|
}
|
|
|
|
func (api *API) UpdateIssue(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var issue models.Issue
|
|
var err error
|
|
var req updateIssueRequest
|
|
issue, err = api.Store.GetIssue(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "issue not found"})
|
|
return
|
|
}
|
|
if !api.requireProjectRole(w, r, issue.ProjectID, "writer") {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
if req.Title != "" {
|
|
issue.Title = req.Title
|
|
}
|
|
if req.Body != "" {
|
|
issue.Body = req.Body
|
|
}
|
|
if req.Status != "" {
|
|
issue.Status = req.Status
|
|
}
|
|
issue.AssigneeID = req.AssigneeID
|
|
err = api.Store.UpdateIssue(issue)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, issue)
|
|
}
|
|
|
|
func (api *API) AddIssueComment(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req issueCommentRequest
|
|
var err error
|
|
var user models.User
|
|
var comment models.IssueComment
|
|
var created models.IssueComment
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
if req.Body == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "comment body required"})
|
|
return
|
|
}
|
|
user, _ = middleware.UserFromContext(r.Context())
|
|
comment = models.IssueComment{IssueID: params["id"], Body: req.Body, CreatedBy: user.ID}
|
|
created, err = api.Store.AddIssueComment(comment)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusCreated, created)
|
|
}
|
|
|
|
func (api *API) ListWikiPages(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var pages []models.WikiPage
|
|
var err error
|
|
if !api.requireProjectRole(w, r, params["projectId"], "viewer") {
|
|
return
|
|
}
|
|
pages, err = api.Store.ListWikiPages(params["projectId"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, pages)
|
|
}
|
|
|
|
func (api *API) CreateWikiPage(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req createWikiRequest
|
|
var err error
|
|
var user models.User
|
|
var page models.WikiPage
|
|
var created models.WikiPage
|
|
if !api.requireProjectRole(w, r, params["projectId"], "writer") {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
if req.Title == "" || req.Slug == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "title and slug required"})
|
|
return
|
|
}
|
|
user, _ = middleware.UserFromContext(r.Context())
|
|
page = models.WikiPage{
|
|
ProjectID: params["projectId"],
|
|
Title: req.Title,
|
|
Slug: req.Slug,
|
|
Body: req.Body,
|
|
CreatedBy: user.ID,
|
|
}
|
|
created, err = api.Store.CreateWikiPage(page)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusCreated, created)
|
|
}
|
|
|
|
func (api *API) UpdateWikiPage(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var page models.WikiPage
|
|
var err error
|
|
var req updateWikiRequest
|
|
page, err = api.Store.GetWikiPage(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "page not found"})
|
|
return
|
|
}
|
|
if !api.requireProjectRole(w, r, page.ProjectID, "writer") {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
return
|
|
}
|
|
if req.Title != "" {
|
|
page.Title = req.Title
|
|
}
|
|
page.Body = req.Body
|
|
err = api.Store.UpdateWikiPage(page)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, page)
|
|
}
|
|
|
|
func (api *API) UploadFile(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var user models.User
|
|
var file multipart.File
|
|
var header *multipart.FileHeader
|
|
var err error
|
|
var id string
|
|
var storedName string
|
|
var tempName string
|
|
var tempPath string
|
|
var finalPath string
|
|
var size int64
|
|
var contentType string
|
|
var upload models.Upload
|
|
var created models.Upload
|
|
var ts string
|
|
if !api.requireProjectRole(w, r, params["projectId"], "writer") {
|
|
return
|
|
}
|
|
user, _ = middleware.UserFromContext(r.Context())
|
|
file, header, err = r.FormFile("file")
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "file required"})
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
id, err = util.NewID()
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to generate id"})
|
|
return
|
|
}
|
|
storedName = id + filepath.Ext(header.Filename)
|
|
ts = strconv.FormatInt(time.Now().UnixNano(), 10)
|
|
tempName = storedName + ".uploading-" + ts
|
|
tempPath, size, err = api.Uploads.Save(tempName, file)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
finalPath = filepath.Join(api.Uploads.BaseDir, storedName)
|
|
err = os.Rename(tempPath, finalPath)
|
|
if err != nil {
|
|
_ = os.Remove(tempPath)
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
contentType = header.Header.Get("Content-Type")
|
|
if contentType == "" {
|
|
contentType = "application/octet-stream"
|
|
}
|
|
upload = models.Upload{
|
|
ID: id,
|
|
ProjectID: params["projectId"],
|
|
Filename: header.Filename,
|
|
ContentType: contentType,
|
|
Size: size,
|
|
StoragePath: finalPath,
|
|
CreatedBy: user.ID,
|
|
}
|
|
created, err = api.Store.CreateUpload(upload)
|
|
if err != nil {
|
|
_ = os.Remove(finalPath)
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusCreated, created)
|
|
}
|
|
|
|
func (api *API) ListUploads(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var uploads []models.Upload
|
|
var err error
|
|
if !api.requireProjectRole(w, r, params["projectId"], "viewer") {
|
|
return
|
|
}
|
|
uploads, err = api.Store.ListUploads(params["projectId"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, uploads)
|
|
}
|
|
|
|
func (api *API) DownloadFile(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var upload models.Upload
|
|
var err error
|
|
var file *os.File
|
|
upload, err = api.Store.GetUpload(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "upload not found"})
|
|
return
|
|
}
|
|
if !api.requireProjectRole(w, r, upload.ProjectID, "viewer") {
|
|
return
|
|
}
|
|
file, err = api.Uploads.Open(upload.StoragePath)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
defer file.Close()
|
|
w.Header().Set("Content-Type", upload.ContentType)
|
|
w.Header().Set("Content-Disposition", "attachment; filename=\""+sanitizeFilename(upload.Filename)+"\"")
|
|
_, _ = io.Copy(w, file)
|
|
}
|
|
|
|
func (api *API) Health(w http.ResponseWriter, _ *http.Request, _ map[string]string) {
|
|
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (api *API) requireAdmin(w http.ResponseWriter, r *http.Request) bool {
|
|
var user models.User
|
|
var ok bool
|
|
var principal models.ServicePrincipal
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if ok && user.IsAdmin {
|
|
return true
|
|
}
|
|
principal, ok = middleware.PrincipalFromContext(r.Context())
|
|
if ok && principal.IsAdmin && !principal.Disabled {
|
|
return true
|
|
}
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return false
|
|
}
|
|
|
|
func (api *API) requireProjectRole(w http.ResponseWriter, r *http.Request, projectID, required string) bool {
|
|
var user models.User
|
|
var principal models.ServicePrincipal
|
|
var ok bool
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if ok && user.IsAdmin {
|
|
return true
|
|
}
|
|
var role string
|
|
var err error
|
|
if ok {
|
|
role, err = api.Store.GetProjectMemberRole(projectID, user.ID)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return false
|
|
}
|
|
if !roleAllows(role, required) {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
principal, ok = middleware.PrincipalFromContext(r.Context())
|
|
if !ok || principal.Disabled {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return false
|
|
}
|
|
if principal.IsAdmin {
|
|
return true
|
|
}
|
|
role, err = api.Store.GetPrincipalProjectRole(principal.ID, projectID)
|
|
if err != nil || !roleAllows(role, required) {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (api *API) requireRepoRole(w http.ResponseWriter, r *http.Request, repoID, required string) bool {
|
|
var user models.User
|
|
var principal models.ServicePrincipal
|
|
var ok bool
|
|
var projectIDs []string
|
|
var err error
|
|
var i int
|
|
var role string
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if ok && user.IsAdmin {
|
|
return true
|
|
}
|
|
projectIDs, err = api.Store.GetRepoProjectIDs(repoID)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return false
|
|
}
|
|
if ok {
|
|
for i = 0; i < len(projectIDs); i++ {
|
|
role, err = api.Store.GetProjectMemberRole(projectIDs[i], user.ID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if roleAllows(role, required) {
|
|
return true
|
|
}
|
|
}
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return false
|
|
}
|
|
principal, ok = middleware.PrincipalFromContext(r.Context())
|
|
if !ok || principal.Disabled {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return false
|
|
}
|
|
if principal.IsAdmin {
|
|
return true
|
|
}
|
|
for i = 0; i < len(projectIDs); i++ {
|
|
role, err = api.Store.GetPrincipalProjectRole(principal.ID, projectIDs[i])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if roleAllows(role, required) {
|
|
return true
|
|
}
|
|
}
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return false
|
|
}
|
|
|
|
func normalizeRole(role string) string {
|
|
switch strings.ToLower(strings.TrimSpace(role)) {
|
|
case "admin":
|
|
return "admin"
|
|
case "writer", "write":
|
|
return "writer"
|
|
default:
|
|
return "viewer"
|
|
}
|
|
}
|
|
|
|
func normalizeProjectHomePage(value string) string {
|
|
var v string
|
|
v = strings.ToLower(strings.TrimSpace(value))
|
|
if v == "repos" || v == "issues" || v == "wiki" || v == "files" || v == "info" {
|
|
return v
|
|
}
|
|
return "info"
|
|
}
|
|
|
|
func normalizeAuthMode(value string) string {
|
|
var v string
|
|
v = strings.ToLower(strings.TrimSpace(value))
|
|
if v == "ldap" || v == "hybrid" || v == "db" {
|
|
return v
|
|
}
|
|
return "db"
|
|
}
|
|
|
|
func (api *API) getMergedAuthSettings() (models.AuthSettings, error) {
|
|
var settings models.AuthSettings
|
|
var saved models.AuthSettings
|
|
var err error
|
|
settings = models.AuthSettings{
|
|
AuthMode: normalizeAuthMode(api.Cfg.AuthMode),
|
|
LDAPURL: api.Cfg.LDAPURL,
|
|
LDAPBindDN: api.Cfg.LDAPBindDN,
|
|
LDAPBindPassword: api.Cfg.LDAPBindPassword,
|
|
LDAPUserBaseDN: api.Cfg.LDAPUserBaseDN,
|
|
LDAPUserFilter: api.Cfg.LDAPUserFilter,
|
|
LDAPTLSInsecureSkipVerify: api.Cfg.LDAPTLSInsecureSkipVerify,
|
|
OIDCClientID: api.Cfg.OIDCClientID,
|
|
OIDCClientSecret: api.Cfg.OIDCClientSecret,
|
|
OIDCAuthorizeURL: api.Cfg.OIDCAuthorizeURL,
|
|
OIDCTokenURL: api.Cfg.OIDCTokenURL,
|
|
OIDCUserInfoURL: api.Cfg.OIDCUserInfoURL,
|
|
OIDCRedirectURL: api.Cfg.OIDCRedirectURL,
|
|
OIDCScopes: api.Cfg.OIDCScopes,
|
|
OIDCEnabled: api.Cfg.OIDCEnabled,
|
|
OIDCTLSInsecureSkipVerify: api.Cfg.OIDCTLSInsecureSkipVerify,
|
|
}
|
|
saved, err = api.Store.GetAuthSettings()
|
|
if err != nil {
|
|
return settings, err
|
|
}
|
|
if strings.TrimSpace(saved.AuthMode) != "" {
|
|
settings.AuthMode = normalizeAuthMode(saved.AuthMode)
|
|
}
|
|
settings.OIDCEnabled = saved.OIDCEnabled
|
|
if strings.ToLower(strings.TrimSpace(saved.AuthMode)) == "oidc" {
|
|
settings.OIDCEnabled = true
|
|
}
|
|
if strings.TrimSpace(saved.LDAPURL) != "" {
|
|
settings.LDAPURL = saved.LDAPURL
|
|
}
|
|
if strings.TrimSpace(saved.LDAPBindDN) != "" {
|
|
settings.LDAPBindDN = saved.LDAPBindDN
|
|
}
|
|
if saved.LDAPBindPassword != "" {
|
|
settings.LDAPBindPassword = saved.LDAPBindPassword
|
|
}
|
|
if strings.TrimSpace(saved.LDAPUserBaseDN) != "" {
|
|
settings.LDAPUserBaseDN = saved.LDAPUserBaseDN
|
|
}
|
|
if strings.TrimSpace(saved.LDAPUserFilter) != "" {
|
|
settings.LDAPUserFilter = saved.LDAPUserFilter
|
|
}
|
|
if settings.LDAPUserFilter == "" {
|
|
settings.LDAPUserFilter = "(uid={username})"
|
|
}
|
|
if strings.TrimSpace(saved.OIDCClientID) != "" {
|
|
settings.OIDCClientID = saved.OIDCClientID
|
|
}
|
|
if strings.TrimSpace(saved.OIDCClientSecret) != "" {
|
|
settings.OIDCClientSecret = saved.OIDCClientSecret
|
|
}
|
|
if strings.TrimSpace(saved.OIDCAuthorizeURL) != "" {
|
|
settings.OIDCAuthorizeURL = saved.OIDCAuthorizeURL
|
|
}
|
|
if strings.TrimSpace(saved.OIDCTokenURL) != "" {
|
|
settings.OIDCTokenURL = saved.OIDCTokenURL
|
|
}
|
|
if strings.TrimSpace(saved.OIDCUserInfoURL) != "" {
|
|
settings.OIDCUserInfoURL = saved.OIDCUserInfoURL
|
|
}
|
|
if strings.TrimSpace(saved.OIDCRedirectURL) != "" {
|
|
settings.OIDCRedirectURL = saved.OIDCRedirectURL
|
|
}
|
|
if strings.TrimSpace(saved.OIDCScopes) != "" {
|
|
settings.OIDCScopes = saved.OIDCScopes
|
|
}
|
|
settings.OIDCTLSInsecureSkipVerify = saved.OIDCTLSInsecureSkipVerify
|
|
if strings.TrimSpace(settings.OIDCScopes) == "" {
|
|
settings.OIDCScopes = "openid profile email"
|
|
}
|
|
return settings, nil
|
|
}
|
|
|
|
func (api *API) effectiveAuthConfig() (config.Config, error) {
|
|
var cfg config.Config
|
|
var settings models.AuthSettings
|
|
var err error
|
|
cfg = api.Cfg
|
|
settings, err = api.getMergedAuthSettings()
|
|
if err != nil {
|
|
return cfg, err
|
|
}
|
|
cfg.AuthMode = settings.AuthMode
|
|
cfg.LDAPURL = settings.LDAPURL
|
|
cfg.LDAPBindDN = settings.LDAPBindDN
|
|
cfg.LDAPBindPassword = settings.LDAPBindPassword
|
|
cfg.LDAPUserBaseDN = settings.LDAPUserBaseDN
|
|
cfg.LDAPUserFilter = settings.LDAPUserFilter
|
|
cfg.LDAPTLSInsecureSkipVerify = settings.LDAPTLSInsecureSkipVerify
|
|
cfg.OIDCClientID = settings.OIDCClientID
|
|
cfg.OIDCClientSecret = settings.OIDCClientSecret
|
|
cfg.OIDCAuthorizeURL = settings.OIDCAuthorizeURL
|
|
cfg.OIDCTokenURL = settings.OIDCTokenURL
|
|
cfg.OIDCUserInfoURL = settings.OIDCUserInfoURL
|
|
cfg.OIDCRedirectURL = settings.OIDCRedirectURL
|
|
cfg.OIDCScopes = settings.OIDCScopes
|
|
cfg.OIDCEnabled = settings.OIDCEnabled
|
|
cfg.OIDCTLSInsecureSkipVerify = settings.OIDCTLSInsecureSkipVerify
|
|
return cfg, nil
|
|
}
|
|
|
|
func (api *API) getMergedTLSSettings() (models.TLSSettings, error) {
|
|
var settings models.TLSSettings
|
|
var saved models.TLSSettings
|
|
var err error
|
|
settings = models.TLSSettings{
|
|
HTTPAddrs: normalizeAddrList(api.Cfg.HTTPAddrs),
|
|
HTTPSAddrs: normalizeAddrList(api.Cfg.HTTPSAddrs),
|
|
TLSServerCertSource: "pki",
|
|
TLSCertFile: "",
|
|
TLSKeyFile: "",
|
|
TLSPKIServerCertID: api.Cfg.TLSPKIServerCertID,
|
|
TLSClientAuth: normalizeTLSClientAuth(api.Cfg.TLSClientAuth),
|
|
TLSClientCAFile: "",
|
|
TLSPKIClientCAID: api.Cfg.TLSPKIClientCAID,
|
|
TLSMinVersion: normalizeTLSMinVersion(api.Cfg.TLSMinVersion),
|
|
}
|
|
saved, err = api.Store.GetTLSSettings()
|
|
if err != nil {
|
|
return settings, err
|
|
}
|
|
if len(saved.HTTPAddrs) > 0 {
|
|
settings.HTTPAddrs = normalizeAddrList(saved.HTTPAddrs)
|
|
}
|
|
if len(saved.HTTPSAddrs) > 0 {
|
|
settings.HTTPSAddrs = normalizeAddrList(saved.HTTPSAddrs)
|
|
}
|
|
settings.TLSServerCertSource = "pki"
|
|
settings.TLSCertFile = ""
|
|
settings.TLSKeyFile = ""
|
|
if strings.TrimSpace(saved.TLSPKIServerCertID) != "" {
|
|
settings.TLSPKIServerCertID = saved.TLSPKIServerCertID
|
|
}
|
|
if strings.TrimSpace(saved.TLSClientAuth) != "" {
|
|
settings.TLSClientAuth = normalizeTLSClientAuth(saved.TLSClientAuth)
|
|
}
|
|
settings.TLSClientCAFile = ""
|
|
if strings.TrimSpace(saved.TLSPKIClientCAID) != "" {
|
|
settings.TLSPKIClientCAID = saved.TLSPKIClientCAID
|
|
}
|
|
if strings.TrimSpace(saved.TLSMinVersion) != "" {
|
|
settings.TLSMinVersion = normalizeTLSMinVersion(saved.TLSMinVersion)
|
|
}
|
|
return settings, nil
|
|
}
|
|
|
|
func normalizeRepoType(value string) (string, bool) {
|
|
var v string
|
|
v = strings.ToLower(strings.TrimSpace(value))
|
|
if v == "" {
|
|
return "git", true
|
|
}
|
|
if v == "git" || v == "rpm" || v == "docker" {
|
|
return v, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func normalizeAddrList(values []string) []string {
|
|
var out []string
|
|
var i int
|
|
var v string
|
|
for i = 0; i < len(values); i++ {
|
|
v = strings.TrimSpace(values[i])
|
|
if v == "" {
|
|
continue
|
|
}
|
|
out = append(out, v)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func normalizeTLSServerCertSource(value string) string {
|
|
_ = value
|
|
return "pki"
|
|
}
|
|
|
|
func normalizeTLSClientAuth(value string) string {
|
|
var v string
|
|
v = strings.ToLower(strings.TrimSpace(value))
|
|
if v == "request" || v == "require" || v == "verify_if_given" || v == "require_and_verify" {
|
|
return v
|
|
}
|
|
return "none"
|
|
}
|
|
|
|
func normalizeTLSMinVersion(value string) string {
|
|
var v string
|
|
v = strings.ToLower(strings.TrimSpace(value))
|
|
if v == "1.0" || v == "1.1" || v == "1.3" || v == "tls1.0" || v == "tls1.1" || v == "tls1.3" {
|
|
return v
|
|
}
|
|
return "1.2"
|
|
}
|
|
|
|
func tlsClientAuthNeedsCA(value string) bool {
|
|
var v string
|
|
v = normalizeTLSClientAuth(value)
|
|
return v == "require_and_verify" || v == "verify_if_given"
|
|
}
|
|
|
|
func tlsClientCAConfigured(pkiCAID string) bool {
|
|
return strings.TrimSpace(pkiCAID) != ""
|
|
}
|
|
|
|
func normalizeTLSListenerRequest(req tlsListenerRequest) models.TLSListener {
|
|
var item models.TLSListener
|
|
var applyAPI bool
|
|
var applyGit bool
|
|
var applyRPM bool
|
|
var applyV2 bool
|
|
applyAPI = req.ApplyPolicyAPI
|
|
applyGit = req.ApplyPolicyGit
|
|
applyRPM = req.ApplyPolicyRPM
|
|
applyV2 = req.ApplyPolicyV2
|
|
if !applyAPI && !applyGit && !applyRPM && !applyV2 {
|
|
applyAPI = true
|
|
applyGit = true
|
|
applyRPM = true
|
|
applyV2 = true
|
|
}
|
|
item = models.TLSListener{
|
|
Name: strings.TrimSpace(req.Name),
|
|
Enabled: req.Enabled,
|
|
HTTPAddrs: normalizeAddrList(req.HTTPAddrs),
|
|
HTTPSAddrs: normalizeAddrList(req.HTTPSAddrs),
|
|
AuthPolicy: normalizeTLSAuthPolicy(req.AuthPolicy),
|
|
ApplyPolicyAPI: applyAPI,
|
|
ApplyPolicyGit: applyGit,
|
|
ApplyPolicyRPM: applyRPM,
|
|
ApplyPolicyV2: applyV2,
|
|
ClientCertAllowlist: normalizeTLSCertAllowlist(req.ClientCertAllowlist),
|
|
TLSServerCertSource: "pki",
|
|
TLSCertFile: "",
|
|
TLSKeyFile: "",
|
|
TLSPKIServerCertID: strings.TrimSpace(req.TLSPKIServerCertID),
|
|
TLSClientAuth: normalizeTLSClientAuth(req.TLSClientAuth),
|
|
TLSClientCAFile: "",
|
|
TLSPKIClientCAID: strings.TrimSpace(req.TLSPKIClientCAID),
|
|
TLSMinVersion: normalizeTLSMinVersion(req.TLSMinVersion),
|
|
}
|
|
return item
|
|
}
|
|
|
|
func mergeTLSListener(current models.TLSListener, updated models.TLSListener) models.TLSListener {
|
|
var out models.TLSListener
|
|
out = current
|
|
out.Name = updated.Name
|
|
out.Enabled = updated.Enabled
|
|
out.HTTPAddrs = updated.HTTPAddrs
|
|
out.HTTPSAddrs = updated.HTTPSAddrs
|
|
out.AuthPolicy = updated.AuthPolicy
|
|
out.ApplyPolicyAPI = updated.ApplyPolicyAPI
|
|
out.ApplyPolicyGit = updated.ApplyPolicyGit
|
|
out.ApplyPolicyRPM = updated.ApplyPolicyRPM
|
|
out.ApplyPolicyV2 = updated.ApplyPolicyV2
|
|
out.ClientCertAllowlist = updated.ClientCertAllowlist
|
|
out.TLSServerCertSource = updated.TLSServerCertSource
|
|
out.TLSCertFile = updated.TLSCertFile
|
|
out.TLSKeyFile = updated.TLSKeyFile
|
|
out.TLSPKIServerCertID = updated.TLSPKIServerCertID
|
|
out.TLSClientAuth = updated.TLSClientAuth
|
|
out.TLSClientCAFile = updated.TLSClientCAFile
|
|
out.TLSPKIClientCAID = updated.TLSPKIClientCAID
|
|
out.TLSMinVersion = updated.TLSMinVersion
|
|
return out
|
|
}
|
|
|
|
func normalizeTLSAuthPolicy(value string) string {
|
|
var v string
|
|
v = strings.ToLower(strings.TrimSpace(value))
|
|
if v == "read_open_write_cert" || v == "read_open_write_cert_or_auth" || v == "cert_only" || v == "read_only_public" {
|
|
return v
|
|
}
|
|
return "default"
|
|
}
|
|
|
|
func normalizeTLSCertAllowlist(values []string) []string {
|
|
var out []string
|
|
var i int
|
|
var v string
|
|
for i = 0; i < len(values); i++ {
|
|
v = strings.ToLower(strings.TrimSpace(values[i]))
|
|
if v == "" {
|
|
continue
|
|
}
|
|
out = append(out, v)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func roleAllows(actual, required string) bool {
|
|
actual = normalizeRole(actual)
|
|
required = normalizeRole(required)
|
|
if actual == "admin" {
|
|
return true
|
|
}
|
|
if required == "admin" {
|
|
return actual == "admin"
|
|
}
|
|
if required == "writer" {
|
|
return actual == "writer"
|
|
}
|
|
return true
|
|
}
|
|
|
|
func sanitizeFilename(name string) string {
|
|
name = strings.ReplaceAll(name, "\n", "")
|
|
name = strings.ReplaceAll(name, "\r", "")
|
|
return name
|
|
}
|
|
|
|
func isSafeSubdirName(name string) bool {
|
|
if name == "" {
|
|
return false
|
|
}
|
|
if strings.Contains(name, "/") || strings.Contains(name, "\\") {
|
|
return false
|
|
}
|
|
if strings.Contains(name, "..") {
|
|
return false
|
|
}
|
|
if strings.ContainsAny(name, "\r\n") {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isSafeSubdirPath(path string) bool {
|
|
var parts []string
|
|
var part string
|
|
path = strings.ReplaceAll(path, "\\", "/")
|
|
if strings.Contains(path, "..") {
|
|
return false
|
|
}
|
|
parts = strings.Split(path, "/")
|
|
for _, part = range parts {
|
|
if part == "" {
|
|
continue
|
|
}
|
|
if !isSafeSubdirName(part) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isRepodataPath(path string) bool {
|
|
var normalized string
|
|
var parts []string
|
|
var part string
|
|
normalized = strings.Trim(path, "/")
|
|
normalized = strings.ReplaceAll(normalized, "\\", "/")
|
|
if normalized == "" {
|
|
return false
|
|
}
|
|
parts = strings.Split(normalized, "/")
|
|
for _, part = range parts {
|
|
if strings.EqualFold(part, "repodata") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func hasRepodataAncestor(root string, parent string) (bool, error) {
|
|
var current string
|
|
var relative string
|
|
var err error
|
|
var repodata string
|
|
current = filepath.Clean(parent)
|
|
root = filepath.Clean(root)
|
|
for {
|
|
relative, err = filepath.Rel(root, current)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if strings.HasPrefix(relative, "..") {
|
|
return false, nil
|
|
}
|
|
repodata = filepath.Join(current, "repodata")
|
|
_, err = os.Stat(repodata)
|
|
if err == nil {
|
|
return true, nil
|
|
}
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return false, err
|
|
}
|
|
if current == root {
|
|
return false, nil
|
|
}
|
|
current = filepath.Dir(current)
|
|
}
|
|
}
|
|
|
|
func nearestRepodataDir(root string, target string) (string, error) {
|
|
var current string
|
|
var relative string
|
|
var err error
|
|
var repodata string
|
|
current = filepath.Clean(target)
|
|
root = filepath.Clean(root)
|
|
for {
|
|
relative, err = filepath.Rel(root, current)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if strings.HasPrefix(relative, "..") {
|
|
return "", nil
|
|
}
|
|
repodata = filepath.Join(current, "repodata")
|
|
_, err = os.Stat(repodata)
|
|
if err == nil {
|
|
return current, nil
|
|
}
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return "", err
|
|
}
|
|
if current == root {
|
|
return "", nil
|
|
}
|
|
current = filepath.Dir(current)
|
|
}
|
|
}
|
|
|
|
func normalizeRPMPath(path string) string {
|
|
var cleaned string
|
|
cleaned = filepath.ToSlash(filepath.Clean(filepath.FromSlash(strings.TrimSpace(path))))
|
|
cleaned = strings.TrimPrefix(cleaned, "./")
|
|
if cleaned == "." {
|
|
return ""
|
|
}
|
|
return cleaned
|
|
}
|
|
|
|
func normalizeRPMRepoDirMode(mode string) string {
|
|
var v string
|
|
v = strings.ToLower(strings.TrimSpace(mode))
|
|
if v == "mirror" {
|
|
return "mirror"
|
|
}
|
|
return "local"
|
|
}
|
|
|
|
func normalizeRPMMirrorIntervalSec(value int64) int64 {
|
|
if value <= 0 {
|
|
return 300
|
|
}
|
|
if value < 10 {
|
|
return 10
|
|
}
|
|
return value
|
|
}
|
|
|
|
func validateRPMMirrorConfig(mode string, remoteURL string, connectHost string, hostHeader string, tlsServerName string) error {
|
|
if normalizeRPMRepoDirMode(mode) != "mirror" {
|
|
return nil
|
|
}
|
|
if strings.TrimSpace(remoteURL) == "" {
|
|
return errors.New("remote_url is required for mirror mode")
|
|
}
|
|
if strings.TrimSpace(connectHost) != "" {
|
|
if !strings.Contains(strings.TrimSpace(connectHost), ":") {
|
|
return errors.New("connect_host must include port")
|
|
}
|
|
}
|
|
_ = hostHeader
|
|
_ = tlsServerName
|
|
return nil
|
|
}
|
|
|
|
func pathUnderRoot(path string, root string) bool {
|
|
if root == "" {
|
|
return true
|
|
}
|
|
if path == root {
|
|
return true
|
|
}
|
|
return strings.HasPrefix(path, root+"/")
|
|
}
|
|
|
|
func (api *API) findRPMMirrorRoot(repo models.Repo, relPath string) (string, error) {
|
|
var dirs []models.RPMRepoDir
|
|
var normalizedPath string
|
|
var i int
|
|
var root string
|
|
var longest string
|
|
var found bool
|
|
var err error
|
|
dirs, err = api.Store.ListRPMRepoDirs(repo.ID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
normalizedPath = normalizeRPMPath(relPath)
|
|
for i = 0; i < len(dirs); i++ {
|
|
if normalizeRPMRepoDirMode(dirs[i].Mode) != "mirror" {
|
|
continue
|
|
}
|
|
root = normalizeRPMPath(dirs[i].Path)
|
|
if !pathUnderRoot(normalizedPath, root) {
|
|
continue
|
|
}
|
|
if !found {
|
|
longest = root
|
|
found = true
|
|
continue
|
|
}
|
|
if len(root) > len(longest) {
|
|
longest = root
|
|
}
|
|
}
|
|
if !found {
|
|
return "", nil
|
|
}
|
|
return longest, nil
|
|
}
|
|
|
|
func (api *API) isRPMWriteBlocked(repo models.Repo, relPath string) (bool, string, error) {
|
|
var root string
|
|
var err error
|
|
root, err = api.findRPMMirrorRoot(repo, relPath)
|
|
if err != nil {
|
|
return false, "", err
|
|
}
|
|
if root == "" {
|
|
return false, "", nil
|
|
}
|
|
return true, root, nil
|
|
}
|
|
|
|
func (api *API) allowRPMMirrorDelete(repo models.Repo, relPath string, isDir bool) (bool, error) {
|
|
var root string
|
|
var cfg models.RPMRepoDir
|
|
var normalizedPath string
|
|
var err error
|
|
root, err = api.findRPMMirrorRoot(repo, relPath)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if root == "" {
|
|
return true, nil
|
|
}
|
|
normalizedPath = normalizeRPMPath(relPath)
|
|
if normalizedPath == normalizeRPMPath(root) {
|
|
return false, nil
|
|
}
|
|
cfg, err = api.Store.GetRPMRepoDir(repo.ID, normalizeRPMPath(root))
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
if !cfg.AllowDelete {
|
|
return false, nil
|
|
}
|
|
if !isDir {
|
|
return true, nil
|
|
}
|
|
_, err = api.Store.GetRPMRepoDir(repo.ID, normalizedPath)
|
|
if err == nil {
|
|
return false, nil
|
|
}
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (api *API) hasBusyMirrorRootUnder(repo models.Repo, relPath string) (bool, string, string, error) {
|
|
var dirs []models.RPMRepoDir
|
|
var target string
|
|
var root string
|
|
var full string
|
|
var i int
|
|
var err error
|
|
target = normalizeRPMPath(relPath)
|
|
dirs, err = api.Store.ListRPMRepoDirs(repo.ID)
|
|
if err != nil {
|
|
return false, "", "", err
|
|
}
|
|
for i = 0; i < len(dirs); i++ {
|
|
if normalizeRPMRepoDirMode(dirs[i].Mode) != "mirror" {
|
|
continue
|
|
}
|
|
root = normalizeRPMPath(dirs[i].Path)
|
|
if !pathUnderRoot(root, target) {
|
|
continue
|
|
}
|
|
if dirs[i].SyncRunning {
|
|
return true, root, "sync_running", nil
|
|
}
|
|
if api.RpmMeta != nil {
|
|
full = filepath.Join(repo.Path, filepath.FromSlash(root))
|
|
if api.RpmMeta.IsRunning(full) {
|
|
return true, root, "metadata_running", nil
|
|
}
|
|
}
|
|
}
|
|
return false, "", "", nil
|
|
}
|
|
|
|
func nameHasWhitespace(name string) bool {
|
|
return strings.IndexFunc(name, unicode.IsSpace) >= 0
|
|
}
|
|
|
|
func slugHasWhitespace(slug string) bool {
|
|
return strings.IndexFunc(slug, unicode.IsSpace) >= 0
|
|
}
|
|
|
|
func contains(list []string, value string) bool {
|
|
var item string
|
|
for _, item = range list {
|
|
if item == value {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (api *API) cloneURL(projectSlug, repoName string) string {
|
|
var base string
|
|
var prefix string
|
|
base = strings.TrimRight(api.Cfg.PublicBaseURL, "/")
|
|
prefix = api.Cfg.GitHTTPPrefix
|
|
if !strings.HasPrefix(prefix, "/") {
|
|
prefix = "/" + prefix
|
|
}
|
|
return base + prefix + "/" + projectSlug + "/" + repoName + ".git"
|
|
}
|
|
|
|
func (api *API) rpmURL(projectSlug, repoName string) string {
|
|
var base string
|
|
var prefix string
|
|
base = strings.TrimRight(api.Cfg.PublicBaseURL, "/")
|
|
prefix = api.Cfg.RPMHTTPPrefix
|
|
if !strings.HasPrefix(prefix, "/") {
|
|
prefix = "/" + prefix
|
|
}
|
|
return base + prefix + "/" + projectSlug + "/" + repoName
|
|
}
|
|
|
|
func (api *API) dockerURL(projectSlug, repoName string) string {
|
|
var base string
|
|
base = strings.TrimRight(api.Cfg.PublicBaseURL, "/")
|
|
return base + "/v2/" + projectSlug + "/" + repoName
|
|
}
|
|
|
|
func (api *API) getRepoResolved(repoID string) (models.Repo, error) {
|
|
var repo models.Repo
|
|
var err error
|
|
repo, err = api.Store.GetRepo(repoID)
|
|
if err != nil {
|
|
return repo, err
|
|
}
|
|
err = api.ensureRepoPath(&repo)
|
|
if err != nil {
|
|
return repo, err
|
|
}
|
|
return repo, nil
|
|
}
|
|
|
|
func (api *API) ensureRepoPath(repo *models.Repo) error {
|
|
var err error
|
|
var projectStorageID int64
|
|
var repoStorageID int64
|
|
projectStorageID, repoStorageID, err = api.Store.GetRepoStorageIDs(repo.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
repo.Path = api.repoStoragePathByType(repo.Type, projectStorageID, repoStorageID)
|
|
return nil
|
|
}
|
|
|
|
func (api *API) repoStoragePathByType(repoType string, projectID int64, repoID int64) string {
|
|
var projectPart string
|
|
var repoPart string
|
|
projectPart = fmt.Sprintf("%016x", projectID)
|
|
repoPart = fmt.Sprintf("%016x", repoID)
|
|
if repoType == "git" {
|
|
return filepath.Join(api.Repos.BaseDir, projectPart, repoPart+".git")
|
|
}
|
|
if repoType == "rpm" {
|
|
return filepath.Join(api.RpmBase, projectPart, repoPart)
|
|
}
|
|
if repoType == "docker" {
|
|
return filepath.Join(api.DockerBase, projectPart, repoPart)
|
|
}
|
|
return ""
|
|
}
|