mirror of https://github.com/coder/coder.git
feat: Add user scoped git ssh keys (#834)
This commit is contained in:
parent
1e9e5f7c76
commit
9da17be61e
11
cli/start.go
11
cli/start.go
|
@ -31,6 +31,7 @@ import (
|
|||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/database/databasefake"
|
||||
"github.com/coder/coder/coderd/gitsshkey"
|
||||
"github.com/coder/coder/coderd/tunnel"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/provisioner/terraform"
|
||||
|
@ -57,6 +58,7 @@ func start() *cobra.Command {
|
|||
useTunnel bool
|
||||
traceDatadog bool
|
||||
secureAuthCookie bool
|
||||
sshKeygenAlgorithmRaw string
|
||||
)
|
||||
root := &cobra.Command{
|
||||
Use: "start",
|
||||
|
@ -126,6 +128,12 @@ func start() *cobra.Command {
|
|||
if err != nil {
|
||||
return xerrors.Errorf("parse access url %q: %w", accessURL, err)
|
||||
}
|
||||
|
||||
sshKeygenAlgorithm, err := gitsshkey.ParseAlgorithm(sshKeygenAlgorithmRaw)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("parse ssh keygen algorithm %s: %w", sshKeygenAlgorithmRaw, err)
|
||||
}
|
||||
|
||||
logger := slog.Make(sloghuman.Sink(os.Stderr))
|
||||
options := &coderd.Options{
|
||||
AccessURL: accessURLParsed,
|
||||
|
@ -134,6 +142,7 @@ func start() *cobra.Command {
|
|||
Pubsub: database.NewPubsubInMemory(),
|
||||
GoogleTokenValidator: validator,
|
||||
SecureAuthCookie: secureAuthCookie,
|
||||
SSHKeygenAlgorithm: sshKeygenAlgorithm,
|
||||
}
|
||||
|
||||
if !dev {
|
||||
|
@ -337,6 +346,8 @@ func start() *cobra.Command {
|
|||
_ = root.Flags().MarkHidden("tunnel")
|
||||
cliflag.BoolVarP(root.Flags(), &traceDatadog, "trace-datadog", "", "CODER_TRACE_DATADOG", false, "Send tracing data to a datadog agent")
|
||||
cliflag.BoolVarP(root.Flags(), &secureAuthCookie, "secure-auth-cookie", "", "CODER_SECURE_AUTH_COOKIE", false, "Specifies if the 'Secure' property is set on browser session cookies")
|
||||
cliflag.StringVarP(root.Flags(), &sshKeygenAlgorithmRaw, "ssh-keygen-algorithm", "", "CODER_SSH_KEYGEN_ALGORITHM", "ed25519", "Specifies the algorithm to use for generating ssh keys. "+
|
||||
`Accepted values are "ed25519", "ecdsa", or "rsa4096"`)
|
||||
|
||||
return root
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/coderd/awsidentity"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/gitsshkey"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/site"
|
||||
|
@ -30,7 +31,8 @@ type Options struct {
|
|||
AWSCertificates awsidentity.Certificates
|
||||
GoogleTokenValidator *idtoken.Validator
|
||||
|
||||
SecureAuthCookie bool
|
||||
SecureAuthCookie bool
|
||||
SSHKeygenAlgorithm gitsshkey.Algorithm
|
||||
}
|
||||
|
||||
// New constructs the Coder API into an HTTP handler.
|
||||
|
@ -146,6 +148,8 @@ func New(options *Options) (http.Handler, func()) {
|
|||
r.Get("/", api.workspacesByUser)
|
||||
r.Get("/{workspacename}", api.workspaceByUserAndName)
|
||||
})
|
||||
r.Get("/gitsshkey", api.gitSSHKey)
|
||||
r.Put("/gitsshkey", api.regenerateGitSSHKey)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -157,6 +161,7 @@ func New(options *Options) (http.Handler, func()) {
|
|||
r.Route("/agent", func(r chi.Router) {
|
||||
r.Use(httpmw.ExtractWorkspaceAgent(options.Database))
|
||||
r.Get("/", api.workspaceAgentListen)
|
||||
r.Get("/gitsshkey", api.agentGitSSHKey)
|
||||
})
|
||||
r.Route("/{workspaceresource}", func(r chi.Router) {
|
||||
r.Use(
|
||||
|
|
|
@ -38,6 +38,7 @@ import (
|
|||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/database/databasefake"
|
||||
"github.com/coder/coder/coderd/database/postgres"
|
||||
"github.com/coder/coder/coderd/gitsshkey"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/cryptorand"
|
||||
"github.com/coder/coder/provisioner/echo"
|
||||
|
@ -49,6 +50,7 @@ import (
|
|||
type Options struct {
|
||||
AWSInstanceIdentity awsidentity.Certificates
|
||||
GoogleInstanceIdentity *idtoken.Validator
|
||||
SSHKeygenAlgorithm gitsshkey.Algorithm
|
||||
}
|
||||
|
||||
// New constructs an in-memory coderd instance and returns
|
||||
|
@ -98,6 +100,12 @@ func New(t *testing.T, options *Options) *codersdk.Client {
|
|||
serverURL, err := url.Parse(srv.URL)
|
||||
require.NoError(t, err)
|
||||
var closeWait func()
|
||||
|
||||
// match default with cli default
|
||||
if options.SSHKeygenAlgorithm == "" {
|
||||
options.SSHKeygenAlgorithm = gitsshkey.AlgorithmEd25519
|
||||
}
|
||||
|
||||
// We set the handler after server creation for the access URL.
|
||||
srv.Config.Handler, closeWait = coderd.New(&coderd.Options{
|
||||
AgentConnectionUpdateFrequency: 25 * time.Millisecond,
|
||||
|
@ -108,6 +116,7 @@ func New(t *testing.T, options *Options) *codersdk.Client {
|
|||
|
||||
AWSCertificates: options.AWSInstanceIdentity,
|
||||
GoogleTokenValidator: options.GoogleInstanceIdentity,
|
||||
SSHKeygenAlgorithm: options.SSHKeygenAlgorithm,
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
srv.Close()
|
||||
|
|
|
@ -31,6 +31,7 @@ func New() database.Store {
|
|||
provisionerJobResource: make([]database.WorkspaceResource, 0),
|
||||
workspaceBuild: make([]database.WorkspaceBuild, 0),
|
||||
provisionerJobAgent: make([]database.WorkspaceAgent, 0),
|
||||
GitSSHKey: make([]database.GitSSHKey, 0),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -57,6 +58,7 @@ type fakeQuerier struct {
|
|||
provisionerJobLog []database.ProvisionerJobLog
|
||||
workspace []database.Workspace
|
||||
workspaceBuild []database.WorkspaceBuild
|
||||
GitSSHKey []database.GitSSHKey
|
||||
}
|
||||
|
||||
// InTx doesn't rollback data properly for in-memory yet.
|
||||
|
@ -1239,3 +1241,63 @@ func (q *fakeQuerier) UpdateWorkspaceDeletedByID(_ context.Context, arg database
|
|||
}
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) InsertGitSSHKey(_ context.Context, arg database.InsertGitSSHKeyParams) (database.GitSSHKey, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
//nolint:gosimple
|
||||
gitSSHKey := database.GitSSHKey{
|
||||
UserID: arg.UserID,
|
||||
CreatedAt: arg.CreatedAt,
|
||||
UpdatedAt: arg.UpdatedAt,
|
||||
PrivateKey: arg.PrivateKey,
|
||||
PublicKey: arg.PublicKey,
|
||||
}
|
||||
q.GitSSHKey = append(q.GitSSHKey, gitSSHKey)
|
||||
return gitSSHKey, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetGitSSHKey(_ context.Context, userID uuid.UUID) (database.GitSSHKey, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, key := range q.GitSSHKey {
|
||||
if key.UserID == userID {
|
||||
return key, nil
|
||||
}
|
||||
}
|
||||
return database.GitSSHKey{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) UpdateGitSSHKey(_ context.Context, arg database.UpdateGitSSHKeyParams) error {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for index, key := range q.GitSSHKey {
|
||||
if key.UserID.String() != arg.UserID.String() {
|
||||
continue
|
||||
}
|
||||
key.UpdatedAt = arg.UpdatedAt
|
||||
key.PrivateKey = arg.PrivateKey
|
||||
key.PublicKey = arg.PublicKey
|
||||
q.GitSSHKey[index] = key
|
||||
return nil
|
||||
}
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) DeleteGitSSHKey(_ context.Context, userID uuid.UUID) error {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for index, key := range q.GitSSHKey {
|
||||
if key.UserID.String() != userID.String() {
|
||||
continue
|
||||
}
|
||||
q.GitSSHKey[index] = q.GitSSHKey[len(q.GitSSHKey)-1]
|
||||
q.GitSSHKey = q.GitSSHKey[:len(q.GitSSHKey)-1]
|
||||
return nil
|
||||
}
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
|
|
@ -89,6 +89,14 @@ CREATE TABLE files (
|
|||
data bytea NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE gitsshkeys (
|
||||
user_id uuid NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
private_key text NOT NULL,
|
||||
public_key text NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE licenses (
|
||||
id integer NOT NULL,
|
||||
license jsonb NOT NULL,
|
||||
|
@ -283,6 +291,9 @@ ALTER TABLE ONLY api_keys
|
|||
ALTER TABLE ONLY files
|
||||
ADD CONSTRAINT files_pkey PRIMARY KEY (hash);
|
||||
|
||||
ALTER TABLE ONLY gitsshkeys
|
||||
ADD CONSTRAINT gitsshkeys_pkey PRIMARY KEY (user_id);
|
||||
|
||||
ALTER TABLE ONLY licenses
|
||||
ADD CONSTRAINT licenses_pkey PRIMARY KEY (id);
|
||||
|
||||
|
@ -379,6 +390,9 @@ CREATE UNIQUE INDEX workspaces_owner_id_name_idx ON workspaces USING btree (owne
|
|||
ALTER TABLE ONLY api_keys
|
||||
ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY gitsshkeys
|
||||
ADD CONSTRAINT gitsshkeys_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
|
||||
|
||||
ALTER TABLE ONLY organization_members
|
||||
ADD CONSTRAINT organization_members_organization_id_uuid_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE gitsshkeys;
|
|
@ -0,0 +1,7 @@
|
|||
CREATE TABLE IF NOT EXISTS gitsshkeys (
|
||||
user_id uuid PRIMARY KEY NOT NULL REFERENCES users (id),
|
||||
created_at timestamptz NOT NULL,
|
||||
updated_at timestamptz NOT NULL,
|
||||
private_key text NOT NULL,
|
||||
public_key text NOT NULL
|
||||
);
|
|
@ -255,6 +255,14 @@ type File struct {
|
|||
Data []byte `db:"data" json:"data"`
|
||||
}
|
||||
|
||||
type GitSSHKey struct {
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
PrivateKey string `db:"private_key" json:"private_key"`
|
||||
PublicKey string `db:"public_key" json:"public_key"`
|
||||
}
|
||||
|
||||
type License struct {
|
||||
ID int32 `db:"id" json:"id"`
|
||||
License json.RawMessage `db:"license" json:"license"`
|
||||
|
|
|
@ -10,9 +10,11 @@ import (
|
|||
|
||||
type querier interface {
|
||||
AcquireProvisionerJob(ctx context.Context, arg AcquireProvisionerJobParams) (ProvisionerJob, error)
|
||||
DeleteGitSSHKey(ctx context.Context, userID uuid.UUID) error
|
||||
DeleteParameterValueByID(ctx context.Context, id uuid.UUID) error
|
||||
GetAPIKeyByID(ctx context.Context, id string) (APIKey, error)
|
||||
GetFileByHash(ctx context.Context, hash string) (File, error)
|
||||
GetGitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error)
|
||||
GetOrganizationByID(ctx context.Context, id uuid.UUID) (Organization, error)
|
||||
GetOrganizationByName(ctx context.Context, name string) (Organization, error)
|
||||
GetOrganizationMemberByUserID(ctx context.Context, arg GetOrganizationMemberByUserIDParams) (OrganizationMember, error)
|
||||
|
@ -54,6 +56,7 @@ type querier interface {
|
|||
GetWorkspacesByUserID(ctx context.Context, arg GetWorkspacesByUserIDParams) ([]Workspace, error)
|
||||
InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error)
|
||||
InsertFile(ctx context.Context, arg InsertFileParams) (File, error)
|
||||
InsertGitSSHKey(ctx context.Context, arg InsertGitSSHKeyParams) (GitSSHKey, error)
|
||||
InsertOrganization(ctx context.Context, arg InsertOrganizationParams) (Organization, error)
|
||||
InsertOrganizationMember(ctx context.Context, arg InsertOrganizationMemberParams) (OrganizationMember, error)
|
||||
InsertParameterSchema(ctx context.Context, arg InsertParameterSchemaParams) (ParameterSchema, error)
|
||||
|
@ -69,6 +72,7 @@ type querier interface {
|
|||
InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) (WorkspaceBuild, error)
|
||||
InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error)
|
||||
UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error
|
||||
UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyParams) error
|
||||
UpdateProjectActiveVersionByID(ctx context.Context, arg UpdateProjectActiveVersionByIDParams) error
|
||||
UpdateProjectDeletedByID(ctx context.Context, arg UpdateProjectDeletedByIDParams) error
|
||||
UpdateProjectVersionByID(ctx context.Context, arg UpdateProjectVersionByIDParams) error
|
||||
|
|
|
@ -235,6 +235,108 @@ func (q *sqlQuerier) InsertFile(ctx context.Context, arg InsertFileParams) (File
|
|||
return i, err
|
||||
}
|
||||
|
||||
const deleteGitSSHKey = `-- name: DeleteGitSSHKey :exec
|
||||
DELETE FROM
|
||||
gitsshkeys
|
||||
WHERE
|
||||
user_id = $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) DeleteGitSSHKey(ctx context.Context, userID uuid.UUID) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteGitSSHKey, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
const getGitSSHKey = `-- name: GetGitSSHKey :one
|
||||
SELECT
|
||||
user_id, created_at, updated_at, private_key, public_key
|
||||
FROM
|
||||
gitsshkeys
|
||||
WHERE
|
||||
user_id = $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetGitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error) {
|
||||
row := q.db.QueryRowContext(ctx, getGitSSHKey, userID)
|
||||
var i GitSSHKey
|
||||
err := row.Scan(
|
||||
&i.UserID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.PrivateKey,
|
||||
&i.PublicKey,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const insertGitSSHKey = `-- name: InsertGitSSHKey :one
|
||||
INSERT INTO
|
||||
gitsshkeys (
|
||||
user_id,
|
||||
created_at,
|
||||
updated_at,
|
||||
private_key,
|
||||
public_key
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5) RETURNING user_id, created_at, updated_at, private_key, public_key
|
||||
`
|
||||
|
||||
type InsertGitSSHKeyParams struct {
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
PrivateKey string `db:"private_key" json:"private_key"`
|
||||
PublicKey string `db:"public_key" json:"public_key"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertGitSSHKey(ctx context.Context, arg InsertGitSSHKeyParams) (GitSSHKey, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertGitSSHKey,
|
||||
arg.UserID,
|
||||
arg.CreatedAt,
|
||||
arg.UpdatedAt,
|
||||
arg.PrivateKey,
|
||||
arg.PublicKey,
|
||||
)
|
||||
var i GitSSHKey
|
||||
err := row.Scan(
|
||||
&i.UserID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.PrivateKey,
|
||||
&i.PublicKey,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateGitSSHKey = `-- name: UpdateGitSSHKey :exec
|
||||
UPDATE
|
||||
gitsshkeys
|
||||
SET
|
||||
updated_at = $2,
|
||||
private_key = $3,
|
||||
public_key = $4
|
||||
WHERE
|
||||
user_id = $1
|
||||
`
|
||||
|
||||
type UpdateGitSSHKeyParams struct {
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
PrivateKey string `db:"private_key" json:"private_key"`
|
||||
PublicKey string `db:"public_key" json:"public_key"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateGitSSHKey,
|
||||
arg.UserID,
|
||||
arg.UpdatedAt,
|
||||
arg.PrivateKey,
|
||||
arg.PublicKey,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const getOrganizationMemberByUserID = `-- name: GetOrganizationMemberByUserID :one
|
||||
SELECT
|
||||
user_id, organization_id, created_at, updated_at, roles
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
-- name: InsertGitSSHKey :one
|
||||
INSERT INTO
|
||||
gitsshkeys (
|
||||
user_id,
|
||||
created_at,
|
||||
updated_at,
|
||||
private_key,
|
||||
public_key
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5) RETURNING *;
|
||||
|
||||
-- name: GetGitSSHKey :one
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
gitsshkeys
|
||||
WHERE
|
||||
user_id = $1;
|
||||
|
||||
-- name: UpdateGitSSHKey :exec
|
||||
UPDATE
|
||||
gitsshkeys
|
||||
SET
|
||||
updated_at = $2,
|
||||
private_key = $3,
|
||||
public_key = $4
|
||||
WHERE
|
||||
user_id = $1;
|
||||
|
||||
-- name: DeleteGitSSHKey :exec
|
||||
DELETE FROM
|
||||
gitsshkeys
|
||||
WHERE
|
||||
user_id = $1;
|
|
@ -27,3 +27,4 @@ rename:
|
|||
oidc_refresh_token: OIDCRefreshToken
|
||||
parameter_type_system_hcl: ParameterTypeSystemHCL
|
||||
userstatus: UserStatus
|
||||
gitsshkey: GitSSHKey
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
package coderd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/gitsshkey"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
func (api *api) regenerateGitSSHKey(rw http.ResponseWriter, r *http.Request) {
|
||||
user := httpmw.UserParam(r)
|
||||
privateKey, publicKey, err := gitsshkey.Generate(api.SSHKeygenAlgorithm)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("regenerate key pair: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err = api.Database.UpdateGitSSHKey(r.Context(), database.UpdateGitSSHKeyParams{
|
||||
UserID: user.ID,
|
||||
UpdatedAt: database.Now(),
|
||||
PrivateKey: privateKey,
|
||||
PublicKey: publicKey,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("update git SSH key: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
newKey, err := api.Database.GetGitSSHKey(r.Context(), user.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get git SSH key: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(rw, r, codersdk.GitSSHKey{
|
||||
UserID: newKey.UserID,
|
||||
CreatedAt: newKey.CreatedAt,
|
||||
UpdatedAt: newKey.UpdatedAt,
|
||||
// No need to return the private key to the user
|
||||
PublicKey: newKey.PublicKey,
|
||||
})
|
||||
}
|
||||
|
||||
func (api *api) gitSSHKey(rw http.ResponseWriter, r *http.Request) {
|
||||
user := httpmw.UserParam(r)
|
||||
gitSSHKey, err := api.Database.GetGitSSHKey(r.Context(), user.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("update git SSH key: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(rw, r, codersdk.GitSSHKey{
|
||||
UserID: gitSSHKey.UserID,
|
||||
CreatedAt: gitSSHKey.CreatedAt,
|
||||
UpdatedAt: gitSSHKey.UpdatedAt,
|
||||
// No need to return the private key to the user
|
||||
PublicKey: gitSSHKey.PublicKey,
|
||||
})
|
||||
}
|
||||
|
||||
func (api *api) agentGitSSHKey(rw http.ResponseWriter, r *http.Request) {
|
||||
agent := httpmw.WorkspaceAgent(r)
|
||||
resource, err := api.Database.GetWorkspaceResourceByID(r.Context(), agent.ResourceID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("getting workspace resources: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
job, err := api.Database.GetWorkspaceBuildByJobID(r.Context(), resource.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("getting workspace build: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
workspace, err := api.Database.GetWorkspaceByID(r.Context(), job.WorkspaceID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("getting workspace: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
gitSSHKey, err := api.Database.GetGitSSHKey(r.Context(), workspace.OwnerID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("getting git SSH key: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(rw, r, codersdk.AgentGitSSHKey{
|
||||
UserID: gitSSHKey.UserID,
|
||||
CreatedAt: gitSSHKey.CreatedAt,
|
||||
UpdatedAt: gitSSHKey.UpdatedAt,
|
||||
PrivateKey: gitSSHKey.PrivateKey,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
// This file contains an adapted version of the original implementation available
|
||||
// under the following URL: https://github.com/mikesmitty/edkey/blob/3356ea4e686a1d47ae5d2d4c3cbc1832ce2df626/edkey.go
|
||||
|
||||
// The following changes have been made:
|
||||
// * Replaced usage of math/rand with crypto/rand
|
||||
|
||||
// This should be removed soon as support for marshaling ED25519 private keys
|
||||
// is added to the Golang standard library.
|
||||
// See: https://github.com/golang/go/issues/37132
|
||||
|
||||
// --- BEGIN ORIGINAL LICENSE ---
|
||||
// MIT License
|
||||
|
||||
// Copyright (c) 2017 Michael Smith
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
// --- END ORIGINAL LICENSE ---
|
||||
|
||||
package gitsshkey
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
|
||||
"golang.org/x/crypto/ed25519"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
func MarshalED25519PrivateKey(key ed25519.PrivateKey) ([]byte, error) {
|
||||
// Add our key header (followed by a null byte)
|
||||
magic := append([]byte("openssh-key-v1"), 0)
|
||||
|
||||
var msg struct {
|
||||
CipherName string
|
||||
KdfName string
|
||||
KdfOpts string
|
||||
NumKeys uint32
|
||||
PubKey []byte
|
||||
PrivKeyBlock []byte
|
||||
}
|
||||
|
||||
// Fill out the private key fields
|
||||
pk1 := struct {
|
||||
Check1 uint32
|
||||
Check2 uint32
|
||||
Keytype string
|
||||
Pub []byte
|
||||
Priv []byte
|
||||
Comment string
|
||||
Pad []byte `ssh:"rest"`
|
||||
}{}
|
||||
|
||||
// Random check bytes
|
||||
var check uint32
|
||||
if err := binary.Read(rand.Reader, binary.BigEndian, &check); err != nil {
|
||||
return nil, xerrors.Errorf("generate random bytes: %w", err)
|
||||
}
|
||||
|
||||
pk1.Check1 = check
|
||||
pk1.Check2 = check
|
||||
|
||||
// Set our key type
|
||||
pk1.Keytype = ssh.KeyAlgoED25519
|
||||
|
||||
// Add the pubkey to the optionally-encrypted block
|
||||
pk, ok := key.Public().(ed25519.PublicKey)
|
||||
if !ok {
|
||||
return nil, xerrors.Errorf("ed25519.PublicKey type assertion failed on an ed25519 public key")
|
||||
}
|
||||
pubKey := []byte(pk)
|
||||
pk1.Pub = pubKey
|
||||
|
||||
// Add our private key
|
||||
pk1.Priv = []byte(key)
|
||||
|
||||
// Might be useful to put something in here at some point
|
||||
pk1.Comment = ""
|
||||
|
||||
// Add some padding to match the encryption block size within PrivKeyBlock (without Pad field)
|
||||
// 8 doesn't match the documentation, but that's what ssh-keygen uses for unencrypted keys. *shrug*
|
||||
bs := 8
|
||||
blockLen := len(ssh.Marshal(pk1))
|
||||
padLen := (bs - (blockLen % bs)) % bs
|
||||
pk1.Pad = make([]byte, padLen)
|
||||
|
||||
// Padding is a sequence of bytes like: 1, 2, 3...
|
||||
for i := 0; i < padLen; i++ {
|
||||
pk1.Pad[i] = byte(i + 1)
|
||||
}
|
||||
|
||||
// Generate the pubkey prefix "\0\0\0\nssh-ed25519\0\0\0 "
|
||||
prefix := []byte{0x0, 0x0, 0x0, 0x0b}
|
||||
prefix = append(prefix, []byte(ssh.KeyAlgoED25519)...)
|
||||
prefix = append(prefix, []byte{0x0, 0x0, 0x0, 0x20}...)
|
||||
prefix = append(prefix, pubKey...)
|
||||
|
||||
// Only going to support unencrypted keys for now
|
||||
msg.CipherName = "none"
|
||||
msg.KdfName = "none"
|
||||
msg.KdfOpts = ""
|
||||
msg.NumKeys = 1
|
||||
msg.PubKey = prefix
|
||||
msg.PrivKeyBlock = ssh.Marshal(pk1)
|
||||
|
||||
magic = append(magic, ssh.Marshal(msg)...)
|
||||
|
||||
return magic, nil
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
package gitsshkey
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
type Algorithm string
|
||||
|
||||
const (
|
||||
// AlgorithmEd25519 is the Edwards-curve Digital Signature Algorithm using Curve25519
|
||||
AlgorithmEd25519 Algorithm = "ed25519"
|
||||
// AlgorithmECDSA is the Digital Signature Algorithm (DSA) using NIST Elliptic Curve
|
||||
AlgorithmECDSA Algorithm = "ecdsa"
|
||||
// AlgorithmRSA4096 is the venerable Rivest-Shamir-Adleman algorithm
|
||||
// and creates a key with a fixed size of 4096-bit.
|
||||
AlgorithmRSA4096 Algorithm = "rsa4096"
|
||||
)
|
||||
|
||||
// ParseAlgorithm returns a valid Algorithm or error if input is not a valid.
|
||||
func ParseAlgorithm(t string) (Algorithm, error) {
|
||||
ok := []string{
|
||||
string(AlgorithmEd25519),
|
||||
string(AlgorithmECDSA),
|
||||
string(AlgorithmRSA4096),
|
||||
}
|
||||
|
||||
for _, a := range ok {
|
||||
if strings.EqualFold(a, t) {
|
||||
return Algorithm(a), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", xerrors.Errorf(`invalid key type: %s, must be one of: %s`, t, strings.Join(ok, ","))
|
||||
}
|
||||
|
||||
// Generate creates a private key in the OpenSSH PEM format and public key in
|
||||
// the authorized key format.
|
||||
func Generate(algo Algorithm) (privateKey string, publicKey string, err error) {
|
||||
switch algo {
|
||||
case AlgorithmEd25519:
|
||||
return ed25519KeyGen()
|
||||
case AlgorithmECDSA:
|
||||
return ecdsaKeyGen()
|
||||
case AlgorithmRSA4096:
|
||||
return rsa4096KeyGen()
|
||||
default:
|
||||
return "", "", xerrors.Errorf("invalid algorithm: %s", algo)
|
||||
}
|
||||
}
|
||||
|
||||
// ed25519KeyGen returns an ED25519-based SSH private key.
|
||||
func ed25519KeyGen() (privateKey string, publicKey string, err error) {
|
||||
_, privateKeyRaw, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return "", "", xerrors.Errorf("generate ed25519 private key: %w", err)
|
||||
}
|
||||
|
||||
// NOTE: as of the time of writing, x/crypto/ssh is unable to marshal an ED25519 private key
|
||||
// into the format expected by OpenSSH. See: https://github.com/golang/go/issues/37132
|
||||
// Until this support is added, using a third-party implementation.
|
||||
byt, err := MarshalED25519PrivateKey(privateKeyRaw)
|
||||
if err != nil {
|
||||
return "", "", xerrors.Errorf("marshal ed25519 private key: %w", err)
|
||||
}
|
||||
|
||||
return generateKeys(pem.Block{
|
||||
Type: "OPENSSH PRIVATE KEY",
|
||||
Bytes: byt,
|
||||
}, privateKeyRaw)
|
||||
}
|
||||
|
||||
// ecdsaKeyGen returns an ECDSA-based SSH private key.
|
||||
func ecdsaKeyGen() (privateKey string, publicKey string, err error) {
|
||||
privateKeyRaw, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return "", "", xerrors.Errorf("generate ecdsa private key: %w", err)
|
||||
}
|
||||
byt, err := x509.MarshalECPrivateKey(privateKeyRaw)
|
||||
if err != nil {
|
||||
return "", "", xerrors.Errorf("marshal private key: %w", err)
|
||||
}
|
||||
|
||||
return generateKeys(pem.Block{
|
||||
Type: "EC PRIVATE KEY",
|
||||
Bytes: byt,
|
||||
}, privateKeyRaw)
|
||||
}
|
||||
|
||||
// rsaKeyGen returns an RSA-based SSH private key of size 4096.
|
||||
//
|
||||
// Administrators may configure this for SSH key compatibility with Azure DevOps.
|
||||
func rsa4096KeyGen() (privateKey string, publicKey string, err error) {
|
||||
privateKeyRaw, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
return "", "", xerrors.Errorf("generate RSA4096 private key: %w", err)
|
||||
}
|
||||
|
||||
return generateKeys(pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(privateKeyRaw),
|
||||
}, privateKeyRaw)
|
||||
}
|
||||
|
||||
func generateKeys(block pem.Block, cp crypto.Signer) (privateKey string, publicKey string, err error) {
|
||||
pkBytes := pem.EncodeToMemory(&block)
|
||||
privateKey = string(pkBytes)
|
||||
|
||||
publicKeyRaw := cp.Public()
|
||||
p, err := ssh.NewPublicKey(publicKeyRaw)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
publicKey = string(ssh.MarshalAuthorizedKey(p))
|
||||
|
||||
return privateKey, publicKey, nil
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package gitsshkey_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/coder/coder/coderd/gitsshkey"
|
||||
"github.com/coder/coder/cryptorand"
|
||||
)
|
||||
|
||||
func TestGitSSHKeys(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
verifyKeyPair := func(t *testing.T, private, public string) {
|
||||
signer, err := ssh.ParsePrivateKey([]byte(private))
|
||||
require.NoError(t, err)
|
||||
p, err := ssh.ParsePublicKey(signer.PublicKey().Marshal())
|
||||
require.NoError(t, err)
|
||||
publicKey := string(ssh.MarshalAuthorizedKey(p))
|
||||
require.Equal(t, publicKey, public)
|
||||
}
|
||||
|
||||
t.Run("Ed25519", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
pv, pb, err := gitsshkey.Generate(gitsshkey.AlgorithmEd25519)
|
||||
require.NoError(t, err)
|
||||
verifyKeyPair(t, pv, pb)
|
||||
})
|
||||
t.Run("ECDSA", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
pv, pb, err := gitsshkey.Generate(gitsshkey.AlgorithmECDSA)
|
||||
require.NoError(t, err)
|
||||
verifyKeyPair(t, pv, pb)
|
||||
})
|
||||
t.Run("RSA4096", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
pv, pb, err := gitsshkey.Generate(gitsshkey.AlgorithmRSA4096)
|
||||
require.NoError(t, err)
|
||||
verifyKeyPair(t, pv, pb)
|
||||
})
|
||||
t.Run("ParseAlgorithm", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := gitsshkey.ParseAlgorithm(string(gitsshkey.AlgorithmEd25519))
|
||||
require.NoError(t, err)
|
||||
_, err = gitsshkey.ParseAlgorithm(string(gitsshkey.AlgorithmECDSA))
|
||||
require.NoError(t, err)
|
||||
_, err = gitsshkey.ParseAlgorithm(string(gitsshkey.AlgorithmRSA4096))
|
||||
require.NoError(t, err)
|
||||
r, _ := cryptorand.String(6)
|
||||
_, err = gitsshkey.ParseAlgorithm(r)
|
||||
require.Error(t, err, "random string should fail")
|
||||
_, err = gitsshkey.ParseAlgorithm("")
|
||||
require.Error(t, err, "empty string should fail")
|
||||
})
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
package coderd_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/coderd/gitsshkey"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/provisioner/echo"
|
||||
"github.com/coder/coder/provisionersdk/proto"
|
||||
)
|
||||
|
||||
func TestGitSSHKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("None", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
client := coderdtest.New(t, nil)
|
||||
res := coderdtest.CreateFirstUser(t, client)
|
||||
key, err := client.GitSSHKey(ctx, res.UserID)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, key.PublicKey)
|
||||
})
|
||||
t.Run("Ed25519", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
SSHKeygenAlgorithm: gitsshkey.AlgorithmEd25519,
|
||||
})
|
||||
res := coderdtest.CreateFirstUser(t, client)
|
||||
key, err := client.GitSSHKey(ctx, res.UserID)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, key.PublicKey)
|
||||
})
|
||||
t.Run("ECDSA", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
SSHKeygenAlgorithm: gitsshkey.AlgorithmECDSA,
|
||||
})
|
||||
res := coderdtest.CreateFirstUser(t, client)
|
||||
key, err := client.GitSSHKey(ctx, res.UserID)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, key.PublicKey)
|
||||
})
|
||||
t.Run("RSA4096", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
SSHKeygenAlgorithm: gitsshkey.AlgorithmRSA4096,
|
||||
})
|
||||
res := coderdtest.CreateFirstUser(t, client)
|
||||
key, err := client.GitSSHKey(ctx, res.UserID)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, key.PublicKey)
|
||||
})
|
||||
t.Run("Regenerate", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
SSHKeygenAlgorithm: gitsshkey.AlgorithmEd25519,
|
||||
})
|
||||
res := coderdtest.CreateFirstUser(t, client)
|
||||
key1, err := client.GitSSHKey(ctx, res.UserID)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, key1.PublicKey)
|
||||
key2, err := client.RegenerateGitSSHKey(ctx, res.UserID)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, key2.UpdatedAt, key1.UpdatedAt)
|
||||
require.NotEmpty(t, key2.PublicKey)
|
||||
require.NotEqual(t, key2.PublicKey, key1.PublicKey)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAgentGitSSHKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
agentClient := func(algo gitsshkey.Algorithm) *codersdk.Client {
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
SSHKeygenAlgorithm: algo,
|
||||
})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
daemonCloser := coderdtest.NewProvisionerDaemon(t, client)
|
||||
authToken := uuid.NewString()
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionDryRun: echo.ProvisionComplete,
|
||||
Provision: []*proto.Provision_Response{{
|
||||
Type: &proto.Provision_Response_Complete{
|
||||
Complete: &proto.Provision_Complete{
|
||||
Resources: []*proto.Resource{{
|
||||
Name: "example",
|
||||
Type: "aws_instance",
|
||||
Agent: &proto.Agent{
|
||||
Id: uuid.NewString(),
|
||||
Auth: &proto.Agent_Token{
|
||||
Token: authToken,
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
})
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
daemonCloser.Close()
|
||||
|
||||
agentClient := codersdk.New(client.URL)
|
||||
agentClient.SessionToken = authToken
|
||||
|
||||
return agentClient
|
||||
}
|
||||
|
||||
t.Run("AgentKey", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
client := agentClient(gitsshkey.AlgorithmEd25519)
|
||||
agentKey, err := client.AgentGitSSHKey(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, agentKey.PrivateKey)
|
||||
})
|
||||
}
|
|
@ -16,6 +16,7 @@ import (
|
|||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/gitsshkey"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/coderd/userpassword"
|
||||
|
@ -80,7 +81,7 @@ func (api *api) postFirstUser(rw http.ResponseWriter, r *http.Request) {
|
|||
// Create the user, organization, and membership to the user.
|
||||
var user database.User
|
||||
var organization database.Organization
|
||||
err = api.Database.InTx(func(s database.Store) error {
|
||||
err = api.Database.InTx(func(db database.Store) error {
|
||||
user, err = api.Database.InsertUser(r.Context(), database.InsertUserParams{
|
||||
ID: uuid.New(),
|
||||
Email: createUser.Email,
|
||||
|
@ -93,6 +94,22 @@ func (api *api) postFirstUser(rw http.ResponseWriter, r *http.Request) {
|
|||
if err != nil {
|
||||
return xerrors.Errorf("create user: %w", err)
|
||||
}
|
||||
|
||||
privateKey, publicKey, err := gitsshkey.Generate(api.SSHKeygenAlgorithm)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("generate user gitsshkey: %w", err)
|
||||
}
|
||||
_, err = db.InsertGitSSHKey(r.Context(), database.InsertGitSSHKeyParams{
|
||||
UserID: user.ID,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
PrivateKey: privateKey,
|
||||
PublicKey: publicKey,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert user gitsshkey: %w", err)
|
||||
}
|
||||
|
||||
organization, err = api.Database.InsertOrganization(r.Context(), database.InsertOrganizationParams{
|
||||
ID: uuid.New(),
|
||||
Name: createUser.OrganizationName,
|
||||
|
@ -206,6 +223,22 @@ func (api *api) postUsers(rw http.ResponseWriter, r *http.Request) {
|
|||
if err != nil {
|
||||
return xerrors.Errorf("create user: %w", err)
|
||||
}
|
||||
|
||||
privateKey, publicKey, err := gitsshkey.Generate(api.SSHKeygenAlgorithm)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("generate user gitsshkey: %w", err)
|
||||
}
|
||||
_, err = db.InsertGitSSHKey(r.Context(), database.InsertGitSSHKeyParams{
|
||||
UserID: user.ID,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
PrivateKey: privateKey,
|
||||
PublicKey: publicKey,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert user gitsshkey: %w", err)
|
||||
}
|
||||
|
||||
_, err = db.InsertOrganizationMember(r.Context(), database.InsertOrganizationMemberParams{
|
||||
OrganizationID: organization.ID,
|
||||
UserID: user.ID,
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
package codersdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
type GitSSHKey struct {
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
PublicKey string `json:"public_key"`
|
||||
}
|
||||
|
||||
type AgentGitSSHKey struct {
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
}
|
||||
|
||||
// GitSSHKey returns the user's git SSH public key.
|
||||
func (c *Client) GitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/gitsshkey", userID.String()), nil)
|
||||
if err != nil {
|
||||
return GitSSHKey{}, xerrors.Errorf("execute request: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return GitSSHKey{}, readBodyAsError(res)
|
||||
}
|
||||
|
||||
var gitsshkey GitSSHKey
|
||||
return gitsshkey, json.NewDecoder(res.Body).Decode(&gitsshkey)
|
||||
}
|
||||
|
||||
// RegenerateGitSSHKey will create a new SSH key pair for the user and return it.
|
||||
func (c *Client) RegenerateGitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error) {
|
||||
res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/gitsshkey", userID.String()), nil)
|
||||
if err != nil {
|
||||
return GitSSHKey{}, xerrors.Errorf("execute request: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return GitSSHKey{}, readBodyAsError(res)
|
||||
}
|
||||
|
||||
var gitsshkey GitSSHKey
|
||||
return gitsshkey, json.NewDecoder(res.Body).Decode(&gitsshkey)
|
||||
}
|
||||
|
||||
// AgentGitSSHKey will return the user's SSH key pair for the workspace.
|
||||
func (c *Client) AgentGitSSHKey(ctx context.Context) (AgentGitSSHKey, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, "/api/v2/workspaceresources/agent/gitsshkey", nil)
|
||||
if err != nil {
|
||||
return AgentGitSSHKey{}, xerrors.Errorf("execute request: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return AgentGitSSHKey{}, readBodyAsError(res)
|
||||
}
|
||||
|
||||
var agentgitsshkey AgentGitSSHKey
|
||||
return agentgitsshkey, json.NewDecoder(res.Body).Decode(&agentgitsshkey)
|
||||
}
|
Loading…
Reference in New Issue