auth

Paddy 2015-04-11 Parent:762953f6a7f2 Child:6f473576c6ae

160:48200d8c4036 Go to Latest

auth/token_postgres.go

Start to support deleting profiles through the API. Create a removeLoginsByProfile method on the profileStore, to allow an easy way to bulk-delete logins associated with a Profile after the Profile has been deleted. Create postgres and memstore implementations of the removeLoginsByProfile method. Create a cleanUpAfterProfileDeletion helper method that will clean up the child objects of a Profile (its Sessions, Tokens, Clients, etc.). The intended usage is to call this in a goroutine after a Profile has been deleted, to try and get things back in order. Detect when the UpdateProfileHandler API is used to set the Deleted flag of a Profile to true, and clean up after the Profile when that's the case. Add a DeleteProfileHandler API endpoint that is a shortcut to setting the Deleted flag of a Profile to true and cleaning up after the Profile. The problem with our approach thus far is that some of it is reversible and some is not. If a Profile is maliciously/accidentally deleted, it's simple enough to use the API as a superuser to restore the Profile. But doing that will not (and cannot) restore the Logins associated with that Profile, for example. While it would be nice to add a Deleted flag to our Logins that we could simply toggle, that would wreak havoc with our database constraints and ensuring uniqueness of Login values. I still don't have a solution for this, outside the superuser manually restoring a Login for the Profile, after which the user can authenticate themselves and add more Logins as desired. But there has to be a better way. I suppose since the passphrase is being stored with the Profile and not the Login, we could offer an endpoint that would automate this, but... well, that would be tricky. It would require the user remembering their Profile ID, and let's be honest, nobody's going to remember a UUID. Maybe such an endpoint would help from a customer service standpoint: we identify their Profile manually, then send them to /profiles/ID/restorelogin or something, and that lets them add a Login back to the Profile. I'll figure it out later. For now, we know we at least have enough information to identify a user is who they say they are and resolve the situation manually.

History
paddy@155 1 package auth
paddy@155 2
paddy@155 3 import (
paddy@155 4 "code.secondbit.org/uuid.hg"
paddy@155 5
paddy@155 6 "github.com/lib/pq"
paddy@155 7 "github.com/secondbit/pan"
paddy@155 8 )
paddy@155 9
paddy@155 10 type tokenScope struct {
paddy@155 11 Token string
paddy@155 12 Scope string
paddy@155 13 }
paddy@155 14
paddy@155 15 func (t tokenScope) GetSQLTableName() string {
paddy@155 16 return "scopes_tokens"
paddy@155 17 }
paddy@155 18
paddy@155 19 func (t Token) GetSQLTableName() string {
paddy@155 20 return "tokens"
paddy@155 21 }
paddy@155 22
paddy@155 23 func (p *postgres) getTokenSQL(token string, refresh bool) *pan.Query {
paddy@155 24 var t Token
paddy@155 25 fields, _ := pan.GetFields(t)
paddy@155 26 query := pan.New(pan.POSTGRES, "SELECT "+pan.QueryList(fields)+" FROM "+pan.GetTableName(t))
paddy@155 27 query.IncludeWhere()
paddy@155 28 if !refresh {
paddy@155 29 query.Include(pan.GetUnquotedColumn(t, "AccessToken")+" = ?", token)
paddy@155 30 } else {
paddy@155 31 query.Include(pan.GetUnquotedColumn(t, "RefreshToken")+" = ?", token)
paddy@155 32 }
paddy@155 33 return query.FlushExpressions(" ")
paddy@155 34 }
paddy@155 35
paddy@155 36 func (p *postgres) getToken(token string, refresh bool) (Token, error) {
paddy@155 37 query := p.getTokenSQL(token, refresh)
paddy@155 38 rows, err := p.db.Query(query.String(), query.Args...)
paddy@155 39 if err != nil {
paddy@155 40 return Token{}, err
paddy@155 41 }
paddy@155 42 var t Token
paddy@155 43 var found bool
paddy@155 44 for rows.Next() {
paddy@155 45 err := pan.Unmarshal(rows, &t)
paddy@155 46 if err != nil {
paddy@155 47 return t, err
paddy@155 48 }
paddy@155 49 found = true
paddy@155 50 }
paddy@155 51 if err = rows.Err(); err != nil {
paddy@155 52 return t, err
paddy@155 53 }
paddy@155 54 if !found {
paddy@155 55 return t, ErrTokenNotFound
paddy@155 56 }
paddy@155 57 query = p.getTokenScopesSQL([]string{t.AccessToken})
paddy@155 58 rows, err = p.db.Query(query.String(), query.Args...)
paddy@155 59 if err != nil {
paddy@155 60 return t, err
paddy@155 61 }
paddy@155 62 for rows.Next() {
paddy@155 63 var ts tokenScope
paddy@155 64 err = pan.Unmarshal(rows, &ts)
paddy@155 65 if err != nil {
paddy@155 66 return t, err
paddy@155 67 }
paddy@155 68 t.Scopes = append(t.Scopes, ts.Scope)
paddy@155 69 }
paddy@155 70 if err = rows.Err(); err != nil {
paddy@155 71 return t, err
paddy@155 72 }
paddy@155 73 return t, nil
paddy@155 74 }
paddy@155 75
paddy@155 76 func (p *postgres) saveTokenSQL(token Token) *pan.Query {
paddy@155 77 fields, values := pan.GetFields(token)
paddy@155 78 query := pan.New(pan.POSTGRES, "INSERT INTO "+pan.GetTableName(token))
paddy@155 79 query.Include("(" + pan.QueryList(fields) + ")")
paddy@155 80 query.Include("VALUES")
paddy@155 81 query.Include("("+pan.VariableList(len(values))+")", values...)
paddy@155 82 return query.FlushExpressions(" ")
paddy@155 83 }
paddy@155 84
paddy@155 85 func (p *postgres) saveTokenScopesSQL(ts []tokenScope) *pan.Query {
paddy@155 86 fields, _ := pan.GetFields(ts[0])
paddy@155 87 query := pan.New(pan.POSTGRES, "INSERT INTO "+pan.GetTableName(ts[0]))
paddy@155 88 query.Include("(" + pan.QueryList(fields) + ")")
paddy@155 89 query.Include("VALUES")
paddy@155 90 query.FlushExpressions(" ")
paddy@155 91 for _, t := range ts {
paddy@155 92 _, values := pan.GetFields(t)
paddy@155 93 query.Include("("+pan.VariableList(len(values))+")", values...)
paddy@155 94 }
paddy@155 95 return query.FlushExpressions(", ")
paddy@155 96 }
paddy@155 97
paddy@155 98 func (p *postgres) saveToken(token Token) error {
paddy@155 99 query := p.saveTokenSQL(token)
paddy@155 100 _, err := p.db.Exec(query.String(), query.Args...)
paddy@155 101 if e, ok := err.(*pq.Error); ok && e.Constraint == "tokens_pkey" {
paddy@155 102 err = ErrTokenAlreadyExists
paddy@155 103 }
paddy@155 104 if err != nil || len(token.Scopes) < 1 {
paddy@155 105 return err
paddy@155 106 }
paddy@155 107 var ts []tokenScope
paddy@155 108 for _, scope := range token.Scopes {
paddy@155 109 ts = append(ts, tokenScope{Token: token.AccessToken, Scope: scope})
paddy@155 110 }
paddy@155 111 query = p.saveTokenScopesSQL(ts)
paddy@155 112 _, err = p.db.Exec(query.String(), query.Args...)
paddy@155 113 return err
paddy@155 114 }
paddy@155 115
paddy@155 116 func (p *postgres) revokeTokenSQL(token string, refresh bool) *pan.Query {
paddy@155 117 var t Token
paddy@155 118 query := pan.New(pan.POSTGRES, "UPDATE "+pan.GetTableName(t)+" SET ")
paddy@155 119 query.Include(pan.GetUnquotedColumn(t, "Revoked")+" = ?", true)
paddy@155 120 query.IncludeWhere()
paddy@155 121 if !refresh {
paddy@155 122 query.Include(pan.GetUnquotedColumn(t, "AccessToken")+" = ?", token)
paddy@155 123 } else {
paddy@155 124 query.Include(pan.GetUnquotedColumn(t, "RefreshToken")+" = ?", token)
paddy@155 125 }
paddy@155 126 return query.FlushExpressions(" ")
paddy@155 127 }
paddy@155 128
paddy@155 129 func (p *postgres) revokeToken(token string, refresh bool) error {
paddy@155 130 query := p.revokeTokenSQL(token, refresh)
paddy@155 131 res, err := p.db.Exec(query.String(), query.Args...)
paddy@155 132 if err != nil {
paddy@155 133 return err
paddy@155 134 }
paddy@155 135 rows, err := res.RowsAffected()
paddy@155 136 if err != nil {
paddy@155 137 return err
paddy@155 138 }
paddy@155 139 if rows == 0 {
paddy@155 140 return ErrTokenNotFound
paddy@155 141 }
paddy@155 142 return nil
paddy@155 143 }
paddy@155 144
paddy@155 145 func (p *postgres) getTokensByProfileIDSQL(profileID uuid.ID, num, offset int) *pan.Query {
paddy@155 146 var token Token
paddy@155 147 fields, _ := pan.GetFields(token)
paddy@155 148 query := pan.New(pan.POSTGRES, "SELECT "+pan.QueryList(fields)+" FROM "+pan.GetTableName(token))
paddy@155 149 query.IncludeWhere()
paddy@155 150 query.Include(pan.GetUnquotedColumn(token, "ProfileID")+" = ?", profileID)
paddy@155 151 query.IncludeLimit(int64(num))
paddy@155 152 query.IncludeOffset(int64(offset))
paddy@155 153 return query.FlushExpressions(" ")
paddy@155 154 }
paddy@155 155
paddy@155 156 func (p *postgres) getTokenScopesSQL(tokens []string) *pan.Query {
paddy@155 157 var t tokenScope
paddy@155 158 fields, _ := pan.GetFields(t)
paddy@155 159 tokensI := make([]interface{}, len(tokens))
paddy@155 160 for pos, token := range tokens {
paddy@155 161 tokensI[pos] = token
paddy@155 162 }
paddy@155 163 query := pan.New(pan.POSTGRES, "SELECT "+pan.QueryList(fields)+" FROM "+pan.GetTableName(t))
paddy@155 164 query.IncludeWhere()
paddy@155 165 query.Include(pan.GetUnquotedColumn(t, "Token")+" IN ("+pan.VariableList(len(tokensI))+")", tokensI...)
paddy@155 166 return query.FlushExpressions(" ")
paddy@155 167 }
paddy@155 168
paddy@155 169 func (p *postgres) getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) {
paddy@155 170 query := p.getTokensByProfileIDSQL(profileID, num, offset)
paddy@155 171 rows, err := p.db.Query(query.String(), query.Args...)
paddy@155 172 if err != nil {
paddy@155 173 return []Token{}, err
paddy@155 174 }
paddy@155 175 var tokens []Token
paddy@155 176 var tokenIDs []string
paddy@155 177 for rows.Next() {
paddy@155 178 var token Token
paddy@155 179 err = pan.Unmarshal(rows, &token)
paddy@155 180 if err != nil {
paddy@155 181 return tokens, err
paddy@155 182 }
paddy@155 183 tokens = append(tokens, token)
paddy@155 184 tokenIDs = append(tokenIDs, token.AccessToken)
paddy@155 185 }
paddy@155 186 if err = rows.Err(); err != nil {
paddy@155 187 return tokens, err
paddy@155 188 }
paddy@155 189 if len(tokenIDs) < 1 {
paddy@155 190 return tokens, nil
paddy@155 191 }
paddy@155 192 scopes := map[string][]string{}
paddy@155 193 query = p.getTokenScopesSQL(tokenIDs)
paddy@155 194 rows, err = p.db.Query(query.String(), query.Args...)
paddy@155 195 if err != nil {
paddy@155 196 return tokens, err
paddy@155 197 }
paddy@155 198 for rows.Next() {
paddy@155 199 var t tokenScope
paddy@155 200 err = pan.Unmarshal(rows, &t)
paddy@155 201 if err != nil {
paddy@155 202 return tokens, err
paddy@155 203 }
paddy@155 204 scopes[t.Token] = append(scopes[t.Token], t.Scope)
paddy@155 205 }
paddy@155 206 if err = rows.Err(); err != nil {
paddy@155 207 return tokens, err
paddy@155 208 }
paddy@155 209 for pos, token := range tokens {
paddy@155 210 token.Scopes = scopes[token.AccessToken]
paddy@155 211 tokens[pos] = token
paddy@155 212 }
paddy@155 213 return tokens, nil
paddy@155 214 }