From a397341ad471cc761f7fb930d77e53cf7eb40a2a Mon Sep 17 00:00:00 2001 From: Max Resnick Date: Sun, 8 Nov 2020 11:45:16 -0800 Subject: adds casbin and accounts --- internal/acct/acct.go | 106 ++++++++++++++++++++++++++++++++++++++++++ internal/auth/handler.go | 35 ++++++-------- internal/auth/middleware.go | 45 ++++++++++++++++++ internal/auth/model.go | 15 +----- internal/auth/repo.go | 13 ++++-- internal/auth/service.go | 36 +++++++++++--- internal/auth/service_test.go | 3 +- internal/db/redis/acct.go | 44 ++++++++++++++++++ internal/db/redis/auth.go | 51 +++++++++++++------- internal/db/redis/image.go | 2 +- internal/goog/goog.go | 89 +++++++++++++++++++++++++++++++++++ internal/image/handler.go | 5 +- internal/image/service.go | 1 + 13 files changed, 381 insertions(+), 64 deletions(-) create mode 100644 internal/acct/acct.go create mode 100644 internal/auth/middleware.go create mode 100644 internal/db/redis/acct.go create mode 100644 internal/goog/goog.go (limited to 'internal') diff --git a/internal/acct/acct.go b/internal/acct/acct.go new file mode 100644 index 0000000..48a71e6 --- /dev/null +++ b/internal/acct/acct.go @@ -0,0 +1,106 @@ +package acct + +import ( + "context" + "net/http" + + "git.ofmax.li/iserv/internal/auth" + "git.ofmax.li/iserv/internal/goog" + "github.com/alexedwards/scs/v2" + "golang.org/x/oauth2" +) + +// handler + +// Handler interface to http handler +type Handler interface { + Register(w http.ResponseWriter, r *http.Request) +} + +type acctHandler struct { + svc Servicer + ses *scs.SessionManager +} + +func (h *acctHandler) Register(w http.ResponseWriter, r *http.Request) { + userID := h.ses.GetString(r.Context(), "profid") + if userID == "" { + http.Redirect(w, r, "/l", http.StatusFound) + } + h.svc.GetGoogProfile(r.Context(), userID) + return +} + +// NewHandler create new instance of handler +// Servicer, session, and oauth client required +func NewHandler(service Servicer, session *scs.SessionManager) Handler { + return &acctHandler{ + service, + session, + } +} + +// endhandler + +// model + +// Profile application account profile +type Profile interface { + ProfileKeyValues() (string, []string) +} + +// endmodel + +// repo +// Repo storage interface + +type Repo interface { + UpdateAcctProfile(p Profile) error +} + +// endrepo + +// service + +type Servicer interface { + UpdateProfile(p *Profile) error + GetGoogProfile(ctx context.Context, id string) error +} + +func NewService(repo Repo, authRepo auth.Repo, goog goog.Servicer) *service { + return &service{ + repo, + authRepo, + goog, + } +} + +type service struct { + repo Repo + authRepo auth.Repo + goog goog.Servicer +} + +func (s *service) UpdateProfile(p *Profile) error { + return nil +} + +func (s *service) GetGoogProfile(ctx context.Context, id string) error { + token, err := s.authRepo.GetProfileToken(id) + if err != nil { + return err + } + client := s.goog.UserClient(ctx, token) + gp, _, err := s.goog.Profile(client) + if err != nil { + return err + } + s.repo.UpdateAcctProfile(gp) + return nil +} + +func (s *service) GetProfileToken(id string) (*oauth2.Token, error) { + return s.authRepo.GetProfileToken(id) +} + +// endservice diff --git a/internal/auth/handler.go b/internal/auth/handler.go index 992608c..d57d47e 100644 --- a/internal/auth/handler.go +++ b/internal/auth/handler.go @@ -3,15 +3,12 @@ package auth import ( "encoding/json" "io/ioutil" - "log" "net/http" "github.com/alexedwards/scs/v2" "golang.org/x/oauth2" -) -var ( - profileURL = "https://www.googleapis.com/oauth2/v3/userinfo" + "git.ofmax.li/iserv/internal/goog" ) // Handler authentication handler @@ -22,30 +19,28 @@ type Handler interface { } type authHandler struct { - service Servicer - ses *scs.SessionManager - oclient *oauth2.Config + svc Servicer + ses *scs.SessionManager } +// TODO migrate to Goog // NewHandler create new instance of handler // Servicer, session, and oauth client required func NewHandler(service Servicer, - session *scs.SessionManager, - oclient *oauth2.Config) Handler { + session *scs.SessionManager) Handler { return &authHandler{ service, session, - oclient, } } func (h *authHandler) Login(w http.ResponseWriter, r *http.Request) { - stateValue, err := h.service.GenerateStateToken() + stateValue, err := h.svc.GenerateStateToken() if err != nil { return } h.ses.Put(r.Context(), "state", stateValue) - url := h.oclient.AuthCodeURL(stateValue, oauth2.AccessTypeOnline) + url := h.svc.Goog().Config().AuthCodeURL(stateValue, oauth2.AccessTypeOnline) http.Redirect(w, r, url, 302) } @@ -58,7 +53,7 @@ func (h *authHandler) OauthCallback(w http.ResponseWriter, r *http.Request) { http.Error(w, "state value miss match bad data", 400) return } - stateValid, err := h.service.ValidateStateToken(stateFromCallback, stateFromSession) + stateValid, err := h.svc.ValidateStateToken(stateFromCallback, stateFromSession) if err != nil { http.Error(w, "error validating", 400) return @@ -69,15 +64,14 @@ func (h *authHandler) OauthCallback(w http.ResponseWriter, r *http.Request) { } // valid and same as state code := r.FormValue("code") - token, err := h.oclient.Exchange(r.Context(), code) + token, err := h.svc.Goog().Config().Exchange(r.Context(), code) if err != nil { http.Error(w, err.Error(), 400) return } - log.Printf("returned token %v", token) // google profile - client := h.oclient.Client(r.Context(), token) - resp, err := client.Get(profileURL) + client := h.svc.Goog().UserClient(r.Context(), token) + resp, err := client.Get(goog.ProfileURL) if err != nil { http.Error(w, err.Error(), 400) return @@ -88,9 +82,9 @@ func (h *authHandler) OauthCallback(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), 400) return } - gp := &GoogleAuthProfile{} + gp := &goog.GoogleProfile{} err = json.Unmarshal(data, &gp) - profileID, newProfile, err := h.service.LoginOrRegisterSessionID(token, gp) + profileID, newProfile, err := h.svc.LoginOrRegisterSessionID(token, gp) if err != nil { http.Error(w, err.Error(), 400) return @@ -98,8 +92,9 @@ func (h *authHandler) OauthCallback(w http.ResponseWriter, r *http.Request) { h.ses.Put(r.Context(), "profid", profileID) // send to registration if newProfile == true { - http.Redirect(w, r, "/account/register", 302) + http.Redirect(w, r, "/u/register", 302) return } http.Redirect(w, r, "/", 302) + return } diff --git a/internal/auth/middleware.go b/internal/auth/middleware.go new file mode 100644 index 0000000..0be033c --- /dev/null +++ b/internal/auth/middleware.go @@ -0,0 +1,45 @@ +package auth + +import ( + "net/http" + + "github.com/alexedwards/scs/v2" + "github.com/apex/log" +) + +const ( + loginURL = "/login" +) + +func AuthOnly(s Servicer, ses *scs.SessionManager) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + userID := ses.GetString(r.Context(), "profid") + if userID == "" { + userID = "anon" + } + resource := r.URL.Path + // set the action to something that will never match + action := "forbidden" + switch r.Method { + case "POST", "PUT", "PATCH": + action = "write" + case "HEAD", "GET": + action = "read" + } + // TODO determine action + enforced, err := s.Enf().EnforceSafe(userID, resource, action) + if err != nil { + log.Errorf("%s", err) + return + } + if !enforced { + // TODO probably need to do something about suggesting to login + http.Error(w, "not found, are you signed in?", http.StatusNotFound) + return + } + next.ServeHTTP(w, r) + } + return http.HandlerFunc(fn) + } +} diff --git a/internal/auth/model.go b/internal/auth/model.go index c51ff05..240b11b 100644 --- a/internal/auth/model.go +++ b/internal/auth/model.go @@ -6,20 +6,10 @@ import ( "golang.org/x/oauth2" ) -// GoogleAuthProfile auth'd user profile -// ProfileId, Email, Name, PictureURL -type GoogleAuthProfile struct { - ProfileID string `json:"sub"` - Email string `json:"email"` - Name string `json:"name"` - PictureURL string `json:"picture"` -} - // Profile profile and token // Identifier + Token type Profile struct { ID string `json:"sub"` - Email string `json:"email"` AccessToken string `json:"access_token"` TokenType string `json:"token_type,omitempty"` RefreshToken string `json:"refresh_token,omitempty"` @@ -37,10 +27,9 @@ func (ap *Profile) Token() (*oauth2.Token, error) { } // NewAuthProfile merge token and profile -func NewAuthProfile(t *oauth2.Token, u *GoogleAuthProfile) *Profile { +func NewAuthProfile(t *oauth2.Token, id string) *Profile { return &Profile{ - u.ProfileID, - u.Email, + id, t.AccessToken, t.TokenType, t.RefreshToken, diff --git a/internal/auth/repo.go b/internal/auth/repo.go index f404c94..9a0420e 100644 --- a/internal/auth/repo.go +++ b/internal/auth/repo.go @@ -1,8 +1,15 @@ package auth +import ( + "git.ofmax.li/iserv/internal/goog" + "golang.org/x/oauth2" +) + // Repo storage interface type Repo interface { - IsAuthorized(gp *GoogleAuthProfile) (bool, error) - LookUpAuthProfileID(gp *GoogleAuthProfile) (string, error) - SaveAuthProfile(ap *Profile) error + IsAuthorized(gp *goog.GoogleProfile) (bool, error) + LookUpAuthProfileID(gp *goog.GoogleProfile) (string, error) + SaveAuthProfile(email string, ap *Profile) error + CheckProfileID(id string) (bool, error) + GetProfileToken(id string) (*oauth2.Token, error) } diff --git a/internal/auth/service.go b/internal/auth/service.go index 9997264..e85c705 100644 --- a/internal/auth/service.go +++ b/internal/auth/service.go @@ -2,11 +2,14 @@ package auth import ( "errors" + "fmt" "log" "time" + "git.ofmax.li/iserv/internal/goog" "golang.org/x/oauth2" + "github.com/casbin/casbin" "github.com/gbrlsnchs/jwt/v3" ) @@ -22,23 +25,40 @@ var ( // Servicer access to auth functionality type Servicer interface { - LoginOrRegisterSessionID(t *oauth2.Token, gp *GoogleAuthProfile) (string, bool, error) + Goog() goog.Servicer + LoginOrRegisterSessionID(t *oauth2.Token, gp *goog.GoogleProfile) (string, bool, error) GenerateStateToken() (string, error) ValidateStateToken(token string, sessionToken string) (bool, error) + CheckProfileID(id string) (bool, error) + Enf() *casbin.Enforcer } // Service a container for auth deps type Service struct { repo Repo + goog goog.Servicer + enf *casbin.Enforcer } // NewService create auth service -func NewService(repo Repo) *Service { +func NewService(repo Repo, goog goog.Servicer, enf *casbin.Enforcer) *Service { return &Service{ repo, + goog, + enf, } } +// Goog get google interface +func (a *Service) Goog() goog.Servicer { + return a.goog +} + +// Enf enforcer instance +func (a *Service) Enf() *casbin.Enforcer { + return a.enf +} + // GenerateStateToken create a random token for oauth exchange func (a *Service) GenerateStateToken() (string, error) { now := time.Now() @@ -67,8 +87,13 @@ func (a *Service) ValidateStateToken(token string, sessionToken string) (bool, e return false, ErrInvalidToken } +// CheckProfileID check if a profileid exists +func (a *Service) CheckProfileID(id string) (bool, error) { + return a.repo.CheckProfileID(id) +} + // LoginOrRegisterSessionID create a login -func (a *Service) LoginOrRegisterSessionID(t *oauth2.Token, gp *GoogleAuthProfile) (string, bool, error) { +func (a *Service) LoginOrRegisterSessionID(t *oauth2.Token, gp *goog.GoogleProfile) (string, bool, error) { isAuthorized, err := a.repo.IsAuthorized(gp) newRegistration := false if err != nil { @@ -84,10 +109,9 @@ func (a *Service) LoginOrRegisterSessionID(t *oauth2.Token, gp *GoogleAuthProfil if profileID == "" { // create profile log.Printf("creating new profile") - profile := NewAuthProfile(t, gp) + profile := NewAuthProfile(t, fmt.Sprintf("goog:%s", gp.ProfileID)) profileID = profile.ID - log.Printf("new profile %+v", profile) - err = a.repo.SaveAuthProfile(profile) + err = a.repo.SaveAuthProfile(gp.Email, profile) if err != nil { return "", newRegistration, err } diff --git a/internal/auth/service_test.go b/internal/auth/service_test.go index 72ff709..b992696 100644 --- a/internal/auth/service_test.go +++ b/internal/auth/service_test.go @@ -11,6 +11,7 @@ import ( "github.com/golang/mock/gomock" "git.ofmax.li/iserv/internal/auth" + "git.ofmax.li/iserv/internal/goog" "git.ofmax.li/iserv/internal/mock/mock_auth" ) @@ -77,7 +78,7 @@ func (s *serviceSuite) testValidateStateToken() func(t *testing.T) { func (s *serviceSuite) testLoginOrRegsiterSessionId() func(t *testing.T) { return func(t *testing.T) { // is authorized err - gp := &auth.GoogleAuthProfile{} + gp := &goog.GoogleProfile{} token := &oauth2.Token{} gofakeit.Struct(token) gofakeit.Struct(gp) diff --git a/internal/db/redis/acct.go b/internal/db/redis/acct.go new file mode 100644 index 0000000..72df741 --- /dev/null +++ b/internal/db/redis/acct.go @@ -0,0 +1,44 @@ +package redis + +import ( + "fmt" + + "github.com/gomodule/redigo/redis" + "github.com/pkg/errors" + + "git.ofmax.li/iserv/internal/acct" +) + +// AcctRepo account +type AcctRepo struct { + db *redis.Pool +} + +var ( + acctProfileKey string = "acct:email:%s" +) + +// NewAcctRepo account repo +func NewAcctRepo(conn *redis.Pool) *AcctRepo { + return &AcctRepo{ + conn, + } +} + +// UpdateAcctProfile write profile to redis +func (db *AcctRepo) UpdateAcctProfile(p acct.Profile) error { + conn := db.db.Get() + defer conn.Close() + profileID, profileKeyValues := p.ProfileKeyValues() + acctProfileKey := fmt.Sprintf(acctProfileKey, profileID) + profileArgs := redis.Args{}.Add(acctProfileKey).AddFlat(profileKeyValues) + res, err := redis.String(conn.Do("HMSET", profileArgs...)) + if err != nil { + fmt.Print(err) + return err + } + if res != "OK" { + return errors.Errorf("%s acct was not saved", acctProfileKey) + } + return nil +} diff --git a/internal/db/redis/auth.go b/internal/db/redis/auth.go index 0c5246c..1f99fac 100644 --- a/internal/db/redis/auth.go +++ b/internal/db/redis/auth.go @@ -1,12 +1,14 @@ package redis import ( + "fmt" "log" "github.com/gomodule/redigo/redis" "golang.org/x/oauth2" "git.ofmax.li/iserv/internal/auth" + "git.ofmax.li/iserv/internal/goog" ) var ( @@ -16,6 +18,14 @@ var ( end return nil `) + checkActiveAuthLua = redis.NewScript(1, ` + local email = redis.call("HMGET",KEYS[1],"email") + if email == nil then + return false + end + return redis.call("SISMEMBER",email) + `) + authProfileKey = func(id string) string { return fmt.Sprintf("auth:%s") } ) // AuthRepo supporting auth @@ -32,11 +42,11 @@ func NewRedisAuthRepo(conn *redis.Pool) *AuthRepo { } // IsAuthorized is member of invites -func (db *AuthRepo) IsAuthorized(gp *auth.GoogleAuthProfile) (bool, error) { +func (db *AuthRepo) IsAuthorized(gp *goog.GoogleProfile) (bool, error) { conn := db.db.Get() defer conn.Close() - authProfileKey := "auth:goog:" + gp.Email - reply, err := redis.Bool(conn.Do("SISMEMBER", "invites", authProfileKey)) + inviteKey := "goog:" + gp.Email + reply, err := redis.Bool(conn.Do("SISMEMBER", "invites", inviteKey)) if err != nil { return reply, err } @@ -44,16 +54,12 @@ func (db *AuthRepo) IsAuthorized(gp *auth.GoogleAuthProfile) (bool, error) { } // LookUpAuthProfileID get internal profileid -func (db *AuthRepo) LookUpAuthProfileID(gp *auth.GoogleAuthProfile) (string, error) { +func (db *AuthRepo) LookUpAuthProfileID(gp *goog.GoogleProfile) (string, error) { conn := db.db.Get() var id string defer conn.Close() - authProfileKey := "auth:goog:" + gp.ProfileID - reply, err := redis.Values(conn.Do("HMGET", authProfileKey, "id")) - if err == redis.ErrNil { - // no profile - return "", nil - } else if err != nil { + reply, err := redis.Values(conn.Do("HMGET", authProfileKey(gp.ProfileID), "id")) + if err != nil { // some other error return "", err } @@ -64,24 +70,35 @@ func (db *AuthRepo) LookUpAuthProfileID(gp *auth.GoogleAuthProfile) (string, err } // SaveAuthProfile save profile, validate against invites -func (db *AuthRepo) SaveAuthProfile(ap *auth.Profile) error { +func (db *AuthRepo) SaveAuthProfile(email string, ap *auth.Profile) error { // TODO this will over-write refresh tokens conn := db.db.Get() defer conn.Close() - authProfileKey := "auth:goog:" + ap.ID - prof := redis.Args{}.Add(ap.Email).Add(authProfileKey).AddFlat(ap) - res, err := createAuthLua.Do(conn, prof...) - log.Printf("auth save response %+v", res) + prof := redis.Args{}.Add(email).Add(ap.ID).AddFlat(ap) + _, err := createAuthLua.Do(conn, prof...) conn.Flush() return err } +// CheckProfileID return true if the profile exists and authorized +func (db *AuthRepo) CheckProfileID(id string) (bool, error) { + conn := db.db.Get() + defer conn.Close() + res, err := redis.Int(checkActiveAuthLua.Do(conn, authProfileKey(id))) + if err == redis.ErrNil { + return false, nil + } else if err != nil { + return false, err + } + // there could be profile created but later revoked authorization + return res == 1, nil +} + func (db *AuthRepo) getAuthProfile(id string) (*auth.Profile, error) { conn := db.db.Get() defer conn.Close() - authProfileKey := "auth:goog:" + id authProfile := auth.Profile{} - res, err := redis.Values(conn.Do("HGETALL", authProfileKey)) + res, err := redis.Values(conn.Do("HGETALL", authProfileKey(id))) if err != nil { log.Fatal(err) } diff --git a/internal/db/redis/image.go b/internal/db/redis/image.go index f216531..3622728 100644 --- a/internal/db/redis/image.go +++ b/internal/db/redis/image.go @@ -48,7 +48,7 @@ func (r *ImageRepo) GetFile(fileUrl string) (*image.PostMeta, error) { key := fileKey(fileUrl, V1FilePathFmt) res, err := redis.Values(conn.Do("HGETALL", key)) if err != nil { - return &image.PostMeta{}, ErrNotFound + return &image.PostMeta{}, err } err = redis.ScanStruct(res, imageMeta) if imageMeta.FilePath == "" { diff --git a/internal/goog/goog.go b/internal/goog/goog.go new file mode 100644 index 0000000..5102a95 --- /dev/null +++ b/internal/goog/goog.go @@ -0,0 +1,89 @@ +package goog + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "golang.org/x/oauth2" +) + +var ( + ProfileURL = "https://www.googleapis.com/oauth2/v3/userinfo" + ProfileKeyFmt = "goog:%s" +) + +// GoogleProfile auth'd user profile +// ProfileId, Email, Name, PictureURL +type GoogleProfile struct { + ProfileID string `json:"sub"` + Email string `json:"email"` + Name string `json:"name"` + PictureURL string `json:"picture"` +} + +// ProfileKeys conform to ProfileKeys +func (gp *GoogleProfile) ProfileKeyValues() (string, []string) { + return fmt.Sprintf(ProfileKeyFmt, gp.Email), []string{ + "profile_id", gp.ProfileID, + "email", gp.Email, + "name", gp.Name, + "picture_url", gp.PictureURL, + } +} + +type Servicer interface { + Config() *oauth2.Config + Profile(client *http.Client) (*GoogleProfile, *oauth2.Token, error) + UserClient(ctx context.Context, token *oauth2.Token) *http.Client +} + +// NewService container for interacting with Google +func NewService(o *oauth2.Config) Servicer { + return &Goog{ + o, + } + +} + +type Goog struct { + oauthConfig *oauth2.Config +} + +func (g *Goog) Config() *oauth2.Config { + return g.oauthConfig +} + +// UserCient just calls oauth2.Client +func (g *Goog) UserClient(ctx context.Context, token *oauth2.Token) *http.Client { + return g.oauthConfig.Client(ctx, token) +} + +// Profile get profile from google +func (g *Goog) Profile(client *http.Client) (*GoogleProfile, *oauth2.Token, error) { + client.Get(ProfileURL) + resp, err := client.Get(ProfileURL) + if err != nil { + return &GoogleProfile{}, &oauth2.Token{}, err + } + defer resp.Body.Close() + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return &GoogleProfile{}, &oauth2.Token{}, err + } + // Google Profile + gp := &GoogleProfile{} + err = json.Unmarshal(data, gp) + if err != nil { + return &GoogleProfile{}, &oauth2.Token{}, err + } + // Token + token := &oauth2.Token{} + err = json.Unmarshal(data, token) + if err != nil { + return &GoogleProfile{}, &oauth2.Token{}, err + } + return gp, token, nil +} diff --git a/internal/image/handler.go b/internal/image/handler.go index f41ed4d..71a5383 100644 --- a/internal/image/handler.go +++ b/internal/image/handler.go @@ -35,9 +35,8 @@ func (h *imageHandler) GetImage(w http.ResponseWriter, r *http.Request) { fileID := chi.URLParam(r, "fileName") fileMeta, err := h.service.GetFile(fileID) if err != nil { - w.WriteHeader(400) - log.Printf("error: %+v", err) - w.Write([]byte("WTF Incorrect Content Type")) + http.Error(w, "an error has occured", http.StatusBadRequest) + return } fileUrl := fmt.Sprintf("/f/%s", fileMeta.FilePath) data := struct { diff --git a/internal/image/service.go b/internal/image/service.go index 10f4148..2a60c67 100644 --- a/internal/image/service.go +++ b/internal/image/service.go @@ -78,6 +78,7 @@ func (is *Service) AddFile(extension string, postMeta *PostMeta, fileBytes []byt func (is *Service) GetFile(fileUrl string) (*PostMeta, error) { result, err := is.db.GetFile(fileUrl) if err != nil { + return &PostMeta{}, err } return result, err } -- cgit v1.2.3