auth
41:113ccb15b919 Browse Files
Added validation for clients, split endpoints out. Split endpoints out into their own type and added associated methods to the ClientStores, so now each client can have more than one redirect endpoint. Added unit testing for endpoint methods. Added validation code to validate client changes.
client.go client_test.go memstore.go
1.1 --- a/client.go Thu Sep 18 19:34:18 2014 -0400 1.2 +++ b/client.go Thu Sep 18 22:13:22 2014 -0400 1.3 @@ -2,35 +2,42 @@ 1.4 1.5 import ( 1.6 "errors" 1.7 + "net/url" 1.8 + "time" 1.9 1.10 + "strings" 1.11 "secondbit.org/uuid" 1.12 ) 1.13 1.14 var ( 1.15 ErrClientNotFound = errors.New("Client not found in ClientStore.") 1.16 ErrClientAlreadyExists = errors.New("Client already exists in ClientStore.") 1.17 + 1.18 + ErrClientNameTooShort = errors.New("Client name must be at least 2 characters.") 1.19 + ErrClientNameTooLong = errors.New("Client name must be at most 32 characters.") 1.20 + ErrClientLogoTooShort = errors.New("Client logo URL must be at least 12 characters.") 1.21 + ErrClientLogoTooLong = errors.New("Client logo must be at most 1024 characters.") 1.22 + ErrClientWebsiteTooShort = errors.New("Client website URL must be at least 12 characters.") 1.23 + ErrClientWebsiteTooLong = errors.New("Client website must be at most 1024 characters.") 1.24 ) 1.25 1.26 // Client represents a client that grants access 1.27 // to the auth server, exchanging grants for tokens, 1.28 // and tokens for access. 1.29 type Client struct { 1.30 - ID uuid.ID 1.31 - Secret string 1.32 - RedirectURI string 1.33 - OwnerID uuid.ID 1.34 - Name string 1.35 - Logo string 1.36 - Website string 1.37 + ID uuid.ID 1.38 + Secret string 1.39 + OwnerID uuid.ID 1.40 + Name string 1.41 + Logo string 1.42 + Website string 1.43 + Type string 1.44 } 1.45 1.46 func (c *Client) ApplyChange(change ClientChange) { 1.47 if change.Secret != nil { 1.48 c.Secret = *change.Secret 1.49 } 1.50 - if change.RedirectURI != nil { 1.51 - c.RedirectURI = *change.RedirectURI 1.52 - } 1.53 if change.OwnerID != nil { 1.54 c.OwnerID = change.OwnerID 1.55 } 1.56 @@ -46,19 +53,56 @@ 1.57 } 1.58 1.59 type ClientChange struct { 1.60 - Secret *string 1.61 - RedirectURI *string 1.62 - OwnerID uuid.ID 1.63 - Name *string 1.64 - Logo *string 1.65 - Website *string 1.66 + Secret *string 1.67 + OwnerID uuid.ID 1.68 + Name *string 1.69 + Logo *string 1.70 + Website *string 1.71 } 1.72 1.73 func (c ClientChange) Validate() error { 1.74 - // TODO: validate client changes 1.75 + if c.Name != nil && len(*c.Name) < 2 { 1.76 + return ErrClientNameTooShort 1.77 + } 1.78 + if c.Name != nil && len(*c.Name) > 32 { 1.79 + return ErrClientNameTooLong 1.80 + } 1.81 + if c.Logo != nil && len(*c.Logo) > 1024 { 1.82 + return ErrClientLogoTooLong 1.83 + } 1.84 + if c.Logo != nil && len(*c.Logo) > 0 && len(*c.Logo) < 12 { 1.85 + return ErrClientLogoTooShort 1.86 + } 1.87 + if c.Website != nil && len(*c.Website) > 140 { 1.88 + return ErrClientWebsiteTooLong 1.89 + } 1.90 + if c.Website != nil && len(*c.Website) > 0 && len(*c.Website) < 12 { 1.91 + return ErrClientWebsiteTooShort 1.92 + } 1.93 return nil 1.94 } 1.95 1.96 +type Endpoint struct { 1.97 + ID uuid.ID 1.98 + ClientID uuid.ID 1.99 + URI url.URL 1.100 + Added time.Time 1.101 +} 1.102 + 1.103 +type sortedEndpoints []Endpoint 1.104 + 1.105 +func (s sortedEndpoints) Len() int { 1.106 + return len(s) 1.107 +} 1.108 + 1.109 +func (s sortedEndpoints) Less(i, j int) bool { 1.110 + return s[i].Added.Before(s[j].Added) 1.111 +} 1.112 + 1.113 +func (s sortedEndpoints) Swap(i, j int) { 1.114 + s[i], s[j] = s[j], s[i] 1.115 +} 1.116 + 1.117 // ClientStore abstracts the storage interface for 1.118 // storing and retrieving Clients. 1.119 type ClientStore interface { 1.120 @@ -67,6 +111,11 @@ 1.121 UpdateClient(id uuid.ID, change ClientChange) error 1.122 DeleteClient(id uuid.ID) error 1.123 ListClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) 1.124 + 1.125 + AddEndpoint(client uuid.ID, endpoint Endpoint) error 1.126 + RemoveEndpoint(client, endpoint uuid.ID) error 1.127 + CheckEndpoint(client uuid.ID, endpoint string) (bool, error) 1.128 + ListEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) 1.129 } 1.130 1.131 func (m *Memstore) GetClient(id uuid.ID) (Client, error) { 1.132 @@ -142,3 +191,43 @@ 1.133 } 1.134 return clients, nil 1.135 } 1.136 + 1.137 +func (m *Memstore) AddEndpoint(client uuid.ID, endpoint Endpoint) error { 1.138 + m.endpointLock.Lock() 1.139 + defer m.endpointLock.Unlock() 1.140 + m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoint) 1.141 + return nil 1.142 +} 1.143 + 1.144 +func (m *Memstore) RemoveEndpoint(client, endpoint uuid.ID) error { 1.145 + m.endpointLock.Lock() 1.146 + defer m.endpointLock.Unlock() 1.147 + pos := -1 1.148 + for p, item := range m.endpoints[client.String()] { 1.149 + if item.ID.Equal(endpoint) { 1.150 + pos = p 1.151 + break 1.152 + } 1.153 + } 1.154 + if pos >= 0 { 1.155 + m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...) 1.156 + } 1.157 + return nil 1.158 +} 1.159 + 1.160 +func (m *Memstore) CheckEndpoint(client uuid.ID, endpoint string) (bool, error) { 1.161 + m.endpointLock.RLock() 1.162 + defer m.endpointLock.RUnlock() 1.163 + for _, candidate := range m.endpoints[client.String()] { 1.164 + if strings.HasPrefix(endpoint, candidate.URI.String()) { 1.165 + return true, nil 1.166 + } 1.167 + } 1.168 + return false, nil 1.169 +} 1.170 + 1.171 +func (m *Memstore) ListEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) { 1.172 + m.endpointLock.RLock() 1.173 + defer m.endpointLock.RUnlock() 1.174 + return m.endpoints[client.String()], nil 1.175 +}
2.1 --- a/client_test.go Thu Sep 18 19:34:18 2014 -0400 2.2 +++ b/client_test.go Thu Sep 18 22:13:22 2014 -0400 2.3 @@ -2,14 +2,16 @@ 2.4 2.5 import ( 2.6 "fmt" 2.7 + "net/url" 2.8 "testing" 2.9 + "time" 2.10 2.11 + "sort" 2.12 "secondbit.org/uuid" 2.13 ) 2.14 2.15 const ( 2.16 clientChangeSecret = 1 << iota 2.17 - clientChangeRedirectURI 2.18 clientChangeOwnerID 2.19 clientChangeName 2.20 clientChangeLogo 2.21 @@ -25,9 +27,6 @@ 2.22 if client1.Secret != client2.Secret { 2.23 return false, "secret", client1.Secret, client2.Secret 2.24 } 2.25 - if client1.RedirectURI != client2.RedirectURI { 2.26 - return false, "redirect URI", client1.RedirectURI, client2.RedirectURI 2.27 - } 2.28 if !client1.OwnerID.Equal(client2.OwnerID) { 2.29 return false, "owner ID", client1.OwnerID, client2.OwnerID 2.30 } 2.31 @@ -40,84 +39,196 @@ 2.32 if client1.Website != client2.Website { 2.33 return false, "website", client1.Website, client2.Website 2.34 } 2.35 + if client1.Type != client2.Type { 2.36 + return false, "type", client1.Type, client2.Type 2.37 + } 2.38 + return true, "", nil, nil 2.39 +} 2.40 + 2.41 +func compareEndpoints(endpoint1, endpoint2 Endpoint) (success bool, field string, val1, val2 interface{}) { 2.42 + if !endpoint1.ID.Equal(endpoint2.ID) { 2.43 + return false, "ID", endpoint1.ID, endpoint2.ID 2.44 + } 2.45 + if !endpoint1.ClientID.Equal(endpoint2.ClientID) { 2.46 + return false, "OwnerID", endpoint1.ClientID, endpoint2.ClientID 2.47 + } 2.48 + if !endpoint1.Added.Equal(endpoint2.Added) { 2.49 + return false, "Added", endpoint1.Added, endpoint2.Added 2.50 + } 2.51 + if endpoint1.URI.String() != endpoint2.URI.String() { 2.52 + return false, "URI", endpoint1.URI, endpoint2.URI 2.53 + } 2.54 return true, "", nil, nil 2.55 } 2.56 2.57 func TestClientStoreSuccess(t *testing.T) { 2.58 t.Parallel() 2.59 client := Client{ 2.60 - ID: uuid.NewID(), 2.61 - Secret: "secret", 2.62 - RedirectURI: "redirectURI", 2.63 - OwnerID: uuid.NewID(), 2.64 - Name: "name", 2.65 - Logo: "logo", 2.66 - Website: "website", 2.67 + ID: uuid.NewID(), 2.68 + Secret: "secret", 2.69 + OwnerID: uuid.NewID(), 2.70 + Name: "name", 2.71 + Logo: "logo", 2.72 + Website: "website", 2.73 } 2.74 for _, store := range clientStores { 2.75 err := store.SaveClient(client) 2.76 if err != nil { 2.77 - t.Errorf("Error saving client to %T: %s", store, err) 2.78 + t.Fatalf("Error saving client to %T: %s", store, err) 2.79 } 2.80 err = store.SaveClient(client) 2.81 if err != ErrClientAlreadyExists { 2.82 - t.Errorf("Expected ErrClientAlreadyExists, got %v from %T", err, store) 2.83 + t.Fatalf("Expected ErrClientAlreadyExists, got %v from %T", err, store) 2.84 } 2.85 retrieved, err := store.GetClient(client.ID) 2.86 if err != nil { 2.87 - t.Errorf("Error retrieving client from %T: %s", store, err) 2.88 + t.Fatalf("Error retrieving client from %T: %s", store, err) 2.89 } 2.90 success, field, expectation, result := compareClients(client, retrieved) 2.91 if !success { 2.92 - t.Errorf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result) 2.93 + t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result) 2.94 } 2.95 clients, err := store.ListClientsByOwner(client.OwnerID, 25, 0) 2.96 if err != nil { 2.97 - t.Errorf("Error retrieving clients by owner from %T: %s", store, err) 2.98 + t.Fatalf("Error retrieving clients by owner from %T: %s", store, err) 2.99 } 2.100 if len(clients) != 1 { 2.101 - t.Errorf("Expected 1 client in response from %T, got %+v", store, clients) 2.102 + t.Fatalf("Expected 1 client in response from %T, got %+v", store, clients) 2.103 } 2.104 success, field, expectation, result = compareClients(client, clients[0]) 2.105 if !success { 2.106 - t.Errorf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result) 2.107 + t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result) 2.108 } 2.109 err = store.DeleteClient(client.ID) 2.110 if err != nil { 2.111 - t.Errorf("Error deleting client from %T: %s", store, err) 2.112 + t.Fatalf("Error deleting client from %T: %s", store, err) 2.113 } 2.114 err = store.DeleteClient(client.ID) 2.115 if err != ErrClientNotFound { 2.116 - t.Errorf("Expected ErrClientNotFound, got %s from %T", err, store) 2.117 + t.Fatalf("Expected ErrClientNotFound, got %s from %T", err, store) 2.118 } 2.119 retrieved, err = store.GetClient(client.ID) 2.120 if err != ErrClientNotFound { 2.121 - t.Errorf("Expected ErrClientNotFound from %T, got %+v and %s", store, retrieved, err) 2.122 + t.Fatalf("Expected ErrClientNotFound from %T, got %+v and %s", store, retrieved, err) 2.123 } 2.124 clients, err = store.ListClientsByOwner(client.OwnerID, 25, 0) 2.125 if err != nil { 2.126 - t.Errorf("Error listing clients by owner from %T: %s", store, err) 2.127 + t.Fatalf("Error listing clients by owner from %T: %s", store, err) 2.128 } 2.129 if len(clients) != 0 { 2.130 - t.Errorf("Expected 0 clients in response from %T, got %+v", store, clients) 2.131 + t.Fatalf("Expected 0 clients in response from %T, got %+v", store, clients) 2.132 + } 2.133 + } 2.134 +} 2.135 + 2.136 +func TestEndpointStoreSuccess(t *testing.T) { 2.137 + t.Parallel() 2.138 + client := Client{ 2.139 + ID: uuid.NewID(), 2.140 + Secret: "secret", 2.141 + OwnerID: uuid.NewID(), 2.142 + Name: "name", 2.143 + Logo: "logo", 2.144 + Website: "website", 2.145 + } 2.146 + uri1, _ := url.Parse("https://www.example.com/") 2.147 + uri2, _ := url.Parse("https://www.example.com/my/full/path") 2.148 + endpoint1 := Endpoint{ 2.149 + ID: uuid.NewID(), 2.150 + ClientID: client.ID, 2.151 + Added: time.Now(), 2.152 + URI: *uri1, 2.153 + } 2.154 + endpoint2 := Endpoint{ 2.155 + ID: uuid.NewID(), 2.156 + ClientID: client.ID, 2.157 + Added: time.Now(), 2.158 + URI: *uri2, 2.159 + } 2.160 + for _, store := range clientStores { 2.161 + err := store.SaveClient(client) 2.162 + if err != nil { 2.163 + t.Fatalf("Error saving client to %T: %s", store, err) 2.164 + } 2.165 + err = store.AddEndpoint(client.ID, endpoint1) 2.166 + if err != nil { 2.167 + t.Fatalf("Error adding endpoint to client in %T: %s", store, err) 2.168 + } 2.169 + endpoints, err := store.ListEndpoints(client.ID, 10, 0) 2.170 + if err != nil { 2.171 + t.Fatalf("Error retrieving endpoints from %T: %s", store, err) 2.172 + } 2.173 + if len(endpoints) != 1 { 2.174 + t.Fatalf("Expected %d endpoints, got %+v from %T", 1, endpoints, store) 2.175 + } 2.176 + success, field, expectation, result := compareEndpoints(endpoint1, endpoints[0]) 2.177 + if !success { 2.178 + t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result) 2.179 + } 2.180 + err = store.AddEndpoint(client.ID, endpoint2) 2.181 + if err != nil { 2.182 + t.Fatalf("Error adding endpoint to client in %T: %s", store, err) 2.183 + } 2.184 + endpoints, err = store.ListEndpoints(client.ID, 10, 0) 2.185 + if err != nil { 2.186 + t.Fatalf("Error retrieving endpoints from %T: %s", store, err) 2.187 + } 2.188 + if len(endpoints) != 2 { 2.189 + t.Fatalf("Expected %d endpoints, got %+v from %T", 2, endpoints, store) 2.190 + } 2.191 + sortedEnd := sortedEndpoints(endpoints) 2.192 + sort.Sort(sortedEnd) 2.193 + endpoints = []Endpoint(sortedEnd) 2.194 + success, field, expectation, result = compareEndpoints(endpoint1, endpoints[0]) 2.195 + if !success { 2.196 + t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result) 2.197 + } 2.198 + success, field, expectation, result = compareEndpoints(endpoint2, endpoints[1]) 2.199 + if !success { 2.200 + t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result) 2.201 + } 2.202 + err = store.RemoveEndpoint(client.ID, endpoint1.ID) 2.203 + if err != nil { 2.204 + t.Fatalf("Error removing endpoint from client in %T: %s", store, err) 2.205 + } 2.206 + endpoints, err = store.ListEndpoints(client.ID, 10, 0) 2.207 + if err != nil { 2.208 + t.Fatalf("Error listing endpoints in %T: %s", store, err) 2.209 + } 2.210 + if len(endpoints) != 1 { 2.211 + t.Fatalf("Expected %d endpoints, got %+v from %T", 1, endpoints, store) 2.212 + } 2.213 + success, field, expectation, result = compareEndpoints(endpoint2, endpoints[0]) 2.214 + if !success { 2.215 + t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result) 2.216 + } 2.217 + err = store.RemoveEndpoint(client.ID, endpoint2.ID) 2.218 + if err != nil { 2.219 + t.Fatalf("Error removing endpoint from client in %T: %s", store, err) 2.220 + } 2.221 + endpoints, err = store.ListEndpoints(client.ID, 10, 0) 2.222 + if err != nil { 2.223 + t.Fatalf("Error listing endpoints in %T: %s", store, err) 2.224 + } 2.225 + if len(endpoints) != 0 { 2.226 + t.Fatalf("Expected %d endpoints, got %+v from %T", 0, endpoints, store) 2.227 } 2.228 } 2.229 } 2.230 2.231 func TestClientUpdates(t *testing.T) { 2.232 t.Parallel() 2.233 - variations := 1 << 10 2.234 + variations := 1 << 5 2.235 client := Client{ 2.236 - ID: uuid.NewID(), 2.237 - Secret: "secret", 2.238 - RedirectURI: "redirectURI", 2.239 - OwnerID: uuid.NewID(), 2.240 - Name: "name", 2.241 - Logo: "logo", 2.242 - Website: "website", 2.243 + ID: uuid.NewID(), 2.244 + Secret: "secret", 2.245 + OwnerID: uuid.NewID(), 2.246 + Name: "name", 2.247 + Logo: "logo", 2.248 + Website: "website", 2.249 } 2.250 for i := 0; i < variations; i++ { 2.251 - var secret, redirectURI, name, logo, website string 2.252 + var secret, name, logo, website string 2.253 change := ClientChange{} 2.254 expectation := client 2.255 result := client 2.256 @@ -126,11 +237,6 @@ 2.257 change.Secret = &secret 2.258 expectation.Secret = secret 2.259 } 2.260 - if i&clientChangeRedirectURI != 0 { 2.261 - redirectURI = fmt.Sprintf("redirect-uri-%d", i) 2.262 - change.RedirectURI = &redirectURI 2.263 - expectation.RedirectURI = redirectURI 2.264 - } 2.265 if i&clientChangeOwnerID != 0 { 2.266 change.OwnerID = uuid.NewID() 2.267 expectation.OwnerID = change.OwnerID 2.268 @@ -153,33 +259,95 @@ 2.269 result.ApplyChange(change) 2.270 match, field, expected, got := compareClients(expectation, result) 2.271 if !match { 2.272 - t.Errorf("Expected field `%s` to be `%v`, got `%v`", field, expected, got) 2.273 + t.Fatalf("Expected field `%s` to be `%v`, got `%v`", field, expected, got) 2.274 } 2.275 for _, store := range clientStores { 2.276 err := store.SaveClient(client) 2.277 if err != nil { 2.278 - t.Errorf("Error saving client in %T: %s", store, err) 2.279 + t.Fatalf("Error saving client in %T: %s", store, err) 2.280 } 2.281 err = store.UpdateClient(client.ID, change) 2.282 if err != nil { 2.283 - t.Errorf("Error updating client in %T: %s", store, err) 2.284 + t.Fatalf("Error updating client in %T: %s", store, err) 2.285 } 2.286 retrieved, err := store.GetClient(client.ID) 2.287 if err != nil { 2.288 - t.Errorf("Error getting profile from %T: %s", store, err) 2.289 + t.Fatalf("Error getting profile from %T: %s", store, err) 2.290 } 2.291 match, field, expected, got = compareClients(expectation, retrieved) 2.292 if !match { 2.293 - t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store) 2.294 + t.Fatalf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store) 2.295 } 2.296 err = store.DeleteClient(client.ID) 2.297 if err != nil { 2.298 - t.Errorf("Error deleting client from %T: %s", store, err) 2.299 + t.Fatalf("Error deleting client from %T: %s", store, err) 2.300 } 2.301 err = store.UpdateClient(client.ID, change) 2.302 if err != ErrClientNotFound { 2.303 - t.Errorf("Expected ErrClientNotFound, got %v from %T", err, store) 2.304 + t.Fatalf("Expected ErrClientNotFound, got %v from %T", err, store) 2.305 } 2.306 } 2.307 } 2.308 } 2.309 + 2.310 +func TestClientEndpointChecks(t *testing.T) { 2.311 + t.Parallel() 2.312 + client := Client{ 2.313 + ID: uuid.NewID(), 2.314 + Secret: "secret", 2.315 + OwnerID: uuid.NewID(), 2.316 + Name: "name", 2.317 + Logo: "logo", 2.318 + Website: "website", 2.319 + } 2.320 + uri1, _ := url.Parse("https://www.example.com/first") 2.321 + uri2, _ := url.Parse("https://www.example.com/my/full/path") 2.322 + endpoint1 := Endpoint{ 2.323 + ID: uuid.NewID(), 2.324 + ClientID: client.ID, 2.325 + Added: time.Now(), 2.326 + URI: *uri1, 2.327 + } 2.328 + endpoint2 := Endpoint{ 2.329 + ID: uuid.NewID(), 2.330 + ClientID: client.ID, 2.331 + Added: time.Now(), 2.332 + URI: *uri2, 2.333 + } 2.334 + candidates := map[string]bool{ 2.335 + "https://www.example.com/": false, 2.336 + "https://www.example.com/first": true, 2.337 + "https://www.example.com/first/extra/path": true, 2.338 + "https://www.example.com/my": false, 2.339 + "https://www.example.com/my/full/path": true, 2.340 + } 2.341 + for _, store := range clientStores { 2.342 + err := store.SaveClient(client) 2.343 + if err != nil { 2.344 + t.Fatalf("Error saving client in %T: %s", store, err) 2.345 + } 2.346 + err = store.AddEndpoint(client.ID, endpoint1) 2.347 + if err != nil { 2.348 + t.Fatalf("Error saving endpoint in %T: %s", store, err) 2.349 + } 2.350 + err = store.AddEndpoint(client.ID, endpoint2) 2.351 + if err != nil { 2.352 + t.Fatalf("Error saving endpoint in %T: %s", store, err) 2.353 + } 2.354 + for candidate, expectation := range candidates { 2.355 + result, err := store.CheckEndpoint(client.ID, candidate) 2.356 + if err != nil { 2.357 + t.Fatalf("Error checking endpoint %s in %T: %s", candidate, store, err) 2.358 + } 2.359 + if result != expectation { 2.360 + expectStr := "no" 2.361 + resultStr := "a" 2.362 + if expectation { 2.363 + expectStr = "a" 2.364 + resultStr = "no" 2.365 + } 2.366 + t.Errorf("Expected %s match for %s in %T, got %s match", expectStr, candidate, store, resultStr) 2.367 + } 2.368 + } 2.369 + } 2.370 +}
3.1 --- a/memstore.go Thu Sep 18 19:34:18 2014 -0400 3.2 +++ b/memstore.go Thu Sep 18 22:13:22 2014 -0400 3.3 @@ -19,6 +19,9 @@ 3.4 profileClientLookup map[string][]uuid.ID 3.5 clientLock sync.RWMutex 3.6 3.7 + endpoints map[string][]Endpoint 3.8 + endpointLock sync.RWMutex 3.9 + 3.10 profiles map[string]Profile 3.11 profileLock sync.RWMutex 3.12 } 3.13 @@ -31,6 +34,7 @@ 3.14 grants: map[string]Grant{}, 3.15 clients: map[string]Client{}, 3.16 profileClientLookup: map[string][]uuid.ID{}, 3.17 + endpoints: map[string][]Endpoint{}, 3.18 profiles: map[string]Profile{}, 3.19 } 3.20 }