Compare commits

...

13 Commits

Author SHA1 Message Date
hyung-hwan 37dfa6edc4 no request-wide transaction for some repo endpoints 2026-06-21 02:21:38 +09:00
hyung-hwan 8b0435f84b more transaction fix in repo and project creation/deletion 2026-06-21 02:16:06 +09:00
hyung-hwan 205c71e1f8 more trasaction fix 2026-06-21 02:08:51 +09:00
hyung-hwan 555f3dcd34 updated UploadFile for transaction 2026-06-21 01:55:23 +09:00
hyung-hwan 7ea5dfe058 updated login for transaction 2026-06-21 01:40:10 +09:00
hyung-hwan 9a7fac0a90 updated oidc callback for improved transaction handling 2026-06-21 00:39:31 +09:00
hyung-hwan 76b33e904d updated acme order finalization flow 2026-06-21 00:23:58 +09:00
hyung-hwan 3645cc14e0 compacted database migration files 2026-06-20 22:12:32 +09:00
hyung-hwan 72011a1221 defined constants for role names - admin, writer, viewer 2026-06-20 22:07:21 +09:00
hyung-hwan 54f0e75b1f no request-wide transaction for large streaming of rpm file, block attachment, pki cet bundle download 2026-06-20 21:55:41 +09:00
hyung-hwan 7f3a8b24ff no request-wide transaction for api.RepoRPMUpload and api.RepoRPMRebuildSubdirMetadata
defined some more constants and places them in models/constannts.go
2026-06-20 21:46:58 +09:00
hyung-hwan d54770112f no requeset-wide transaction for Upload/Download/Copy for a ssh session 2026-06-20 21:33:36 +09:00
hyung-hwan f7d94f7aa9 removed transaction acquisition for api.InspectSSHCertficate, api.DiscoverSSHServerHostKeyForSelf and api.DiscoverSSHServerHostKeyAdmin when registring them to the api routes 2026-06-20 21:22:55 +09:00
35 changed files with 963 additions and 650 deletions
+7 -13
View File
@@ -10,15 +10,9 @@ import "time"
import "codit/internal/models" import "codit/internal/models"
import "codit/internal/util" import "codit/internal/util"
const AuthProviderTypeDB = "db"
const AuthProviderTypeLDAP = "ldap"
const AuthProviderTypeOIDC = "oidc"
const BuiltinDBProviderPublicID = "db" const BuiltinDBProviderPublicID = "db"
const GroupSyncModeOff string = "off"
const GroupSyncModeFirstLogin string = "first_login"
const GroupSyncModeSync string = "sync"
func scanAuthProvider(row interface { func scanAuthProvider(row interface {
Scan(dest ...any) error Scan(dest ...any) error
@@ -81,9 +75,9 @@ func normalizeAuthProviderGroupSyncMode(value string) (string, error) {
mode = strings.TrimSpace(value) mode = strings.TrimSpace(value)
if mode == "" { if mode == "" {
return GroupSyncModeOff, nil return models.GroupSyncModeOff, nil
} }
if mode == GroupSyncModeOff || mode == GroupSyncModeFirstLogin || mode == GroupSyncModeSync { if mode == models.GroupSyncModeOff || mode == models.GroupSyncModeFirstLogin || mode == models.GroupSyncModeSync {
return mode, nil return mode, nil
} }
return "", errors.New("invalid group_sync_mode") return "", errors.New("invalid group_sync_mode")
@@ -160,7 +154,7 @@ func insertAuthProviderGroupMappings(exec sqlExecutor, providerID string, mappin
FROM auth_providers p FROM auth_providers p
JOIN user_groups g ON g.public_id = ? JOIN user_groups g ON g.public_id = ?
WHERE p.public_id = ? AND g.scope = ?`, WHERE p.public_id = ? AND g.scope = ?`,
claimValue, now, now, groupID, strings.TrimSpace(providerID), UserGroupScopeExplicit) claimValue, now, now, groupID, strings.TrimSpace(providerID), models.UserGroupScopeExplicit)
if err != nil { if err != nil {
return err return err
} }
@@ -287,10 +281,10 @@ func (s *Store) CreateAuthProvider(ctx context.Context, item models.AuthProvider
item.ID, err = util.NewID() item.ID, err = util.NewID()
if err != nil { return item, err } if err != nil { return item, err }
} }
if item.Type == AuthProviderTypeLDAP && strings.TrimSpace(item.LDAPUserFilter) == "" { if item.Type == models.AuthProviderTypeLDAP && strings.TrimSpace(item.LDAPUserFilter) == "" {
item.LDAPUserFilter = "(uid={username})" item.LDAPUserFilter = "(uid={username})"
} }
if item.Type == AuthProviderTypeOIDC && strings.TrimSpace(item.OIDCScopes) == "" { if item.Type == models.AuthProviderTypeOIDC && strings.TrimSpace(item.OIDCScopes) == "" {
item.OIDCScopes = "openid profile email" item.OIDCScopes = "openid profile email"
} }
item.GroupSyncMode, err = normalizeAuthProviderGroupSyncMode(item.GroupSyncMode) item.GroupSyncMode, err = normalizeAuthProviderGroupSyncMode(item.GroupSyncMode)
@@ -343,10 +337,10 @@ func (s *Store) UpdateAuthProvider(ctx context.Context, item models.AuthProvider
now = time.Now().Unix() now = time.Now().Unix()
item.UpdatedAt = now item.UpdatedAt = now
if item.Type == AuthProviderTypeLDAP && strings.TrimSpace(item.LDAPUserFilter) == "" { if item.Type == models.AuthProviderTypeLDAP && strings.TrimSpace(item.LDAPUserFilter) == "" {
item.LDAPUserFilter = "(uid={username})" item.LDAPUserFilter = "(uid={username})"
} }
if item.Type == AuthProviderTypeOIDC && strings.TrimSpace(item.OIDCScopes) == "" { if item.Type == models.AuthProviderTypeOIDC && strings.TrimSpace(item.OIDCScopes) == "" {
item.OIDCScopes = "openid profile email" item.OIDCScopes = "openid profile email"
} }
item.GroupSyncMode, err = normalizeAuthProviderGroupSyncMode(item.GroupSyncMode) item.GroupSyncMode, err = normalizeAuthProviderGroupSyncMode(item.GroupSyncMode)
+1 -3
View File
@@ -498,9 +498,7 @@ func (s *Store) CreateBlockAttachment(ctx context.Context, boardID, blockID stri
return item, err return item, err
} }
err = commitIfOwned(tx, owned) err = commitIfOwned(tx, owned)
if err != nil { if err != nil { return item, err }
return item, err
}
return item, nil return item, nil
} }
+11 -13
View File
@@ -9,16 +9,14 @@ import "time"
import "codit/internal/models" import "codit/internal/models"
import "codit/internal/util" import "codit/internal/util"
const UserGroupScopeExplicit string = "explicit"
const UserGroupScopeAllUsers string = "all_users"
func NormalizeUserGroupScope(value string) string { func NormalizeUserGroupScope(value string) string {
value = strings.ToLower(strings.TrimSpace(value)) value = strings.ToLower(strings.TrimSpace(value))
switch value { switch value {
case UserGroupScopeAllUsers: case models.UserGroupScopeAllUsers:
return UserGroupScopeAllUsers return models.UserGroupScopeAllUsers
default: default:
return UserGroupScopeExplicit return models.UserGroupScopeExplicit
} }
} }
@@ -100,7 +98,7 @@ func (s *Store) UpdateUserGroup(ctx context.Context, item models.UserGroup) (mod
item.Scope = NormalizeUserGroupScope(item.Scope) item.Scope = NormalizeUserGroupScope(item.Scope)
tx, owned, err = s.beginImmediateContext(ctx) tx, owned, err = s.beginImmediateContext(ctx)
if err != nil { return item, err } if err != nil { return item, err }
if item.Scope != UserGroupScopeExplicit { if item.Scope != models.UserGroupScopeExplicit {
err = tx.QueryRow(`SELECT COUNT(*) err = tx.QueryRow(`SELECT COUNT(*)
FROM auth_provider_group_mappings m FROM auth_provider_group_mappings m
JOIN user_groups g ON g.id = m.group_id JOIN user_groups g ON g.id = m.group_id
@@ -157,7 +155,7 @@ func (s *Store) DeleteUserGroup(ctx context.Context, groupID string) error {
// TODO: this part needs to be re-thought through. This is still user created group. If it can't be deleted, that's horrible // TODO: this part needs to be re-thought through. This is still user created group. If it can't be deleted, that's horrible
// above all creating another user group with this scope gives the unique constraint error... so Creating a user group // above all creating another user group with this scope gives the unique constraint error... so Creating a user group
// should translate it to human friendly message. // should translate it to human friendly message.
if NormalizeUserGroupScope(scope) == UserGroupScopeAllUsers { if NormalizeUserGroupScope(scope) == models.UserGroupScopeAllUsers {
rollbackIfOwned(tx, owned) rollbackIfOwned(tx, owned)
return errors.New("all users group cannot be deleted") return errors.New("all users group cannot be deleted")
} }
@@ -196,7 +194,7 @@ func (s *Store) ListUserGroupMembers(groupID string) ([]models.UserGroupMember,
if err != nil { if err != nil {
return nil, err return nil, err
} }
if NormalizeUserGroupScope(group.Scope) == UserGroupScopeAllUsers { if NormalizeUserGroupScope(group.Scope) == models.UserGroupScopeAllUsers {
rows, err = s.Query(`SELECT ? AS group_public_id, u.public_id, u.username, u.display_name, 0 AS created_at rows, err = s.Query(`SELECT ? AS group_public_id, u.public_id, u.username, u.display_name, 0 AS created_at
FROM users u FROM users u
WHERE u.disabled = 0 WHERE u.disabled = 0
@@ -242,7 +240,7 @@ func (s *Store) AddUserGroupMember(ctx context.Context, groupID string, userID s
rollbackIfOwned(tx, owned) rollbackIfOwned(tx, owned)
return err return err
} }
if NormalizeUserGroupScope(scope) != UserGroupScopeExplicit { if NormalizeUserGroupScope(scope) != models.UserGroupScopeExplicit {
rollbackIfOwned(tx, owned) rollbackIfOwned(tx, owned)
return errors.New("virtual groups do not accept explicit members") return errors.New("virtual groups do not accept explicit members")
} }
@@ -272,7 +270,7 @@ func (s *Store) RemoveUserGroupMember(ctx context.Context, groupID string, userI
rollbackIfOwned(tx, owned) rollbackIfOwned(tx, owned)
return err return err
} }
if NormalizeUserGroupScope(scope) != UserGroupScopeExplicit { if NormalizeUserGroupScope(scope) != models.UserGroupScopeExplicit {
rollbackIfOwned(tx, owned) rollbackIfOwned(tx, owned)
return errors.New("virtual groups do not have explicit members") return errors.New("virtual groups do not have explicit members")
} }
@@ -433,13 +431,13 @@ func (s *Store) GetProjectRoleForPrincipal(projectID string, principalID string)
func projectRoleRank(value string) int { func projectRoleRank(value string) int {
var v string var v string
v = strings.ToLower(strings.TrimSpace(value)) v = strings.ToLower(strings.TrimSpace(value))
if v == "admin" { if v == models.RoleAdmin {
return 3 return 3
} }
if v == "writer" { if v == models.RoleWriter {
return 2 return 2
} }
if v == "viewer" { if v == models.RoleViewer {
return 1 return 1
} }
return 0 return 0
+4 -6
View File
@@ -8,8 +8,6 @@ import "time"
import "codit/internal/models" import "codit/internal/models"
import "codit/internal/util" import "codit/internal/util"
const RPMRepoModeLocal string = "local"
const RPMRepoModeMirror string = "mirror"
func (s *Store) ListRPMRepoDirs(repoID string) ([]models.RPMRepoDir, error) { func (s *Store) ListRPMRepoDirs(repoID string) ([]models.RPMRepoDir, error) {
var rows *sql.Rows var rows *sql.Rows
@@ -74,7 +72,7 @@ func (s *Store) UpsertRPMRepoDir(item models.RPMRepoDir) error {
item.TLSInsecureSkipVerify, item.TLSInsecureSkipVerify,
item.SyncIntervalSec, item.SyncIntervalSec,
item.SyncEnabled, item.SyncEnabled,
NormalizeRPMRepoMode(item.Mode) == RPMRepoModeMirror, NormalizeRPMRepoMode(item.Mode) == models.RPMRepoModeMirror,
int64(0), int64(0),
now, now,
now) now)
@@ -524,10 +522,10 @@ func (s *Store) CleanupRPMMirrorRunsRetention(repoID string, path string, keepCo
func NormalizeRPMRepoMode(mode string) string { func NormalizeRPMRepoMode(mode string) string {
var v string var v string
v = strings.ToLower(strings.TrimSpace(mode)) v = strings.ToLower(strings.TrimSpace(mode))
if v == RPMRepoModeMirror { if v == models.RPMRepoModeMirror {
return RPMRepoModeMirror return models.RPMRepoModeMirror
} }
return RPMRepoModeLocal return models.RPMRepoModeLocal
} }
func (s *Store) DeleteRPMRepoDir(repoID string, path string) error { func (s *Store) DeleteRPMRepoDir(repoID string, path string) error {
+1 -1
View File
@@ -513,7 +513,7 @@ func (s *Store) CreateProject(ctx context.Context, project models.Project) (mode
} }
_, err = tx.Exec(`INSERT INTO project_role_bindings (project_id, subject_type, subject_id, role, created_at, updated_at) _, err = tx.Exec(`INSERT INTO project_role_bindings (project_id, subject_type, subject_id, role, created_at, updated_at)
VALUES ((SELECT id FROM projects WHERE public_id = ?), 'user', (SELECT id FROM users WHERE public_id = ?), ?, ?, ?)`, VALUES ((SELECT id FROM projects WHERE public_id = ?), 'user', (SELECT id FROM users WHERE public_id = ?), ?, ?, ?)`,
project.ID, project.CreatedBy, "admin", project.CreatedAt, project.UpdatedAt) project.ID, project.CreatedBy, models.RoleAdmin, project.CreatedAt, project.UpdatedAt)
if err != nil { if err != nil {
rollbackIfOwned(tx, owned) rollbackIfOwned(tx, owned)
return project, err return project, err
+106 -36
View File
@@ -21,6 +21,7 @@ import "time"
import "golang.org/x/crypto/acme" import "golang.org/x/crypto/acme"
import "codit/internal/db"
import "codit/internal/models" import "codit/internal/models"
type acmeProfileRequest struct { type acmeProfileRequest struct {
@@ -466,7 +467,6 @@ func (api *API) FinalizeACMEOrder(w http.ResponseWriter, r *http.Request, params
var certDER [][]byte var certDER [][]byte
var certURL string var certURL string
var cert models.PKICert var cert models.PKICert
var existing models.PKICert
var certBlock *pem.Block var certBlock *pem.Block
var leaf *x509.Certificate var leaf *x509.Certificate
var challenge *acme.Challenge var challenge *acme.Challenge
@@ -478,6 +478,7 @@ func (api *API) FinalizeACMEOrder(w http.ResponseWriter, r *http.Request, params
var createdBySubjectID string var createdBySubjectID string
var createdBySubjectName string var createdBySubjectName string
var issuanceSource string var issuanceSource string
var persistErr error
var err error var err error
if !api.requireAdmin(w, r) { if !api.requireAdmin(w, r) {
return return
@@ -521,8 +522,7 @@ func (api *API) FinalizeACMEOrder(w http.ResponseWriter, r *http.Request, params
api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "order finalize failed id=%s step=account err=%v", order.ID, err) api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "order finalize failed id=%s step=account err=%v", order.ID, err)
order.Status = "failed" order.Status = "failed"
order.LastError = err.Error() order.LastError = err.Error()
_, _ = api.store(r).UpdateACMEOrder(order) api.persistACMEFinalizeFailure(order, profile.ID, true)
_ = api.store(r).UpdateACMEProfileLastError(profile.ID, err.Error())
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error()) WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
return return
} }
@@ -532,7 +532,7 @@ func (api *API) FinalizeACMEOrder(w http.ResponseWriter, r *http.Request, params
if err != nil { if err != nil {
order.Status = "failed" order.Status = "failed"
order.LastError = err.Error() order.LastError = err.Error()
_, _ = api.store(r).UpdateACMEOrder(order) api.persistACMEFinalizeFailure(order, profile.ID, false)
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error()) WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
return return
} }
@@ -548,8 +548,7 @@ func (api *API) FinalizeACMEOrder(w http.ResponseWriter, r *http.Request, params
order.Status = "failed" order.Status = "failed"
order.LastError = err.Error() order.LastError = err.Error()
order.Challenges[i].Status = "failed" order.Challenges[i].Status = "failed"
_, _ = api.store(r).UpdateACMEOrder(order) api.persistACMEFinalizeFailure(order, profile.ID, true)
_ = api.store(r).UpdateACMEProfileLastError(profile.ID, err.Error())
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error()) WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
return return
} }
@@ -561,17 +560,23 @@ func (api *API) FinalizeACMEOrder(w http.ResponseWriter, r *http.Request, params
order.Status = "failed" order.Status = "failed"
order.LastError = err.Error() order.LastError = err.Error()
order.Challenges[i].Status = "failed" order.Challenges[i].Status = "failed"
_, _ = api.store(r).UpdateACMEOrder(order) api.persistACMEFinalizeFailure(order, profile.ID, false)
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error()) WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
return return
} }
// [NOTE]
// It may take a long time (30-120 seconds) for ACME CA to confirm DNS propagation.
// If the user closes the connection during this window, the wait is aborted. The
// upstream CA may have already verified the domain, but the order status on our
// side is still "pending". To recover this, you will have to Finalize order again.
authz, err = client.WaitAuthorization(r.Context(), order.Challenges[i].AuthorizationURL) authz, err = client.WaitAuthorization(r.Context(), order.Challenges[i].AuthorizationURL)
if err != nil { if err != nil {
api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "challenge wait failed order=%s domain=%s err=%v", order.ID, order.Challenges[i].Identifier, err) api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "challenge wait failed order=%s domain=%s err=%v", order.ID, order.Challenges[i].Identifier, err)
order.Status = "failed" order.Status = "failed"
order.LastError = err.Error() order.LastError = err.Error()
order.Challenges[i].Status = "failed" order.Challenges[i].Status = "failed"
_, _ = api.store(r).UpdateACMEOrder(order) api.persistACMEFinalizeFailure(order, profile.ID, false)
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error()) WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
return return
} }
@@ -589,7 +594,7 @@ func (api *API) FinalizeACMEOrder(w http.ResponseWriter, r *http.Request, params
api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "order finalize failed id=%s step=create_cert err=%v", order.ID, err) api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "order finalize failed id=%s step=create_cert err=%v", order.ID, err)
order.Status = "failed" order.Status = "failed"
order.LastError = err.Error() order.LastError = err.Error()
_, _ = api.store(r).UpdateACMEOrder(order) api.persistACMEFinalizeFailure(order, profile.ID, false)
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error()) WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
return return
} }
@@ -598,7 +603,7 @@ func (api *API) FinalizeACMEOrder(w http.ResponseWriter, r *http.Request, params
if err != nil { if err != nil {
order.Status = "failed" order.Status = "failed"
order.LastError = err.Error() order.LastError = err.Error()
_, _ = api.store(r).UpdateACMEOrder(order) api.persistACMEFinalizeFailure(order, profile.ID, false)
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error()) WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
return return
} }
@@ -607,7 +612,7 @@ func (api *API) FinalizeACMEOrder(w http.ResponseWriter, r *http.Request, params
if certBlock == nil { if certBlock == nil {
order.Status = "failed" order.Status = "failed"
order.LastError = "invalid issued certificate" order.LastError = "invalid issued certificate"
_, _ = api.store(r).UpdateACMEOrder(order) api.persistACMEFinalizeFailure(order, profile.ID, false)
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": order.LastError}) WriteJSON(w, http.StatusBadRequest, map[string]string{"error": order.LastError})
return return
} }
@@ -615,7 +620,7 @@ func (api *API) FinalizeACMEOrder(w http.ResponseWriter, r *http.Request, params
if err != nil { if err != nil {
order.Status = "failed" order.Status = "failed"
order.LastError = err.Error() order.LastError = err.Error()
_, _ = api.store(r).UpdateACMEOrder(order) api.persistACMEFinalizeFailure(order, profile.ID, false)
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error()) WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
return return
} }
@@ -640,46 +645,111 @@ func (api *API) FinalizeACMEOrder(w http.ResponseWriter, r *http.Request, params
RevocationReason: "", RevocationReason: "",
} }
replaceExisting = req.RenewMode == "override" replaceExisting = req.RenewMode == "override"
order.Status = "valid"
order.LastError = ""
order, cert, err = api.persistACMEFinalizeSuccess(order, profile.ID, cert, replaceExisting)
if err != nil {
api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "order finalize failed id=%s step=persist_success err=%v", order.ID, err)
order.Status = "failed"
order.LastError = err.Error()
persistErr = api.persistACMEFinalizeFailure(order, profile.ID, false)
if persistErr != nil {
api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "order finalize failure persist failed id=%s err=%v", order.ID, persistErr)
}
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return
}
_ = certURL
api.Logger.Write(acmeLogID, codit_logger.LOG_INFO, "order finalize success id=%s cert_id=%s cert_url=%s", order.ID, order.CertID, certURL)
WriteJSON(w, http.StatusOK, order)
}
func (api *API) persistACMEFinalizeFailure(order models.ACMEOrder, profileID string, updateProfileError bool) error {
var ctx context.Context
var cancel context.CancelFunc
var store *db.Store
var err error
ctx, cancel = context.WithTimeout(context.Background(), 10 * time.Second)
defer cancel()
store, err = api.Store.BeginImmediateStore(ctx)
if err != nil {
api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "order finalize failure persist failed id=%s err=%v", order.ID, err)
return err
}
_, err = store.UpdateACMEOrder(order)
if err != nil {
_ = store.Rollback()
api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "order finalize failure persist failed id=%s err=%v", order.ID, err)
return err
}
if updateProfileError {
err = store.UpdateACMEProfileLastError(profileID, order.LastError)
if err != nil {
_ = store.Rollback()
api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "order finalize profile error persist failed id=%s profile=%s err=%v", order.ID, profileID, err)
return err
}
}
err = store.Commit()
if err != nil {
api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "order finalize failure persist commit failed id=%s err=%v", order.ID, err)
return err
}
return nil
}
func (api *API) persistACMEFinalizeSuccess(order models.ACMEOrder, profileID string, cert models.PKICert, replaceExisting bool) (models.ACMEOrder, models.PKICert, error) {
var ctx context.Context
var cancel context.CancelFunc
var store *db.Store
var existing models.PKICert
var err error
ctx, cancel = context.WithTimeout(context.Background(), 10 * time.Second)
defer cancel()
store, err = api.Store.BeginImmediateStore(ctx)
if err != nil {
return order, cert, err
}
if replaceExisting && strings.TrimSpace(order.CertID) != "" { if replaceExisting && strings.TrimSpace(order.CertID) != "" {
existing, err = api.store(r).GetPKICert(order.CertID) existing, err = store.GetPKICert(order.CertID)
if err == nil { if err == nil {
cert.ID = existing.ID cert.ID = existing.ID
cert.CAID = existing.CAID cert.CAID = existing.CAID
cert, err = api.store(r).ReplacePKICert(cert) cert, err = store.ReplacePKICert(cert)
if err != nil { if err != nil {
api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "order finalize failed id=%s step=replace_cert err=%v", order.ID, err) _ = store.Rollback()
order.Status = "failed" return order, cert, err
order.LastError = err.Error()
_, _ = api.store(r).UpdateACMEOrder(order)
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
return
} }
api.Logger.Write(acmeLogID, codit_logger.LOG_INFO, "order finalize replaced existing cert id=%s", cert.ID) api.Logger.Write(acmeLogID, codit_logger.LOG_INFO, "order finalize replaced existing cert id=%s", cert.ID)
} }
} }
if strings.TrimSpace(cert.ID) == "" { if strings.TrimSpace(cert.ID) == "" {
cert, err = api.store(r).CreatePKICert(cert) cert, err = store.CreatePKICert(cert)
if err != nil { if err != nil {
api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "order finalize failed id=%s step=store_cert err=%v", order.ID, err) _ = store.Rollback()
order.Status = "failed" return order, cert, err
order.LastError = err.Error()
_, _ = api.store(r).UpdateACMEOrder(order)
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
return
} }
} }
order.Status = "valid"
order.LastError = ""
order.CertID = cert.ID order.CertID = cert.ID
order, err = api.store(r).UpdateACMEOrder(order) order, err = store.UpdateACMEOrder(order)
if err != nil { if err != nil {
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error()) _ = store.Rollback()
return return order, cert, err
} }
_ = api.store(r).UpdateACMEProfileLastError(profile.ID, "") err = store.UpdateACMEProfileLastError(profileID, "")
_ = certURL if err != nil {
api.Logger.Write(acmeLogID, codit_logger.LOG_INFO, "order finalize success id=%s cert_id=%s cert_url=%s", order.ID, order.CertID, certURL) _ = store.Rollback()
WriteJSON(w, http.StatusOK, order) return order, cert, err
}
err = store.Commit()
if err != nil {
return order, cert, err
}
return order, cert, nil
} }
func (api *API) DeleteACMEOrder(w http.ResponseWriter, r *http.Request, params map[string]string) { func (api *API) DeleteACMEOrder(w http.ResponseWriter, r *http.Request, params map[string]string) {
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -52,11 +52,11 @@ func (api *API) CreateAuthProvider(w http.ResponseWriter, r *http.Request, _ map
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "name is required") WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "name is required")
return return
} }
if req.Type != db.AuthProviderTypeLDAP && req.Type != db.AuthProviderTypeOIDC { if req.Type != models.AuthProviderTypeLDAP && req.Type != models.AuthProviderTypeOIDC {
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "type must be ldap or oidc") WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "type must be ldap or oidc")
return return
} }
if req.Type == db.AuthProviderTypeOIDC && strings.TrimSpace(req.OIDCAdmissionExpr) != "" { if req.Type == models.AuthProviderTypeOIDC && strings.TrimSpace(req.OIDCAdmissionExpr) != "" {
err = oidcCompileAdmissionExpr(strings.TrimSpace(req.OIDCAdmissionExpr)) err = oidcCompileAdmissionExpr(strings.TrimSpace(req.OIDCAdmissionExpr))
if err != nil { if err != nil {
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid admission expression: "+err.Error()) WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid admission expression: "+err.Error())
@@ -91,7 +91,7 @@ func (api *API) UpdateAuthProvider(w http.ResponseWriter, r *http.Request, param
} }
req.ID = existing.ID req.ID = existing.ID
req.Type = existing.Type req.Type = existing.Type
if req.Type == db.AuthProviderTypeOIDC && strings.TrimSpace(req.OIDCAdmissionExpr) != "" { if req.Type == models.AuthProviderTypeOIDC && strings.TrimSpace(req.OIDCAdmissionExpr) != "" {
err = oidcCompileAdmissionExpr(strings.TrimSpace(req.OIDCAdmissionExpr)) err = oidcCompileAdmissionExpr(strings.TrimSpace(req.OIDCAdmissionExpr))
if err != nil { if err != nil {
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid admission expression: "+err.Error()) WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid admission expression: "+err.Error())
@@ -165,7 +165,7 @@ func (api *API) TestAuthProvider(w http.ResponseWriter, r *http.Request, params
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "auth provider not found") WriteJSONWithErrorReason(w, r, http.StatusNotFound, "auth provider not found")
return return
} }
if provider.Type != db.AuthProviderTypeLDAP { if provider.Type != models.AuthProviderTypeLDAP {
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "test is only supported for ldap providers") WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "test is only supported for ldap providers")
return return
} }
+18 -17
View File
@@ -24,7 +24,7 @@ func (api *API) ListBlockComments(w http.ResponseWriter, r *http.Request, params
var role string var role string
var i int var i int
if !api.requireBoardRole(w, r, params["boardId"], "viewer") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
return return
} }
comments, err = api.store(r).ListBlockComments(params["boardId"], params["blockId"]) comments, err = api.store(r).ListBlockComments(params["boardId"], params["blockId"])
@@ -42,7 +42,7 @@ func (api *API) ListBlockComments(w http.ResponseWriter, r *http.Request, params
role = "" role = ""
} }
for i = 0; i < len(comments); i++ { for i = 0; i < len(comments); i++ {
comments[i].CanDelete = user.IsAdmin || comments[i].CreatedBy == user.ID || boardRoleAllows(role, "admin") comments[i].CanDelete = user.IsAdmin || comments[i].CreatedBy == user.ID || boardRoleAllows(role, models.RoleAdmin)
} }
} }
WriteJSON(w, http.StatusOK, comments) WriteJSON(w, http.StatusOK, comments)
@@ -64,7 +64,7 @@ func (api *API) CreateBlockComment(w http.ResponseWriter, r *http.Request, param
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "requires user account") WriteJSONWithErrorReason(w, r, http.StatusForbidden, "requires user account")
return return
} }
if !api.requireBoardRole(w, r, params["boardId"], "editor") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return return
} }
err = DecodeJSON(r, &req) err = DecodeJSON(r, &req)
@@ -99,7 +99,7 @@ func (api *API) DeleteBlockComment(w http.ResponseWriter, r *http.Request, param
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "requires user account") WriteJSONWithErrorReason(w, r, http.StatusForbidden, "requires user account")
return return
} }
if !api.requireBoardRole(w, r, params["boardId"], "viewer") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
return return
} }
comments, err = api.store(r).ListBlockComments(params["boardId"], params["blockId"]) comments, err = api.store(r).ListBlockComments(params["boardId"], params["blockId"])
@@ -124,7 +124,7 @@ func (api *API) DeleteBlockComment(w http.ResponseWriter, r *http.Request, param
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "comment deletion requires the comment author or board admin") WriteJSONWithErrorReason(w, r, http.StatusForbidden, "comment deletion requires the comment author or board admin")
return return
} }
canDelete = user.IsAdmin || boardRoleAllows(role, "admin") canDelete = user.IsAdmin || boardRoleAllows(role, models.RoleAdmin)
} }
if !canDelete { if !canDelete {
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "comment deletion requires the comment author or board admin") WriteJSONWithErrorReason(w, r, http.StatusForbidden, "comment deletion requires the comment author or board admin")
@@ -146,7 +146,7 @@ func (api *API) ListBlockChecklistItems(w http.ResponseWriter, r *http.Request,
var items []models.BlockChecklistItem var items []models.BlockChecklistItem
var err error var err error
if !api.requireBoardRole(w, r, params["boardId"], "viewer") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
return return
} }
items, err = api.store(r).ListBlockChecklistItems(params["boardId"], params["blockId"]) items, err = api.store(r).ListBlockChecklistItems(params["boardId"], params["blockId"])
@@ -176,7 +176,7 @@ func (api *API) CreateBlockChecklistItem(w http.ResponseWriter, r *http.Request,
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "requires user account") WriteJSONWithErrorReason(w, r, http.StatusForbidden, "requires user account")
return return
} }
if !api.requireBoardRole(w, r, params["boardId"], "editor") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return return
} }
err = DecodeJSON(r, &req) err = DecodeJSON(r, &req)
@@ -213,7 +213,7 @@ func (api *API) UpdateBlockChecklistItem(w http.ResponseWriter, r *http.Request,
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "requires user account") WriteJSONWithErrorReason(w, r, http.StatusForbidden, "requires user account")
return return
} }
if !api.requireBoardRole(w, r, params["boardId"], "editor") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return return
} }
err = DecodeJSON(r, &req) err = DecodeJSON(r, &req)
@@ -236,7 +236,7 @@ func (api *API) UpdateBlockChecklistItem(w http.ResponseWriter, r *http.Request,
func (api *API) DeleteBlockChecklistItem(w http.ResponseWriter, r *http.Request, params map[string]string) { func (api *API) DeleteBlockChecklistItem(w http.ResponseWriter, r *http.Request, params map[string]string) {
var err error var err error
if !api.requireBoardRole(w, r, params["boardId"], "editor") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return return
} }
err = api.store(r).DeleteBlockChecklistItem(params["boardId"], params["blockId"], params["itemId"]) err = api.store(r).DeleteBlockChecklistItem(params["boardId"], params["blockId"], params["itemId"])
@@ -258,7 +258,7 @@ func (api *API) ReorderBlockChecklistItems(w http.ResponseWriter, r *http.Reques
var items []models.BlockChecklistItem var items []models.BlockChecklistItem
var err error var err error
if !api.requireBoardRole(w, r, params["boardId"], "editor") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return return
} }
err = DecodeJSON(r, &req) err = DecodeJSON(r, &req)
@@ -289,7 +289,7 @@ func (api *API) ListBlockAttachments(w http.ResponseWriter, r *http.Request, par
var items []models.BlockAttachment var items []models.BlockAttachment
var err error var err error
if !api.requireBoardRole(w, r, params["boardId"], "viewer") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
return return
} }
items, err = api.store(r).ListBlockAttachments(params["boardId"], params["blockId"]) items, err = api.store(r).ListBlockAttachments(params["boardId"], params["blockId"])
@@ -320,7 +320,7 @@ func (api *API) CreateBlockAttachment(w http.ResponseWriter, r *http.Request, pa
var created models.BlockAttachment var created models.BlockAttachment
var ts string var ts string
if !api.requireBoardRole(w, r, params["boardId"], "editor") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return return
} }
user, ok = middleware.UserFromContext(r.Context()) user, ok = middleware.UserFromContext(r.Context())
@@ -345,6 +345,7 @@ func (api *API) CreateBlockAttachment(w http.ResponseWriter, r *http.Request, pa
tempName = storedName + ".uploading-" + ts tempName = storedName + ".uploading-" + ts
tempPath, size, err = api.Uploads.Save(tempName, file) tempPath, size, err = api.Uploads.Save(tempName, file)
if err != nil { if err != nil {
_ = os.Remove(tempPath)
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error()) WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return return
} }
@@ -388,7 +389,7 @@ func (api *API) DownloadBlockAttachment(w http.ResponseWriter, r *http.Request,
var inline bool var inline bool
var stat os.FileInfo var stat os.FileInfo
if !api.requireBoardRole(w, r, params["boardId"], "viewer") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
return return
} }
item, err = api.store(r).GetBlockAttachment(params["boardId"], params["blockId"], params["attachmentId"]) item, err = api.store(r).GetBlockAttachment(params["boardId"], params["blockId"], params["attachmentId"])
@@ -420,7 +421,7 @@ func (api *API) DeleteBlockAttachment(w http.ResponseWriter, r *http.Request, pa
var item models.BlockAttachment var item models.BlockAttachment
var err error var err error
if !api.requireBoardRole(w, r, params["boardId"], "editor") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return return
} }
item, err = api.store(r).DeleteBlockAttachment(r.Context(), params["boardId"], params["blockId"], params["attachmentId"]) item, err = api.store(r).DeleteBlockAttachment(r.Context(), params["boardId"], params["blockId"], params["attachmentId"])
@@ -440,7 +441,7 @@ func (api *API) ListBoardBlockProperties(w http.ResponseWriter, r *http.Request,
var props []models.BlockProperties var props []models.BlockProperties
var err error var err error
if !api.requireBoardRole(w, r, params["boardId"], "viewer") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
return return
} }
props, err = api.store(r).ListAllBlockProperties(params["boardId"]) props, err = api.store(r).ListAllBlockProperties(params["boardId"])
@@ -458,7 +459,7 @@ func (api *API) GetBlockProperties(w http.ResponseWriter, r *http.Request, param
var bp models.BlockProperties var bp models.BlockProperties
var err error var err error
if !api.requireBoardRole(w, r, params["boardId"], "viewer") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
return return
} }
bp, err = api.store(r).GetBlockProperties(params["boardId"], params["blockId"]) bp, err = api.store(r).GetBlockProperties(params["boardId"], params["blockId"])
@@ -489,7 +490,7 @@ func (api *API) UpsertBlockProperties(w http.ResponseWriter, r *http.Request, pa
var bp models.BlockProperties var bp models.BlockProperties
var err error var err error
if !api.requireBoardRole(w, r, params["boardId"], "editor") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return return
} }
err = DecodeJSON(r, &req) err = DecodeJSON(r, &req)
@@ -11,7 +11,7 @@ func (api *API) ListBoardFieldValues(w http.ResponseWriter, r *http.Request, par
var values []models.BoardFieldValue var values []models.BoardFieldValue
var err error var err error
if !api.requireBoardRole(w, r, params["boardId"], "viewer") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
return return
} }
values, err = api.store(r).ListBoardFieldValues(params["boardId"], params["field"]) values, err = api.store(r).ListBoardFieldValues(params["boardId"], params["field"])
@@ -39,7 +39,7 @@ func (api *API) CreateBoardFieldValue(w http.ResponseWriter, r *http.Request, pa
var created models.BoardFieldValue var created models.BoardFieldValue
var err error var err error
if !api.requireBoardRole(w, r, params["boardId"], "admin") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleAdmin) {
return return
} }
err = DecodeJSON(r, &req) err = DecodeJSON(r, &req)
@@ -73,7 +73,7 @@ func (api *API) UpdateBoardFieldValue(w http.ResponseWriter, r *http.Request, pa
var updated models.BoardFieldValue var updated models.BoardFieldValue
var err error var err error
if !api.requireBoardRole(w, r, params["boardId"], "admin") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleAdmin) {
return return
} }
err = DecodeJSON(r, &req) err = DecodeJSON(r, &req)
@@ -106,7 +106,7 @@ func (api *API) ReorderBoardFieldValues(w http.ResponseWriter, r *http.Request,
var values []models.BoardFieldValue var values []models.BoardFieldValue
var err error var err error
if !api.requireBoardRole(w, r, params["boardId"], "admin") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleAdmin) {
return return
} }
err = DecodeJSON(r, &req) err = DecodeJSON(r, &req)
@@ -132,7 +132,7 @@ func (api *API) ReorderBoardFieldValues(w http.ResponseWriter, r *http.Request,
func (api *API) DeleteBoardFieldValue(w http.ResponseWriter, r *http.Request, params map[string]string) { func (api *API) DeleteBoardFieldValue(w http.ResponseWriter, r *http.Request, params map[string]string) {
var err error var err error
if !api.requireBoardRole(w, r, params["boardId"], "admin") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleAdmin) {
return return
} }
err = api.store(r).DeleteBoardFieldValue(r.Context(), params["boardId"], params["valueId"]) err = api.store(r).DeleteBoardFieldValue(r.Context(), params["boardId"], params["valueId"])
+29 -29
View File
@@ -12,11 +12,11 @@ import "codit/internal/models"
// boardRoleRank returns a numeric rank for board roles (higher = more access). // boardRoleRank returns a numeric rank for board roles (higher = more access).
func boardRoleRank(role string) int { func boardRoleRank(role string) int {
switch role { switch role {
case "admin": case models.RoleAdmin:
return 3 return 3
case "editor": case models.RoleWriter:
return 2 return 2
case "viewer": case models.RoleViewer:
return 1 return 1
default: default:
return 0 return 0
@@ -24,7 +24,7 @@ func boardRoleRank(role string) int {
} }
// boardRoleAllows checks whether actual satisfies the required board role. // boardRoleAllows checks whether actual satisfies the required board role.
// Hierarchy: admin > editor > viewer. // Hierarchy: admin > writer > viewer.
func boardRoleAllows(actual, required string) bool { func boardRoleAllows(actual, required string) bool {
return boardRoleRank(actual) >= boardRoleRank(required) return boardRoleRank(actual) >= boardRoleRank(required)
} }
@@ -38,7 +38,7 @@ func (api *API) boardRoleForUser(r *http.Request, boardID string, user models.Us
var err error var err error
if user.IsAdmin { if user.IsAdmin {
return "admin", nil return models.RoleAdmin, nil
} }
projectID, err = api.store(r).GetBoardProjectID(boardID) projectID, err = api.store(r).GetBoardProjectID(boardID)
if err != nil { if err != nil {
@@ -61,12 +61,12 @@ func (api *API) boardRoleForUser(r *http.Request, boardID string, user models.Us
func projectRoleToBoardRole(projectRole string) string { func projectRoleToBoardRole(projectRole string) string {
switch projectRole { switch projectRole {
case "admin": case models.RoleAdmin:
return "admin" return models.RoleAdmin
case "writer": case models.RoleWriter:
return "editor" return models.RoleWriter
default: default:
return "viewer" return models.RoleViewer
} }
} }
@@ -74,7 +74,7 @@ func projectRoleToBoardRole(projectRole string) string {
// Returns "" for unknown roles. // Returns "" for unknown roles.
func normalizeBoardRole(role string) string { func normalizeBoardRole(role string) string {
switch role { switch role {
case "admin", "editor", "viewer": case models.RoleAdmin, models.RoleWriter, models.RoleViewer:
return role return role
default: default:
return "" return ""
@@ -240,7 +240,7 @@ func (api *API) CreateBoard(w http.ResponseWriter, r *http.Request, params map[s
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "board creation requires a user account") WriteJSONWithErrorReason(w, r, http.StatusForbidden, "board creation requires a user account")
return return
} }
if !api.requireProjectRole(w, r, params["projectId"], "writer") { if !api.requireProjectRole(w, r, params["projectId"], models.RoleWriter) {
return return
} }
err = DecodeJSON(r, &req) err = DecodeJSON(r, &req)
@@ -286,7 +286,7 @@ func (api *API) GetBoard(w http.ResponseWriter, r *http.Request, params map[stri
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "board not found") WriteJSONWithErrorReason(w, r, http.StatusNotFound, "board not found")
return return
} }
if !api.requireBoardRole(w, r, board.ID, "viewer") { if !api.requireBoardRole(w, r, board.ID, models.RoleViewer) {
return return
} }
WriteJSON(w, http.StatusOK, board) WriteJSON(w, http.StatusOK, board)
@@ -310,7 +310,7 @@ func (api *API) UpdateBoard(w http.ResponseWriter, r *http.Request, params map[s
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "board not found") WriteJSONWithErrorReason(w, r, http.StatusNotFound, "board not found")
return return
} }
if !api.requireBoardRole(w, r, board.ID, "editor") { if !api.requireBoardRole(w, r, board.ID, models.RoleWriter) {
return return
} }
err = DecodeJSON(r, &req) err = DecodeJSON(r, &req)
@@ -358,7 +358,7 @@ func (api *API) DeleteBoard(w http.ResponseWriter, r *http.Request, params map[s
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "board not found") WriteJSONWithErrorReason(w, r, http.StatusNotFound, "board not found")
return return
} }
if !api.requireBoardRole(w, r, board.ID, "admin") { if !api.requireBoardRole(w, r, board.ID, models.RoleAdmin) {
return return
} }
err = api.store(r).DeleteBoard(board.ID, user.ID) err = api.store(r).DeleteBoard(board.ID, user.ID)
@@ -390,7 +390,7 @@ func (api *API) ListBlocks(w http.ResponseWriter, r *http.Request, params map[st
var err error var err error
var blockType string var blockType string
if !api.requireBoardRole(w, r, params["boardId"], "viewer") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
return return
} }
blockType = r.URL.Query().Get("type") blockType = r.URL.Query().Get("type")
@@ -422,7 +422,7 @@ func (api *API) CreateBlock(w http.ResponseWriter, r *http.Request, params map[s
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "block creation requires a user account") WriteJSONWithErrorReason(w, r, http.StatusForbidden, "block creation requires a user account")
return return
} }
if !api.requireBoardRole(w, r, params["boardId"], "editor") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return return
} }
err = DecodeJSON(r, &req) err = DecodeJSON(r, &req)
@@ -459,7 +459,7 @@ func (api *API) GetBlock(w http.ResponseWriter, r *http.Request, params map[stri
var block models.Block var block models.Block
var err error var err error
if !api.requireBoardRole(w, r, params["boardId"], "viewer") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
return return
} }
block, err = api.store(r).GetBlock(params["boardId"], params["blockId"]) block, err = api.store(r).GetBlock(params["boardId"], params["blockId"])
@@ -483,7 +483,7 @@ func (api *API) UpdateBlock(w http.ResponseWriter, r *http.Request, params map[s
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "block updates require a user account") WriteJSONWithErrorReason(w, r, http.StatusForbidden, "block updates require a user account")
return return
} }
if !api.requireBoardRole(w, r, params["boardId"], "editor") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return return
} }
block, err = api.store(r).GetBlock(params["boardId"], params["blockId"]) block, err = api.store(r).GetBlock(params["boardId"], params["blockId"])
@@ -538,7 +538,7 @@ func (api *API) PatchBlocks(w http.ResponseWriter, r *http.Request, params map[s
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "block updates require a user account") WriteJSONWithErrorReason(w, r, http.StatusForbidden, "block updates require a user account")
return return
} }
if !api.requireBoardRole(w, r, params["boardId"], "editor") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return return
} }
err = DecodeJSON(r, &patches) err = DecodeJSON(r, &patches)
@@ -572,7 +572,7 @@ func (api *API) DeleteBlock(w http.ResponseWriter, r *http.Request, params map[s
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "block deletion requires a user account") WriteJSONWithErrorReason(w, r, http.StatusForbidden, "block deletion requires a user account")
return return
} }
if !api.requireBoardRole(w, r, params["boardId"], "editor") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return return
} }
err = api.store(r).DeleteBlock(r.Context(), params["boardId"], params["blockId"], user.ID) err = api.store(r).DeleteBlock(r.Context(), params["boardId"], params["blockId"], user.ID)
@@ -598,7 +598,7 @@ func (api *API) ListBoardMembers(w http.ResponseWriter, r *http.Request, params
var members []models.BoardMember var members []models.BoardMember
var err error var err error
if !api.requireBoardRole(w, r, params["boardId"], "admin") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleAdmin) {
return return
} }
members, err = api.store(r).ListBoardMembers(params["boardId"]) members, err = api.store(r).ListBoardMembers(params["boardId"])
@@ -616,7 +616,7 @@ func (api *API) ListBoardAssignableUsers(w http.ResponseWriter, r *http.Request,
var users []models.BoardAssignableUser var users []models.BoardAssignableUser
var err error var err error
if !api.requireBoardRole(w, r, params["boardId"], "viewer") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
return return
} }
users, err = api.store(r).ListBoardAssignableUsers(params["boardId"]) users, err = api.store(r).ListBoardAssignableUsers(params["boardId"])
@@ -636,7 +636,7 @@ func (api *API) AddBoardMember(w http.ResponseWriter, r *http.Request, params ma
var member models.BoardMember var member models.BoardMember
var role string var role string
if !api.requireBoardRole(w, r, params["boardId"], "admin") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleAdmin) {
return return
} }
err = DecodeJSON(r, &req) err = DecodeJSON(r, &req)
@@ -649,11 +649,11 @@ func (api *API) AddBoardMember(w http.ResponseWriter, r *http.Request, params ma
return return
} }
if req.Role == "" { if req.Role == "" {
req.Role = "editor" req.Role = models.RoleWriter
} }
role = normalizeBoardRole(req.Role) role = normalizeBoardRole(req.Role)
if role == "" { if role == "" {
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "role must be admin, editor, or viewer") WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "role must be admin, writer, or viewer")
return return
} }
member = models.BoardMember{ member = models.BoardMember{
@@ -679,7 +679,7 @@ func (api *API) UpdateBoardMember(w http.ResponseWriter, r *http.Request, params
var member models.BoardMember var member models.BoardMember
var role string var role string
if !api.requireBoardRole(w, r, params["boardId"], "admin") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleAdmin) {
return return
} }
err = DecodeJSON(r, &req) err = DecodeJSON(r, &req)
@@ -689,7 +689,7 @@ func (api *API) UpdateBoardMember(w http.ResponseWriter, r *http.Request, params
} }
role = normalizeBoardRole(req.Role) role = normalizeBoardRole(req.Role)
if role == "" { if role == "" {
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "role must be admin, editor, or viewer") WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "role must be admin, writer, or viewer")
return return
} }
member = models.BoardMember{ member = models.BoardMember{
@@ -712,7 +712,7 @@ func (api *API) UpdateBoardMember(w http.ResponseWriter, r *http.Request, params
func (api *API) RemoveBoardMember(w http.ResponseWriter, r *http.Request, params map[string]string) { func (api *API) RemoveBoardMember(w http.ResponseWriter, r *http.Request, params map[string]string) {
var err error var err error
if !api.requireBoardRole(w, r, params["boardId"], "admin") { if !api.requireBoardRole(w, r, params["boardId"], models.RoleAdmin) {
return return
} }
err = api.store(r).DeleteBoardMember(params["boardId"], params["userId"]) err = api.store(r).DeleteBoardMember(params["boardId"], params["userId"])
+52 -27
View File
@@ -58,7 +58,7 @@ func (api *API) ListPublicAuthProviders(w http.ResponseWriter, r *http.Request,
} }
result = make([]map[string]string, 0, len(providers)) result = make([]map[string]string, 0, len(providers))
for _, p = range providers { for _, p = range providers {
if p.Type == db.AuthProviderTypeDB { if p.Type == models.AuthProviderTypeDB {
continue continue
} }
result = append(result, map[string]string{ result = append(result, map[string]string{
@@ -76,7 +76,7 @@ func (api *API) OIDCLoginWithProvider(w http.ResponseWriter, r *http.Request, pa
var err error var err error
var authURL string var authURL string
provider, err = api.store(r).GetAuthProvider(params["id"]) provider, err = api.store(r).GetAuthProvider(params["id"])
if err != nil || provider.Type != db.AuthProviderTypeOIDC { if err != nil || provider.Type != models.AuthProviderTypeOIDC {
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "oidc provider not found") WriteJSONWithErrorReason(w, r, http.StatusNotFound, "oidc provider not found")
return return
} }
@@ -117,8 +117,12 @@ func (api *API) OIDCCallbackWithProvider(w http.ResponseWriter, r *http.Request,
var user models.User var user models.User
var required bool var required bool
var sessionVerified bool var sessionVerified bool
var txStore *db.Store
var tokenValue string
var expires time.Time
provider, err = api.store(r).GetAuthProvider(params["id"]) provider, err = api.store(r).GetAuthProvider(params["id"])
if err != nil || provider.Type != db.AuthProviderTypeOIDC { if err != nil || provider.Type != models.AuthProviderTypeOIDC {
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "oidc provider not found") WriteJSONWithErrorReason(w, r, http.StatusNotFound, "oidc provider not found")
return return
} }
@@ -142,42 +146,63 @@ func (api *API) OIDCCallbackWithProvider(w http.ResponseWriter, r *http.Request,
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "missing authorization code") WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "missing authorization code")
return return
} }
// this is an external http call - it must stay outside a transaction
token, err = api.oidcExchangeCodeFromProvider(r.Context(), provider, code) token, err = api.oidcExchangeCodeFromProvider(r.Context(), provider, code)
if err != nil { if err != nil {
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "oidc token exchange failed provider=%s err=%v", provider.Name, err) api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "oidc token exchange failed provider=%s err=%v", provider.Name, err)
WriteJSONWithErrorReason(w, r, http.StatusUnauthorized, err.Error()) WriteJSONWithErrorReason(w, r, http.StatusUnauthorized, err.Error())
return return
} }
// this is another external http call - it must stay outside a transaction
claims, err = api.oidcResolveClaimsFromProvider(r.Context(), provider, token) claims, err = api.oidcResolveClaimsFromProvider(r.Context(), provider, token)
if err != nil { if err != nil {
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "oidc claims fetch failed provider=%s err=%v", provider.Name, err) api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "oidc claims fetch failed provider=%s err=%v", provider.Name, err)
WriteJSONWithErrorReason(w, r, http.StatusUnauthorized, "oidc claims fetch failed") WriteJSONWithErrorReason(w, r, http.StatusUnauthorized, "oidc claims fetch failed")
return return
} }
user, err = api.oidcGetOrCreateUser(r.Context(), claims, provider)
if err != nil { txStore, err = api.Store.BeginImmediateStore(r.Context())
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "oidc user mapping failed provider=%s err=%v", provider.Name, err)
WriteJSONWithErrorReason(w, r, http.StatusUnauthorized, fmt.Sprintf("oidc user mapping failed - %s", err.Error()))
return
}
required, err = api.store(r).UserRequiresTOTP(user.ID)
if err != nil { if err != nil {
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error()) WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return return
} }
user, err = api.store(r).GetUserByID(user.ID) user, err = api.oidcGetOrCreateUser(r.Context(), txStore, claims, provider)
if err != nil { if err != nil {
_ = txStore.Rollback()
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "oidc user mapping failed provider=%s err=%v", provider.Name, err)
WriteJSONWithErrorReason(w, r, http.StatusUnauthorized, fmt.Sprintf("oidc user mapping failed - %s", err.Error()))
return
}
required, err = txStore.UserRequiresTOTP(user.ID)
if err != nil {
_ = txStore.Rollback()
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return
}
user, err = txStore.GetUserByID(user.ID)
if err != nil {
_ = txStore.Rollback()
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error()) WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return return
} }
user.TOTPEffectiveRequired = required user.TOTPEffectiveRequired = required
user.TOTPSetupRequired = required && !user.TOTPEnabled user.TOTPSetupRequired = required && !user.TOTPEnabled
sessionVerified = !required sessionVerified = !required
err = api.issueSession(w, r, user, sessionVerified, token.IDToken) tokenValue, expires, err = api.createSessionWithStore(txStore, user, sessionVerified, token.IDToken)
if err != nil {
_ = txStore.Rollback()
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "session create failed user=%s err=%v", user.Username, err)
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return
}
err = txStore.Commit()
if err != nil { if err != nil {
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error()) WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return return
} }
api.setSessionCookie(w, tokenValue, expires)
api.Logger.Write(logIDAuth, codit_logger.LOG_INFO, "oidc login success username=%s provider=%s", user.Username, provider.Name) api.Logger.Write(logIDAuth, codit_logger.LOG_INFO, "oidc login success username=%s provider=%s", user.Username, provider.Name)
http.Redirect(w, r, "/", http.StatusFound) http.Redirect(w, r, "/", http.StatusFound)
} }
@@ -373,7 +398,7 @@ func (api *API) oidcUserInfoFromProvider(ctx context.Context, p models.AuthProvi
return claims, nil return claims, nil
} }
func (api *API) oidcGetOrCreateUser(ctx context.Context, claims oidcUserClaims, provider models.AuthProvider) (models.User, error) { func (api *API) oidcGetOrCreateUser(ctx context.Context, store *db.Store, claims oidcUserClaims, provider models.AuthProvider) (models.User, error) {
var username string var username string
var displayName string var displayName string
var email string var email string
@@ -386,12 +411,12 @@ func (api *API) oidcGetOrCreateUser(ctx context.Context, claims oidcUserClaims,
return user, err return user, err
} }
user, err = api.storeContext(ctx).GetUserByProviderAndSub(provider.ID, claims.Sub) user, err = store.GetUserByProviderAndSub(provider.ID, claims.Sub)
if err == nil { if err == nil {
if user.Disabled { if user.Disabled {
return user, errors.New("user disabled") return user, errors.New("user disabled")
} }
err = api.oidcSyncUserGroups(ctx, provider, user, claims, false) err = api.oidcSyncUserGroups(ctx, store, provider, user, claims, false)
if err != nil { return user, err } if err != nil { return user, err }
return user, nil return user, nil
} }
@@ -411,15 +436,15 @@ func (api *API) oidcGetOrCreateUser(ctx context.Context, claims oidcUserClaims,
AuthProviderID: provider.ID, AuthProviderID: provider.ID,
ExternalSubject: strings.TrimSpace(claims.Sub), ExternalSubject: strings.TrimSpace(claims.Sub),
} }
created, err = api.storeContext(ctx).CreateUser(user, "") created, err = store.CreateUser(user, "")
if err != nil { return created, err } if err != nil { return created, err }
err = api.oidcSyncUserGroups(ctx, provider, created, claims, true) err = api.oidcSyncUserGroups(ctx, store, provider, created, claims, true)
if err != nil { return created, err } if err != nil { return created, err }
return created, nil return created, nil
} }
func (api *API) oidcSyncUserGroups(ctx context.Context, provider models.AuthProvider, user models.User, claims oidcUserClaims, isNew bool) error { func (api *API) oidcSyncUserGroups(ctx context.Context, store *db.Store, provider models.AuthProvider, user models.User, claims oidcUserClaims, isNew bool) error {
var mode string var mode string
var groupsClaim string var groupsClaim string
var claimValues []string var claimValues []string
@@ -433,17 +458,17 @@ func (api *API) oidcSyncUserGroups(ctx context.Context, provider models.AuthProv
var matched bool var matched bool
mode = strings.TrimSpace(provider.GroupSyncMode) mode = strings.TrimSpace(provider.GroupSyncMode)
if mode == db.GroupSyncModeOff || mode == "" { if mode == models.GroupSyncModeOff || mode == "" {
if isNew && strings.TrimSpace(provider.DefaultUserGroupID) != "" { if isNew && strings.TrimSpace(provider.DefaultUserGroupID) != "" {
err = api.storeContext(ctx).AddUserGroupMember(ctx, provider.DefaultUserGroupID, user.ID) err = store.AddUserGroupMember(ctx, provider.DefaultUserGroupID, user.ID)
if err != nil { return fmt.Errorf("oidc default user group assignment failed: %w", err) } if err != nil { return fmt.Errorf("oidc default user group assignment failed: %w", err) }
} }
return nil return nil
} }
if mode == db.GroupSyncModeFirstLogin && !isNew { if mode == models.GroupSyncModeFirstLogin && !isNew {
return nil return nil
} }
if mode != db.GroupSyncModeFirstLogin && mode != db.GroupSyncModeSync { if mode != models.GroupSyncModeFirstLogin && mode != models.GroupSyncModeSync {
return fmt.Errorf("invalid oidc group sync mode: %s", mode) return fmt.Errorf("invalid oidc group sync mode: %s", mode)
} }
@@ -457,7 +482,7 @@ func (api *API) oidcSyncUserGroups(ctx context.Context, provider models.AuthProv
claimSet[claimValues[i]] = true claimSet[claimValues[i]] = true
} }
mappings, err = api.storeContext(ctx).ListAuthProviderGroupMappings(provider.ID) mappings, err = store.ListAuthProviderGroupMappings(provider.ID)
if err != nil { if err != nil {
return fmt.Errorf("oidc group mapping lookup failed: %w", err) return fmt.Errorf("oidc group mapping lookup failed: %w", err)
} }
@@ -466,17 +491,17 @@ func (api *API) oidcSyncUserGroups(ctx context.Context, provider models.AuthProv
assignedAny = false assignedAny = false
for groupID, matched = range groupMatched { for groupID, matched = range groupMatched {
if matched { if matched {
err = api.storeContext(ctx).AddUserGroupMember(ctx, groupID, user.ID) err = store.AddUserGroupMember(ctx, groupID, user.ID)
if err != nil { return fmt.Errorf("oidc sso group assignment failed: %w", err) } if err != nil { return fmt.Errorf("oidc sso group assignment failed: %w", err) }
assignedAny = true assignedAny = true
} else if mode == db.GroupSyncModeSync { } else if mode == models.GroupSyncModeSync {
err = api.storeContext(ctx).RemoveUserGroupMember(ctx, groupID, user.ID) err = store.RemoveUserGroupMember(ctx, groupID, user.ID)
if err != nil { return fmt.Errorf("oidc sso group removal failed: %w", err) } if err != nil { return fmt.Errorf("oidc sso group removal failed: %w", err) }
} }
} }
if isNew && !assignedAny && strings.TrimSpace(provider.DefaultUserGroupID) != "" { if isNew && !assignedAny && strings.TrimSpace(provider.DefaultUserGroupID) != "" {
err = api.storeContext(ctx).AddUserGroupMember(ctx, provider.DefaultUserGroupID, user.ID) err = store.AddUserGroupMember(ctx, provider.DefaultUserGroupID, user.ID)
if err != nil { return fmt.Errorf("oidc default user group assignment failed: %w", err) } if err != nil { return fmt.Errorf("oidc default user group assignment failed: %w", err) }
} }
@@ -159,13 +159,18 @@ func (api *API) startSSHFileTransferAudit(store *db.Store, r *http.Request, oper
} }
func (api *API) finishSSHFileTransferAudit(audit *sshFileTransferAudit, status string) { func (api *API) finishSSHFileTransferAudit(audit *sshFileTransferAudit, status string) {
var ctx context.Context
var cancel context.CancelFunc
var err error var err error
if audit == nil || audit.Finished { return } if audit == nil || audit.Finished { return }
audit.Finished = true audit.Finished = true
// i don't bind this to a request context because i want the audit record write to persist regardless of client connection. // Use an independent bounded context so client cancellation does not skip the final audit update.
err = audit.Store.FinishSSHFileTransfer(context.Background(), audit.Item.ID, status, audit.Paths, audit.Bytes, audit.ErrorText) ctx, cancel = context.WithTimeout(context.Background(), 10 * time.Second)
defer cancel()
err = audit.Store.FinishSSHFileTransfer(ctx, audit.Item.ID, status, audit.Paths, audit.Bytes, audit.ErrorText)
if err != nil { if err != nil {
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN, "ssh file transfer audit finish failed id=%s op=%s err=%s", audit.Item.ID, audit.Item.Operation, err.Error()) api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN, "ssh file transfer audit finish failed id=%s op=%s err=%s", audit.Item.ID, audit.Item.Operation, err.Error())
return return
+8 -15
View File
@@ -1600,9 +1600,8 @@ func (api *API) DiscoverSSHServerHostKeyAdmin(w http.ResponseWriter, r *http.Req
var discovered models.SSHServerHostKey var discovered models.SSHServerHostKey
var err error var err error
if !api.requireAdmin(w, r) { if !api.requireAdmin(w, r) { return }
return
}
item, err = api.store(r).GetSSHServer(params["id"]) item, err = api.store(r).GetSSHServer(params["id"])
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
@@ -2540,20 +2539,14 @@ func discoverSSHServerHostKey(host string, port int, defaultUser string) (models
Timeout: 10 * time.Second, Timeout: 10 * time.Second,
} }
conn, err = net.DialTimeout("tcp", addr, 10*time.Second) conn, err = net.DialTimeout("tcp", addr, 10*time.Second)
if err != nil { if err != nil { return item, err }
return item, err
}
defer conn.Close() defer conn.Close()
clientConn, _, reqs, err = ssh.NewClientConn(conn, addr, config) clientConn, _, reqs, err = ssh.NewClientConn(conn, addr, config)
if clientConn != nil { if clientConn != nil { clientConn.Close() }
clientConn.Close() if reqs != nil { go ssh.DiscardRequests(reqs) }
} if discovered == nil { return item, errors.New("failed to discover ssh host key") }
if reqs != nil {
go ssh.DiscardRequests(reqs)
}
if discovered == nil {
return item, errors.New("failed to discover ssh host key")
}
item.Algorithm = discovered.Type() item.Algorithm = discovered.Type()
item.PublicKey = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(discovered))) item.PublicKey = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(discovered)))
item.Fingerprint = strings.TrimSpace(ssh.FingerprintSHA256(discovered)) item.Fingerprint = strings.TrimSpace(ssh.FingerprintSHA256(discovered))
+1
View File
@@ -556,6 +556,7 @@ func (api *API) InspectSSHCertificate(w http.ResponseWriter, r *http.Request, _
var cert *ssh.Certificate var cert *ssh.Certificate
var dump string var dump string
var err error var err error
user, ok = middleware.UserFromContext(r.Context()) user, ok = middleware.UserFromContext(r.Context())
if !ok || user.Disabled { if !ok || user.Disabled {
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
+40 -4
View File
@@ -1,5 +1,6 @@
package handlers package handlers
import codit_logger "codit/logger"
import "database/sql" import "database/sql"
import "net/http" import "net/http"
import "net/url" import "net/url"
@@ -37,19 +38,42 @@ func (api *API) finishLogin(w http.ResponseWriter, r *http.Request, user models.
var err error var err error
var required bool var required bool
var sessionVerified bool var sessionVerified bool
required, err = api.store(r).UserRequiresTOTP(user.ID) var txStore *db.Store
var token string
var expires time.Time
txStore, err = api.Store.BeginImmediateStore(r.Context())
if err != nil { if err != nil {
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error()) WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return return
} }
required, err = txStore.UserRequiresTOTP(user.ID)
if err != nil {
_ = txStore.Rollback()
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return
}
user, err = txStore.GetUserByID(user.ID)
if err != nil {
_ = txStore.Rollback()
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return
}
if user.Disabled {
_ = txStore.Rollback()
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "user disabled")
return
}
user.TOTPEffectiveRequired = required user.TOTPEffectiveRequired = required
user.TOTPSetupRequired = required && !user.TOTPEnabled user.TOTPSetupRequired = required && !user.TOTPEnabled
ok, err = api.verifyUserTOTPForLogin(r, user, otpCode) ok, err = api.verifyUserTOTPForLoginWithStore(txStore, user, otpCode)
if err != nil { if err != nil {
_ = txStore.Rollback()
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error()) WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return return
} }
if !ok { if !ok {
_ = txStore.Rollback()
if strings.TrimSpace(otpCode) == "" { if strings.TrimSpace(otpCode) == "" {
WriteJSON(w, http.StatusUnauthorized, map[string]any{"error": "totp_required", "totp_required": true}) WriteJSON(w, http.StatusUnauthorized, map[string]any{"error": "totp_required", "totp_required": true})
return return
@@ -61,19 +85,31 @@ func (api *API) finishLogin(w http.ResponseWriter, r *http.Request, user models.
if required && !user.TOTPEnabled { if required && !user.TOTPEnabled {
sessionVerified = false sessionVerified = false
} }
err = api.issueSession(w, r, user, sessionVerified, "") token, expires, err = api.createSessionWithStore(txStore, user, sessionVerified, "")
if err != nil {
_ = txStore.Rollback()
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "session create failed user=%s err=%v", user.Username, err)
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return
}
err = txStore.Commit()
if err != nil { if err != nil {
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error()) WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return return
} }
api.setSessionCookie(w, token, expires)
WriteJSON(w, http.StatusOK, user) WriteJSON(w, http.StatusOK, user)
} }
func (api *API) verifyUserTOTPForLogin(r *http.Request, user models.User, otpCode string) (bool, error) { func (api *API) verifyUserTOTPForLogin(r *http.Request, user models.User, otpCode string) (bool, error) {
return api.verifyUserTOTPForLoginWithStore(api.store(r), user, otpCode)
}
func (api *API) verifyUserTOTPForLoginWithStore(store *db.Store, user models.User, otpCode string) (bool, error) {
var item db.UserTOTP var item db.UserTOTP
var secret string var secret string
var err error var err error
item, err = api.store(r).GetUserTOTP(user.ID) item, err = store.GetUserTOTP(user.ID)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return true, nil return true, nil
+23
View File
@@ -0,0 +1,23 @@
package models
const RepoTypeGit string = "git"
const RepoTypeRPM string = "rpm"
const RepoTypeDocker string = "docker"
const RoleAdmin string = "admin"
const RoleWriter string = "writer"
const RoleViewer string = "viewer"
const AuthProviderTypeDB string = "db"
const AuthProviderTypeLDAP string = "ldap"
const AuthProviderTypeOIDC string = "oidc"
const GroupSyncModeOff string = "off"
const GroupSyncModeFirstLogin string = "first_login"
const GroupSyncModeSync string = "sync"
const UserGroupScopeExplicit string = "explicit"
const UserGroupScopeAllUsers string = "all_users"
const RPMRepoModeLocal string = "local"
const RPMRepoModeMirror string = "mirror"
+130 -1
View File
@@ -28,6 +28,8 @@ CREATE TABLE blocks (
type TEXT NOT NULL, type TEXT NOT NULL,
title TEXT NOT NULL DEFAULT '', title TEXT NOT NULL DEFAULT '',
fields TEXT NOT NULL DEFAULT '{}', fields TEXT NOT NULL DEFAULT '{}',
completed_at INTEGER NOT NULL DEFAULT 0,
completed_by INTEGER,
created_at INTEGER NOT NULL, created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL, updated_at INTEGER NOT NULL,
delete_at INTEGER NOT NULL DEFAULT 0, delete_at INTEGER NOT NULL DEFAULT 0,
@@ -47,6 +49,8 @@ CREATE TABLE blocks_history (
type TEXT NOT NULL, type TEXT NOT NULL,
title TEXT NOT NULL DEFAULT '', title TEXT NOT NULL DEFAULT '',
fields TEXT NOT NULL DEFAULT '{}', fields TEXT NOT NULL DEFAULT '{}',
completed_at INTEGER NOT NULL DEFAULT 0,
completed_by INTEGER,
created_at INTEGER NOT NULL, created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL, updated_at INTEGER NOT NULL,
delete_at INTEGER NOT NULL DEFAULT 0, delete_at INTEGER NOT NULL DEFAULT 0,
@@ -56,16 +60,141 @@ CREATE TABLE blocks_history (
CREATE TABLE board_members ( CREATE TABLE board_members (
board_id INTEGER NOT NULL, board_id INTEGER NOT NULL,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
role TEXT NOT NULL DEFAULT 'editor', role TEXT NOT NULL DEFAULT 'writer',
created_at INTEGER NOT NULL, created_at INTEGER NOT NULL,
PRIMARY KEY (board_id, user_id), PRIMARY KEY (board_id, user_id),
FOREIGN KEY (board_id) REFERENCES boards(id) ON DELETE CASCADE, FOREIGN KEY (board_id) REFERENCES boards(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
); );
CREATE TABLE block_comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
block_id INTEGER NOT NULL,
content TEXT NOT NULL,
created_by INTEGER NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
delete_at INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (block_id) REFERENCES blocks(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id)
);
CREATE TABLE block_properties (
id INTEGER PRIMARY KEY AUTOINCREMENT,
block_id INTEGER NOT NULL UNIQUE,
status TEXT NOT NULL DEFAULT '',
card_type TEXT NOT NULL DEFAULT '',
priority TEXT NOT NULL DEFAULT '',
due_date TEXT NOT NULL DEFAULT '',
assignee_id INTEGER,
sprint TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL DEFAULT '',
FOREIGN KEY (block_id) REFERENCES blocks(id) ON DELETE CASCADE,
FOREIGN KEY (assignee_id) REFERENCES users(id) ON DELETE SET NULL
);
CREATE TABLE board_field_values (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
board_id INTEGER NOT NULL REFERENCES boards(id) ON DELETE CASCADE,
field TEXT NOT NULL,
value TEXT NOT NULL,
label TEXT NOT NULL,
color TEXT NOT NULL DEFAULT '',
display_order INTEGER NOT NULL DEFAULT 0,
start_date TEXT NOT NULL DEFAULT '',
end_date TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
UNIQUE(board_id, field, value)
);
CREATE TABLE block_checklist_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
block_id INTEGER NOT NULL,
title TEXT NOT NULL,
done INTEGER NOT NULL DEFAULT 0,
display_order INTEGER NOT NULL DEFAULT 0,
created_by INTEGER NOT NULL,
updated_by INTEGER NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
delete_at INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (block_id) REFERENCES blocks(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id),
FOREIGN KEY (updated_by) REFERENCES users(id)
);
CREATE TABLE block_attachments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
block_id INTEGER NOT NULL,
filename TEXT NOT NULL,
content_type TEXT NOT NULL DEFAULT 'application/octet-stream',
size INTEGER NOT NULL DEFAULT 0,
storage_path TEXT NOT NULL,
created_by INTEGER NOT NULL,
created_at INTEGER NOT NULL,
delete_at INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (block_id) REFERENCES blocks(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id)
);
CREATE TABLE block_assignees (
block_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
assigned_at INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (block_id, user_id),
FOREIGN KEY (block_id) REFERENCES blocks(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS auth_provider_group_mappings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
auth_provider_id INTEGER NOT NULL,
group_id INTEGER NOT NULL,
claim_value TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (auth_provider_id) REFERENCES auth_providers(id) ON DELETE CASCADE,
FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE,
UNIQUE (auth_provider_id, claim_value, group_id)
);
ALTER TABLE repos ADD COLUMN description TEXT NOT NULL DEFAULT '';
ALTER TABLE users ADD COLUMN avatar_storage_path TEXT NOT NULL DEFAULT '';
ALTER TABLE users ADD COLUMN avatar_content_type TEXT NOT NULL DEFAULT '';
ALTER TABLE users ADD COLUMN avatar_updated_at INTEGER NOT NULL DEFAULT 0;
ALTER TABLE auth_providers ADD COLUMN oidc_end_session_url TEXT NOT NULL DEFAULT '';
ALTER TABLE auth_providers ADD COLUMN group_sync_mode TEXT NOT NULL DEFAULT 'off';
ALTER TABLE auth_providers ADD COLUMN oidc_post_logout_redirect INTEGER NOT NULL DEFAULT 0;
ALTER TABLE sessions ADD COLUMN oidc_id_token TEXT NOT NULL DEFAULT '';
INSERT OR IGNORE INTO block_properties (block_id, status, description)
SELECT bl.id,
COALESCE(json_extract(bl.fields, '$.status'), ''),
COALESCE(json_extract(bl.fields, '$.description'), '')
FROM blocks bl
WHERE bl.type = 'card' AND bl.delete_at = 0;
INSERT OR IGNORE INTO block_assignees (block_id, user_id, assigned_at)
SELECT block_id, assignee_id, strftime('%s', 'now')
FROM block_properties
WHERE assignee_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_boards_project ON boards(project_id, delete_at); CREATE INDEX IF NOT EXISTS idx_boards_project ON boards(project_id, delete_at);
CREATE INDEX IF NOT EXISTS idx_blocks_board ON blocks(board_id, delete_at); CREATE INDEX IF NOT EXISTS idx_blocks_board ON blocks(board_id, delete_at);
CREATE INDEX IF NOT EXISTS idx_blocks_parent ON blocks(parent_id); CREATE INDEX IF NOT EXISTS idx_blocks_parent ON blocks(parent_id);
CREATE INDEX IF NOT EXISTS idx_blocks_type ON blocks(board_id, type, delete_at); CREATE INDEX IF NOT EXISTS idx_blocks_type ON blocks(board_id, type, delete_at);
CREATE INDEX IF NOT EXISTS idx_blocks_history_block ON blocks_history(block_id, insert_at DESC); CREATE INDEX IF NOT EXISTS idx_blocks_history_block ON blocks_history(block_id, insert_at DESC);
CREATE INDEX IF NOT EXISTS idx_board_members_user ON board_members(user_id); CREATE INDEX IF NOT EXISTS idx_board_members_user ON board_members(user_id);
CREATE INDEX IF NOT EXISTS idx_block_comments_block ON block_comments(block_id, delete_at);
CREATE INDEX IF NOT EXISTS idx_block_properties_block ON block_properties(block_id);
CREATE INDEX IF NOT EXISTS idx_board_field_values_board_field ON board_field_values(board_id, field);
CREATE INDEX IF NOT EXISTS idx_block_checklist_items_block ON block_checklist_items(block_id, delete_at, display_order);
CREATE INDEX IF NOT EXISTS idx_block_attachments_block ON block_attachments(block_id, delete_at, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_block_assignees_user ON block_assignees(user_id, block_id);
CREATE INDEX IF NOT EXISTS idx_auth_provider_group_mappings_provider ON auth_provider_group_mappings(auth_provider_id);
CREATE INDEX IF NOT EXISTS idx_auth_provider_group_mappings_claim ON auth_provider_group_mappings(auth_provider_id, claim_value);
-36
View File
@@ -1,36 +0,0 @@
CREATE TABLE block_comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
block_id INTEGER NOT NULL,
content TEXT NOT NULL,
created_by INTEGER NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
delete_at INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (block_id) REFERENCES blocks(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id)
);
CREATE TABLE block_properties (
id INTEGER PRIMARY KEY AUTOINCREMENT,
block_id INTEGER NOT NULL UNIQUE,
status TEXT NOT NULL DEFAULT '',
card_type TEXT NOT NULL DEFAULT '',
priority TEXT NOT NULL DEFAULT '',
due_date TEXT NOT NULL DEFAULT '',
assignee_id INTEGER,
sprint TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL DEFAULT '',
FOREIGN KEY (block_id) REFERENCES blocks(id) ON DELETE CASCADE,
FOREIGN KEY (assignee_id) REFERENCES users(id) ON DELETE SET NULL
);
INSERT OR IGNORE INTO block_properties (block_id, status, description)
SELECT bl.id,
COALESCE(json_extract(bl.fields, '$.status'), ''),
COALESCE(json_extract(bl.fields, '$.description'), '')
FROM blocks bl
WHERE bl.type = 'card' AND bl.delete_at = 0;
CREATE INDEX IF NOT EXISTS idx_block_comments_block ON block_comments(block_id, delete_at);
CREATE INDEX IF NOT EXISTS idx_block_properties_block ON block_properties(block_id);
-15
View File
@@ -1,15 +0,0 @@
CREATE TABLE board_sprints (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
board_id INTEGER NOT NULL,
name TEXT NOT NULL,
start_date TEXT NOT NULL DEFAULT '',
end_date TEXT NOT NULL DEFAULT '',
display_order INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
delete_at INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (board_id) REFERENCES boards(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_board_sprints_board ON board_sprints(board_id, delete_at);
@@ -1,66 +0,0 @@
-- Migration 005: unified board_field_values (replaces board_sprints; adds custom status/type/priority)
CREATE TABLE board_field_values (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
board_id INTEGER NOT NULL REFERENCES boards(id) ON DELETE CASCADE,
field TEXT NOT NULL,
value TEXT NOT NULL,
label TEXT NOT NULL,
color TEXT NOT NULL DEFAULT '',
display_order INTEGER NOT NULL DEFAULT 0,
start_date TEXT NOT NULL DEFAULT '',
end_date TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
UNIQUE(board_id, field, value)
);
CREATE INDEX idx_board_field_values_board_field ON board_field_values(board_id, field);
-- Seed default status values for every existing board
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
SELECT lower(hex(randomblob(8))), b.id, 'status', 'todo', 'To Do', '#1976d2', 0, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
SELECT lower(hex(randomblob(8))), b.id, 'status', 'in_progress','In Progress', '#f59e0b', 1, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
SELECT lower(hex(randomblob(8))), b.id, 'status', 'done', 'Done', '#16a34a', 2, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
-- Seed default type values
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
SELECT lower(hex(randomblob(8))), b.id, 'type', 'task', 'Task', '#1976d2', 0, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
SELECT lower(hex(randomblob(8))), b.id, 'type', 'bug', 'Bug', '#d32f2f', 1, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
SELECT lower(hex(randomblob(8))), b.id, 'type', 'story', 'Story', '#388e3c', 2, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
SELECT lower(hex(randomblob(8))), b.id, 'type', 'epic', 'Epic', '#7b1fa2', 3, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
-- Seed default priority values
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
SELECT lower(hex(randomblob(8))), b.id, 'priority', 'low', 'Low', '#4caf50', 0, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
SELECT lower(hex(randomblob(8))), b.id, 'priority', 'medium', 'Medium', '#2196f3', 1, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
SELECT lower(hex(randomblob(8))), b.id, 'priority', 'high', 'High', '#ff9800', 2, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
SELECT lower(hex(randomblob(8))), b.id, 'priority', 'urgent', 'Urgent', '#f44336', 3, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
-- Migrate existing board_sprints rows (value = public_id for rename safety)
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, start_date, end_date, created_at, updated_at)
SELECT
s.public_id,
s.board_id,
'sprint',
s.public_id,
s.name,
'#7c3aed',
s.display_order,
COALESCE(s.start_date, ''),
COALESCE(s.end_date, ''),
s.created_at,
s.updated_at
FROM board_sprints s
WHERE s.delete_at = 0;
DROP TABLE board_sprints;
@@ -1 +0,0 @@
ALTER TABLE repos ADD COLUMN description TEXT NOT NULL DEFAULT '';
@@ -1,4 +0,0 @@
ALTER TABLE blocks ADD COLUMN completed_at INTEGER NOT NULL DEFAULT 0;
ALTER TABLE blocks ADD COLUMN completed_by INTEGER;
ALTER TABLE blocks_history ADD COLUMN completed_at INTEGER NOT NULL DEFAULT 0;
ALTER TABLE blocks_history ADD COLUMN completed_by INTEGER;
@@ -1,18 +0,0 @@
CREATE TABLE block_checklist_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
block_id INTEGER NOT NULL,
title TEXT NOT NULL,
done INTEGER NOT NULL DEFAULT 0,
display_order INTEGER NOT NULL DEFAULT 0,
created_by INTEGER NOT NULL,
updated_by INTEGER NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
delete_at INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (block_id) REFERENCES blocks(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id),
FOREIGN KEY (updated_by) REFERENCES users(id)
);
CREATE INDEX idx_block_checklist_items_block ON block_checklist_items(block_id, delete_at, display_order);
@@ -1,16 +0,0 @@
CREATE TABLE block_attachments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
block_id INTEGER NOT NULL,
filename TEXT NOT NULL,
content_type TEXT NOT NULL DEFAULT 'application/octet-stream',
size INTEGER NOT NULL DEFAULT 0,
storage_path TEXT NOT NULL,
created_by INTEGER NOT NULL,
created_at INTEGER NOT NULL,
delete_at INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (block_id) REFERENCES blocks(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id)
);
CREATE INDEX idx_block_attachments_block ON block_attachments(block_id, delete_at, created_at DESC);
@@ -1,15 +0,0 @@
CREATE TABLE block_assignees (
block_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
assigned_at INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (block_id, user_id),
FOREIGN KEY (block_id) REFERENCES blocks(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
INSERT OR IGNORE INTO block_assignees (block_id, user_id, assigned_at)
SELECT block_id, assignee_id, strftime('%s', 'now')
FROM block_properties
WHERE assignee_id IS NOT NULL;
CREATE INDEX idx_block_assignees_user ON block_assignees(user_id, block_id);
-3
View File
@@ -1,3 +0,0 @@
ALTER TABLE users ADD COLUMN avatar_storage_path TEXT NOT NULL DEFAULT '';
ALTER TABLE users ADD COLUMN avatar_content_type TEXT NOT NULL DEFAULT '';
ALTER TABLE users ADD COLUMN avatar_updated_at INTEGER NOT NULL DEFAULT 0;
@@ -1 +0,0 @@
ALTER TABLE auth_providers ADD COLUMN oidc_end_session_url TEXT NOT NULL DEFAULT '';
@@ -1,16 +0,0 @@
ALTER TABLE auth_providers ADD COLUMN group_sync_mode TEXT NOT NULL DEFAULT 'off';
CREATE TABLE IF NOT EXISTS auth_provider_group_mappings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
auth_provider_id INTEGER NOT NULL,
group_id INTEGER NOT NULL,
claim_value TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (auth_provider_id) REFERENCES auth_providers(id) ON DELETE CASCADE,
FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE,
UNIQUE (auth_provider_id, claim_value, group_id)
);
CREATE INDEX IF NOT EXISTS idx_auth_provider_group_mappings_provider ON auth_provider_group_mappings(auth_provider_id);
CREATE INDEX IF NOT EXISTS idx_auth_provider_group_mappings_claim ON auth_provider_group_mappings(auth_provider_id, claim_value);
@@ -1 +0,0 @@
ALTER TABLE auth_providers ADD COLUMN oidc_post_logout_redirect INTEGER NOT NULL DEFAULT 0;
@@ -1 +0,0 @@
ALTER TABLE sessions ADD COLUMN oidc_id_token TEXT NOT NULL DEFAULT '';
+42 -42
View File
@@ -745,11 +745,11 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("GET", "/api/health", api.Health) router.Handle("GET", "/api/health", api.Health)
router.Handle("GET", "/api/app-info", txRead(api.AppInfo)) router.Handle("GET", "/api/app-info", txRead(api.AppInfo))
router.Handle("POST", "/api/login", txWrite(api.Login)) router.Handle("POST", "/api/login", api.Login)
router.Handle("POST", "/api/logout", txWrite(api.Logout)) router.Handle("POST", "/api/logout", txWrite(api.Logout))
router.Handle("GET", "/api/auth/providers", txRead(api.ListPublicAuthProviders)) router.Handle("GET", "/api/auth/providers", txRead(api.ListPublicAuthProviders))
router.Handle("GET", "/api/auth/oidc/:id/login", txRead(api.OIDCLoginWithProvider)) router.Handle("GET", "/api/auth/oidc/:id/login", txRead(api.OIDCLoginWithProvider))
router.Handle("GET", "/api/auth/oidc/:id/callback", txRead(api.OIDCCallbackWithProvider)) router.Handle("GET", "/api/auth/oidc/:id/callback", api.OIDCCallbackWithProvider)
router.Handle("GET", "/api/me", txRead(api.Me)) router.Handle("GET", "/api/me", txRead(api.Me))
router.Handle("PATCH", "/api/me", txWrite(api.UpdateMe)) router.Handle("PATCH", "/api/me", txWrite(api.UpdateMe))
router.Handle("POST", "/api/me/avatar", txWrite(api.UploadMyAvatar)) router.Handle("POST", "/api/me/avatar", txWrite(api.UploadMyAvatar))
@@ -761,7 +761,7 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("POST", "/api/me/totp/disable", txWrite(api.DisableMyTOTP)) router.Handle("POST", "/api/me/totp/disable", txWrite(api.DisableMyTOTP))
router.Handle("GET", "/api/users", txRead(api.ListUsers)) router.Handle("GET", "/api/users", txRead(api.ListUsers))
router.Handle("GET", "/api/users/:id/avatar", txRead(api.GetUserAvatar)) router.Handle("GET", "/api/users/:id/avatar", api.GetUserAvatar)
router.Handle("POST", "/api/users", txWrite(api.CreateUser)) router.Handle("POST", "/api/users", txWrite(api.CreateUser))
router.Handle("PATCH", "/api/users/:id", txWrite(api.UpdateUser)) router.Handle("PATCH", "/api/users/:id", txWrite(api.UpdateUser))
router.Handle("DELETE", "/api/users/:id", txWrite(api.DeleteUser)) router.Handle("DELETE", "/api/users/:id", txWrite(api.DeleteUser))
@@ -818,7 +818,7 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("GET", "/api/admin/pki/cas", txRead(api.ListPKICAs)) router.Handle("GET", "/api/admin/pki/cas", txRead(api.ListPKICAs))
router.Handle("GET", "/api/admin/pki/cas/:id", txRead(api.GetPKICA)) router.Handle("GET", "/api/admin/pki/cas/:id", txRead(api.GetPKICA))
router.Handle("PATCH", "/api/admin/pki/cas/:id", txWrite(api.UpdatePKICA)) router.Handle("PATCH", "/api/admin/pki/cas/:id", txWrite(api.UpdatePKICA))
router.Handle("GET", "/api/admin/pki/cas/:id/bundle", txRead(api.DownloadPKICABundle)) router.Handle("GET", "/api/admin/pki/cas/:id/bundle", api.DownloadPKICABundle)
router.Handle("POST", "/api/admin/pki/cas/root", txWrite(api.CreatePKIRootCA)) router.Handle("POST", "/api/admin/pki/cas/root", txWrite(api.CreatePKIRootCA))
router.Handle("POST", "/api/admin/pki/cas/intermediate", txWrite(api.CreatePKIIntermediateCA)) router.Handle("POST", "/api/admin/pki/cas/intermediate", txWrite(api.CreatePKIIntermediateCA))
router.Handle("GET", "/api/admin/pki/cas/:id/crl", txRead(api.GetPKICRL)) router.Handle("GET", "/api/admin/pki/cas/:id/crl", txRead(api.GetPKICRL))
@@ -827,7 +827,7 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("GET", "/api/admin/pki/certs/:id", txRead(api.GetPKICert)) router.Handle("GET", "/api/admin/pki/certs/:id", txRead(api.GetPKICert))
router.Handle("GET", "/api/admin/pki/certs/:id/inspect", txRead(api.GetPKICertInspect)) router.Handle("GET", "/api/admin/pki/certs/:id/inspect", txRead(api.GetPKICertInspect))
router.Handle("POST", "/api/admin/pki/certs/:id/renew-acme", txWrite(api.RenewPKICertWithACME)) router.Handle("POST", "/api/admin/pki/certs/:id/renew-acme", txWrite(api.RenewPKICertWithACME))
router.Handle("GET", "/api/admin/pki/certs/:id/bundle", txRead(api.DownloadPKICertBundle)) router.Handle("GET", "/api/admin/pki/certs/:id/bundle", api.DownloadPKICertBundle)
router.Handle("POST", "/api/admin/pki/certs", txWrite(api.IssuePKICert)) router.Handle("POST", "/api/admin/pki/certs", txWrite(api.IssuePKICert))
router.Handle("POST", "/api/admin/pki/certs/import", txWrite(api.ImportPKICert)) router.Handle("POST", "/api/admin/pki/certs/import", txWrite(api.ImportPKICert))
router.Handle("POST", "/api/admin/pki/certs/:id/revoke", txWrite(api.RevokePKICert)) router.Handle("POST", "/api/admin/pki/certs/:id/revoke", txWrite(api.RevokePKICert))
@@ -842,7 +842,7 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("DELETE", "/api/admin/pki/acme/profiles/:id", txWrite(api.DeleteACMEProfile)) router.Handle("DELETE", "/api/admin/pki/acme/profiles/:id", txWrite(api.DeleteACMEProfile))
router.Handle("GET", "/api/admin/pki/acme/orders", txRead(api.ListACMEOrders)) router.Handle("GET", "/api/admin/pki/acme/orders", txRead(api.ListACMEOrders))
router.Handle("POST", "/api/admin/pki/acme/orders", txWrite(api.CreateACMEOrder)) router.Handle("POST", "/api/admin/pki/acme/orders", txWrite(api.CreateACMEOrder))
router.Handle("POST", "/api/admin/pki/acme/orders/:id/finalize", txWrite(api.FinalizeACMEOrder)) router.Handle("POST", "/api/admin/pki/acme/orders/:id/finalize", api.FinalizeACMEOrder)
router.Handle("DELETE", "/api/admin/pki/acme/orders/:id", txWrite(api.DeleteACMEOrder)) router.Handle("DELETE", "/api/admin/pki/acme/orders/:id", txWrite(api.DeleteACMEOrder))
router.Handle("GET", "/api/admin/rpm/mirrors", txRead(api.ListAdminRPMMirrors)) router.Handle("GET", "/api/admin/rpm/mirrors", txRead(api.ListAdminRPMMirrors))
router.Handle("GET", "/api/admin/ssh/user-cas", txRead(api.ListSSHUserCAs)) router.Handle("GET", "/api/admin/ssh/user-cas", txRead(api.ListSSHUserCAs))
@@ -867,7 +867,7 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("DELETE", "/api/admin/ssh/servers/:id", txWrite(api.DeleteSSHServerAdmin)) router.Handle("DELETE", "/api/admin/ssh/servers/:id", txWrite(api.DeleteSSHServerAdmin))
router.Handle("GET", "/api/admin/ssh/servers/:id/host-keys", txRead(api.ListSSHServerHostKeysAdmin)) router.Handle("GET", "/api/admin/ssh/servers/:id/host-keys", txRead(api.ListSSHServerHostKeysAdmin))
router.Handle("POST", "/api/admin/ssh/servers/:id/host-keys", txWrite(api.CreateSSHServerHostKeyAdmin)) router.Handle("POST", "/api/admin/ssh/servers/:id/host-keys", txWrite(api.CreateSSHServerHostKeyAdmin))
router.Handle("POST", "/api/admin/ssh/servers/:id/host-keys/discover", txWrite(api.DiscoverSSHServerHostKeyAdmin)) router.Handle("POST", "/api/admin/ssh/servers/:id/host-keys/discover", api.DiscoverSSHServerHostKeyAdmin)
router.Handle("DELETE", "/api/admin/ssh/servers/:id/host-keys/:hostKeyId", txWrite(api.DeleteSSHServerHostKeyAdmin)) router.Handle("DELETE", "/api/admin/ssh/servers/:id/host-keys/:hostKeyId", txWrite(api.DeleteSSHServerHostKeyAdmin))
router.Handle("GET", "/api/admin/ssh/server-groups", txRead(api.ListSSHServerGroupsAdmin)) router.Handle("GET", "/api/admin/ssh/server-groups", txRead(api.ListSSHServerGroupsAdmin))
router.Handle("POST", "/api/admin/ssh/server-groups", txWrite(api.CreateSSHServerGroupAdmin)) router.Handle("POST", "/api/admin/ssh/server-groups", txWrite(api.CreateSSHServerGroupAdmin))
@@ -887,7 +887,7 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("DELETE", "/api/admin/ssh/access-profiles/:id", txWrite(api.DeleteSSHAccessProfileAdmin)) router.Handle("DELETE", "/api/admin/ssh/access-profiles/:id", txWrite(api.DeleteSSHAccessProfileAdmin))
router.Handle("GET", "/api/admin/ssh/sessions", txRead(api.ListSSHSessionsAdmin)) router.Handle("GET", "/api/admin/ssh/sessions", txRead(api.ListSSHSessionsAdmin))
router.Handle("GET", "/api/admin/ssh/file-transfers", txRead(api.ListSSHFileTransfersAdmin)) router.Handle("GET", "/api/admin/ssh/file-transfers", txRead(api.ListSSHFileTransfersAdmin))
router.Handle("GET", "/api/admin/ssh/sessions/:id/transcript", txRead(api.GetSSHSessionTranscriptAdmin)) router.Handle("GET", "/api/admin/ssh/sessions/:id/transcript", api.GetSSHSessionTranscriptAdmin)
router.Handle("GET", "/api/ssh/user-cas", txRead(api.ListSSHUserCAsForSelf)) router.Handle("GET", "/api/ssh/user-cas", txRead(api.ListSSHUserCAsForSelf))
router.Handle("GET", "/api/ssh/user-cas/:id", txRead(api.GetSSHUserCAForSelf)) router.Handle("GET", "/api/ssh/user-cas/:id", txRead(api.GetSSHUserCAForSelf))
router.Handle("GET", "/api/ssh/user-cas/:id/public-key", txRead(api.DownloadSSHUserCAPublicKeyForSelf)) router.Handle("GET", "/api/ssh/user-cas/:id/public-key", txRead(api.DownloadSSHUserCAPublicKeyForSelf))
@@ -899,7 +899,7 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("DELETE", "/api/ssh/servers/:id", txWrite(api.DeleteSSHServerForSelf)) router.Handle("DELETE", "/api/ssh/servers/:id", txWrite(api.DeleteSSHServerForSelf))
router.Handle("GET", "/api/ssh/servers/:id/host-keys", txRead(api.ListSSHServerHostKeysForSelf)) router.Handle("GET", "/api/ssh/servers/:id/host-keys", txRead(api.ListSSHServerHostKeysForSelf))
router.Handle("POST", "/api/ssh/servers/:id/host-keys", txWrite(api.CreateSSHServerHostKeyForSelf)) router.Handle("POST", "/api/ssh/servers/:id/host-keys", txWrite(api.CreateSSHServerHostKeyForSelf))
router.Handle("POST", "/api/ssh/servers/:id/host-keys/discover", txWrite(api.DiscoverSSHServerHostKeyForSelf)) router.Handle("POST", "/api/ssh/servers/:id/host-keys/discover", api.DiscoverSSHServerHostKeyForSelf)
router.Handle("DELETE", "/api/ssh/servers/:id/host-keys/:hostKeyId", txWrite(api.DeleteSSHServerHostKeyForSelf)) router.Handle("DELETE", "/api/ssh/servers/:id/host-keys/:hostKeyId", txWrite(api.DeleteSSHServerHostKeyForSelf))
router.Handle("GET", "/api/ssh/credentials", txRead(api.ListSSHCredentialsForSelf)) router.Handle("GET", "/api/ssh/credentials", txRead(api.ListSSHCredentialsForSelf))
router.Handle("POST", "/api/ssh/credentials", txWrite(api.CreateSSHCredentialForSelf)) router.Handle("POST", "/api/ssh/credentials", txWrite(api.CreateSSHCredentialForSelf))
@@ -921,12 +921,12 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("GET", "/api/ssh/file-transfers", txRead(api.ListSSHFileTransfersForSelf)) router.Handle("GET", "/api/ssh/file-transfers", txRead(api.ListSSHFileTransfersForSelf))
router.Handle("GET", "/api/ssh/stream", txRead(api.StreamSSHWorkspaceForSelf)) router.Handle("GET", "/api/ssh/stream", txRead(api.StreamSSHWorkspaceForSelf))
router.Handle("GET", "/api/ssh/sessions/:id", txRead(api.GetSSHSessionForSelf)) router.Handle("GET", "/api/ssh/sessions/:id", txRead(api.GetSSHSessionForSelf))
router.Handle("GET", "/api/ssh/sessions/:id/transcript", txRead(api.GetSSHSessionTranscriptForSelf)) router.Handle("GET", "/api/ssh/sessions/:id/transcript", api.GetSSHSessionTranscriptForSelf)
router.Handle("POST", "/api/ssh/sessions/:id/disconnect", txWrite(api.DisconnectSSHSessionForSelf)) router.Handle("POST", "/api/ssh/sessions/:id/disconnect", txWrite(api.DisconnectSSHSessionForSelf))
router.Handle("POST", "/api/ssh/sessions/:id/files/upload", txWrite(api.UploadSSHSessionFilesForSelf)) router.Handle("POST", "/api/ssh/sessions/:id/files/upload", api.UploadSSHSessionFilesForSelf)
router.Handle("POST", "/api/ssh/sessions/:id/files/download", txWrite(api.DownloadSSHSessionFilesForSelf)) router.Handle("POST", "/api/ssh/sessions/:id/files/download", api.DownloadSSHSessionFilesForSelf)
router.Handle("POST", "/api/ssh/sessions/:id/files/copy-to", txWrite(api.CopySSHSessionFilesForSelf)) router.Handle("POST", "/api/ssh/sessions/:id/files/copy-to", api.CopySSHSessionFilesForSelf)
router.Handle("POST", "/api/ssh/cert/inspect", txWrite(api.InspectSSHCertificate)) router.Handle("POST", "/api/ssh/cert/inspect", api.InspectSSHCertificate)
router.Handle("POST", "/api/ssh/user-cas/:id/sign", txWrite(api.SignSSHUserKeyForSelf)) router.Handle("POST", "/api/ssh/user-cas/:id/sign", txWrite(api.SignSSHUserKeyForSelf))
router.Handle("GET", "/api/ssh/issuances", txRead(api.ListSSHUserCAIssuancesForSelf)) router.Handle("GET", "/api/ssh/issuances", txRead(api.ListSSHUserCAIssuancesForSelf))
router.Handle("GET", "/api/pki/cas/:id/crl", txRead(api.GetPKICRLForSelf)) router.Handle("GET", "/api/pki/cas/:id/crl", txRead(api.GetPKICRLForSelf))
@@ -936,14 +936,14 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("POST", "/api/pki/client/certs", txWrite(api.IssuePKIClientCertForSelf)) router.Handle("POST", "/api/pki/client/certs", txWrite(api.IssuePKIClientCertForSelf))
router.Handle("GET", "/api/pki/client/certs/:id", txRead(api.GetPKIClientCertForSelf)) router.Handle("GET", "/api/pki/client/certs/:id", txRead(api.GetPKIClientCertForSelf))
router.Handle("GET", "/api/pki/client/certs/:id/inspect", txRead(api.GetPKIClientCertInspectForSelf)) router.Handle("GET", "/api/pki/client/certs/:id/inspect", txRead(api.GetPKIClientCertInspectForSelf))
router.Handle("GET", "/api/pki/client/certs/:id/bundle", txRead(api.DownloadPKIClientCertBundleForSelf)) router.Handle("GET", "/api/pki/client/certs/:id/bundle", api.DownloadPKIClientCertBundleForSelf)
router.Handle("POST", "/api/pki/client/certs/:id/revoke", txWrite(api.RevokePKIClientCertForSelf)) router.Handle("POST", "/api/pki/client/certs/:id/revoke", txWrite(api.RevokePKIClientCertForSelf))
router.Handle("GET", "/api/projects", txRead(api.ListProjects)) router.Handle("GET", "/api/projects", txRead(api.ListProjects))
router.Handle("POST", "/api/projects", txWrite(api.CreateProject)) router.Handle("POST", "/api/projects", txWrite(api.CreateProject))
router.Handle("GET", "/api/projects/:id", txRead(api.GetProject)) router.Handle("GET", "/api/projects/:id", txRead(api.GetProject))
router.Handle("PATCH", "/api/projects/:id", txWrite(api.UpdateProject)) router.Handle("PATCH", "/api/projects/:id", txWrite(api.UpdateProject))
router.Handle("DELETE", "/api/projects/:id", txWrite(api.DeleteProject)) router.Handle("DELETE", "/api/projects/:id", api.DeleteProject)
router.Handle("GET", "/api/projects/:projectId/members", txRead(api.ListProjectMembers)) router.Handle("GET", "/api/projects/:projectId/members", txRead(api.ListProjectMembers))
router.Handle("GET", "/api/projects/:projectId/member-candidates", txRead(api.ListProjectMemberCandidates)) router.Handle("GET", "/api/projects/:projectId/member-candidates", txRead(api.ListProjectMemberCandidates))
@@ -983,8 +983,8 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("PUT", "/api/boards/:boardId/blocks/:blockId/checklist/:itemId", txWrite(api.UpdateBlockChecklistItem)) router.Handle("PUT", "/api/boards/:boardId/blocks/:blockId/checklist/:itemId", txWrite(api.UpdateBlockChecklistItem))
router.Handle("DELETE", "/api/boards/:boardId/blocks/:blockId/checklist/:itemId", txWrite(api.DeleteBlockChecklistItem)) router.Handle("DELETE", "/api/boards/:boardId/blocks/:blockId/checklist/:itemId", txWrite(api.DeleteBlockChecklistItem))
router.Handle("GET", "/api/boards/:boardId/blocks/:blockId/attachments", txRead(api.ListBlockAttachments)) router.Handle("GET", "/api/boards/:boardId/blocks/:blockId/attachments", txRead(api.ListBlockAttachments))
router.Handle("POST", "/api/boards/:boardId/blocks/:blockId/attachments", txWrite(api.CreateBlockAttachment)) router.Handle("POST", "/api/boards/:boardId/blocks/:blockId/attachments", api.CreateBlockAttachment)
router.Handle("GET", "/api/boards/:boardId/blocks/:blockId/attachments/:attachmentId/download", txRead(api.DownloadBlockAttachment)) router.Handle("GET", "/api/boards/:boardId/blocks/:blockId/attachments/:attachmentId/download", api.DownloadBlockAttachment)
router.Handle("DELETE", "/api/boards/:boardId/blocks/:blockId/attachments/:attachmentId", txWrite(api.DeleteBlockAttachment)) router.Handle("DELETE", "/api/boards/:boardId/blocks/:blockId/attachments/:attachmentId", txWrite(api.DeleteBlockAttachment))
router.Handle("GET", "/api/boards/:boardId/blocks/:blockId/properties", txRead(api.GetBlockProperties)) router.Handle("GET", "/api/boards/:boardId/blocks/:blockId/properties", txRead(api.GetBlockProperties))
router.Handle("PUT", "/api/boards/:boardId/blocks/:blockId/properties", txWrite(api.UpsertBlockProperties)) router.Handle("PUT", "/api/boards/:boardId/blocks/:blockId/properties", txWrite(api.UpsertBlockProperties))
@@ -994,7 +994,7 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("DELETE", "/api/boards/:boardId/members/:userId", txWrite(api.RemoveBoardMember)) router.Handle("DELETE", "/api/boards/:boardId/members/:userId", txWrite(api.RemoveBoardMember))
router.Handle("GET", "/api/projects/:projectId/repos", txRead(api.ListRepos)) router.Handle("GET", "/api/projects/:projectId/repos", txRead(api.ListRepos))
router.Handle("POST", "/api/projects/:projectId/repos", txWrite(api.CreateRepo)) router.Handle("POST", "/api/projects/:projectId/repos", api.CreateRepo)
router.Handle("GET", "/api/repos/types", txRead(api.RepoTypes)) router.Handle("GET", "/api/repos/types", txRead(api.RepoTypes))
router.Handle("GET", "/api/repos", txRead(api.ListAllRepos)) router.Handle("GET", "/api/repos", txRead(api.ListAllRepos))
router.Handle("GET", "/api/projects/:projectId/foreign-repos/available", txRead(api.ListAvailableRepos)) router.Handle("GET", "/api/projects/:projectId/foreign-repos/available", txRead(api.ListAvailableRepos))
@@ -1002,23 +1002,23 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("DELETE", "/api/projects/:projectId/foreign-repos/:repoId", txWrite(api.DetachForeignRepo)) router.Handle("DELETE", "/api/projects/:projectId/foreign-repos/:repoId", txWrite(api.DetachForeignRepo))
router.Handle("GET", "/api/repos/:id", txRead(api.GetRepo)) router.Handle("GET", "/api/repos/:id", txRead(api.GetRepo))
router.Handle("PATCH", "/api/repos/:id", txWrite(api.UpdateRepo)) router.Handle("PATCH", "/api/repos/:id", txWrite(api.UpdateRepo))
router.Handle("DELETE", "/api/repos/:id", txWrite(api.DeleteRepo)) router.Handle("DELETE", "/api/repos/:id", api.DeleteRepo)
router.Handle("GET", "/api/repos/:id/branches", txRead(api.RepoBranches)) router.Handle("GET", "/api/repos/:id/branches", api.RepoBranches)
router.Handle("GET", "/api/repos/:id/branches/info", txRead(api.RepoBranchesInfo)) router.Handle("GET", "/api/repos/:id/branches/info", api.RepoBranchesInfo)
router.Handle("PUT", "/api/repos/:id/branches/default", txWrite(api.RepoSetDefaultBranch)) router.Handle("PUT", "/api/repos/:id/branches/default", api.RepoSetDefaultBranch)
router.Handle("POST", "/api/repos/:id/branches/rename", txWrite(api.RepoRenameBranch)) router.Handle("POST", "/api/repos/:id/branches/rename", api.RepoRenameBranch)
router.Handle("POST", "/api/repos/:id/branches/delete", txWrite(api.RepoDeleteBranch)) router.Handle("POST", "/api/repos/:id/branches/delete", api.RepoDeleteBranch)
router.Handle("POST", "/api/repos/:id/branches/create", txWrite(api.RepoCreateBranch)) router.Handle("POST", "/api/repos/:id/branches/create", api.RepoCreateBranch)
router.Handle("GET", "/api/repos/:id/commits", txRead(api.RepoCommits)) router.Handle("GET", "/api/repos/:id/commits", api.RepoCommits)
router.Handle("GET", "/api/repos/:id/tree", txRead(api.RepoTree)) router.Handle("GET", "/api/repos/:id/tree", api.RepoTree)
router.Handle("GET", "/api/repos/:id/blob", txRead(api.RepoBlob)) router.Handle("GET", "/api/repos/:id/blob", api.RepoBlob)
router.Handle("GET", "/api/repos/:id/blob/raw", txRead(api.RepoBlobRaw)) router.Handle("GET", "/api/repos/:id/blob/raw", api.RepoBlobRaw)
router.Handle("GET", "/api/repos/:id/history", txRead(api.RepoFileHistory)) router.Handle("GET", "/api/repos/:id/history", api.RepoFileHistory)
router.Handle("GET", "/api/repos/:id/diff", txRead(api.RepoFileDiff)) router.Handle("GET", "/api/repos/:id/diff", api.RepoFileDiff)
router.Handle("GET", "/api/repos/:id/commit", txRead(api.RepoCommitDetail)) router.Handle("GET", "/api/repos/:id/commit", api.RepoCommitDetail)
router.Handle("GET", "/api/repos/:id/commit/diff", txRead(api.RepoCommitDiff)) router.Handle("GET", "/api/repos/:id/commit/diff", api.RepoCommitDiff)
router.Handle("GET", "/api/repos/:id/compare", txRead(api.RepoCompare)) router.Handle("GET", "/api/repos/:id/compare", api.RepoCompare)
router.Handle("GET", "/api/repos/:id/stats", txRead(api.RepoStats)) router.Handle("GET", "/api/repos/:id/stats", api.RepoStats)
router.Handle("GET", "/api/repos/:id/rpm/packages", txRead(api.RepoRPMPackages)) router.Handle("GET", "/api/repos/:id/rpm/packages", txRead(api.RepoRPMPackages))
router.Handle("GET", "/api/repos/:id/rpm/package", txRead(api.RepoRPMPackage)) router.Handle("GET", "/api/repos/:id/rpm/package", txRead(api.RepoRPMPackage))
router.Handle("POST", "/api/repos/:id/rpm/subdirs", txWrite(api.RepoRPMCreateSubdir)) router.Handle("POST", "/api/repos/:id/rpm/subdirs", txWrite(api.RepoRPMCreateSubdir))
@@ -1028,15 +1028,15 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("POST", "/api/repos/:id/rpm/subdir/sync", txWrite(api.RepoRPMSyncSubdir)) router.Handle("POST", "/api/repos/:id/rpm/subdir/sync", txWrite(api.RepoRPMSyncSubdir))
router.Handle("POST", "/api/repos/:id/rpm/subdir/suspend", txWrite(api.RepoRPMSuspendSubdir)) router.Handle("POST", "/api/repos/:id/rpm/subdir/suspend", txWrite(api.RepoRPMSuspendSubdir))
router.Handle("POST", "/api/repos/:id/rpm/subdir/resume", txWrite(api.RepoRPMResumeSubdir)) router.Handle("POST", "/api/repos/:id/rpm/subdir/resume", txWrite(api.RepoRPMResumeSubdir))
router.Handle("POST", "/api/repos/:id/rpm/subdir/rebuild-metadata", txWrite(api.RepoRPMRebuildSubdirMetadata)) router.Handle("POST", "/api/repos/:id/rpm/subdir/rebuild-metadata", api.RepoRPMRebuildSubdirMetadata)
router.Handle("POST", "/api/repos/:id/rpm/subdir/cancel", txWrite(api.RepoRPMCancelSubdirSync)) router.Handle("POST", "/api/repos/:id/rpm/subdir/cancel", txWrite(api.RepoRPMCancelSubdirSync))
router.Handle("GET", "/api/repos/:id/rpm/subdir/runs", txRead(api.RepoRPMMirrorRuns)) router.Handle("GET", "/api/repos/:id/rpm/subdir/runs", txRead(api.RepoRPMMirrorRuns))
router.Handle("DELETE", "/api/repos/:id/rpm/subdir/runs", txWrite(api.RepoRPMClearMirrorRuns)) router.Handle("DELETE", "/api/repos/:id/rpm/subdir/runs", txWrite(api.RepoRPMClearMirrorRuns))
router.Handle("DELETE", "/api/repos/:id/rpm/subdir", txWrite(api.RepoRPMDeleteSubdir)) router.Handle("DELETE", "/api/repos/:id/rpm/subdir", txWrite(api.RepoRPMDeleteSubdir))
router.Handle("DELETE", "/api/repos/:id/rpm/file", txWrite(api.RepoRPMDeleteFile)) router.Handle("DELETE", "/api/repos/:id/rpm/file", txWrite(api.RepoRPMDeleteFile))
router.Handle("GET", "/api/repos/:id/rpm/file", txRead(api.RepoRPMFile)) router.Handle("GET", "/api/repos/:id/rpm/file", api.RepoRPMFile)
router.Handle("GET", "/api/repos/:id/rpm/tree", txRead(api.RepoRPMTree)) router.Handle("GET", "/api/repos/:id/rpm/tree", txRead(api.RepoRPMTree))
router.Handle("POST", "/api/repos/:id/rpm/upload", txWrite(api.RepoRPMUpload)) router.Handle("POST", "/api/repos/:id/rpm/upload", api.RepoRPMUpload)
router.Handle("GET", "/api/repos/:id/docker/images", txRead(api.RepoDockerImages)) router.Handle("GET", "/api/repos/:id/docker/images", txRead(api.RepoDockerImages))
router.Handle("GET", "/api/repos/:id/docker/tags", txRead(api.RepoDockerTags)) router.Handle("GET", "/api/repos/:id/docker/tags", txRead(api.RepoDockerTags))
router.Handle("GET", "/api/repos/:id/docker/manifest", txRead(api.RepoDockerManifest)) router.Handle("GET", "/api/repos/:id/docker/manifest", txRead(api.RepoDockerManifest))
@@ -1063,9 +1063,9 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("POST", "/api/projects/:projectId/wiki/pages", txWrite(api.CreateWikiPage)) router.Handle("POST", "/api/projects/:projectId/wiki/pages", txWrite(api.CreateWikiPage))
router.Handle("PATCH", "/api/wiki/pages/:id", txWrite(api.UpdateWikiPage)) router.Handle("PATCH", "/api/wiki/pages/:id", txWrite(api.UpdateWikiPage))
router.Handle("POST", "/api/projects/:projectId/uploads", txWrite(api.UploadFile)) router.Handle("POST", "/api/projects/:projectId/uploads", api.UploadFile)
router.Handle("GET", "/api/projects/:projectId/uploads", txRead(api.ListUploads)) router.Handle("GET", "/api/projects/:projectId/uploads", txRead(api.ListUploads))
router.Handle("GET", "/api/uploads/:id", txRead(api.DownloadFile)) router.Handle("GET", "/api/uploads/:id", api.DownloadFile)
// --------------------------------------------------------------- // ---------------------------------------------------------------
@@ -1920,7 +1920,7 @@ func principalCanWriteService(service serviceKind, servicePrefix string, princip
func roleAllowsWrite(role string) bool { func roleAllowsWrite(role string) bool {
var r string var r string
r = strings.ToLower(strings.TrimSpace(role)) r = strings.ToLower(strings.TrimSpace(role))
return r == "writer" || r == "admin" return r == models.RoleWriter || r == models.RoleAdmin
} }
func resolveProjectIDForServiceWrite(service serviceKind, servicePrefix string, r *http.Request, store *db.Store) (string, error) { func resolveProjectIDForServiceWrite(service serviceKind, servicePrefix string, r *http.Request, store *db.Store) (string, error) {
+1 -1
View File
@@ -17,7 +17,7 @@ func createTestProject(t *testing.T, store *db.Store, user models.User, slug str
Description: slug + " project", Description: slug + " project",
CreatedBy: user.ID, CreatedBy: user.ID,
UpdatedBy: user.ID, UpdatedBy: user.ID,
HomePage: "info", HomePage: "settings",
} }
project, err = store.CreateProject(context.Background(), project) project, err = store.CreateProject(context.Background(), project)
if err != nil { if err != nil {
+1 -1
View File
@@ -458,7 +458,7 @@ Stores explicit per-board user sharing roles.
| --- | --- | | --- | --- |
| `board_id` | Internal board FK. | | `board_id` | Internal board FK. |
| `user_id` | Internal user FK. | | `user_id` | Internal user FK. |
| `role` | Board role: `admin`, `editor`, or `viewer`. | | `role` | Board role: `admin`, `writer`, or `viewer`. |
| `created_at` | Assignment timestamp. | | `created_at` | Assignment timestamp. |
Primary key: `(board_id, user_id)`. Primary key: `(board_id, user_id)`.
+2 -2
View File
@@ -11318,7 +11318,7 @@ components:
type: string type: string
enum: enum:
- admin - admin
- editor - writer
- viewer - viewer
created_at: created_at:
type: integer type: integer
@@ -11333,7 +11333,7 @@ components:
type: string type: string
enum: enum:
- admin - admin
- editor - writer
- viewer - viewer
additionalProperties: true additionalProperties: true
RepositoryObject: RepositoryObject: