fix: use unique ID for linked accounts (#3441)

- move OAuth-related fields off of api_keys into a new user_links table
- restrict users to single form of login
- process updates to user email/usernames for OIDC
- added a login_type column to users
This commit is contained in:
Jon Ayers
2022-08-17 18:00:53 -05:00
committed by GitHub
parent 53d1fb36db
commit c3eea98db0
29 changed files with 931 additions and 266 deletions

View File

@ -77,10 +77,13 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
return
}
user, organizationID, err := api.createUser(r.Context(), codersdk.CreateUserRequest{
Email: createUser.Email,
Username: createUser.Username,
Password: createUser.Password,
user, organizationID, err := api.createUser(r.Context(), createUserRequest{
CreateUserRequest: codersdk.CreateUserRequest{
Email: createUser.Email,
Username: createUser.Username,
Password: createUser.Password,
},
LoginType: database.LoginTypePassword,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
@ -196,14 +199,14 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
return
}
var createUser codersdk.CreateUserRequest
if !httpapi.Read(rw, r, &createUser) {
var req codersdk.CreateUserRequest
if !httpapi.Read(rw, r, &req) {
return
}
// Create the organization member in the org.
if !api.Authorize(r, rbac.ActionCreate,
rbac.ResourceOrganizationMember.InOrg(createUser.OrganizationID)) {
rbac.ResourceOrganizationMember.InOrg(req.OrganizationID)) {
httpapi.ResourceNotFound(rw)
return
}
@ -211,8 +214,8 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
// TODO: @emyrk Authorize the organization create if the createUser will do that.
_, err := api.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
Username: createUser.Username,
Email: createUser.Email,
Username: req.Username,
Email: req.Email,
})
if err == nil {
httpapi.Write(rw, http.StatusConflict, codersdk.Response{
@ -228,10 +231,10 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
return
}
_, err = api.Database.GetOrganizationByID(r.Context(), createUser.OrganizationID)
_, err = api.Database.GetOrganizationByID(r.Context(), req.OrganizationID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, codersdk.Response{
Message: fmt.Sprintf("Organization does not exist with the provided id %q.", createUser.OrganizationID),
Message: fmt.Sprintf("Organization does not exist with the provided id %q.", req.OrganizationID),
})
return
}
@ -243,7 +246,10 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
return
}
user, _, err := api.createUser(r.Context(), createUser)
user, _, err := api.createUser(r.Context(), createUserRequest{
CreateUserRequest: req,
LoginType: database.LoginTypePassword,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error creating user.",
@ -257,7 +263,7 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
Users: []telemetry.User{telemetry.ConvertUser(user)},
})
httpapi.Write(rw, http.StatusCreated, convertUser(user, []uuid.UUID{createUser.OrganizationID}))
httpapi.Write(rw, http.StatusCreated, convertUser(user, []uuid.UUID{req.OrganizationID}))
}
// Returns the parameterized user requested. All validation
@ -701,6 +707,13 @@ func (api *API) postLogin(rw http.ResponseWriter, r *http.Request) {
return
}
if user.LoginType != database.LoginTypePassword {
httpapi.Write(rw, http.StatusForbidden, codersdk.Response{
Message: fmt.Sprintf("Incorrect login type, attempting to use %q but user is of login type %q", database.LoginTypePassword, user.LoginType),
})
return
}
// If the user logged into a suspended account, reject the login request.
if user.Status != database.UserStatusActive {
httpapi.Write(rw, http.StatusUnauthorized, codersdk.Response{
@ -709,7 +722,7 @@ func (api *API) postLogin(rw http.ResponseWriter, r *http.Request) {
return
}
sessionToken, created := api.createAPIKey(rw, r, database.InsertAPIKeyParams{
sessionToken, created := api.createAPIKey(rw, r, createAPIKeyParams{
UserID: user.ID,
LoginType: database.LoginTypePassword,
})
@ -732,7 +745,7 @@ func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) {
}
lifeTime := time.Hour * 24 * 7
sessionToken, created := api.createAPIKey(rw, r, database.InsertAPIKeyParams{
sessionToken, created := api.createAPIKey(rw, r, createAPIKeyParams{
UserID: user.ID,
LoginType: database.LoginTypePassword,
// All api generated keys will last 1 week. Browser login tokens have
@ -818,7 +831,16 @@ func generateAPIKeyIDSecret() (id string, secret string, err error) {
return id, secret, nil
}
func (api *API) createAPIKey(rw http.ResponseWriter, r *http.Request, params database.InsertAPIKeyParams) (string, bool) {
type createAPIKeyParams struct {
UserID uuid.UUID
LoginType database.LoginType
// Optional.
ExpiresAt time.Time
LifetimeSeconds int64
}
func (api *API) createAPIKey(rw http.ResponseWriter, r *http.Request, params createAPIKeyParams) (string, bool) {
keyID, keySecret, err := generateAPIKeyIDSecret()
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
@ -856,15 +878,11 @@ func (api *API) createAPIKey(rw http.ResponseWriter, r *http.Request, params dat
Valid: true,
},
// Make sure in UTC time for common time zone
ExpiresAt: params.ExpiresAt.UTC(),
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
HashedSecret: hashed[:],
LoginType: params.LoginType,
OAuthAccessToken: params.OAuthAccessToken,
OAuthRefreshToken: params.OAuthRefreshToken,
OAuthIDToken: params.OAuthIDToken,
OAuthExpiry: params.OAuthExpiry,
ExpiresAt: params.ExpiresAt.UTC(),
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
HashedSecret: hashed[:],
LoginType: params.LoginType,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
@ -891,7 +909,12 @@ func (api *API) createAPIKey(rw http.ResponseWriter, r *http.Request, params dat
return sessionToken, true
}
func (api *API) createUser(ctx context.Context, req codersdk.CreateUserRequest) (database.User, uuid.UUID, error) {
type createUserRequest struct {
codersdk.CreateUserRequest
LoginType database.LoginType
}
func (api *API) createUser(ctx context.Context, req createUserRequest) (database.User, uuid.UUID, error) {
var user database.User
return user, req.OrganizationID, api.Database.InTx(func(db database.Store) error {
orgRoles := make([]string, 0)
@ -918,6 +941,7 @@ func (api *API) createUser(ctx context.Context, req codersdk.CreateUserRequest)
UpdatedAt: database.Now(),
// All new users are defaulted to members of the site.
RBACRoles: []string{},
LoginType: req.LoginType,
}
// If a user signs up with OAuth, they can have no password!
if req.Password != "" {