chore: add github.com user id association (#14045)

* chore: add github.com user id association

This will eventually be used to show an indicator in the UI
to star the repository if you've been using Coder for a while
and have not starred the repo.

If you have, we'll never show a thing!

* gen

* Fix model query

* Fix linting

* Ignore auditing github.com user id

* Add test

* Fix gh url var name

* Update migration

* Update coderd/database/dbauthz/dbauthz.go

Co-authored-by: Steven Masley <Emyrk@users.noreply.github.com>

* Fix updating to when the token changes

* Fix migration

---------

Co-authored-by: Steven Masley <Emyrk@users.noreply.github.com>
This commit is contained in:
Kyle Carberry
2024-08-02 11:49:36 -05:00
committed by GitHub
parent 4d4d27c509
commit 6e36082b0f
25 changed files with 222 additions and 37 deletions
+3
View File
@@ -9811,6 +9811,9 @@ const docTemplate = `{
"avatar_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"login": {
"type": "string"
},
+3
View File
@@ -8801,6 +8801,9 @@
"avatar_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"login": {
"type": "string"
},
+17
View File
@@ -3260,6 +3260,23 @@ func (q *querier) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error
return deleteQ(q.log, q.auth, q.db.GetUserByID, q.db.UpdateUserDeletedByID)(ctx, id)
}
func (q *querier) UpdateUserGithubComUserID(ctx context.Context, arg database.UpdateUserGithubComUserIDParams) error {
user, err := q.db.GetUserByID(ctx, arg.ID)
if err != nil {
return err
}
err = q.authorizeContext(ctx, policy.ActionUpdatePersonal, user)
if err != nil {
// System user can also update
err = q.authorizeContext(ctx, policy.ActionUpdate, user)
if err != nil {
return err
}
}
return q.db.UpdateUserGithubComUserID(ctx, arg)
}
func (q *querier) UpdateUserHashedPassword(ctx context.Context, arg database.UpdateUserHashedPasswordParams) error {
user, err := q.db.GetUserByID(ctx, arg.ID)
if err != nil {
+6
View File
@@ -1105,6 +1105,12 @@ func (s *MethodTestSuite) TestUser() {
u := dbgen.User(s.T(), db, database.User{})
check.Args(u.ID).Asserts(u, policy.ActionDelete).Returns()
}))
s.Run("UpdateUserGithubComUserID", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{})
check.Args(database.UpdateUserGithubComUserIDParams{
ID: u.ID,
}).Asserts(u, policy.ActionUpdatePersonal)
}))
s.Run("UpdateUserHashedPassword", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{})
check.Args(database.UpdateUserHashedPasswordParams{
+20
View File
@@ -7985,6 +7985,26 @@ func (q *FakeQuerier) UpdateUserDeletedByID(_ context.Context, id uuid.UUID) err
return sql.ErrNoRows
}
func (q *FakeQuerier) UpdateUserGithubComUserID(_ context.Context, arg database.UpdateUserGithubComUserIDParams) error {
err := validateDatabaseType(arg)
if err != nil {
return err
}
q.mutex.Lock()
defer q.mutex.Unlock()
for i, user := range q.users {
if user.ID != arg.ID {
continue
}
user.GithubComUserID = arg.GithubComUserID
q.users[i] = user
return nil
}
return sql.ErrNoRows
}
func (q *FakeQuerier) UpdateUserHashedPassword(_ context.Context, arg database.UpdateUserHashedPasswordParams) error {
if err := validateDatabaseType(arg); err != nil {
return err
+7
View File
@@ -2097,6 +2097,13 @@ func (m metricsStore) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) e
return r0
}
func (m metricsStore) UpdateUserGithubComUserID(ctx context.Context, arg database.UpdateUserGithubComUserIDParams) error {
start := time.Now()
r0 := m.s.UpdateUserGithubComUserID(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateUserGithubComUserID").Observe(time.Since(start).Seconds())
return r0
}
func (m metricsStore) UpdateUserHashedPassword(ctx context.Context, arg database.UpdateUserHashedPasswordParams) error {
start := time.Now()
err := m.s.UpdateUserHashedPassword(ctx, arg)
+14
View File
@@ -4416,6 +4416,20 @@ func (mr *MockStoreMockRecorder) UpdateUserDeletedByID(arg0, arg1 any) *gomock.C
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserDeletedByID", reflect.TypeOf((*MockStore)(nil).UpdateUserDeletedByID), arg0, arg1)
}
// UpdateUserGithubComUserID mocks base method.
func (m *MockStore) UpdateUserGithubComUserID(arg0 context.Context, arg1 database.UpdateUserGithubComUserIDParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUserGithubComUserID", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateUserGithubComUserID indicates an expected call of UpdateUserGithubComUserID.
func (mr *MockStoreMockRecorder) UpdateUserGithubComUserID(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserGithubComUserID", reflect.TypeOf((*MockStore)(nil).UpdateUserGithubComUserID), arg0, arg1)
}
// UpdateUserHashedPassword mocks base method.
func (m *MockStore) UpdateUserHashedPassword(arg0 context.Context, arg1 database.UpdateUserHashedPasswordParams) error {
m.ctrl.T.Helper()
+4 -1
View File
@@ -974,7 +974,8 @@ CREATE TABLE users (
last_seen_at timestamp without time zone DEFAULT '0001-01-01 00:00:00'::timestamp without time zone NOT NULL,
quiet_hours_schedule text DEFAULT ''::text NOT NULL,
theme_preference text DEFAULT ''::text NOT NULL,
name text DEFAULT ''::text NOT NULL
name text DEFAULT ''::text NOT NULL,
github_com_user_id bigint
);
COMMENT ON COLUMN users.quiet_hours_schedule IS 'Daily (!) cron schedule (with optional CRON_TZ) signifying the start of the user''s quiet hours. If empty, the default quiet hours on the instance is used instead.';
@@ -983,6 +984,8 @@ COMMENT ON COLUMN users.theme_preference IS '"" can be interpreted as "the user
COMMENT ON COLUMN users.name IS 'Name of the Coder user';
COMMENT ON COLUMN users.github_com_user_id IS 'The GitHub.com numerical user ID. At time of implementation, this is used to check if the user has starred the Coder repository.';
CREATE VIEW visible_users AS
SELECT users.id,
users.username,
@@ -0,0 +1 @@
ALTER TABLE users DROP COLUMN github_com_user_id;
@@ -0,0 +1,3 @@
ALTER TABLE users ADD COLUMN github_com_user_id BIGINT;
COMMENT ON COLUMN users.github_com_user_id IS 'The GitHub.com numerical user ID. At time of implementation, this is used to check if the user has starred the Coder repository.';
+1
View File
@@ -361,6 +361,7 @@ func (q *sqlQuerier) GetAuthorizedUsers(ctx context.Context, arg GetUsersParams,
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
&i.GithubComUserID,
&i.Count,
); err != nil {
return nil, err
+2
View File
@@ -2475,6 +2475,8 @@ type User struct {
ThemePreference string `db:"theme_preference" json:"theme_preference"`
// Name of the Coder user
Name string `db:"name" json:"name"`
// The GitHub.com numerical user ID. At time of implementation, this is used to check if the user has starred the Coder repository.
GithubComUserID sql.NullInt64 `db:"github_com_user_id" json:"github_com_user_id"`
}
type UserLink struct {
+1
View File
@@ -421,6 +421,7 @@ type sqlcQuerier interface {
UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg UpdateTemplateWorkspacesLastUsedAtParams) error
UpdateUserAppearanceSettings(ctx context.Context, arg UpdateUserAppearanceSettingsParams) (User, error)
UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error
UpdateUserGithubComUserID(ctx context.Context, arg UpdateUserGithubComUserIDParams) error
UpdateUserHashedPassword(ctx context.Context, arg UpdateUserHashedPasswordParams) error
UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLastSeenAtParams) (User, error)
UpdateUserLink(ctx context.Context, arg UpdateUserLinkParams) (UserLink, error)
+46 -13
View File
@@ -1350,7 +1350,7 @@ func (q *sqlQuerier) GetGroupMembers(ctx context.Context) ([]GroupMember, error)
const getGroupMembersByGroupID = `-- name: GetGroupMembersByGroupID :many
SELECT
users.id, users.email, users.username, users.hashed_password, users.created_at, users.updated_at, users.status, users.rbac_roles, users.login_type, users.avatar_url, users.deleted, users.last_seen_at, users.quiet_hours_schedule, users.theme_preference, users.name
users.id, users.email, users.username, users.hashed_password, users.created_at, users.updated_at, users.status, users.rbac_roles, users.login_type, users.avatar_url, users.deleted, users.last_seen_at, users.quiet_hours_schedule, users.theme_preference, users.name, users.github_com_user_id
FROM
users
LEFT JOIN
@@ -1399,6 +1399,7 @@ func (q *sqlQuerier) GetGroupMembersByGroupID(ctx context.Context, groupID uuid.
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
&i.GithubComUserID,
); err != nil {
return nil, err
}
@@ -9222,7 +9223,7 @@ func (q *sqlQuerier) GetAuthorizationUserRoles(ctx context.Context, userID uuid.
const getUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one
SELECT
id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name
id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id
FROM
users
WHERE
@@ -9256,13 +9257,14 @@ func (q *sqlQuerier) GetUserByEmailOrUsername(ctx context.Context, arg GetUserBy
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
&i.GithubComUserID,
)
return i, err
}
const getUserByID = `-- name: GetUserByID :one
SELECT
id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name
id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id
FROM
users
WHERE
@@ -9290,6 +9292,7 @@ func (q *sqlQuerier) GetUserByID(ctx context.Context, id uuid.UUID) (User, error
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
&i.GithubComUserID,
)
return i, err
}
@@ -9312,7 +9315,7 @@ func (q *sqlQuerier) GetUserCount(ctx context.Context) (int64, error) {
const getUsers = `-- name: GetUsers :many
SELECT
id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, COUNT(*) OVER() AS count
id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id, COUNT(*) OVER() AS count
FROM
users
WHERE
@@ -9411,6 +9414,7 @@ type GetUsersRow struct {
QuietHoursSchedule string `db:"quiet_hours_schedule" json:"quiet_hours_schedule"`
ThemePreference string `db:"theme_preference" json:"theme_preference"`
Name string `db:"name" json:"name"`
GithubComUserID sql.NullInt64 `db:"github_com_user_id" json:"github_com_user_id"`
Count int64 `db:"count" json:"count"`
}
@@ -9449,6 +9453,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUse
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
&i.GithubComUserID,
&i.Count,
); err != nil {
return nil, err
@@ -9465,7 +9470,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUse
}
const getUsersByIDs = `-- name: GetUsersByIDs :many
SELECT id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name FROM users WHERE id = ANY($1 :: uuid [ ])
SELECT id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id FROM users WHERE id = ANY($1 :: uuid [ ])
`
// This shouldn't check for deleted, because it's frequently used
@@ -9496,6 +9501,7 @@ func (q *sqlQuerier) GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]User
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
&i.GithubComUserID,
); err != nil {
return nil, err
}
@@ -9524,7 +9530,7 @@ INSERT INTO
login_type
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name
($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id
`
type InsertUserParams struct {
@@ -9568,6 +9574,7 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
&i.GithubComUserID,
)
return i, err
}
@@ -9626,7 +9633,7 @@ SET
updated_at = $3
WHERE
id = $1
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id
`
type UpdateUserAppearanceSettingsParams struct {
@@ -9654,6 +9661,7 @@ func (q *sqlQuerier) UpdateUserAppearanceSettings(ctx context.Context, arg Updat
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
&i.GithubComUserID,
)
return i, err
}
@@ -9672,6 +9680,25 @@ func (q *sqlQuerier) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) er
return err
}
const updateUserGithubComUserID = `-- name: UpdateUserGithubComUserID :exec
UPDATE
users
SET
github_com_user_id = $2
WHERE
id = $1
`
type UpdateUserGithubComUserIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
GithubComUserID sql.NullInt64 `db:"github_com_user_id" json:"github_com_user_id"`
}
func (q *sqlQuerier) UpdateUserGithubComUserID(ctx context.Context, arg UpdateUserGithubComUserIDParams) error {
_, err := q.db.ExecContext(ctx, updateUserGithubComUserID, arg.ID, arg.GithubComUserID)
return err
}
const updateUserHashedPassword = `-- name: UpdateUserHashedPassword :exec
UPDATE
users
@@ -9698,7 +9725,7 @@ SET
last_seen_at = $2,
updated_at = $3
WHERE
id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name
id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id
`
type UpdateUserLastSeenAtParams struct {
@@ -9726,6 +9753,7 @@ func (q *sqlQuerier) UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLas
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
&i.GithubComUserID,
)
return i, err
}
@@ -9743,7 +9771,7 @@ SET
'':: bytea
END
WHERE
id = $2 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name
id = $2 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id
`
type UpdateUserLoginTypeParams struct {
@@ -9770,6 +9798,7 @@ func (q *sqlQuerier) UpdateUserLoginType(ctx context.Context, arg UpdateUserLogi
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
&i.GithubComUserID,
)
return i, err
}
@@ -9785,7 +9814,7 @@ SET
name = $6
WHERE
id = $1
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id
`
type UpdateUserProfileParams struct {
@@ -9823,6 +9852,7 @@ func (q *sqlQuerier) UpdateUserProfile(ctx context.Context, arg UpdateUserProfil
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
&i.GithubComUserID,
)
return i, err
}
@@ -9834,7 +9864,7 @@ SET
quiet_hours_schedule = $2
WHERE
id = $1
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id
`
type UpdateUserQuietHoursScheduleParams struct {
@@ -9861,6 +9891,7 @@ func (q *sqlQuerier) UpdateUserQuietHoursSchedule(ctx context.Context, arg Updat
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
&i.GithubComUserID,
)
return i, err
}
@@ -9873,7 +9904,7 @@ SET
rbac_roles = ARRAY(SELECT DISTINCT UNNEST($1 :: text[]))
WHERE
id = $2
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id
`
type UpdateUserRolesParams struct {
@@ -9900,6 +9931,7 @@ func (q *sqlQuerier) UpdateUserRoles(ctx context.Context, arg UpdateUserRolesPar
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
&i.GithubComUserID,
)
return i, err
}
@@ -9911,7 +9943,7 @@ SET
status = $2,
updated_at = $3
WHERE
id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name
id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id
`
type UpdateUserStatusParams struct {
@@ -9939,6 +9971,7 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
&i.GithubComUserID,
)
return i, err
}
+8
View File
@@ -85,6 +85,14 @@ WHERE
id = $1
RETURNING *;
-- name: UpdateUserGithubComUserID :exec
UPDATE
users
SET
github_com_user_id = $2
WHERE
id = $1;
-- name: UpdateUserAppearanceSettings :one
UPDATE
users
+28 -1
View File
@@ -154,7 +154,7 @@ func (c *Config) RefreshToken(ctx context.Context, db database.Store, externalAu
retryCtx, retryCtxCancel := context.WithTimeout(ctx, time.Second)
defer retryCtxCancel()
validate:
valid, _, err := c.ValidateToken(ctx, token)
valid, user, err := c.ValidateToken(ctx, token)
if err != nil {
return externalAuthLink, xerrors.Errorf("validate external auth token: %w", err)
}
@@ -189,7 +189,22 @@ validate:
return updatedAuthLink, xerrors.Errorf("update external auth link: %w", err)
}
externalAuthLink = updatedAuthLink
// Update the associated users github.com username if the token is for github.com.
if IsGithubDotComURL(c.AuthCodeURL("")) && user != nil {
err = db.UpdateUserGithubComUserID(ctx, database.UpdateUserGithubComUserIDParams{
ID: externalAuthLink.UserID,
GithubComUserID: sql.NullInt64{
Int64: user.ID,
Valid: true,
},
})
if err != nil {
return externalAuthLink, xerrors.Errorf("update user github com user id: %w", err)
}
}
}
return externalAuthLink, nil
}
@@ -233,6 +248,7 @@ func (c *Config) ValidateToken(ctx context.Context, link *oauth2.Token) (bool, *
err = json.NewDecoder(res.Body).Decode(&ghUser)
if err == nil {
user = &codersdk.ExternalAuthUser{
ID: ghUser.GetID(),
Login: ghUser.GetLogin(),
AvatarURL: ghUser.GetAvatarURL(),
ProfileURL: ghUser.GetHTMLURL(),
@@ -291,6 +307,7 @@ func (c *Config) AppInstallations(ctx context.Context, token string) ([]codersdk
ID: int(installation.GetID()),
ConfigureURL: installation.GetHTMLURL(),
Account: codersdk.ExternalAuthUser{
ID: account.GetID(),
Login: account.GetLogin(),
AvatarURL: account.GetAvatarURL(),
ProfileURL: account.GetHTMLURL(),
@@ -947,3 +964,13 @@ type roundTripper func(req *http.Request) (*http.Response, error)
func (r roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return r(req)
}
// IsGithubDotComURL returns true if the given URL is a github.com URL.
func IsGithubDotComURL(str string) bool {
str = strings.ToLower(str)
ghURL, err := url.Parse(str)
if err != nil {
return false
}
return ghURL.Host == "github.com"
}
+11 -9
View File
@@ -660,11 +660,12 @@ func ConvertUser(dbUser database.User) User {
emailHashed = fmt.Sprintf("%x%s", hash[:], dbUser.Email[atSymbol:])
}
return User{
ID: dbUser.ID,
EmailHashed: emailHashed,
RBACRoles: dbUser.RBACRoles,
CreatedAt: dbUser.CreatedAt,
Status: dbUser.Status,
ID: dbUser.ID,
EmailHashed: emailHashed,
RBACRoles: dbUser.RBACRoles,
CreatedAt: dbUser.CreatedAt,
Status: dbUser.Status,
GithubComUserID: dbUser.GithubComUserID.Int64,
}
}
@@ -836,10 +837,11 @@ type User struct {
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"created_at"`
// Email is only filled in for the first/admin user!
Email *string `json:"email"`
EmailHashed string `json:"email_hashed"`
RBACRoles []string `json:"rbac_roles"`
Status database.UserStatus `json:"status"`
Email *string `json:"email"`
EmailHashed string `json:"email_hashed"`
RBACRoles []string `json:"rbac_roles"`
Status database.UserStatus `json:"status"`
GithubComUserID int64 `json:"github_com_user_id"`
}
type Group struct {
+26 -6
View File
@@ -31,6 +31,7 @@ import (
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/externalauth"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/promoauth"
@@ -661,7 +662,7 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
}).SetInitAuditRequest(func(params *audit.RequestParams) (*audit.Request[database.User], func()) {
return audit.InitRequest[database.User](rw, params)
})
cookies, key, err := api.oauthLogin(r, params)
cookies, user, key, err := api.oauthLogin(r, params)
defer params.CommitAuditLogs()
var httpErr httpError
if xerrors.As(err, &httpErr) {
@@ -676,6 +677,25 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
})
return
}
// If the user is logging in with github.com we update their associated
// GitHub user ID to the new one.
if externalauth.IsGithubDotComURL(api.GithubOAuth2Config.AuthCodeURL("")) && user.GithubComUserID.Int64 != ghUser.GetID() {
err = api.Database.UpdateUserGithubComUserID(ctx, database.UpdateUserGithubComUserIDParams{
ID: user.ID,
GithubComUserID: sql.NullInt64{
Int64: ghUser.GetID(),
Valid: true,
},
})
if err != nil {
logger.Error(ctx, "oauth2: unable to update user github id", slog.F("user", user.Username), slog.Error(err))
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to update user GitHub ID.",
Detail: err.Error(),
})
return
}
}
aReq.New = key
aReq.UserID = key.UserID
@@ -1030,7 +1050,7 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
}).SetInitAuditRequest(func(params *audit.RequestParams) (*audit.Request[database.User], func()) {
return audit.InitRequest[database.User](rw, params)
})
cookies, key, err := api.oauthLogin(r, params)
cookies, user, key, err := api.oauthLogin(r, params)
defer params.CommitAuditLogs()
var httpErr httpError
if xerrors.As(err, &httpErr) {
@@ -1320,7 +1340,7 @@ func (e httpError) Error() string {
return e.msg
}
func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.Cookie, database.APIKey, error) {
func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.Cookie, database.User, database.APIKey, error) {
var (
ctx = r.Context()
user database.User
@@ -1610,7 +1630,7 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
return nil
}, nil)
if err != nil {
return nil, database.APIKey{}, xerrors.Errorf("in tx: %w", err)
return nil, database.User{}, database.APIKey{}, xerrors.Errorf("in tx: %w", err)
}
var key database.APIKey
@@ -1647,13 +1667,13 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
RemoteAddr: r.RemoteAddr,
})
if err != nil {
return nil, database.APIKey{}, xerrors.Errorf("create API key: %w", err)
return nil, database.User{}, database.APIKey{}, xerrors.Errorf("create API key: %w", err)
}
cookies = append(cookies, cookie)
key = *newKey
}
return cookies, key, nil
return cookies, user, key, nil
}
// convertUserToOauth will convert a user from password base loginType to
+1
View File
@@ -103,6 +103,7 @@ type ExternalAuthAppInstallation struct {
}
type ExternalAuthUser struct {
ID int64 `json:"id"`
Login string `json:"login"`
AvatarURL string `json:"avatar_url"`
ProfileURL string `json:"profile_url"`
+1 -1
View File
@@ -24,7 +24,7 @@ We track the following resources:
| Organization<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>is_default</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr></tbody></table> |
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>active_version_id</td><td>true</td></tr><tr><td>activity_bump</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deprecated</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_port_sharing_level</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_display_name</td><td>false</td></tr><tr><td>organization_icon</td><td>false</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>organization_name</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>require_active_version</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>theme_preference</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>github_com_user_id</td><td>false</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>theme_preference</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
| Workspace<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>favorite</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
| WorkspaceProxy<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>derp_enabled</td><td>true</td></tr><tr><td>derp_only</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>region_id</td><td>true</td></tr><tr><td>token_hashed_secret</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>url</td><td>true</td></tr><tr><td>version</td><td>true</td></tr><tr><td>wildcard_hostname</td><td>true</td></tr></tbody></table> |
+2
View File
@@ -71,6 +71,7 @@ curl -X GET http://coder-server:8080/api/v2/external-auth/{externalauth} \
{
"account": {
"avatar_url": "string",
"id": 0,
"login": "string",
"name": "string",
"profile_url": "string"
@@ -81,6 +82,7 @@ curl -X GET http://coder-server:8080/api/v2/external-auth/{externalauth} \
],
"user": {
"avatar_url": "string",
"id": 0,
"login": "string",
"name": "string",
"profile_url": "string"
+11 -6
View File
@@ -2521,6 +2521,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
{
"account": {
"avatar_url": "string",
"id": 0,
"login": "string",
"name": "string",
"profile_url": "string"
@@ -2531,6 +2532,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
],
"user": {
"avatar_url": "string",
"id": 0,
"login": "string",
"name": "string",
"profile_url": "string"
@@ -2556,6 +2558,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
{
"account": {
"avatar_url": "string",
"id": 0,
"login": "string",
"name": "string",
"profile_url": "string"
@@ -2669,6 +2672,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
```json
{
"avatar_url": "string",
"id": 0,
"login": "string",
"name": "string",
"profile_url": "string"
@@ -2677,12 +2681,13 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
### Properties
| Name | Type | Required | Restrictions | Description |
| ------------- | ------ | -------- | ------------ | ----------- |
| `avatar_url` | string | false | | |
| `login` | string | false | | |
| `name` | string | false | | |
| `profile_url` | string | false | | |
| Name | Type | Required | Restrictions | Description |
| ------------- | ------- | -------- | ------------ | ----------- |
| `avatar_url` | string | false | | |
| `id` | integer | false | | |
| `login` | string | false | | |
| `name` | string | false | | |
| `profile_url` | string | false | | |
## codersdk.Feature
+1
View File
@@ -144,6 +144,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
"quiet_hours_schedule": ActionTrack,
"theme_preference": ActionIgnore,
"name": ActionTrack,
"github_com_user_id": ActionIgnore,
},
&database.Workspace{}: {
"id": ActionTrack,
+1
View File
@@ -572,6 +572,7 @@ export interface ExternalAuthLinkProvider {
// From codersdk/externalauth.go
export interface ExternalAuthUser {
readonly id: number;
readonly login: string;
readonly avatar_url: string;
readonly profile_url: string;
@@ -22,6 +22,7 @@ WebAuthenticated.args = {
app_installable: false,
display_name: "BitBucket",
user: {
id: 0,
avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4",
login: "kylecarbs",
name: "Kyle Carberry",
@@ -104,6 +105,7 @@ DeviceAuthenticatedNotInstalled.args = {
app_install_url: "https://example.com",
app_installable: true,
user: {
id: 0,
avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4",
login: "kylecarbs",
name: "Kyle Carberry",
@@ -123,6 +125,7 @@ DeviceAuthenticatedInstalled.args = {
configure_url: "https://example.com",
id: 1,
account: {
id: 0,
avatar_url: "https://github.com/coder.png",
login: "coder",
name: "Coder",
@@ -133,6 +136,7 @@ DeviceAuthenticatedInstalled.args = {
app_install_url: "https://example.com",
app_installable: true,
user: {
id: 0,
avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4",
login: "kylecarbs",
name: "Kyle Carberry",