auth
134:d103a598548c Browse Files
Introduced scopes. Created a Scope type and a scopeStore interface, along with the memstore methods for the scopeStore. This will allow applications to define access with granularity, so users can grant access to some data, not _all_ data. We're operating on the assumption that there won't be an unreasonable number of scopes defined, so there is no paging operation included for the ListScopes method. This is a decision that may have to be revisited in the future, depending on usecases.
config.go context.go memstore.go scope.go scope_test.go
1.1 --- a/config.go Thu Jan 29 20:40:55 2015 -0500 1.2 +++ b/config.go Fri Feb 20 22:34:43 2015 -0500 1.3 @@ -21,6 +21,7 @@ 1.4 ProfileStore profileStore 1.5 TokenStore tokenStore 1.6 SessionStore sessionStore 1.7 + ScopeStore scopeStore 1.8 Template *template.Template 1.9 LoginURI string 1.10 iterations int
2.1 --- a/context.go Thu Jan 29 20:40:55 2015 -0500 2.2 +++ b/context.go Fri Feb 20 22:34:43 2015 -0500 2.3 @@ -21,6 +21,7 @@ 2.4 profiles profileStore 2.5 tokens tokenStore 2.6 sessions sessionStore 2.7 + scopes scopeStore 2.8 config Config 2.9 } 2.10 2.11 @@ -36,6 +37,7 @@ 2.12 profiles: config.ProfileStore, 2.13 tokens: config.TokenStore, 2.14 sessions: config.SessionStore, 2.15 + scopes: config.ScopeStore, 2.16 template: config.Template, 2.17 config: config, 2.18 } 2.19 @@ -363,3 +365,38 @@ 2.20 } 2.21 return c.sessions.listSessions(profile, before, num) 2.22 } 2.23 + 2.24 +func (c Context) CreateScopes(scopes []Scope) error { 2.25 + if c.scopes == nil { 2.26 + return ErrNoScopeStore 2.27 + } 2.28 + return c.scopes.createScopes(scopes) 2.29 +} 2.30 + 2.31 +func (c Context) GetScopes(ids []string) ([]Scope, error) { 2.32 + if c.scopes == nil { 2.33 + return []Scope{}, ErrNoScopeStore 2.34 + } 2.35 + return c.scopes.getScopes(ids) 2.36 +} 2.37 + 2.38 +func (c Context) UpdateScopes(changes []ScopeChange) ([]Scope, error) { 2.39 + if c.scopes == nil { 2.40 + return []Scope{}, ErrNoScopeStore 2.41 + } 2.42 + return c.scopes.updateScopes(changes) 2.43 +} 2.44 + 2.45 +func (c Context) RemoveScopes(ids []string) error { 2.46 + if c.scopes == nil { 2.47 + return ErrNoScopeStore 2.48 + } 2.49 + return c.scopes.removeScopes(ids) 2.50 +} 2.51 + 2.52 +func (c Context) ListScopes() ([]Scope, error) { 2.53 + if c.scopes == nil { 2.54 + return []Scope{}, ErrNoScopeStore 2.55 + } 2.56 + return c.scopes.listScopes() 2.57 +}
3.1 --- a/memstore.go Thu Jan 29 20:40:55 2015 -0500 3.2 +++ b/memstore.go Fri Feb 20 22:34:43 2015 -0500 3.3 @@ -31,6 +31,9 @@ 3.4 3.5 sessions map[string]Session 3.6 sessionLock sync.RWMutex 3.7 + 3.8 + scopes map[string]Scope 3.9 + scopeLock sync.RWMutex 3.10 } 3.11 3.12 // NewMemstore returns an in-memory version of our datastores, 3.13 @@ -50,6 +53,7 @@ 3.14 logins: map[string]Login{}, 3.15 profileLoginLookup: map[string][]string{}, 3.16 sessions: map[string]Session{}, 3.17 + scopes: map[string]Scope{}, 3.18 } 3.19 } 3.20
4.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 4.2 +++ b/scope.go Fri Feb 20 22:34:43 2015 -0500 4.3 @@ -0,0 +1,159 @@ 4.4 +package auth 4.5 + 4.6 +import ( 4.7 + "errors" 4.8 + "fmt" 4.9 + "sort" 4.10 +) 4.11 + 4.12 +var ( 4.13 + ErrNoScopeStore = errors.New("scopeStore not set in Context") 4.14 +) 4.15 + 4.16 +type ErrScopeNotFound struct { 4.17 + Pos int 4.18 + ID string 4.19 +} 4.20 + 4.21 +func (e ErrScopeNotFound) Error() string { 4.22 + return fmt.Sprintf("scope %s couldn't be found", e.ID) 4.23 +} 4.24 + 4.25 +type ErrScopeAlreadyExists struct { 4.26 + Pos int 4.27 + ID string 4.28 +} 4.29 + 4.30 +func (e ErrScopeAlreadyExists) Error() string { 4.31 + return fmt.Sprintf("scope %s already exists", e.ID) 4.32 +} 4.33 + 4.34 +// Scope represents a limit on the access that a grant provides. 4.35 +type Scope struct { 4.36 + ID string 4.37 + Name string 4.38 + Description string 4.39 +} 4.40 + 4.41 +func (s *Scope) ApplyChange(change ScopeChange) { 4.42 + if change.Name != nil { 4.43 + s.Name = *change.Name 4.44 + } 4.45 + if change.Description != nil { 4.46 + s.Description = *change.Description 4.47 + } 4.48 +} 4.49 + 4.50 +type sortedScopes []Scope 4.51 + 4.52 +func (s sortedScopes) Len() int { 4.53 + return len(s) 4.54 +} 4.55 + 4.56 +func (s sortedScopes) Swap(i, j int) { 4.57 + s[i], s[j] = s[j], s[i] 4.58 +} 4.59 + 4.60 +func (s sortedScopes) Less(i, j int) bool { 4.61 + return s[i].ID < s[j].ID 4.62 +} 4.63 + 4.64 +// ScopeChange represents a change to a Scope. 4.65 +type ScopeChange struct { 4.66 + ID string 4.67 + Name *string 4.68 + Description *string 4.69 +} 4.70 + 4.71 +type scopeStore interface { 4.72 + createScopes(scopes []Scope) error 4.73 + getScopes(ids []string) ([]Scope, error) 4.74 + updateScopes(changes []ScopeChange) ([]Scope, error) 4.75 + removeScopes(ids []string) error 4.76 + listScopes() ([]Scope, error) 4.77 +} 4.78 + 4.79 +func (m *memstore) createScopes(scopes []Scope) error { 4.80 + m.scopeLock.Lock() 4.81 + defer m.scopeLock.Unlock() 4.82 + 4.83 + for pos, scope := range scopes { 4.84 + if _, ok := m.scopes[scope.ID]; ok { 4.85 + return ErrScopeAlreadyExists{Pos: pos, ID: scope.ID} 4.86 + } 4.87 + } 4.88 + for _, scope := range scopes { 4.89 + m.scopes[scope.ID] = scope 4.90 + } 4.91 + return nil 4.92 +} 4.93 + 4.94 +func (m *memstore) getScopes(ids []string) ([]Scope, error) { 4.95 + m.scopeLock.RLock() 4.96 + defer m.scopeLock.RUnlock() 4.97 + 4.98 + scopes := []Scope{} 4.99 + for pos, id := range ids { 4.100 + scope, ok := m.scopes[id] 4.101 + if !ok { 4.102 + return []Scope{}, ErrScopeNotFound{ID: id, Pos: pos} 4.103 + } 4.104 + scopes = append(scopes, scope) 4.105 + } 4.106 + sorted := sortedScopes(scopes) 4.107 + sort.Sort(sorted) 4.108 + scopes = sorted 4.109 + return scopes, nil 4.110 +} 4.111 + 4.112 +func (m *memstore) updateScopes(changes []ScopeChange) ([]Scope, error) { 4.113 + m.scopeLock.Lock() 4.114 + defer m.scopeLock.Unlock() 4.115 + 4.116 + scopes := []Scope{} 4.117 + 4.118 + for pos, change := range changes { 4.119 + if _, ok := m.scopes[change.ID]; !ok { 4.120 + return []Scope{}, ErrScopeNotFound{Pos: pos, ID: change.ID} 4.121 + } 4.122 + } 4.123 + for _, change := range changes { 4.124 + scope := m.scopes[change.ID] 4.125 + scope.ApplyChange(change) 4.126 + m.scopes[change.ID] = scope 4.127 + scopes = append(scopes, scope) 4.128 + } 4.129 + sorted := sortedScopes(scopes) 4.130 + sort.Sort(sorted) 4.131 + scopes = sorted 4.132 + return scopes, nil 4.133 +} 4.134 + 4.135 +func (m *memstore) removeScopes(ids []string) error { 4.136 + m.scopeLock.Lock() 4.137 + defer m.scopeLock.Unlock() 4.138 + 4.139 + for pos, id := range ids { 4.140 + if _, ok := m.scopes[id]; !ok { 4.141 + return ErrScopeNotFound{Pos: pos, ID: id} 4.142 + } 4.143 + } 4.144 + for _, id := range ids { 4.145 + delete(m.scopes, id) 4.146 + } 4.147 + return nil 4.148 +} 4.149 + 4.150 +func (m *memstore) listScopes() ([]Scope, error) { 4.151 + m.scopeLock.RLock() 4.152 + defer m.scopeLock.RUnlock() 4.153 + 4.154 + scopes := []Scope{} 4.155 + for _, scope := range m.scopes { 4.156 + scopes = append(scopes, scope) 4.157 + } 4.158 + sorted := sortedScopes(scopes) 4.159 + sort.Sort(sorted) 4.160 + scopes = sorted 4.161 + return scopes, nil 4.162 +}
5.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 5.2 +++ b/scope_test.go Fri Feb 20 22:34:43 2015 -0500 5.3 @@ -0,0 +1,197 @@ 5.4 +package auth 5.5 + 5.6 +import "testing" 5.7 + 5.8 +var scopeStores = []scopeStore{NewMemstore()} 5.9 + 5.10 +func compareScopes(scope1, scope2 Scope) (success bool, field string, val1, val2 interface{}) { 5.11 + if scope1.ID != scope2.ID { 5.12 + return false, "ID", scope1.ID, scope2.ID 5.13 + } 5.14 + if scope1.Name != scope2.Name { 5.15 + return false, "Name", scope1.Name, scope2.Name 5.16 + } 5.17 + if scope1.Description != scope2.Description { 5.18 + return false, "Description", scope1.Description, scope2.Description 5.19 + } 5.20 + return true, "", nil, nil 5.21 +} 5.22 + 5.23 +func TestScopeInScopeStore(t *testing.T) { 5.24 + scope := Scope{ 5.25 + ID: "testscope", 5.26 + Name: "Test Scope", 5.27 + Description: "Access to testing data.", 5.28 + } 5.29 + scope2 := Scope{ 5.30 + ID: "testscope2", 5.31 + Name: "Test Scope 2", 5.32 + Description: "Access to minions.", 5.33 + } 5.34 + scope3 := Scope{ 5.35 + ID: "testscope3", 5.36 + Name: "Test Scope 3", 5.37 + Description: "Access to bananas.", 5.38 + } 5.39 + updatedName := "Updated Scope" 5.40 + updatedDescription := "An updated scope." 5.41 + update := ScopeChange{ 5.42 + ID: scope.ID, 5.43 + Name: &updatedName, 5.44 + } 5.45 + update2 := ScopeChange{ 5.46 + ID: scope2.ID, 5.47 + Description: &updatedDescription, 5.48 + } 5.49 + update3 := ScopeChange{ 5.50 + ID: scope3.ID, 5.51 + Name: &updatedName, 5.52 + Description: &updatedDescription, 5.53 + } 5.54 + for _, store := range scopeStores { 5.55 + context := Context{scopes: store} 5.56 + retrieved, err := context.GetScopes([]string{scope.ID}) 5.57 + if len(retrieved) != 0 { 5.58 + t.Logf("%+v", retrieved) 5.59 + t.Errorf("Expected %d results, got %d from %T", 0, len(retrieved), store) 5.60 + } 5.61 + if e, ok := err.(ErrScopeNotFound); !ok { 5.62 + t.Errorf("Expected ErrScopeNotFound, got %+v instead for %T", err, store) 5.63 + } else { 5.64 + if e.Pos != 0 { 5.65 + t.Errorf("Expected the error to be in position %d, got position %d from %T", 0, e.Pos, store) 5.66 + } 5.67 + if e.ID != scope.ID { 5.68 + t.Errorf("Expected the error to be with scope %s, got %s from %T", scope.ID, e.ID, store) 5.69 + } 5.70 + } 5.71 + err = context.CreateScopes([]Scope{scope}) 5.72 + if err != nil { 5.73 + t.Errorf("Error saving scope to %T: %s", store, err) 5.74 + } 5.75 + err = context.CreateScopes([]Scope{scope}) 5.76 + if e, ok := err.(ErrScopeAlreadyExists); !ok { 5.77 + t.Errorf("Expected ErrScopeAlreadyExists, got %s instead for %T", err, store) 5.78 + } else { 5.79 + if e.Pos != 0 { 5.80 + t.Errorf("Expected the error to be in position %d, got position %d from %T", 0, e.Pos, store) 5.81 + } 5.82 + if e.ID != scope.ID { 5.83 + t.Errorf("Expected the error to be for ID %s, got %s from %T", scope.ID, e.ID, store) 5.84 + } 5.85 + } 5.86 + retrieved, err = context.GetScopes([]string{scope.ID}) 5.87 + if err != nil { 5.88 + t.Errorf("Unexpected error retrieving scopes from %T: %+v", store, err) 5.89 + } 5.90 + if len(retrieved) != 1 { 5.91 + t.Logf("%+v", retrieved) 5.92 + t.Errorf("Expected %d results, got %d from %T", 1, len(retrieved), store) 5.93 + } 5.94 + success, field, val1, val2 := compareScopes(scope, retrieved[0]) 5.95 + if !success { 5.96 + t.Errorf("Expected %s to be %+v, got %+v from %T", field, val1, val2, store) 5.97 + } 5.98 + err = context.CreateScopes([]Scope{scope2, scope3}) 5.99 + if err != nil { 5.100 + t.Errorf("Unexpected error trying to create scope2 and scope3 in %T: %+v", store, err) 5.101 + } 5.102 + retrieved, err = context.GetScopes([]string{scope.ID, scope2.ID, scope3.ID}) 5.103 + if err != nil { 5.104 + t.Errorf("Unexpected error retrieving scopes from %T: %+v", store, err) 5.105 + } 5.106 + if len(retrieved) != 3 { 5.107 + t.Logf("%+v", retrieved) 5.108 + t.Errorf("Expected %d results, got %d from %T", 3, len(retrieved), store) 5.109 + } 5.110 + for pos, s := range []Scope{scope, scope2, scope3} { 5.111 + success, field, val1, val2 = compareScopes(s, retrieved[pos]) 5.112 + if !success { 5.113 + t.Errorf("Expected %s to be %+v for scope %s, got %+v from %T", field, val1, s.ID, val2, store) 5.114 + } 5.115 + } 5.116 + updated, err := context.UpdateScopes([]ScopeChange{update, update2, update3}) 5.117 + if err != nil { 5.118 + t.Errorf("Unexpected error updating scopes in %T: %+v", store, err) 5.119 + } 5.120 + if len(updated) != 3 { 5.121 + t.Logf("%+v", updated) 5.122 + t.Errorf("Expected %d results, got %d from %T", 3, len(updated), store) 5.123 + } 5.124 + scope.ApplyChange(update) 5.125 + scope2.ApplyChange(update2) 5.126 + scope3.ApplyChange(update3) 5.127 + for pos, s := range []Scope{scope, scope2, scope3} { 5.128 + success, field, val1, val2 = compareScopes(s, updated[pos]) 5.129 + if !success { 5.130 + t.Errorf("Expected %s to be %+v for scope %s, got %+v from %T", field, val1, s.ID, val2, store) 5.131 + } 5.132 + } 5.133 + retrieved, err = context.ListScopes() 5.134 + if err != nil { 5.135 + t.Errorf("Unexpected error retrieving scopes from %T: %+v", store, err) 5.136 + } 5.137 + if len(retrieved) != 3 { 5.138 + t.Logf("%+v", retrieved) 5.139 + t.Errorf("Expected %d results, got %d from %T", 3, len(retrieved), store) 5.140 + } 5.141 + for pos, s := range []Scope{scope, scope2, scope3} { 5.142 + success, field, val1, val2 = compareScopes(s, retrieved[pos]) 5.143 + if !success { 5.144 + t.Errorf("Expected %s to be %+v for scope %s, got $+v from %T", field, val1, s.ID, val2, store) 5.145 + } 5.146 + } 5.147 + err = context.RemoveScopes([]string{scope.ID, scope2.ID, scope3.ID}) 5.148 + if err != nil { 5.149 + t.Errorf("Unexpected error removing scopes from %T: %+v", store, err) 5.150 + } 5.151 + retrieved, err = context.ListScopes() 5.152 + if err != nil { 5.153 + t.Errorf("Unexpected error retrieving scopes from %T: %+v", store, err) 5.154 + } 5.155 + if len(retrieved) != 0 { 5.156 + t.Logf("%+v", retrieved) 5.157 + t.Errorf("Expected %d results, got %d from %T", 0, len(retrieved), store) 5.158 + } 5.159 + err = context.RemoveScopes([]string{scope.ID}) 5.160 + if err == nil { 5.161 + t.Errorf("No error returned removing non-existent scopes from %T", store) 5.162 + } 5.163 + if e, ok := err.(ErrScopeNotFound); !ok { 5.164 + t.Errorf("Unexpected error removing non-existent scopes from %T: %+v", store, err) 5.165 + } else { 5.166 + if e.Pos != 0 { 5.167 + t.Errorf("Expected error to be for scope ID in pos %d, but got %d from %T", 0, e.Pos, store) 5.168 + } 5.169 + if e.ID != scope.ID { 5.170 + t.Errorf("Expected error to be for scope ID %s, but got %s from %T", scope.ID, e.ID, store) 5.171 + } 5.172 + } 5.173 + updated, err = context.UpdateScopes([]ScopeChange{update}) 5.174 + if err == nil { 5.175 + t.Errorf("No error returned updating non-existent scopes from %T", store) 5.176 + } 5.177 + if e, ok := err.(ErrScopeNotFound); !ok { 5.178 + t.Errorf("Unexpected error updating non-existent scopes from %T: %+v", store, err) 5.179 + } else { 5.180 + if e.Pos != 0 { 5.181 + t.Errorf("Expected error to be for scope ID in pos %d, but got %d from %T", 0, e.Pos, store) 5.182 + } 5.183 + if e.ID != scope.ID { 5.184 + t.Errorf("Expected error to be for scope ID %s, but got %s from %T", scope.ID, e.ID, store) 5.185 + } 5.186 + } 5.187 + } 5.188 +} 5.189 + 5.190 +func TestScopeErrors(t *testing.T) { 5.191 + errors := map[string]error{ 5.192 + "scope test couldn't be found": ErrScopeNotFound{ID: "test", Pos: 0}, 5.193 + "scope test already exists": ErrScopeAlreadyExists{ID: "test", Pos: 0}, 5.194 + } 5.195 + for expectation, e := range errors { 5.196 + if e.Error() != expectation { 5.197 + t.Errorf("Expected %+v to produce '%s', produced '%s'", e, expectation, e.Error()) 5.198 + } 5.199 + } 5.200 +}