#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/Unknwon/paginater = commit:cab2d086fa
github.com/codegangsta/cli = commit:2bcd11f863 github.com/codegangsta/cli = commit:2bcd11f863
github.com/go-sql-driver/mysql = commit:a197e5d405 github.com/go-sql-driver/mysql = commit:a197e5d405
github.com/go-xorm/core = commit:bacc62db6e github.com/go-xorm/core =
github.com/go-xorm/xorm = commit:7b8945acfe github.com/go-xorm/xorm =
github.com/gogits/chardet = commit:2404f77725 github.com/gogits/chardet = commit:2404f77725
github.com/gogits/go-gogs-client = commit:92e76d616a github.com/gogits/go-gogs-client = commit:92e76d616a
github.com/lib/pq = commit:0dad96c0b9 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) { func runServ(c *cli.Context) {
if c.IsSet("config") { if c.IsSet("config") {
setting.CustomConf = c.String("config") setting.CustomConf = c.String("config")
} }
setup("serv.log") 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 { if len(c.Args()) < 1 {
fail("Not enough arguments", "Not enough arguments") fail("Not enough arguments", "Not enough arguments")
} }
@ -131,30 +131,53 @@ func runServ(c *cli.Context) {
if requestedMode == models.ACCESS_MODE_WRITE || repo.IsPrivate { if requestedMode == models.ACCESS_MODE_WRITE || repo.IsPrivate {
keys := strings.Split(c.Args()[0], "-") keys := strings.Split(c.Args()[0], "-")
if len(keys) != 2 { 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 { 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) // Check deploy key or user key.
if err != nil { if key.Type == models.KEY_TYPE_DEPLOY {
fail("internal error", "Failed to get user by key ID(%d): %v", keyID, err) if key.Mode < requestedMode {
} fail("Key permission denied", "Cannot push with deployment key: %d", key.ID)
}
mode, err := models.AccessLevel(user, repo) // Check if this deploy key belongs to current repository.
if err != nil { if !models.HasDeployKey(key.ID, repo.Id) {
fail("Internal error", "Fail to check access: %v", err) fail("Key access denied", "Key access denied: %d-%d", key.ID, repo.Id)
} else if mode < requestedMode { }
clientMessage := _ACCESS_DENIED_MESSAGE
if mode >= models.ACCESS_MODE_READ { // Update deploy key activity.
clientMessage = "You do not have sufficient authorization for this action" 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() resp.Body.Close()
} }
// Update key activity. // Update user key activity.
if keyID > 0 { if keyID > 0 {
key, err := models.GetPublicKeyById(keyID) key, err := models.GetPublicKeyByID(keyID)
if err != nil { if err != nil {
fail("Internal error", "GetPublicKeyById: %v", err) fail("Internal error", "GetPublicKeyById: %v", err)
} }

View file

@ -404,6 +404,13 @@ func runWeb(ctx *cli.Context) {
m.Get("/:name", repo.GitHooksEdit) m.Get("/:name", repo.GitHooksEdit)
m.Post("/:name", repo.GitHooksEditPost) m.Post("/:name", repo.GitHooksEditPost)
}, middleware.GitHookService()) }, 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) }, 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. org_name_been_taken = Organization name has been already taken.
team_name_been_taken = Team 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. 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. illegal_team_name = Team name contains illegal characters.
username_password_incorrect = Username or password is not correct. username_password_incorrect = Username or password is not correct.
enterred_invalid_repo_name = Please make sure that the repository name you entered is 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_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. 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 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_name = Key Name
key_content = Content 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 delete_key = Delete
add_on = Added on add_on = Added on
last_used = Last used on last_used = Last used on
no_activity = No recent activity no_activity = No recent activity
key_state_desc = This key is used in last 7 days
manage_social = Manage Associated Social Accounts manage_social = Manage Associated Social Accounts
social_desc = This is a list of associated social accounts. Remove any binding that you do not recognize. 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_delete = Delete
issues.label_modify = Label Modification issues.label_modify = Label Modification
issues.label_deletion = Label Deletion 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! issues.label_deletion_success = Label has been deleted successfully!
milestones.new = New Milestone milestones.new = New Milestone
@ -414,7 +416,7 @@ milestones.cancel = Cancel
milestones.modify = Modify Milestone milestones.modify = Modify Milestone
milestones.edit_success = Changes of milestone '%s' has been saved successfully! milestones.edit_success = Changes of milestone '%s' has been saved successfully!
milestones.deletion = Milestone Deletion 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! milestones.deletion_success = Milestone has been deleted successfully!
settings = Settings settings = Settings
@ -422,7 +424,6 @@ settings.options = Options
settings.collaboration = Collaboration settings.collaboration = Collaboration
settings.hooks = Webhooks settings.hooks = Webhooks
settings.githooks = Git Hooks settings.githooks = Git Hooks
settings.deploy_keys = Deploy Keys
settings.basic_settings = Basic Settings settings.basic_settings = Basic Settings
settings.danger_zone = Danger Zone settings.danger_zone = Danger Zone
settings.site = Official Site 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_token = Token
settings.slack_domain = Domain settings.slack_domain = Domain
settings.slack_channel = Channel 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.browse_source = Browse Source
diff.parent = parent diff.parent = parent

View file

@ -17,7 +17,7 @@ import (
"github.com/gogits/gogs/modules/setting" "github.com/gogits/gogs/modules/setting"
) )
const APP_VER = "0.6.4.0805 Beta" const APP_VER = "0.6.4.0806 Beta"
func init() { func init() {
runtime.GOMAXPROCS(runtime.NumCPU()) 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) 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"` 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 var err error
switch colName { switch colName {
case "milestone_id": case "milestone_id":
mid := (*val).(int64) i.Milestone, err = GetMilestoneById(i.MilestoneID)
if mid <= 0 {
return
}
i.Milestone, err = GetMilestoneById(mid)
if err != nil { if err != nil {
log.Error(3, "GetMilestoneById: %v", err) log.Error(3, "GetMilestoneById: %v", err)
} }
@ -664,15 +659,14 @@ type Milestone struct {
ClosedDate time.Time ClosedDate time.Time
} }
func (m *Milestone) BeforeSet(colName string, val xorm.Cell) { func (m *Milestone) AfterSet(colName string, _ xorm.Cell) {
if colName == "deadline" { if colName == "deadline" {
t := (*val).(time.Time) if m.Deadline.Year() == 9999 {
if t.Year() == 9999 {
return return
} }
m.DeadlineString = t.Format("2006-01-02") m.DeadlineString = m.Deadline.Format("2006-01-02")
if time.Now().After(t) { if time.Now().After(m.Deadline) {
m.IsOverDue = true 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("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("generate team-repo from team", teamToTeamRepo), // V3 -> V4:v0.5.13
NewMigration("fix locale file load panic", fixLocaleFileLoadPanic), // V4 -> V5:v0.6.0 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 // Migrate database to current version

View file

@ -55,7 +55,7 @@ var (
func init() { func init() {
tables = append(tables, tables = append(tables,
new(User), new(PublicKey), new(Oauth2), new(AccessToken), 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(Watch), new(Star), new(Follow), new(Action),
new(Issue), new(Comment), new(Attachment), new(IssueUser), new(Label), new(Milestone), new(Issue), new(Comment), new(Attachment), new(IssueUser), new(Label), new(Milestone),
new(Mirror), new(Release), new(LoginSource), new(Webhook), new(Mirror), new(Release), new(LoginSource), new(Webhook),
@ -132,7 +132,7 @@ func NewTestEngine(x *xorm.Engine) (err error) {
func SetEngine() (err error) { func SetEngine() (err error) {
x, err = getEngine() x, err = getEngine()
if err != nil { 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{}) x.SetMapper(core.GonicMapper{})

View file

@ -21,6 +21,7 @@ import (
"time" "time"
"github.com/Unknwon/com" "github.com/Unknwon/com"
"github.com/go-xorm/xorm"
"github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/log"
"github.com/gogits/gogs/modules/process" "github.com/gogits/gogs/modules/process"
@ -33,8 +34,6 @@ const (
) )
var ( var (
ErrKeyAlreadyExist = errors.New("Public key already exists")
ErrKeyNotExist = errors.New("Public key does not exist")
ErrKeyUnableVerify = errors.New("Unable to verify public key") 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 { type PublicKey struct {
Id int64 ID int64 `xorm:"pk autoincr"`
OwnerId int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` OwnerID int64 `xorm:"INDEX NOT NULL"`
Name string `xorm:"UNIQUE(s) NOT NULL"` Name string `xorm:"NOT NULL"`
Fingerprint string `xorm:"INDEX NOT NULL"` Fingerprint string `xorm:"NOT NULL"`
Content string `xorm:"TEXT NOT NULL"` Content string `xorm:"UNIQUE TEXT NOT NULL"`
Created time.Time `xorm:"CREATED"` Mode AccessMode `xorm:"NOT NULL DEFAULT 2"`
Updated time.Time Type KeyType `xorm:"NOT NULL DEFAULT 1"`
HasRecentActivity bool `xorm:"-"` Created time.Time `xorm:"CREATED"`
HasUsed bool `xorm:"-"` 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. // 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. // GetAuthorizedString generates and returns formatted public key string for authorized_keys file.
func (key *PublicKey) GetAuthorizedString() string { 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{ var minimumKeySizes = map[string]int{
@ -126,8 +142,8 @@ func extractTypeFromBase64Key(key string) (string, error) {
return string(b[4 : 4+keyLength]), nil return string(b[4 : 4+keyLength]), nil
} }
// Parse any key string in openssh or ssh2 format to clean openssh string (rfc4253) // parseKeyString parses any key string in openssh or ssh2 format to clean openssh string (rfc4253)
func ParseKeyString(content string) (string, error) { func parseKeyString(content string) (string, error) {
// Transform all legal line endings to a single "\n" // Transform all legal line endings to a single "\n"
s := strings.Replace(strings.Replace(strings.TrimSpace(content), "\r\n", "\n", -1), "\r", "\n", -1) 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. // 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") content = strings.TrimRight(content, "\n\r")
if strings.ContainsAny(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… // write the key to a file…
tmpFile, err := ioutil.TempFile(os.TempDir(), "keytest") tmpFile, err := ioutil.TempFile(os.TempDir(), "keytest")
if err != nil { if err != nil {
return false, err return "", err
} }
tmpPath := tmpFile.Name() tmpPath := tmpFile.Name()
defer os.Remove(tmpPath) defer os.Remove(tmpPath)
@ -209,37 +230,36 @@ func CheckPublicKeyString(content string) (bool, error) {
// Check if ssh-keygen recognizes its contents. // Check if ssh-keygen recognizes its contents.
stdout, stderr, err := process.Exec("CheckPublicKeyString", "ssh-keygen", "-l", "-f", tmpPath) stdout, stderr, err := process.Exec("CheckPublicKeyString", "ssh-keygen", "-l", "-f", tmpPath)
if err != nil { 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 { } 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. // The ssh-keygen in Windows does not print key type, so no need go further.
if setting.IsWindows { if setting.IsWindows {
return true, nil return content, nil
} }
fmt.Println(stdout)
sshKeygenOutput := strings.Split(stdout, " ") sshKeygenOutput := strings.Split(stdout, " ")
if len(sshKeygenOutput) < 4 { if len(sshKeygenOutput) < 4 {
return false, ErrKeyUnableVerify return content, ErrKeyUnableVerify
} }
// Check if key type and key size match. // Check if key type and key size match.
if !setting.Service.DisableMinimumKeySizeCheck { if !setting.Service.DisableMinimumKeySizeCheck {
keySize := com.StrTo(sshKeygenOutput[0]).MustInt() keySize := com.StrTo(sshKeygenOutput[0]).MustInt()
if keySize == 0 { 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]) keyType := strings.TrimSpace(sshKeygenOutput[len(sshKeygenOutput)-1])
if minimumKeySize := minimumKeySizes[keyType]; minimumKeySize == 0 { 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 { } 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. // saveAuthorizedKeyFile writes SSH key content to authorized_keys file.
@ -278,20 +298,23 @@ func saveAuthorizedKeyFile(keys ...*PublicKey) error {
return nil return nil
} }
// AddPublicKey adds new public key to database and authorized_keys file. func checkKeyContent(content string) error {
func AddPublicKey(key *PublicKey) (err error) { // Same key can only be added once.
has, err := x.Get(key) has, err := x.Where("content=?", content).Get(new(PublicKey))
if err != nil { if err != nil {
return err return err
} else if has { } else if has {
return ErrKeyAlreadyExist return ErrKeyAlreadyExist{0, content}
} }
return nil
}
func addKey(e Engine, key *PublicKey) (err error) {
// Calculate fingerprint. // Calculate fingerprint.
tmpPath := strings.Replace(path.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().Nanosecond()), tmpPath := strings.Replace(path.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().Nanosecond()),
"id_rsa.pub"), "\\", "/", -1) "id_rsa.pub"), "\\", "/", -1)
os.MkdirAll(path.Dir(tmpPath), os.ModePerm) 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 return err
} }
stdout, stderr, err := process.Exec("AddPublicKey", "ssh-keygen", "-l", "-f", tmpPath) 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) return errors.New("not enough output for calculating fingerprint: " + stdout)
} }
key.Fingerprint = strings.Split(stdout, " ")[1] key.Fingerprint = strings.Split(stdout, " ")[1]
if has, err := x.Get(&PublicKey{Fingerprint: key.Fingerprint}); err == nil && has {
return ErrKeyAlreadyExist
}
// Save SSH key. // Save SSH key.
if _, err = x.Insert(key); err != nil { if _, err = e.Insert(key); err != nil {
return err return err
} else if err = saveAuthorizedKeyFile(key); err != nil { }
// Roll back. return saveAuthorizedKeyFile(key)
if _, err2 := x.Delete(key); err2 != nil { }
return err2
} // 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 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. // GetPublicKeyByID returns public key by given ID.
func GetPublicKeyById(keyId int64) (*PublicKey, error) { func GetPublicKeyByID(keyID int64) (*PublicKey, error) {
key := new(PublicKey) key := new(PublicKey)
has, err := x.Id(keyId).Get(key) has, err := x.Id(keyID).Get(key)
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has { } else if !has {
return nil, ErrKeyNotExist return nil, ErrKeyNotExist{keyID}
} }
return key, nil return key, nil
} }
@ -334,16 +381,7 @@ func GetPublicKeyById(keyId int64) (*PublicKey, error) {
// ListPublicKeys returns a list of public keys belongs to given user. // ListPublicKeys returns a list of public keys belongs to given user.
func ListPublicKeys(uid int64) ([]*PublicKey, error) { func ListPublicKeys(uid int64) ([]*PublicKey, error) {
keys := make([]*PublicKey, 0, 5) keys := make([]*PublicKey, 0, 5)
err := x.Where("owner_id=?", uid).Find(&keys) return keys, 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
} }
// rewriteAuthorizedKeys finds and deletes corresponding line in authorized_keys file. // 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() defer fw.Close()
isFound := false isFound := false
keyword := fmt.Sprintf("key-%d", key.Id) keyword := fmt.Sprintf("key-%d", key.ID)
buf := bufio.NewReader(fr) buf := bufio.NewReader(fr)
for { for {
line, errRead := buf.ReadString('\n') line, errRead := buf.ReadString('\n')
@ -401,20 +439,19 @@ func rewriteAuthorizedKeys(key *PublicKey, p, tmpP string) error {
// UpdatePublicKey updates given public key. // UpdatePublicKey updates given public key.
func UpdatePublicKey(key *PublicKey) error { func UpdatePublicKey(key *PublicKey) error {
_, err := x.Id(key.Id).AllCols().Update(key) _, err := x.Id(key.ID).AllCols().Update(key)
return err return err
} }
// DeletePublicKey deletes SSH key information both in database and authorized_keys file. func deletePublicKey(e *xorm.Session, key *PublicKey) error {
func DeletePublicKey(key *PublicKey) error { has, err := e.Get(key)
has, err := x.Get(key)
if err != nil { if err != nil {
return err return err
} else if !has { } 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 return err
} }
@ -428,6 +465,21 @@ func DeletePublicKey(key *PublicKey) error {
return os.Rename(tmpPath, fpath) 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. // RewriteAllPublicKeys removes any authorized key and rewrite all keys from database again.
func RewriteAllPublicKeys() error { func RewriteAllPublicKeys() error {
sshOpLocker.Lock() sshOpLocker.Lock()
@ -461,3 +513,162 @@ func RewriteAllPublicKeys() error {
return nil 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. // Delete all SSH keys.
keys := make([]*PublicKey, 0, 10) 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 return err
} }
for _, key := range keys { for _, key := range keys {

View file

@ -126,8 +126,8 @@ func (f *ChangePasswordForm) Validate(ctx *macaron.Context, errs binding.Errors)
} }
type AddSSHKeyForm struct { type AddSSHKeyForm struct {
SSHTitle string `form:"title" binding:"Required"` Title string `binding:"Required;MaxSize(50)"`
Content string `form:"content" binding:"Required"` Content string `binding:"Required"`
} }
func (f *AddSSHKeyForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { 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; return false;
}); });
} }
// Settings
if ($('.repository.settings').length > 0) {
$('#add-deploy-key').click(function () {
$('#add-deploy-key-panel').show();
});
}
}; };
$(document).ready(function () { $(document).ready(function () {

View file

@ -6,3 +6,9 @@
display: inline-block; display: inline-block;
} }
} }
.ui.attached.header {
background: #f0f0f0;
.right {
margin-top: -5px;
}
}

View file

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

View file

@ -217,6 +217,36 @@
height: 200px; 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 { .edit-label.modal {

View file

@ -27,10 +27,11 @@ const (
SETTINGS_OPTIONS base.TplName = "repo/settings/options" SETTINGS_OPTIONS base.TplName = "repo/settings/options"
COLLABORATION base.TplName = "repo/settings/collaboration" COLLABORATION base.TplName = "repo/settings/collaboration"
HOOKS base.TplName = "repo/settings/hooks" 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" HOOK_NEW base.TplName = "repo/settings/hook_new"
ORG_HOOK_NEW base.TplName = "org/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) { 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) { func GitHooks(ctx *middleware.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings") ctx.Data["Title"] = ctx.Tr("repo.settings")
ctx.Data["PageIsSettingsGitHooks"] = true ctx.Data["PageIsSettingsGitHooks"] = true
@ -635,6 +640,70 @@ func GitHooksEditPost(ctx *middleware.Context) {
ctx.Redirect(ctx.Repo.RepoLink + "/settings/hooks/git") ctx.Redirect(ctx.Repo.RepoLink + "/settings/hooks/git")
} }
func TriggerHook(ctx *middleware.Context) { func SettingsDeployKeys(ctx *middleware.Context) {
models.HookQueue.AddRepoID(ctx.Repo.Repository.Id) 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 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) ctx.Handle(500, "DeletePublicKey", err)
} else { } else {
log.Trace("SSH key deleted: %s", ctx.User.Name) log.Trace("SSH key deleted: %s", ctx.User.Name)
@ -321,15 +321,8 @@ func SettingsSSHKeysPost(ctx *middleware.Context, form auth.AddSSHKeyForm) {
return return
} }
// Parse openssh style string from form content content, err := models.CheckPublicKeyString(form.Content)
content, err := models.ParseKeyString(form.Content)
if err != nil { 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 { if err == models.ErrKeyUnableVerify {
ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key")) ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
} else { } else {
@ -339,21 +332,19 @@ func SettingsSSHKeysPost(ctx *middleware.Context, form auth.AddSSHKeyForm) {
} }
} }
k := &models.PublicKey{ if err = models.AddPublicKey(ctx.User.Id, form.Title, content); err != nil {
OwnerId: ctx.User.Id, switch {
Name: form.SSHTitle, case models.IsErrKeyAlreadyExist(err):
Content: content, ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), SETTINGS_SSH_KEYS, &form)
} case models.IsErrKeyNameAlreadyUsed(err):
if err := models.AddPublicKey(k); err != nil { ctx.RenderWithErr(ctx.Tr("settings.ssh_key_name_used"), SETTINGS_SSH_KEYS, &form)
if err == models.ErrKeyAlreadyExist { default:
ctx.RenderWithErr(ctx.Tr("form.ssh_key_been_used"), SETTINGS_SSH_KEYS, &form) ctx.Handle(500, "AddPublicKey", err)
return
} }
ctx.Handle(500, "ssh.AddPublicKey", err)
return return
} else { } else {
log.Trace("SSH key added: %s", ctx.User.Name) 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") ctx.Redirect(setting.AppSubUrl + "/user/settings/ssh")
return return
} }

View file

@ -1 +1 @@
0.6.4.0805 Beta 0.6.4.0806 Beta

View file

@ -8,3 +8,8 @@
<p>{{.Flash.SuccessMsg}}</p> <p>{{.Flash.SuccessMsg}}</p>
</div> </div>
{{end}} {{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}} {{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> <li {{if .PageIsSettingsGitHooks}}class="current"{{end}}><a href="{{.RepoLink}}/settings/hooks/git">{{.i18n.Tr "repo.settings.githooks"}}</a></li>
{{end}} {{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> </ul>
</div> </div>
</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"> <form action="{{AppSubUrl}}/user/settings/ssh" method="post">
{{$.CsrfTokenHtml}} {{$.CsrfTokenHtml}}
<input name="_method" type="hidden" value="DELETE"> <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> <button class="right ssh-btn btn btn-red btn-radius btn-small">{{$.i18n.Tr "settings.delete_key"}}</button>
</form> </form>
</li> </li>