1// Copyright 2021 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package asymkey
6
7import (
8	"context"
9	"fmt"
10	"time"
11
12	"code.gitea.io/gitea/models/db"
13	"code.gitea.io/gitea/models/perm"
14	"code.gitea.io/gitea/modules/timeutil"
15
16	"xorm.io/builder"
17)
18
19// ________                .__                 ____  __.
20// \______ \   ____ ______ |  |   ____ ___.__.|    |/ _|____ ___.__.
21//  |    |  \_/ __ \\____ \|  |  /  _ <   |  ||      <_/ __ <   |  |
22//  |    `   \  ___/|  |_> >  |_(  <_> )___  ||    |  \  ___/\___  |
23// /_______  /\___  >   __/|____/\____// ____||____|__ \___  > ____|
24//         \/     \/|__|               \/             \/   \/\/
25//
26// This file contains functions specific to DeployKeys
27
28// DeployKey represents deploy key information and its relation with repository.
29type DeployKey struct {
30	ID          int64 `xorm:"pk autoincr"`
31	KeyID       int64 `xorm:"UNIQUE(s) INDEX"`
32	RepoID      int64 `xorm:"UNIQUE(s) INDEX"`
33	Name        string
34	Fingerprint string
35	Content     string `xorm:"-"`
36
37	Mode perm.AccessMode `xorm:"NOT NULL DEFAULT 1"`
38
39	CreatedUnix       timeutil.TimeStamp `xorm:"created"`
40	UpdatedUnix       timeutil.TimeStamp `xorm:"updated"`
41	HasRecentActivity bool               `xorm:"-"`
42	HasUsed           bool               `xorm:"-"`
43}
44
45// AfterLoad is invoked from XORM after setting the values of all fields of this object.
46func (key *DeployKey) AfterLoad() {
47	key.HasUsed = key.UpdatedUnix > key.CreatedUnix
48	key.HasRecentActivity = key.UpdatedUnix.AddDuration(7*24*time.Hour) > timeutil.TimeStampNow()
49}
50
51// GetContent gets associated public key content.
52func (key *DeployKey) GetContent() error {
53	pkey, err := GetPublicKeyByID(key.KeyID)
54	if err != nil {
55		return err
56	}
57	key.Content = pkey.Content
58	return nil
59}
60
61// IsReadOnly checks if the key can only be used for read operations, used by template
62func (key *DeployKey) IsReadOnly() bool {
63	return key.Mode == perm.AccessModeRead
64}
65
66func init() {
67	db.RegisterModel(new(DeployKey))
68}
69
70func checkDeployKey(e db.Engine, keyID, repoID int64, name string) error {
71	// Note: We want error detail, not just true or false here.
72	has, err := e.
73		Where("key_id = ? AND repo_id = ?", keyID, repoID).
74		Get(new(DeployKey))
75	if err != nil {
76		return err
77	} else if has {
78		return ErrDeployKeyAlreadyExist{keyID, repoID}
79	}
80
81	has, err = e.
82		Where("repo_id = ? AND name = ?", repoID, name).
83		Get(new(DeployKey))
84	if err != nil {
85		return err
86	} else if has {
87		return ErrDeployKeyNameAlreadyUsed{repoID, name}
88	}
89
90	return nil
91}
92
93// addDeployKey adds new key-repo relation.
94func addDeployKey(e db.Engine, keyID, repoID int64, name, fingerprint string, mode perm.AccessMode) (*DeployKey, error) {
95	if err := checkDeployKey(e, keyID, repoID, name); err != nil {
96		return nil, err
97	}
98
99	key := &DeployKey{
100		KeyID:       keyID,
101		RepoID:      repoID,
102		Name:        name,
103		Fingerprint: fingerprint,
104		Mode:        mode,
105	}
106	_, err := e.Insert(key)
107	return key, err
108}
109
110// HasDeployKey returns true if public key is a deploy key of given repository.
111func HasDeployKey(keyID, repoID int64) bool {
112	has, _ := db.GetEngine(db.DefaultContext).
113		Where("key_id = ? AND repo_id = ?", keyID, repoID).
114		Get(new(DeployKey))
115	return has
116}
117
118// AddDeployKey add new deploy key to database and authorized_keys file.
119func AddDeployKey(repoID int64, name, content string, readOnly bool) (*DeployKey, error) {
120	fingerprint, err := calcFingerprint(content)
121	if err != nil {
122		return nil, err
123	}
124
125	accessMode := perm.AccessModeRead
126	if !readOnly {
127		accessMode = perm.AccessModeWrite
128	}
129
130	ctx, committer, err := db.TxContext()
131	if err != nil {
132		return nil, err
133	}
134	defer committer.Close()
135
136	sess := db.GetEngine(ctx)
137
138	pkey := &PublicKey{
139		Fingerprint: fingerprint,
140	}
141	has, err := sess.Get(pkey)
142	if err != nil {
143		return nil, err
144	}
145
146	if has {
147		if pkey.Type != KeyTypeDeploy {
148			return nil, ErrKeyAlreadyExist{0, fingerprint, ""}
149		}
150	} else {
151		// First time use this deploy key.
152		pkey.Mode = accessMode
153		pkey.Type = KeyTypeDeploy
154		pkey.Content = content
155		pkey.Name = name
156		if err = addKey(sess, pkey); err != nil {
157			return nil, fmt.Errorf("addKey: %v", err)
158		}
159	}
160
161	key, err := addDeployKey(sess, pkey.ID, repoID, name, pkey.Fingerprint, accessMode)
162	if err != nil {
163		return nil, err
164	}
165
166	return key, committer.Commit()
167}
168
169// GetDeployKeyByID returns deploy key by given ID.
170func GetDeployKeyByID(ctx context.Context, id int64) (*DeployKey, error) {
171	key := new(DeployKey)
172	has, err := db.GetEngine(ctx).ID(id).Get(key)
173	if err != nil {
174		return nil, err
175	} else if !has {
176		return nil, ErrDeployKeyNotExist{id, 0, 0}
177	}
178	return key, nil
179}
180
181// GetDeployKeyByRepo returns deploy key by given public key ID and repository ID.
182func GetDeployKeyByRepo(keyID, repoID int64) (*DeployKey, error) {
183	return getDeployKeyByRepo(db.GetEngine(db.DefaultContext), keyID, repoID)
184}
185
186func getDeployKeyByRepo(e db.Engine, keyID, repoID int64) (*DeployKey, error) {
187	key := &DeployKey{
188		KeyID:  keyID,
189		RepoID: repoID,
190	}
191	has, err := e.Get(key)
192	if err != nil {
193		return nil, err
194	} else if !has {
195		return nil, ErrDeployKeyNotExist{0, keyID, repoID}
196	}
197	return key, nil
198}
199
200// UpdateDeployKeyCols updates deploy key information in the specified columns.
201func UpdateDeployKeyCols(key *DeployKey, cols ...string) error {
202	_, err := db.GetEngine(db.DefaultContext).ID(key.ID).Cols(cols...).Update(key)
203	return err
204}
205
206// ListDeployKeysOptions are options for ListDeployKeys
207type ListDeployKeysOptions struct {
208	db.ListOptions
209	RepoID      int64
210	KeyID       int64
211	Fingerprint string
212}
213
214func (opt ListDeployKeysOptions) toCond() builder.Cond {
215	cond := builder.NewCond()
216	if opt.RepoID != 0 {
217		cond = cond.And(builder.Eq{"repo_id": opt.RepoID})
218	}
219	if opt.KeyID != 0 {
220		cond = cond.And(builder.Eq{"key_id": opt.KeyID})
221	}
222	if opt.Fingerprint != "" {
223		cond = cond.And(builder.Eq{"fingerprint": opt.Fingerprint})
224	}
225	return cond
226}
227
228// ListDeployKeys returns a list of deploy keys matching the provided arguments.
229func ListDeployKeys(ctx context.Context, opts *ListDeployKeysOptions) ([]*DeployKey, error) {
230	sess := db.GetEngine(ctx).Where(opts.toCond())
231
232	if opts.Page != 0 {
233		sess = db.SetSessionPagination(sess, opts)
234
235		keys := make([]*DeployKey, 0, opts.PageSize)
236		return keys, sess.Find(&keys)
237	}
238
239	keys := make([]*DeployKey, 0, 5)
240	return keys, sess.Find(&keys)
241}
242
243// CountDeployKeys returns count deploy keys matching the provided arguments.
244func CountDeployKeys(opts *ListDeployKeysOptions) (int64, error) {
245	return db.GetEngine(db.DefaultContext).Where(opts.toCond()).Count(&DeployKey{})
246}
247