#334: Add Deployment Key Support

This commit is contained in:
Unknwon 2015-08-06 22:48:11 +08:00
parent 9f12ab0e88
commit 39a3b768bc
26 changed files with 693 additions and 149 deletions

View file

@ -10,8 +10,8 @@ github.com/Unknwon/macaron = commit:635c89ac74
github.com/Unknwon/paginater = commit:cab2d086fa
github.com/codegangsta/cli = commit:2bcd11f863
github.com/go-sql-driver/mysql = commit:a197e5d405
github.com/go-xorm/core = commit:bacc62db6e
github.com/go-xorm/xorm = commit:7b8945acfe
github.com/go-xorm/core =
github.com/go-xorm/xorm =
github.com/gogits/chardet = commit:2404f77725
github.com/gogits/go-gogs-client = commit:92e76d616a
github.com/lib/pq = commit:0dad96c0b9

View file

@ -71,17 +71,17 @@ var (
}
)
func fail(userMessage, logMessage string, args ...interface{}) {
fmt.Fprintln(os.Stderr, "Gogs:", userMessage)
log.GitLogger.Fatal(3, logMessage, args...)
}
func runServ(c *cli.Context) {
if c.IsSet("config") {
setting.CustomConf = c.String("config")
}
setup("serv.log")
fail := func(userMessage, logMessage string, args ...interface{}) {
fmt.Fprintln(os.Stderr, "Gogs:", userMessage)
log.GitLogger.Fatal(3, logMessage, args...)
}
if len(c.Args()) < 1 {
fail("Not enough arguments", "Not enough arguments")
}
@ -131,30 +131,53 @@ func runServ(c *cli.Context) {
if requestedMode == models.ACCESS_MODE_WRITE || repo.IsPrivate {
keys := strings.Split(c.Args()[0], "-")
if len(keys) != 2 {
fail("key-id format error", "Invalid key id: %s", c.Args()[0])
fail("Key ID format error", "Invalid key ID: %s", c.Args()[0])
}
keyID, err = com.StrTo(keys[1]).Int64()
key, err := models.GetPublicKeyByID(com.StrTo(keys[1]).MustInt64())
if err != nil {
fail("key-id format error", "Invalid key id: %s", err)
fail("Key ID format error", "Invalid key ID[%s]: %v", c.Args()[0], err)
}
keyID = key.ID
user, err = models.GetUserByKeyId(keyID)
if err != nil {
fail("internal error", "Failed to get user by key ID(%d): %v", keyID, err)
}
mode, err := models.AccessLevel(user, repo)
if err != nil {
fail("Internal error", "Fail to check access: %v", err)
} else if mode < requestedMode {
clientMessage := _ACCESS_DENIED_MESSAGE
if mode >= models.ACCESS_MODE_READ {
clientMessage = "You do not have sufficient authorization for this action"
// Check deploy key or user key.
if key.Type == models.KEY_TYPE_DEPLOY {
if key.Mode < requestedMode {
fail("Key permission denied", "Cannot push with deployment key: %d", key.ID)
}
// Check if this deploy key belongs to current repository.
if !models.HasDeployKey(key.ID, repo.Id) {
fail("Key access denied", "Key access denied: %d-%d", key.ID, repo.Id)
}
// Update deploy key activity.
deployKey, err := models.GetDeployKeyByRepo(key.ID, repo.Id)
if err != nil {
fail("Internal error", "GetDeployKey: %v", err)
}
deployKey.Updated = time.Now()
if err = models.UpdateDeployKey(deployKey); err != nil {
fail("Internal error", "UpdateDeployKey: %v", err)
}
} else {
user, err = models.GetUserByKeyId(key.ID)
if err != nil {
fail("internal error", "Failed to get user by key ID(%d): %v", keyID, err)
}
mode, err := models.AccessLevel(user, repo)
if err != nil {
fail("Internal error", "Fail to check access: %v", err)
} else if mode < requestedMode {
clientMessage := _ACCESS_DENIED_MESSAGE
if mode >= models.ACCESS_MODE_READ {
clientMessage = "You do not have sufficient authorization for this action"
}
fail(clientMessage,
"User %s does not have level %v access to repository %s",
user.Name, requestedMode, repoPath)
}
fail(clientMessage,
"User %s does not have level %v access to repository %s",
user.Name, requestedMode, repoPath)
}
}
@ -201,9 +224,9 @@ func runServ(c *cli.Context) {
resp.Body.Close()
}
// Update key activity.
// Update user key activity.
if keyID > 0 {
key, err := models.GetPublicKeyById(keyID)
key, err := models.GetPublicKeyByID(keyID)
if err != nil {
fail("Internal error", "GetPublicKeyById: %v", err)
}

View file

@ -404,6 +404,13 @@ func runWeb(ctx *cli.Context) {
m.Get("/:name", repo.GitHooksEdit)
m.Post("/:name", repo.GitHooksEditPost)
}, middleware.GitHookService())
m.Group("/keys", func() {
m.Combo("").Get(repo.SettingsDeployKeys).
Post(bindIgnErr(auth.AddSSHKeyForm{}), repo.SettingsDeployKeysPost)
m.Post("/delete", repo.DeleteDeployKey)
})
})
}, reqSignIn, middleware.RepoAssignment(true), reqRepoAdmin)

View file

@ -178,7 +178,6 @@ repo_name_been_taken = Repository name has been already taken.
org_name_been_taken = Organization name has been already taken.
team_name_been_taken = Team name has been already taken.
email_been_used = E-mail address has been already used.
ssh_key_been_used = Public key name or content has been used.
illegal_team_name = Team name contains illegal characters.
username_password_incorrect = Username or password is not correct.
enterred_invalid_repo_name = Please make sure that the repository name you entered is correct.
@ -264,13 +263,16 @@ add_key = Add Key
ssh_desc = This is a list of SSH keys associated with your account. As these keys allow anyone using them to gain access to your repositories, it is highly important that you make sure you recognize them.
ssh_helper = <strong>Don't know how?</strong> Check out GitHub's guide to <a href="%s">create your own SSH keys</a> or solve <a href="%s">common problems</a> you might encounter using SSH.
add_new_key = Add SSH Key
ssh_key_been_used = Public key content has been used.
ssh_key_name_used = Public key with same name has already existed.
key_name = Key Name
key_content = Content
add_key_success = New SSH Key has been added!
add_key_success = New SSH key '%s' has been added successfully!
delete_key = Delete
add_on = Added on
last_used = Last used on
no_activity = No recent activity
key_state_desc = This key is used in last 7 days
manage_social = Manage Associated Social Accounts
social_desc = This is a list of associated social accounts. Remove any binding that you do not recognize.
@ -390,7 +392,7 @@ issues.label_edit = Edit
issues.label_delete = Delete
issues.label_modify = Label Modification
issues.label_deletion = Label Deletion
issues.label_deletion_desc = Delete label will remove its information in all related issues. Do you want to continue?
issues.label_deletion_desc = Delete this label will remove its information in all related issues. Do you want to continue?
issues.label_deletion_success = Label has been deleted successfully!
milestones.new = New Milestone
@ -414,7 +416,7 @@ milestones.cancel = Cancel
milestones.modify = Modify Milestone
milestones.edit_success = Changes of milestone '%s' has been saved successfully!
milestones.deletion = Milestone Deletion
milestones.deletion_desc = Delete milestone will remove its information in all related issues. Do you want to continue?
milestones.deletion_desc = Delete this milestone will remove its information in all related issues. Do you want to continue?
milestones.deletion_success = Milestone has been deleted successfully!
settings = Settings
@ -422,7 +424,6 @@ settings.options = Options
settings.collaboration = Collaboration
settings.hooks = Webhooks
settings.githooks = Git Hooks
settings.deploy_keys = Deploy Keys
settings.basic_settings = Basic Settings
settings.danger_zone = Danger Zone
settings.site = Official Site
@ -470,6 +471,17 @@ settings.add_slack_hook_desc = Add <a href="%s">Slack</a> integration to your re
settings.slack_token = Token
settings.slack_domain = Domain
settings.slack_channel = Channel
settings.deploy_keys = Deploy Keys
settings.add_deploy_key = Add Deploy Key
settings.no_deploy_keys = You haven't added any deploy key.
settings.title = Title
settings.deploy_key_content = Content
settings.key_been_used = Deploy key content has been used.
settings.key_name_used = Deploy key with same name has already existed.
settings.add_key_success = New deploy key '%s' has been added successfully!
settings.deploy_key_deletion = Delete Deploy Key
settings.deploy_key_deletion_desc = Delete this deploy key will remove all related accesses for this repository. Do you want to continue?
settings.deploy_key_deletion_success = Deploy key has been deleted successfully!
diff.browse_source = Browse Source
diff.parent = parent

View file

@ -17,7 +17,7 @@ import (
"github.com/gogits/gogs/modules/setting"
)
const APP_VER = "0.6.4.0805 Beta"
const APP_VER = "0.6.4.0806 Beta"
func init() {
runtime.GOMAXPROCS(runtime.NumCPU())

View file

@ -107,6 +107,82 @@ func (err ErrUserHasOrgs) Error() string {
return fmt.Sprintf("user still has membership of organizations: [uid: %d]", err.UID)
}
// __________ ___. .__ .__ ____ __.
// \______ \__ _\_ |__ | | |__| ____ | |/ _|____ ___.__.
// | ___/ | \ __ \| | | |/ ___\ | <_/ __ < | |
// | | | | / \_\ \ |_| \ \___ | | \ ___/\___ |
// |____| |____/|___ /____/__|\___ > |____|__ \___ > ____|
// \/ \/ \/ \/\/
type ErrKeyNotExist struct {
ID int64
}
func IsErrKeyNotExist(err error) bool {
_, ok := err.(ErrKeyNotExist)
return ok
}
func (err ErrKeyNotExist) Error() string {
return fmt.Sprintf("public key does not exist: [id: %d]", err.ID)
}
type ErrKeyAlreadyExist struct {
OwnerID int64
Content string
}
func IsErrKeyAlreadyExist(err error) bool {
_, ok := err.(ErrKeyAlreadyExist)
return ok
}
func (err ErrKeyAlreadyExist) Error() string {
return fmt.Sprintf("public key already exists: [owner_id: %d, content: %s]", err.OwnerID, err.Content)
}
type ErrKeyNameAlreadyUsed struct {
OwnerID int64
Name string
}
func IsErrKeyNameAlreadyUsed(err error) bool {
_, ok := err.(ErrKeyNameAlreadyUsed)
return ok
}
func (err ErrKeyNameAlreadyUsed) Error() string {
return fmt.Sprintf("public key already exists: [owner_id: %d, name: %s]", err.OwnerID, err.Name)
}
type ErrDeployKeyAlreadyExist struct {
KeyID int64
RepoID int64
}
func IsErrDeployKeyAlreadyExist(err error) bool {
_, ok := err.(ErrDeployKeyAlreadyExist)
return ok
}
func (err ErrDeployKeyAlreadyExist) Error() string {
return fmt.Sprintf("public key already exists: [key_id: %d, repo_id: %d]", err.KeyID, err.RepoID)
}
type ErrDeployKeyNameAlreadyUsed struct {
RepoID int64
Name string
}
func IsErrDeployKeyNameAlreadyUsed(err error) bool {
_, ok := err.(ErrDeployKeyNameAlreadyUsed)
return ok
}
func (err ErrDeployKeyNameAlreadyUsed) Error() string {
return fmt.Sprintf("public key already exists: [repo_id: %d, name: %s]", err.RepoID, err.Name)
}
// ________ .__ __ .__
// \_____ \_______ _________ ____ |__|____________ _/ |_|__| ____ ____
// / | \_ __ \/ ___\__ \ / \| \___ /\__ \\ __\ |/ _ \ / \

View file

@ -56,16 +56,11 @@ type Issue struct {
Updated time.Time `xorm:"UPDATED"`
}
func (i *Issue) BeforeSet(colName string, val xorm.Cell) {
func (i *Issue) AfterSet(colName string, _ xorm.Cell) {
var err error
switch colName {
case "milestone_id":
mid := (*val).(int64)
if mid <= 0 {
return
}
i.Milestone, err = GetMilestoneById(mid)
i.Milestone, err = GetMilestoneById(i.MilestoneID)
if err != nil {
log.Error(3, "GetMilestoneById: %v", err)
}
@ -664,15 +659,14 @@ type Milestone struct {
ClosedDate time.Time
}
func (m *Milestone) BeforeSet(colName string, val xorm.Cell) {
func (m *Milestone) AfterSet(colName string, _ xorm.Cell) {
if colName == "deadline" {
t := (*val).(time.Time)
if t.Year() == 9999 {
if m.Deadline.Year() == 9999 {
return
}
m.DeadlineString = t.Format("2006-01-02")
if time.Now().After(t) {
m.DeadlineString = m.Deadline.Format("2006-01-02")
if time.Now().After(m.Deadline) {
m.IsOverDue = true
}
}

View file

@ -57,7 +57,7 @@ var migrations = []Migration{
NewMigration("refactor access table to use id's", accessRefactor), // V2 -> V3:v0.5.13
NewMigration("generate team-repo from team", teamToTeamRepo), // V3 -> V4:v0.5.13
NewMigration("fix locale file load panic", fixLocaleFileLoadPanic), // V4 -> V5:v0.6.0
NewMigration("trim action compare URL prefix", trimCommitActionAppUrlPrefix), // V5 -> V6:v0.6.3 // V4 -> V5:v0.6.0
NewMigration("trim action compare URL prefix", trimCommitActionAppUrlPrefix), // V5 -> V6:v0.6.3
}
// Migrate database to current version

View file

@ -55,7 +55,7 @@ var (
func init() {
tables = append(tables,
new(User), new(PublicKey), new(Oauth2), new(AccessToken),
new(Repository), new(Collaboration), new(Access),
new(Repository), new(DeployKey), new(Collaboration), new(Access),
new(Watch), new(Star), new(Follow), new(Action),
new(Issue), new(Comment), new(Attachment), new(IssueUser), new(Label), new(Milestone),
new(Mirror), new(Release), new(LoginSource), new(Webhook),
@ -132,7 +132,7 @@ func NewTestEngine(x *xorm.Engine) (err error) {
func SetEngine() (err error) {
x, err = getEngine()
if err != nil {
return fmt.Errorf("Connect to database: %v", err)
return fmt.Errorf("Fail to connect to database: %v", err)
}
x.SetMapper(core.GonicMapper{})

View file

@ -21,6 +21,7 @@ import (
"time"
"github.com/Unknwon/com"
"github.com/go-xorm/xorm"
"github.com/gogits/gogs/modules/log"
"github.com/gogits/gogs/modules/process"
@ -33,8 +34,6 @@ const (
)
var (
ErrKeyAlreadyExist = errors.New("Public key already exists")
ErrKeyNotExist = errors.New("Public key does not exist")
ErrKeyUnableVerify = errors.New("Unable to verify public key")
)
@ -78,17 +77,34 @@ func init() {
}
}
// PublicKey represents a SSH key.
type KeyType int
const (
KEY_TYPE_USER = iota + 1
KEY_TYPE_DEPLOY
)
// PublicKey represents a SSH or deploy key.
type PublicKey struct {
Id int64
OwnerId int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
Name string `xorm:"UNIQUE(s) NOT NULL"`
Fingerprint string `xorm:"INDEX NOT NULL"`
Content string `xorm:"TEXT NOT NULL"`
Created time.Time `xorm:"CREATED"`
Updated time.Time
HasRecentActivity bool `xorm:"-"`
HasUsed bool `xorm:"-"`
ID int64 `xorm:"pk autoincr"`
OwnerID int64 `xorm:"INDEX NOT NULL"`
Name string `xorm:"NOT NULL"`
Fingerprint string `xorm:"NOT NULL"`
Content string `xorm:"UNIQUE TEXT NOT NULL"`
Mode AccessMode `xorm:"NOT NULL DEFAULT 2"`
Type KeyType `xorm:"NOT NULL DEFAULT 1"`
Created time.Time `xorm:"CREATED"`
Updated time.Time // Note: Updated must below Created for AfterSet.
HasRecentActivity bool `xorm:"-"`
HasUsed bool `xorm:"-"`
}
func (k *PublicKey) AfterSet(colName string, _ xorm.Cell) {
switch colName {
case "created":
k.HasUsed = k.Updated.After(k.Created)
k.HasRecentActivity = k.Updated.Add(7 * 24 * time.Hour).After(time.Now())
}
}
// OmitEmail returns content of public key but without e-mail address.
@ -98,7 +114,7 @@ func (k *PublicKey) OmitEmail() string {
// GetAuthorizedString generates and returns formatted public key string for authorized_keys file.
func (key *PublicKey) GetAuthorizedString() string {
return fmt.Sprintf(_TPL_PUBLICK_KEY, appPath, key.Id, setting.CustomConf, key.Content)
return fmt.Sprintf(_TPL_PUBLICK_KEY, appPath, key.ID, setting.CustomConf, key.Content)
}
var minimumKeySizes = map[string]int{
@ -126,8 +142,8 @@ func extractTypeFromBase64Key(key string) (string, error) {
return string(b[4 : 4+keyLength]), nil
}
// Parse any key string in openssh or ssh2 format to clean openssh string (rfc4253)
func ParseKeyString(content string) (string, error) {
// parseKeyString parses any key string in openssh or ssh2 format to clean openssh string (rfc4253)
func parseKeyString(content string) (string, error) {
// Transform all legal line endings to a single "\n"
s := strings.Replace(strings.Replace(strings.TrimSpace(content), "\r\n", "\n", -1), "\r", "\n", -1)
@ -190,16 +206,21 @@ func ParseKeyString(content string) (string, error) {
}
// CheckPublicKeyString checks if the given public key string is recognized by SSH.
func CheckPublicKeyString(content string) (bool, error) {
func CheckPublicKeyString(content string) (_ string, err error) {
content, err = parseKeyString(content)
if err != nil {
return "", err
}
content = strings.TrimRight(content, "\n\r")
if strings.ContainsAny(content, "\n\r") {
return false, errors.New("only a single line with a single key please")
return "", errors.New("only a single line with a single key please")
}
// write the key to a file…
tmpFile, err := ioutil.TempFile(os.TempDir(), "keytest")
if err != nil {
return false, err
return "", err
}
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath)
@ -209,37 +230,36 @@ func CheckPublicKeyString(content string) (bool, error) {
// Check if ssh-keygen recognizes its contents.
stdout, stderr, err := process.Exec("CheckPublicKeyString", "ssh-keygen", "-l", "-f", tmpPath)
if err != nil {
return false, errors.New("ssh-keygen -l -f: " + stderr)
return "", errors.New("ssh-keygen -l -f: " + stderr)
} else if len(stdout) < 2 {
return false, errors.New("ssh-keygen returned not enough output to evaluate the key: " + stdout)
return "", errors.New("ssh-keygen returned not enough output to evaluate the key: " + stdout)
}
// The ssh-keygen in Windows does not print key type, so no need go further.
if setting.IsWindows {
return true, nil
return content, nil
}
fmt.Println(stdout)
sshKeygenOutput := strings.Split(stdout, " ")
if len(sshKeygenOutput) < 4 {
return false, ErrKeyUnableVerify
return content, ErrKeyUnableVerify
}
// Check if key type and key size match.
if !setting.Service.DisableMinimumKeySizeCheck {
keySize := com.StrTo(sshKeygenOutput[0]).MustInt()
if keySize == 0 {
return false, errors.New("cannot get key size of the given key")
return "", errors.New("cannot get key size of the given key")
}
keyType := strings.TrimSpace(sshKeygenOutput[len(sshKeygenOutput)-1])
if minimumKeySize := minimumKeySizes[keyType]; minimumKeySize == 0 {
return false, errors.New("sorry, unrecognized public key type")
return "", errors.New("sorry, unrecognized public key type")
} else if keySize < minimumKeySize {
return false, fmt.Errorf("the minimum accepted size of a public key %s is %d", keyType, minimumKeySize)
return "", fmt.Errorf("the minimum accepted size of a public key %s is %d", keyType, minimumKeySize)
}
}
return true, nil
return content, nil
}
// saveAuthorizedKeyFile writes SSH key content to authorized_keys file.
@ -278,20 +298,23 @@ func saveAuthorizedKeyFile(keys ...*PublicKey) error {
return nil
}
// AddPublicKey adds new public key to database and authorized_keys file.
func AddPublicKey(key *PublicKey) (err error) {
has, err := x.Get(key)
func checkKeyContent(content string) error {
// Same key can only be added once.
has, err := x.Where("content=?", content).Get(new(PublicKey))
if err != nil {
return err
} else if has {
return ErrKeyAlreadyExist
return ErrKeyAlreadyExist{0, content}
}
return nil
}
func addKey(e Engine, key *PublicKey) (err error) {
// Calculate fingerprint.
tmpPath := strings.Replace(path.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().Nanosecond()),
"id_rsa.pub"), "\\", "/", -1)
os.MkdirAll(path.Dir(tmpPath), os.ModePerm)
if err = ioutil.WriteFile(tmpPath, []byte(key.Content), os.ModePerm); err != nil {
if err = ioutil.WriteFile(tmpPath, []byte(key.Content), 0644); err != nil {
return err
}
stdout, stderr, err := process.Exec("AddPublicKey", "ssh-keygen", "-l", "-f", tmpPath)
@ -301,32 +324,56 @@ func AddPublicKey(key *PublicKey) (err error) {
return errors.New("not enough output for calculating fingerprint: " + stdout)
}
key.Fingerprint = strings.Split(stdout, " ")[1]
if has, err := x.Get(&PublicKey{Fingerprint: key.Fingerprint}); err == nil && has {
return ErrKeyAlreadyExist
}
// Save SSH key.
if _, err = x.Insert(key); err != nil {
if _, err = e.Insert(key); err != nil {
return err
} else if err = saveAuthorizedKeyFile(key); err != nil {
// Roll back.
if _, err2 := x.Delete(key); err2 != nil {
return err2
}
}
return saveAuthorizedKeyFile(key)
}
// AddPublicKey adds new public key to database and authorized_keys file.
func AddPublicKey(ownerID int64, name, content string) (err error) {
if err = checkKeyContent(content); err != nil {
return err
}
return nil
// Key name of same user cannot be duplicated.
has, err := x.Where("owner_id=? AND name=?", ownerID, name).Get(new(PublicKey))
if err != nil {
return err
} else if has {
return ErrKeyNameAlreadyUsed{ownerID, name}
}
sess := x.NewSession()
defer sessionRelease(sess)
if err = sess.Begin(); err != nil {
return err
}
key := &PublicKey{
OwnerID: ownerID,
Name: name,
Content: content,
Mode: ACCESS_MODE_WRITE,
Type: KEY_TYPE_USER,
}
if err = addKey(sess, key); err != nil {
return fmt.Errorf("addKey: %v", err)
}
return sess.Commit()
}
// GetPublicKeyById returns public key by given ID.
func GetPublicKeyById(keyId int64) (*PublicKey, error) {
// GetPublicKeyByID returns public key by given ID.
func GetPublicKeyByID(keyID int64) (*PublicKey, error) {
key := new(PublicKey)
has, err := x.Id(keyId).Get(key)
has, err := x.Id(keyID).Get(key)
if err != nil {
return nil, err
} else if !has {
return nil, ErrKeyNotExist
return nil, ErrKeyNotExist{keyID}
}
return key, nil
}
@ -334,16 +381,7 @@ func GetPublicKeyById(keyId int64) (*PublicKey, error) {
// ListPublicKeys returns a list of public keys belongs to given user.
func ListPublicKeys(uid int64) ([]*PublicKey, error) {
keys := make([]*PublicKey, 0, 5)
err := x.Where("owner_id=?", uid).Find(&keys)
if err != nil {
return nil, err
}
for _, key := range keys {
key.HasUsed = key.Updated.After(key.Created)
key.HasRecentActivity = key.Updated.Add(7 * 24 * time.Hour).After(time.Now())
}
return keys, nil
return keys, x.Where("owner_id=?", uid).Find(&keys)
}
// rewriteAuthorizedKeys finds and deletes corresponding line in authorized_keys file.
@ -364,7 +402,7 @@ func rewriteAuthorizedKeys(key *PublicKey, p, tmpP string) error {
defer fw.Close()
isFound := false
keyword := fmt.Sprintf("key-%d", key.Id)
keyword := fmt.Sprintf("key-%d", key.ID)
buf := bufio.NewReader(fr)
for {
line, errRead := buf.ReadString('\n')
@ -401,20 +439,19 @@ func rewriteAuthorizedKeys(key *PublicKey, p, tmpP string) error {
// UpdatePublicKey updates given public key.
func UpdatePublicKey(key *PublicKey) error {
_, err := x.Id(key.Id).AllCols().Update(key)
_, err := x.Id(key.ID).AllCols().Update(key)
return err
}
// DeletePublicKey deletes SSH key information both in database and authorized_keys file.
func DeletePublicKey(key *PublicKey) error {
has, err := x.Get(key)
func deletePublicKey(e *xorm.Session, key *PublicKey) error {
has, err := e.Get(key)
if err != nil {
return err
} else if !has {
return ErrKeyNotExist
return nil
}
if _, err = x.Delete(key); err != nil {
if _, err = e.Id(key.ID).Delete(key); err != nil {
return err
}
@ -428,6 +465,21 @@ func DeletePublicKey(key *PublicKey) error {
return os.Rename(tmpPath, fpath)
}
// DeletePublicKey deletes SSH key information both in database and authorized_keys file.
func DeletePublicKey(key *PublicKey) (err error) {
sess := x.NewSession()
defer sessionRelease(sess)
if err = sess.Begin(); err != nil {
return err
}
if err = deletePublicKey(sess, key); err != nil {
return err
}
return sess.Commit()
}
// RewriteAllPublicKeys removes any authorized key and rewrite all keys from database again.
func RewriteAllPublicKeys() error {
sshOpLocker.Lock()
@ -461,3 +513,162 @@ func RewriteAllPublicKeys() error {
return nil
}
// ________ .__ ____ __.
// \______ \ ____ ______ | | ____ ___.__.| |/ _|____ ___.__.
// | | \_/ __ \\____ \| | / _ < | || <_/ __ < | |
// | ` \ ___/| |_> > |_( <_> )___ || | \ ___/\___ |
// /_______ /\___ > __/|____/\____// ____||____|__ \___ > ____|
// \/ \/|__| \/ \/ \/\/
// DeployKey represents deploy key information and its relation with repository.
type DeployKey struct {
ID int64 `xorm:"pk autoincr"`
KeyID int64 `xorm:"UNIQUE(s) INDEX"`
RepoID int64 `xorm:"UNIQUE(s) INDEX"`
Name string
Fingerprint string
Created time.Time `xorm:"CREATED"`
Updated time.Time // Note: Updated must below Created for AfterSet.
HasRecentActivity bool `xorm:"-"`
HasUsed bool `xorm:"-"`
}
func (k *DeployKey) AfterSet(colName string, _ xorm.Cell) {
switch colName {
case "created":
k.HasUsed = k.Updated.After(k.Created)
k.HasRecentActivity = k.Updated.Add(7 * 24 * time.Hour).After(time.Now())
}
}
func checkDeployKey(e Engine, keyID, repoID int64, name string) error {
// Note: We want error detail, not just true or false here.
has, err := e.Where("key_id=? AND repo_id=?", keyID, repoID).Get(new(DeployKey))
if err != nil {
return err
} else if has {
return ErrDeployKeyAlreadyExist{keyID, repoID}
}
has, err = e.Where("repo_id=? AND name=?", repoID, name).Get(new(DeployKey))
if err != nil {
return err
} else if has {
return ErrDeployKeyNameAlreadyUsed{repoID, name}
}
return nil
}
// addDeployKey adds new key-repo relation.
func addDeployKey(e *xorm.Session, keyID, repoID int64, name, fingerprint string) (err error) {
if err = checkDeployKey(e, keyID, repoID, name); err != nil {
return err
}
_, err = e.Insert(&DeployKey{
KeyID: keyID,
RepoID: repoID,
Name: name,
Fingerprint: fingerprint,
})
return err
}
// HasDeployKey returns true if public key is a deploy key of given repository.
func HasDeployKey(keyID, repoID int64) bool {
has, _ := x.Where("key_id=? AND repo_id=?", keyID, repoID).Get(new(DeployKey))
return has
}
// AddDeployKey add new deploy key to database and authorized_keys file.
func AddDeployKey(repoID int64, name, content string) (err error) {
if err = checkKeyContent(content); err != nil {
return err
}
key := &PublicKey{
Content: content,
Mode: ACCESS_MODE_READ,
Type: KEY_TYPE_DEPLOY,
}
has, err := x.Get(key)
if err != nil {
return err
}
sess := x.NewSession()
defer sessionRelease(sess)
if err = sess.Begin(); err != nil {
return err
}
// First time use this deploy key.
if !has {
if err = addKey(sess, key); err != nil {
return nil
}
}
if err = addDeployKey(sess, key.ID, repoID, name, key.Fingerprint); err != nil {
return err
}
return sess.Commit()
}
// GetDeployKeyByRepo returns deploy key by given public key ID and repository ID.
func GetDeployKeyByRepo(keyID, repoID int64) (*DeployKey, error) {
key := &DeployKey{
KeyID: keyID,
RepoID: repoID,
}
_, err := x.Get(key)
return key, err
}
// UpdateDeployKey updates deploy key information.
func UpdateDeployKey(key *DeployKey) error {
_, err := x.Id(key.ID).AllCols().Update(key)
return err
}
// DeleteDeployKey deletes deploy key from its repository authorized_keys file if needed.
func DeleteDeployKey(id int64) error {
key := &DeployKey{ID: id}
has, err := x.Id(key.ID).Get(key)
if err != nil {
return err
} else if !has {
return nil
}
sess := x.NewSession()
defer sessionRelease(sess)
if err = sess.Begin(); err != nil {
return err
}
if _, err = sess.Id(key.ID).Delete(key); err != nil {
return fmt.Errorf("delete deploy key[%d]: %v", key.ID, err)
}
// Check if this is the last reference to same key content.
has, err = sess.Where("key_id=?", key.KeyID).Get(new(DeployKey))
if err != nil {
return err
} else if !has {
if err = deletePublicKey(sess, &PublicKey{ID: key.KeyID}); err != nil {
return err
}
}
return sess.Commit()
}
// ListDeployKeys returns all deploy keys by given repository ID.
func ListDeployKeys(repoID int64) ([]*DeployKey, error) {
keys := make([]*DeployKey, 0, 5)
return keys, x.Where("repo_id=?", repoID).Find(&keys)
}

View file

@ -505,7 +505,7 @@ func DeleteUser(u *User) error {
// Delete all SSH keys.
keys := make([]*PublicKey, 0, 10)
if err = sess.Find(&keys, &PublicKey{OwnerId: u.Id}); err != nil {
if err = sess.Find(&keys, &PublicKey{OwnerID: u.Id}); err != nil {
return err
}
for _, key := range keys {

View file

@ -126,8 +126,8 @@ func (f *ChangePasswordForm) Validate(ctx *macaron.Context, errs binding.Errors)
}
type AddSSHKeyForm struct {
SSHTitle string `form:"title" binding:"Required"`
Content string `form:"content" binding:"Required"`
Title string `binding:"Required;MaxSize(50)"`
Content string `binding:"Required"`
}
func (f *AddSSHKeyForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -86,6 +86,13 @@ function initRepository() {
return false;
});
}
// Settings
if ($('.repository.settings').length > 0) {
$('#add-deploy-key').click(function () {
$('#add-deploy-key-panel').show();
});
}
};
$(document).ready(function () {

View file

@ -5,4 +5,10 @@
padding-bottom: .6em;
display: inline-block;
}
}
.ui.attached.header {
background: #f0f0f0;
.right {
margin-top: -5px;
}
}

View file

@ -1,9 +1,6 @@
.install {
padding-top: 45px;
padding-bottom: @footer-margin * 3;
.attached.header {
background: #f0f0f0;
}
form {
label {
text-align: right;

View file

@ -217,6 +217,36 @@
height: 200px;
}
}
&.settings {
.content {
padding-left: 20px!important;
}
}
}
.settings .key.list {
.item:not(:first-child) {
border-top: 1px solid #eaeaea;
}
.ssh-key-state-indicator {
float: left;
color: gray;
padding-left: 10px;
padding-top: 10px;
&.active {
color: #6cc644;
}
}
.meta {
padding-top: 5px;
}
.print {
color: #767676;
}
.activity {
color: #666;
}
}
.edit-label.modal {

View file

@ -27,10 +27,11 @@ const (
SETTINGS_OPTIONS base.TplName = "repo/settings/options"
COLLABORATION base.TplName = "repo/settings/collaboration"
HOOKS base.TplName = "repo/settings/hooks"
GITHOOKS base.TplName = "repo/settings/githooks"
GITHOOK_EDIT base.TplName = "repo/settings/githook_edit"
HOOK_NEW base.TplName = "repo/settings/hook_new"
ORG_HOOK_NEW base.TplName = "org/settings/hook_new"
GITHOOKS base.TplName = "repo/settings/githooks"
GITHOOK_EDIT base.TplName = "repo/settings/githook_edit"
DEPLOY_KEYS base.TplName = "repo/settings/deploy_keys"
)
func Settings(ctx *middleware.Context) {
@ -584,6 +585,10 @@ func getOrgRepoCtx(ctx *middleware.Context) (*OrgRepoCtx, error) {
}
}
func TriggerHook(ctx *middleware.Context) {
models.HookQueue.AddRepoID(ctx.Repo.Repository.Id)
}
func GitHooks(ctx *middleware.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings")
ctx.Data["PageIsSettingsGitHooks"] = true
@ -635,6 +640,70 @@ func GitHooksEditPost(ctx *middleware.Context) {
ctx.Redirect(ctx.Repo.RepoLink + "/settings/hooks/git")
}
func TriggerHook(ctx *middleware.Context) {
models.HookQueue.AddRepoID(ctx.Repo.Repository.Id)
func SettingsDeployKeys(ctx *middleware.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings")
ctx.Data["PageIsSettingsKeys"] = true
keys, err := models.ListDeployKeys(ctx.Repo.Repository.Id)
if err != nil {
ctx.Handle(500, "ListDeployKeys", err)
return
}
ctx.Data["Deploykeys"] = keys
ctx.HTML(200, DEPLOY_KEYS)
}
func SettingsDeployKeysPost(ctx *middleware.Context, form auth.AddSSHKeyForm) {
ctx.Data["Title"] = ctx.Tr("repo.settings")
ctx.Data["PageIsSettingsKeys"] = true
if ctx.HasError() {
ctx.HTML(200, DEPLOY_KEYS)
return
}
content, err := models.CheckPublicKeyString(form.Content)
if err != nil {
if err == models.ErrKeyUnableVerify {
ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
} else {
ctx.Data["HasError"] = true
ctx.Data["Err_Content"] = true
ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
return
}
}
if err = models.AddDeployKey(ctx.Repo.Repository.Id, form.Title, content); err != nil {
ctx.Data["HasError"] = true
switch {
case models.IsErrKeyAlreadyExist(err):
ctx.Data["Err_Content"] = true
ctx.RenderWithErr(ctx.Tr("repo.settings.key_been_used"), DEPLOY_KEYS, &form)
case models.IsErrKeyNameAlreadyUsed(err):
ctx.Data["Err_Title"] = true
ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), DEPLOY_KEYS, &form)
default:
ctx.Handle(500, "AddDeployKey", err)
}
return
}
log.Trace("Deploy key added: %d", ctx.Repo.Repository.Id)
ctx.Flash.Success(ctx.Tr("repo.settings.add_key_success", form.Title))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
}
func DeleteDeployKey(ctx *middleware.Context) {
if err := models.DeleteDeployKey(ctx.QueryInt64("id")); err != nil {
ctx.Flash.Error("DeleteDeployKey: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("repo.settings.deploy_key_deletion_success"))
}
ctx.JSON(200, map[string]interface{}{
"redirect": ctx.Repo.RepoLink + "/settings/keys",
})
}

View file

@ -305,7 +305,7 @@ func SettingsSSHKeysPost(ctx *middleware.Context, form auth.AddSSHKeyForm) {
return
}
if err = models.DeletePublicKey(&models.PublicKey{Id: id}); err != nil {
if err = models.DeletePublicKey(&models.PublicKey{ID: id}); err != nil {
ctx.Handle(500, "DeletePublicKey", err)
} else {
log.Trace("SSH key deleted: %s", ctx.User.Name)
@ -321,15 +321,8 @@ func SettingsSSHKeysPost(ctx *middleware.Context, form auth.AddSSHKeyForm) {
return
}
// Parse openssh style string from form content
content, err := models.ParseKeyString(form.Content)
content, err := models.CheckPublicKeyString(form.Content)
if err != nil {
ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
ctx.Redirect(setting.AppSubUrl + "/user/settings/ssh")
return
}
if ok, err := models.CheckPublicKeyString(content); !ok {
if err == models.ErrKeyUnableVerify {
ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
} else {
@ -339,21 +332,19 @@ func SettingsSSHKeysPost(ctx *middleware.Context, form auth.AddSSHKeyForm) {
}
}
k := &models.PublicKey{
OwnerId: ctx.User.Id,
Name: form.SSHTitle,
Content: content,
}
if err := models.AddPublicKey(k); err != nil {
if err == models.ErrKeyAlreadyExist {
ctx.RenderWithErr(ctx.Tr("form.ssh_key_been_used"), SETTINGS_SSH_KEYS, &form)
return
if err = models.AddPublicKey(ctx.User.Id, form.Title, content); err != nil {
switch {
case models.IsErrKeyAlreadyExist(err):
ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), SETTINGS_SSH_KEYS, &form)
case models.IsErrKeyNameAlreadyUsed(err):
ctx.RenderWithErr(ctx.Tr("settings.ssh_key_name_used"), SETTINGS_SSH_KEYS, &form)
default:
ctx.Handle(500, "AddPublicKey", err)
}
ctx.Handle(500, "ssh.AddPublicKey", err)
return
} else {
log.Trace("SSH key added: %s", ctx.User.Name)
ctx.Flash.Success(ctx.Tr("settings.add_key_success"))
ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
ctx.Redirect(setting.AppSubUrl + "/user/settings/ssh")
return
}

View file

@ -1 +1 @@
0.6.4.0805 Beta
0.6.4.0806 Beta

View file

@ -7,4 +7,9 @@
<div class="ui positive message">
<p>{{.Flash.SuccessMsg}}</p>
</div>
{{end}}
{{if .Flash.InfoMsg}}
<div class="ui info message">
<p>{{.Flash.InfoMsg}}</p>
</div>
{{end}}

View file

@ -0,0 +1,97 @@
{{template "base/head" .}}
<div class="repository settings">
{{template "repo/header" .}}
<div class="ui page grid">
{{template "repo/settings/navbar" .}}
<div class="twelve wide column content">
{{template "base/alert" .}}
<h4 class="ui top attached header">
{{.i18n.Tr "repo.settings.deploy_keys"}}
<div class="ui right">
<div id="add-deploy-key" class="ui blue tiny button">{{.i18n.Tr "repo.settings.add_deploy_key"}}</div>
</div>
</h4>
<div class="ui attached segment">
{{if .Deploykeys}}
<div class="ui key list">
{{range .Deploykeys}}
<div class="item ui grid">
<div class="one wide column">
<i class="ssh-key-state-indicator fa fa-circle{{if .HasRecentActivity}} active invert poping up{{else}}-o{{end}}" {{if .HasRecentActivity}}data-content="{{$.i18n.Tr "settings.key_state_desc"}}" data-variation="inverted"{{end}}></i>
</div>
<div class="one wide column">
<i class="mega-octicon octicon-key left"></i>
</div>
<div class="eleven wide column">
<strong>{{.Name}}</strong>
<div class="print meta">
{{.Fingerprint}}
</div>
<div class="activity meta">
<i>{{$.i18n.Tr "settings.add_on"}} <span>{{DateFmtShort .Created}}</span> — <i class="octicon octicon-info"></i> {{if .HasUsed}}{{$.i18n.Tr "settings.last_used"}} <span>{{DateFmtShort .Updated}}</span>{{else}}{{$.i18n.Tr "settings.no_activity"}}{{end}}</i>
</div>
</div>
<div class="three wide column">
<button class="ui red tiny button delete-button" data-url="{{$.Link}}/delete" data-id="{{.ID}}">
{{$.i18n.Tr "settings.delete_key"}}
</button>
</div>
</div>
{{end}}
</div>
{{else}}
{{.i18n.Tr "repo.settings.no_deploy_keys"}}
{{end}}
</div>
<br>
<div {{if not .HasError}}class="hide"{{end}} id="add-deploy-key-panel">
<h4 class="ui top attached header">
{{.i18n.Tr "repo.settings.add_deploy_key"}}
</h4>
<div class="ui attached segment">
<form class="ui form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<div class="field {{if .Err_Title}}error{{end}}">
<label>{{.i18n.Tr "repo.settings.title"}}</label>
<input name="title" value="{{.title}}" autofocus required>
</div>
<div class="field {{if .Err_Content}}error{{end}}">
<label>{{.i18n.Tr "repo.settings.deploy_key_content"}}</label>
<textarea name="content" required>{{.content}}</textarea>
</div>
<button class="ui green button">
{{.i18n.Tr "repo.settings.add_deploy_key"}}
</button>
</form>
</div>
</div>
</div>
</div>
</div>
<div class="ui basic delete modal">
<div class="header">
{{.i18n.Tr "repo.settings.deploy_key_deletion"}}
</div>
<div class="content">
<div class="image">
<i class="trash icon"></i>
</div>
<div class="description">
<p>{{.i18n.Tr "repo.settings.deploy_key_deletion_desc"}}</p>
</div>
</div>
<div class="actions">
<div class="two fluid ui inverted buttons">
<div class="ui red basic inverted button">
<i class="remove icon"></i>
{{.i18n.Tr "modal.no"}}
</div>
<div class="ui green basic inverted positive button">
<i class="checkmark icon"></i>
{{.i18n.Tr "modal.yes"}}
</div>
</div>
</div>
</div>
{{template "base/footer" .}}

View file

@ -8,7 +8,7 @@
{{if or .SignedUser.AllowGitHook .SignedUser.IsAdmin}}
<li {{if .PageIsSettingsGitHooks}}class="current"{{end}}><a href="{{.RepoLink}}/settings/hooks/git">{{.i18n.Tr "repo.settings.githooks"}}</a></li>
{{end}}
<!-- <li {{if .PageIsSettingsKeys}}class="current"{{end}}><a href="{{.RepoLink}}/settings/keys">{{.i18n.Tr "repo.settings.deploy_keys"}}</a></li> -->
<li {{if .PageIsSettingsKeys}}class="current"{{end}}><a href="{{.RepoLink}}/settings/keys">{{.i18n.Tr "repo.settings.deploy_keys"}}</a></li>
</ul>
</div>
</div>

View file

@ -0,0 +1,19 @@
<div class="four wide column">
<div class="ui vertical menu">
<a class="{{if .PageIsSettingsOptions}}active{{end}} item" href="{{.RepoLink}}/settings">
{{.i18n.Tr "repo.settings.options"}}
</a>
<a class="{{if .PageIsSettingsCollaboration}}active{{end}} item" href="{.RepoLink}}/settings/collaboration">
{{.i18n.Tr "repo.settings.collaboration"}}
</a>
<a class="{{if .PageIsSettingsHooks}}active{{end}} item" href="{{.RepoLink}}/settings/hooks">
{{.i18n.Tr "repo.settings.hooks"}}
</a>
<a class="{{if .PageIsSettingsGitHooks}}active{{end}} item" href="{{.RepoLink}}/settings/hooks/git">
{{.i18n.Tr "repo.settings.githooks"}}
</a>
<a class="{{if .PageIsSettingsKeys}}active{{end}} item" href="{{.RepoLink}}/settings/keys">
{{.i18n.Tr "repo.settings.deploy_keys"}}
</a>
</div>
</div>

View file

@ -28,7 +28,7 @@
<form action="{{AppSubUrl}}/user/settings/ssh" method="post">
{{$.CsrfTokenHtml}}
<input name="_method" type="hidden" value="DELETE">
<input name="id" type="hidden" value="{{.Id}}">
<input name="id" type="hidden" value="{{.ID}}">
<button class="right ssh-btn btn btn-red btn-radius btn-small">{{$.i18n.Tr "settings.delete_key"}}</button>
</form>
</li>