Skip to content

Commit

Permalink
Merge pull request #1806 from transcom/roc-ig-#164282562-add-disable-…
Browse files Browse the repository at this point in the history
…user

Add Disabled column to user table so individual users can be blocked.
  • Loading branch information
Isaac Garfinkle committed Mar 6, 2019
2 parents aeda88b + 3d4055d commit b2be02e
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 106 deletions.
Expand Up @@ -110,7 +110,7 @@ func (suite *DutyStationsLoaderSuite) TestCreateInsertQuery() {
query := builder.createInsertQuery(model, &pop.Model{Value: models.User{}})

suite.Equal(
"INSERT into users (id, created_at, updated_at, login_gov_uuid, login_gov_email) VALUES ('cd40c92e-7c8a-4da4-ad58-4480df84b3f0', now(), now(), 'cd40c92e-7c8a-4da4-ad58-4480df84b3f1', 'email@example.com');\n",
"INSERT into users (id, created_at, updated_at, login_gov_uuid, login_gov_email, disabled) VALUES ('cd40c92e-7c8a-4da4-ad58-4480df84b3f0', now(), now(), 'cd40c92e-7c8a-4da4-ad58-4480df84b3f1', 'email@example.com', false);\n",
query)
}

Expand Down
@@ -0,0 +1 @@
-- Local test migration. Nothing needed for this migration. Will run on staging only.
1 change: 1 addition & 0 deletions migrations/20190227184037_add-user-disable.up.fizz
@@ -0,0 +1 @@
add_column("users", "disabled", "bool", {"default": false })
@@ -0,0 +1 @@
exec("./apply-secure-migration.sh 20190304210740_disable-unwanted-staging-user.sql")
230 changes: 125 additions & 105 deletions pkg/auth/authentication/auth.go
Expand Up @@ -10,7 +10,9 @@ import (

"github.com/gobuffalo/pop"
"github.com/gofrs/uuid"
"github.com/honeycombio/beeline-go"
beeline "github.com/honeycombio/beeline-go"
"github.com/honeycombio/beeline-go/trace"
"github.com/markbates/goth"
"github.com/markbates/goth/providers/openidConnect"
"github.com/pkg/errors"
"go.uber.org/zap"
Expand Down Expand Up @@ -225,124 +227,142 @@ func (h CallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

userIdentity, err := models.FetchUserIdentity(h.db, openIDUser.UserID)
if err == nil { // Someone we know already
authorizeKnownUser(userIdentity, h, session, w, span, r, lURL)
return
} else if err == models.ErrFetchNotFound { // Never heard of them so far
authorizeUnknownUser(openIDUser, h, session, w, span, r, lURL)
return
} else {
h.logger.Error("Error loading Identity.", zap.Error(err))
http.Error(w, http.StatusText(500), http.StatusInternalServerError)
return
}
}

session.UserID = userIdentity.ID
span.AddField("session.user_id", session.UserID)
if userIdentity.ServiceMemberID != nil {
session.ServiceMemberID = *(userIdentity.ServiceMemberID)
span.AddField("session.service_member_id", session.ServiceMemberID)
}
func authorizeKnownUser(userIdentity *models.UserIdentity, h CallbackHandler, session *auth.Session, w http.ResponseWriter, span *trace.Span, r *http.Request, lURL string) {
if userIdentity.Disabled {
h.logger.Error("Disabled user requesting authentication", zap.String("email", session.Email))
http.Error(w, http.StatusText(403), http.StatusForbidden)
return
}
session.UserID = userIdentity.ID
span.AddField("session.user_id", session.UserID)
if userIdentity.ServiceMemberID != nil {
session.ServiceMemberID = *(userIdentity.ServiceMemberID)
span.AddField("session.service_member_id", session.ServiceMemberID)
}

if userIdentity.DpsUserID != nil {
session.DpsUserID = *(userIdentity.DpsUserID)
}
if userIdentity.DpsUserID != nil {
session.DpsUserID = *(userIdentity.DpsUserID)
}

if userIdentity.OfficeUserID != nil {
session.OfficeUserID = *(userIdentity.OfficeUserID)
} else if session.IsOfficeApp() {
// In case they managed to login before the office_user record was created
officeUser, err := models.FetchOfficeUserByEmail(h.db, session.Email)
if err == models.ErrFetchNotFound {
h.logger.Error("Non-office user authenticated at office site", zap.String("email", session.Email))
http.Error(w, http.StatusText(401), http.StatusUnauthorized)
return
} else if err != nil {
h.logger.Error("Checking for office user", zap.String("email", session.Email), zap.Error(err))
http.Error(w, http.StatusText(500), http.StatusInternalServerError)
return
}
session.OfficeUserID = officeUser.ID
span.AddField("session.office_user_id", session.OfficeUserID)
officeUser.UserID = &userIdentity.ID
err = h.db.Save(officeUser)
if err != nil {
h.logger.Error("Updating office user", zap.String("email", session.Email), zap.Error(err))
http.Error(w, http.StatusText(500), http.StatusInternalServerError)
return
}
if userIdentity.OfficeUserID != nil {
session.OfficeUserID = *(userIdentity.OfficeUserID)
} else if session.IsOfficeApp() {
// In case they managed to login before the office_user record was created
officeUser, err := models.FetchOfficeUserByEmail(h.db, session.Email)
if err == models.ErrFetchNotFound {
h.logger.Error("Non-office user authenticated at office site", zap.String("email", session.Email))
http.Error(w, http.StatusText(401), http.StatusUnauthorized)
return
} else if err != nil {
h.logger.Error("Checking for office user", zap.String("email", session.Email), zap.Error(err))
http.Error(w, http.StatusText(500), http.StatusInternalServerError)
return
}
session.OfficeUserID = officeUser.ID
span.AddField("session.office_user_id", session.OfficeUserID)
officeUser.UserID = &userIdentity.ID
err = h.db.Save(officeUser)
if err != nil {
h.logger.Error("Updating office user", zap.String("email", session.Email), zap.Error(err))
http.Error(w, http.StatusText(500), http.StatusInternalServerError)
return
}
}

if userIdentity.TspUserID != nil {
session.TspUserID = *(userIdentity.TspUserID)
} else if session.IsTspApp() {
// In case they managed to login before the tsp_user record was created
tspUser, err := models.FetchTspUserByEmail(h.db, session.Email)
if err == models.ErrFetchNotFound {
h.logger.Error("Non-TSP user authenticated at tsp site", zap.String("email", session.Email))
http.Error(w, http.StatusText(401), http.StatusUnauthorized)
return
} else if err != nil {
h.logger.Error("Checking for TSP user", zap.String("email", session.Email), zap.Error(err))
http.Error(w, http.StatusText(500), http.StatusInternalServerError)
return
}
session.TspUserID = tspUser.ID
span.AddField("session.tsp_user_id", session.TspUserID)
tspUser.UserID = &userIdentity.ID
err = h.db.Save(tspUser)
if err != nil {
h.logger.Error("Updating TSP user", zap.String("email", session.Email), zap.Error(err))
http.Error(w, http.StatusText(500), http.StatusInternalServerError)
return
}
if userIdentity.TspUserID != nil {
session.TspUserID = *(userIdentity.TspUserID)
} else if session.IsTspApp() {
// In case they managed to login before the tsp_user record was created
tspUser, err := models.FetchTspUserByEmail(h.db, session.Email)
if err == models.ErrFetchNotFound {
h.logger.Error("Non-TSP user authenticated at tsp site", zap.String("email", session.Email))
http.Error(w, http.StatusText(401), http.StatusUnauthorized)
return
} else if err != nil {
h.logger.Error("Checking for TSP user", zap.String("email", session.Email), zap.Error(err))
http.Error(w, http.StatusText(500), http.StatusInternalServerError)
return
}
session.TspUserID = tspUser.ID
span.AddField("session.tsp_user_id", session.TspUserID)
tspUser.UserID = &userIdentity.ID
err = h.db.Save(tspUser)
if err != nil {
h.logger.Error("Updating TSP user", zap.String("email", session.Email), zap.Error(err))
http.Error(w, http.StatusText(500), http.StatusInternalServerError)
return
}
session.FirstName = userIdentity.FirstName()
session.LastName = userIdentity.LastName()
session.Middle = userIdentity.Middle()
}
session.FirstName = userIdentity.FirstName()
session.LastName = userIdentity.LastName()
session.Middle = userIdentity.Middle()

} else if err == models.ErrFetchNotFound { // Never heard of them so far
h.logger.Info("logged in", zap.Any("session", session))

var officeUser *models.OfficeUser
if session.IsOfficeApp() { // Look to see if we have OfficeUser with this email address
officeUser, err = models.FetchOfficeUserByEmail(h.db, session.Email)
if err == models.ErrFetchNotFound {
h.logger.Error("No Office user found", zap.String("email", session.Email))
http.Error(w, http.StatusText(401), http.StatusUnauthorized)
return
} else if err != nil {
h.logger.Error("Checking for office user", zap.String("email", session.Email), zap.Error(err))
http.Error(w, http.StatusText(500), http.StatusInternalServerError)
return
}
}
auth.WriteSessionCookie(w, session, h.clientAuthSecretKey, h.noSessionTimeout, h.logger)
http.Redirect(w, r, lURL, http.StatusTemporaryRedirect)
}

var tspUser *models.TspUser
if session.IsTspApp() { // Look to see if we have TspUser with this email address
tspUser, err = models.FetchTspUserByEmail(h.db, session.Email)
if err == models.ErrFetchNotFound {
h.logger.Error("No TSP user found", zap.String("email", session.Email))
http.Error(w, http.StatusText(401), http.StatusUnauthorized)
return
} else if err != nil {
h.logger.Error("Checking for TSP user", zap.String("email", session.Email), zap.Error(err))
http.Error(w, http.StatusText(500), http.StatusInternalServerError)
return
}
func authorizeUnknownUser(openIDUser goth.User, h CallbackHandler, session *auth.Session, w http.ResponseWriter, span *trace.Span, r *http.Request, lURL string) {
var officeUser *models.OfficeUser
var err error
if session.IsOfficeApp() { // Look to see if we have OfficeUser with this email address
officeUser, err = models.FetchOfficeUserByEmail(h.db, session.Email)
if err == models.ErrFetchNotFound {
h.logger.Error("No Office user found", zap.String("email", session.Email))
http.Error(w, http.StatusText(401), http.StatusUnauthorized)
return
} else if err != nil {
h.logger.Error("Checking for office user", zap.String("email", session.Email), zap.Error(err))
http.Error(w, http.StatusText(500), http.StatusInternalServerError)
return
}
}

user, err := models.CreateUser(h.db, openIDUser.UserID, openIDUser.Email)
if err == nil { // Successfully created the user
session.UserID = user.ID
span.AddField("session.user_id", session.UserID)
if officeUser != nil {
session.OfficeUserID = officeUser.ID
span.AddField("session.office_user_id", session.OfficeUserID)
officeUser.UserID = &user.ID
err = h.db.Save(officeUser)
} else if tspUser != nil {
session.TspUserID = tspUser.ID
span.AddField("session.tsp_user_id", session.TspUserID)
tspUser.UserID = &user.ID
err = h.db.Save(tspUser)
}
}
if err != nil {
h.logger.Error("Error creating user", zap.Error(err))
var tspUser *models.TspUser
if session.IsTspApp() { // Look to see if we have TspUser with this email address
tspUser, err = models.FetchTspUserByEmail(h.db, session.Email)
if err == models.ErrFetchNotFound {
h.logger.Error("No TSP user found", zap.String("email", session.Email))
http.Error(w, http.StatusText(401), http.StatusUnauthorized)
return
} else if err != nil {
h.logger.Error("Checking for TSP user", zap.String("email", session.Email), zap.Error(err))
http.Error(w, http.StatusText(500), http.StatusInternalServerError)
return
}
} else {
h.logger.Error("Error loading Identity.", zap.Error(err))
}

user, err := models.CreateUser(h.db, openIDUser.UserID, openIDUser.Email)
if err == nil { // Successfully created the user
session.UserID = user.ID
span.AddField("session.user_id", session.UserID)
if officeUser != nil {
session.OfficeUserID = officeUser.ID
span.AddField("session.office_user_id", session.OfficeUserID)
officeUser.UserID = &user.ID
err = h.db.Save(officeUser)
} else if tspUser != nil {
session.TspUserID = tspUser.ID
span.AddField("session.tsp_user_id", session.TspUserID)
tspUser.UserID = &user.ID
err = h.db.Save(tspUser)
}
}
if err != nil {
h.logger.Error("Error creating user", zap.Error(err))
http.Error(w, http.StatusText(500), http.StatusInternalServerError)
return
}
Expand Down
35 changes: 35 additions & 0 deletions pkg/auth/authentication/auth_test.go
Expand Up @@ -10,6 +10,8 @@ import (
"strconv"
"testing"

"github.com/honeycombio/beeline-go/trace"

"github.com/gofrs/uuid"
"github.com/stretchr/testify/suite"
"go.uber.org/zap"
Expand Down Expand Up @@ -144,3 +146,36 @@ func (suite *AuthSuite) TestRequireAuthMiddlewareUnauthorized() {
t.Errorf("handler returned wrong status code: got %v wanted %v", status, http.StatusUnauthorized)
}
}

func (suite *AuthSuite) TestAuthorizeDisableUser() {
userIdentity := models.UserIdentity{
Disabled: true,
}

req := httptest.NewRequest("GET", fmt.Sprintf("http://%s/auth/logout", "office.example.com"), nil)

fakeToken := "some_token"
fakeUUID, _ := uuid.FromString("39b28c92-0506-4bef-8b57-e39519f42dc2")
officeTestHost := "office.example.com"
session := auth.Session{
ApplicationName: auth.OfficeApp,
UserID: fakeUUID,
IDToken: fakeToken,
Hostname: officeTestHost,
}
ctx := auth.SetSessionInRequestContext(req, &session)
callbackPort := 1234
authContext := NewAuthContext(suite.logger, fakeLoginGovProvider(suite.logger), "http", callbackPort)
h := CallbackHandler{
authContext,
suite.DB(),
"fake key",
false,
}
rr := httptest.NewRecorder()
span := trace.Span{}
authorizeKnownUser(&userIdentity, h, &session, rr, &span, req.WithContext(ctx), "")

suite.Equal(http.StatusForbidden, rr.Code, "authorizer did not recognize disabled user")

}
7 changes: 7 additions & 0 deletions pkg/auth/authentication/devlocal.go
Expand Up @@ -216,6 +216,13 @@ func loginUser(handler devlocalAuthHandler, user *models.User, w http.ResponseWr
session.IDToken = "devlocal"
session.UserID = userIdentity.ID
session.Email = userIdentity.Email

if userIdentity.Disabled {
handler.logger.Error("Disabled user requesting authentication", zap.String("email", session.Email))
http.Error(w, http.StatusText(403), http.StatusForbidden)
return
}

if userIdentity.ServiceMemberID != nil {
session.ServiceMemberID = *(userIdentity.ServiceMemberID)
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/models/user.go
Expand Up @@ -19,6 +19,7 @@ type User struct {
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
LoginGovUUID uuid.UUID `json:"login_gov_uuid" db:"login_gov_uuid"`
LoginGovEmail string `json:"login_gov_email" db:"login_gov_email"`
Disabled bool `json:"disabled" db:"disabled"`
}

// Users is not required by pop and may be deleted
Expand Down Expand Up @@ -75,6 +76,7 @@ func CreateUser(db *pop.Connection, loginGovID string, email string) (*User, err
// UserIdentity is summary of the information about a user from the database
type UserIdentity struct {
ID uuid.UUID `db:"id"`
Disabled bool `db:"disabled"`
Email string `db:"email"`
ServiceMemberID *uuid.UUID `db:"sm_id"`
ServiceMemberFirstName *string `db:"sm_fname"`
Expand All @@ -96,6 +98,7 @@ func FetchUserIdentity(db *pop.Connection, loginGovID string) (*UserIdentity, er
var identities []UserIdentity
query := `SELECT users.id,
users.login_gov_email as email,
users.disabled as disabled,
sm.id as sm_id,
sm.first_name as sm_fname,
sm.last_name as sm_lname,
Expand Down

0 comments on commit b2be02e

Please sign in to comment.