1ce33aa38d
- Add a `purpose` column, this allows the `forgejo_auth_token` table to be used by other parts of Forgejo, while still enjoying the no-compromise architecture. - Remove the 'roll your own crypto' time limited code functions and migrate them to the `forgejo_auth_token` table. This migration ensures generated codes can only be used for their purpose and ensure they are invalidated after their usage by deleting it from the database, this also should help making auditing of the security code easier, as we're no longer trying to stuff a lot of data into a HMAC construction. -Helper functions are rewritten to ensure a safe-by-design approach to these tokens. - Add the `forgejo_auth_token` to dbconsistency doctor and add it to the `deleteUser` function. - TODO: Add cron job to delete expired authorization tokens. - Unit and integration tests added.
268 lines
9.9 KiB
Go
268 lines
9.9 KiB
Go
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package doctor
|
|
|
|
import (
|
|
"context"
|
|
|
|
actions_model "code.gitea.io/gitea/models/actions"
|
|
activities_model "code.gitea.io/gitea/models/activities"
|
|
auth_model "code.gitea.io/gitea/models/auth"
|
|
"code.gitea.io/gitea/models/db"
|
|
issues_model "code.gitea.io/gitea/models/issues"
|
|
"code.gitea.io/gitea/models/migrations"
|
|
org_model "code.gitea.io/gitea/models/organization"
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
)
|
|
|
|
type consistencyCheck struct {
|
|
Name string
|
|
Counter func(context.Context) (int64, error)
|
|
Fixer func(context.Context) (int64, error)
|
|
FixedMessage string
|
|
}
|
|
|
|
func (c *consistencyCheck) Run(ctx context.Context, logger log.Logger, autofix bool) error {
|
|
count, err := c.Counter(ctx)
|
|
if err != nil {
|
|
logger.Critical("Error: %v whilst counting %s", err, c.Name)
|
|
return err
|
|
}
|
|
if count > 0 {
|
|
if autofix {
|
|
var fixed int64
|
|
if fixed, err = c.Fixer(ctx); err != nil {
|
|
logger.Critical("Error: %v whilst fixing %s", err, c.Name)
|
|
return err
|
|
}
|
|
|
|
prompt := "Deleted"
|
|
if c.FixedMessage != "" {
|
|
prompt = c.FixedMessage
|
|
}
|
|
|
|
if fixed < 0 {
|
|
logger.Info(prompt+" %d %s", count, c.Name)
|
|
} else {
|
|
logger.Info(prompt+" %d/%d %s", fixed, count, c.Name)
|
|
}
|
|
} else {
|
|
logger.Warn("Found %d %s", count, c.Name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func asFixer(fn func(ctx context.Context) error) func(ctx context.Context) (int64, error) {
|
|
return func(ctx context.Context) (int64, error) {
|
|
err := fn(ctx)
|
|
return -1, err
|
|
}
|
|
}
|
|
|
|
func genericOrphanCheck(name, subject, refobject, joincond string) consistencyCheck {
|
|
return consistencyCheck{
|
|
Name: name,
|
|
Counter: func(ctx context.Context) (int64, error) {
|
|
return db.CountOrphanedObjects(ctx, subject, refobject, joincond)
|
|
},
|
|
Fixer: func(ctx context.Context) (int64, error) {
|
|
err := db.DeleteOrphanedObjects(ctx, subject, refobject, joincond)
|
|
return -1, err
|
|
},
|
|
}
|
|
}
|
|
|
|
func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) error {
|
|
// make sure DB version is up-to-date
|
|
if err := db.InitEngineWithMigration(ctx, migrations.EnsureUpToDate); err != nil {
|
|
logger.Critical("Model version on the database does not match the current Gitea version. Model consistency will not be checked until the database is upgraded")
|
|
return err
|
|
}
|
|
|
|
consistencyChecks := []consistencyCheck{
|
|
{
|
|
// find labels without existing repo or org
|
|
Name: "Orphaned Labels without existing repository or organisation",
|
|
Counter: issues_model.CountOrphanedLabels,
|
|
Fixer: asFixer(issues_model.DeleteOrphanedLabels),
|
|
},
|
|
{
|
|
// find IssueLabels without existing label
|
|
Name: "Orphaned Issue Labels without existing label",
|
|
Counter: issues_model.CountOrphanedIssueLabels,
|
|
Fixer: asFixer(issues_model.DeleteOrphanedIssueLabels),
|
|
},
|
|
{
|
|
// find issues without existing repository
|
|
Name: "Orphaned Issues without existing repository",
|
|
Counter: issues_model.CountOrphanedIssues,
|
|
Fixer: asFixer(issues_model.DeleteOrphanedIssues),
|
|
},
|
|
// find releases without existing repository
|
|
genericOrphanCheck("Orphaned Releases without existing repository",
|
|
"release", "repository", "`release`.repo_id=repository.id"),
|
|
// find pulls without existing issues
|
|
genericOrphanCheck("Orphaned PullRequests without existing issue",
|
|
"pull_request", "issue", "pull_request.issue_id=issue.id"),
|
|
// find pull requests without base repository
|
|
genericOrphanCheck("Pull request entries without existing base repository",
|
|
"pull_request", "repository", "pull_request.base_repo_id=repository.id"),
|
|
// find tracked times without existing issues/pulls
|
|
genericOrphanCheck("Orphaned TrackedTimes without existing issue",
|
|
"tracked_time", "issue", "tracked_time.issue_id=issue.id"),
|
|
// find attachments without existing issues or releases
|
|
{
|
|
Name: "Orphaned Attachments without existing issues or releases",
|
|
Counter: repo_model.CountOrphanedAttachments,
|
|
Fixer: asFixer(repo_model.DeleteOrphanedAttachments),
|
|
},
|
|
// find null archived repositories
|
|
{
|
|
Name: "Repositories with is_archived IS NULL",
|
|
Counter: repo_model.CountNullArchivedRepository,
|
|
Fixer: repo_model.FixNullArchivedRepository,
|
|
FixedMessage: "Fixed",
|
|
},
|
|
// find label comments with empty labels
|
|
{
|
|
Name: "Label comments with empty labels",
|
|
Counter: issues_model.CountCommentTypeLabelWithEmptyLabel,
|
|
Fixer: issues_model.FixCommentTypeLabelWithEmptyLabel,
|
|
FixedMessage: "Fixed",
|
|
},
|
|
// find label comments with labels from outside the repository
|
|
{
|
|
Name: "Label comments with labels from outside the repository",
|
|
Counter: issues_model.CountCommentTypeLabelWithOutsideLabels,
|
|
Fixer: issues_model.FixCommentTypeLabelWithOutsideLabels,
|
|
FixedMessage: "Removed",
|
|
},
|
|
// find issue_label with labels from outside the repository
|
|
{
|
|
Name: "IssueLabels with Labels from outside the repository",
|
|
Counter: issues_model.CountIssueLabelWithOutsideLabels,
|
|
Fixer: issues_model.FixIssueLabelWithOutsideLabels,
|
|
FixedMessage: "Removed",
|
|
},
|
|
{
|
|
Name: "Action with created_unix set as an empty string",
|
|
Counter: activities_model.CountActionCreatedUnixString,
|
|
Fixer: activities_model.FixActionCreatedUnixString,
|
|
FixedMessage: "Set to zero",
|
|
},
|
|
{
|
|
Name: "Action Runners without existing owner",
|
|
Counter: actions_model.CountRunnersWithoutBelongingOwner,
|
|
Fixer: actions_model.FixRunnersWithoutBelongingOwner,
|
|
FixedMessage: "Removed",
|
|
},
|
|
{
|
|
Name: "Action Runners without existing repository",
|
|
Counter: actions_model.CountRunnersWithoutBelongingRepo,
|
|
Fixer: actions_model.FixRunnersWithoutBelongingRepo,
|
|
FixedMessage: "Removed",
|
|
},
|
|
{
|
|
Name: "Topics with empty repository count",
|
|
Counter: repo_model.CountOrphanedTopics,
|
|
Fixer: repo_model.DeleteOrphanedTopics,
|
|
FixedMessage: "Removed",
|
|
},
|
|
{
|
|
Name: "Orphaned OAuth2Application without existing User",
|
|
Counter: auth_model.CountOrphanedOAuth2Applications,
|
|
Fixer: auth_model.DeleteOrphanedOAuth2Applications,
|
|
FixedMessage: "Removed",
|
|
},
|
|
{
|
|
Name: "Owner teams with no admin access",
|
|
Counter: org_model.CountInconsistentOwnerTeams,
|
|
Fixer: org_model.FixInconsistentOwnerTeams,
|
|
FixedMessage: "Fixed",
|
|
},
|
|
}
|
|
|
|
// TODO: function to recalc all counters
|
|
|
|
if setting.Database.Type.IsPostgreSQL() {
|
|
consistencyChecks = append(consistencyChecks, consistencyCheck{
|
|
Name: "Sequence values",
|
|
Counter: db.CountBadSequences,
|
|
Fixer: asFixer(db.FixBadSequences),
|
|
FixedMessage: "Updated",
|
|
})
|
|
}
|
|
|
|
consistencyChecks = append(consistencyChecks,
|
|
// find protected branches without existing repository
|
|
genericOrphanCheck("Protected Branches without existing repository",
|
|
"protected_branch", "repository", "protected_branch.repo_id=repository.id"),
|
|
// find branches without existing repository
|
|
genericOrphanCheck("Branches without existing repository",
|
|
"branch", "repository", "branch.repo_id=repository.id"),
|
|
// find LFS locks without existing repository
|
|
genericOrphanCheck("LFS locks without existing repository",
|
|
"lfs_lock", "repository", "lfs_lock.repo_id=repository.id"),
|
|
// find collaborations without users
|
|
genericOrphanCheck("Collaborations without existing user",
|
|
"collaboration", "user", "collaboration.user_id=`user`.id"),
|
|
// find collaborations without repository
|
|
genericOrphanCheck("Collaborations without existing repository",
|
|
"collaboration", "repository", "collaboration.repo_id=repository.id"),
|
|
// find access without users
|
|
genericOrphanCheck("Access entries without existing user",
|
|
"access", "user", "access.user_id=`user`.id"),
|
|
// find access without repository
|
|
genericOrphanCheck("Access entries without existing repository",
|
|
"access", "repository", "access.repo_id=repository.id"),
|
|
// find action without repository
|
|
genericOrphanCheck("Action entries without existing repository",
|
|
"action", "repository", "action.repo_id=repository.id"),
|
|
// find action without user
|
|
genericOrphanCheck("Action entries without existing user",
|
|
"action", "user", "action.act_user_id=`user`.id"),
|
|
// find OAuth2Grant without existing user
|
|
genericOrphanCheck("Orphaned OAuth2Grant without existing User",
|
|
"oauth2_grant", "user", "oauth2_grant.user_id=`user`.id"),
|
|
// find OAuth2AuthorizationCode without existing OAuth2Grant
|
|
genericOrphanCheck("Orphaned OAuth2AuthorizationCode without existing OAuth2Grant",
|
|
"oauth2_authorization_code", "oauth2_grant", "oauth2_authorization_code.grant_id=oauth2_grant.id"),
|
|
// find stopwatches without existing user
|
|
genericOrphanCheck("Orphaned Stopwatches without existing User",
|
|
"stopwatch", "user", "stopwatch.user_id=`user`.id"),
|
|
// find stopwatches without existing issue
|
|
genericOrphanCheck("Orphaned Stopwatches without existing Issue",
|
|
"stopwatch", "issue", "stopwatch.issue_id=`issue`.id"),
|
|
// find redirects without existing user.
|
|
genericOrphanCheck("Orphaned Redirects without existing redirect user",
|
|
"user_redirect", "user", "user_redirect.redirect_user_id=`user`.id"),
|
|
// find archive download count without existing release
|
|
genericOrphanCheck("Archive download count without existing Release",
|
|
"repo_archive_download_count", "release", "repo_archive_download_count.release_id=release.id"),
|
|
// find authorization tokens without existing user
|
|
genericOrphanCheck("Authorization token without existing User",
|
|
"forgejo_auth_token", "user", "forgejo_auth_token.uid=user.id"),
|
|
)
|
|
|
|
for _, c := range consistencyChecks {
|
|
if err := c.Run(ctx, logger, autofix); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func init() {
|
|
Register(&Check{
|
|
Title: "Check consistency of database",
|
|
Name: "check-db-consistency",
|
|
IsDefault: false,
|
|
Run: checkDBConsistency,
|
|
Priority: 3,
|
|
})
|
|
}
|