auth

Paddy 2015-12-14 Parent:0a2c3d677161

181:b7e685839a1b Go to Latest

auth/context.go

Break out scopes and events. This repo has gotten unwieldy, and there are portions of it that need to be imported by a large number of other packages. For example, scopes will be used in almost every API we write. Rather than importing the entirety of this codebase into every API we write, I've opted to move the scope logic out into a scopes package, with a subpackage for the defined types, which is all most projects actually want to import. We also define some event type constants, and importing those shouldn't require a project to import all our dependencies, either. So I made an events subpackage that just holds those constants. This package has become a little bit of a red-headed stepchild and is do for a refactor, but I'm trying to put that off as long as I can. The refactoring of our scopes stuff has left a bug wherein a token can be granted for scopes that don't exist. I'm going to need to revisit that, and also how to limit scopes to only be granted to the users that should be able to request them. But that's a battle for another day.

History
paddy@50 1 package auth
paddy@50 2
paddy@50 3 import (
paddy@50 4 "html/template"
paddy@50 5 "io"
paddy@55 6 "log"
paddy@77 7 "net/url"
paddy@50 8 "time"
paddy@50 9
paddy@178 10 "code.secondbit.org/events.hg"
paddy@107 11 "code.secondbit.org/uuid.hg"
paddy@50 12 )
paddy@50 13
paddy@57 14 // Context wraps the different storage interfaces and should
paddy@57 15 // be used as the main point of interaction for the data storage
paddy@57 16 // layer.
paddy@50 17 type Context struct {
paddy@178 18 template *template.Template
paddy@178 19 loginURI *url.URL
paddy@178 20 clients clientStore
paddy@178 21 authCodes authorizationCodeStore
paddy@178 22 profiles profileStore
paddy@178 23 tokens tokenStore
paddy@178 24 sessions sessionStore
paddy@178 25 eventsPublisher events.Publisher
paddy@178 26 config Config
paddy@96 27 }
paddy@96 28
paddy@96 29 // NewContext takes a Config instance and uses it to bootstrap a Context
paddy@96 30 // using the information provided in the Config variable.
paddy@96 31 func NewContext(config Config) (Context, error) {
paddy@102 32 if config.iterations == 0 {
paddy@102 33 return Context{}, ErrConfigNotInitialized
paddy@102 34 }
paddy@96 35 context := Context{
paddy@178 36 clients: config.ClientStore,
paddy@178 37 authCodes: config.AuthCodeStore,
paddy@178 38 profiles: config.ProfileStore,
paddy@178 39 tokens: config.TokenStore,
paddy@178 40 sessions: config.SessionStore,
paddy@178 41 eventsPublisher: config.EventsPublisher,
paddy@178 42 template: config.Template,
paddy@178 43 config: config,
paddy@96 44 }
paddy@96 45 var err error
paddy@96 46 context.loginURI, err = url.Parse(config.LoginURI)
paddy@96 47 if err != nil {
paddy@96 48 log.Println(err)
paddy@96 49 return Context{}, ErrInvalidLoginURI
paddy@96 50 }
paddy@96 51 return context, nil
paddy@50 52 }
paddy@50 53
paddy@57 54 // Render uses the HTML templates associated with the Context to render the
paddy@57 55 // template specified by name to out using data to fill any template variables.
paddy@55 56 func (c Context) Render(out io.Writer, name string, data interface{}) {
paddy@50 57 if c.template == nil {
paddy@57 58 log.Println("No template set on Context, can't render anything!")
paddy@57 59 return
paddy@50 60 }
paddy@55 61 err := c.template.ExecuteTemplate(out, name, data)
paddy@55 62 if err != nil {
paddy@55 63 log.Println("Error executing template", name, ":", err)
paddy@55 64 }
paddy@50 65 }
paddy@50 66
paddy@57 67 // GetClient returns a single Client by its ID from the
paddy@57 68 // clientStore associated with the Context.
paddy@50 69 func (c Context) GetClient(id uuid.ID) (Client, error) {
paddy@50 70 if c.clients == nil {
paddy@50 71 return Client{}, ErrNoClientStore
paddy@50 72 }
paddy@57 73 return c.clients.getClient(id)
paddy@50 74 }
paddy@50 75
paddy@57 76 // SaveClient stores the passed Client in the clientStore
paddy@57 77 // associated with the Context.
paddy@50 78 func (c Context) SaveClient(client Client) error {
paddy@50 79 if c.clients == nil {
paddy@50 80 return ErrNoClientStore
paddy@50 81 }
paddy@57 82 return c.clients.saveClient(client)
paddy@50 83 }
paddy@50 84
paddy@57 85 // UpdateClient applies the specified ClientChange to the Client
paddy@57 86 // with the specified ID in the clientStore associated with the
paddy@57 87 // Context.
paddy@50 88 func (c Context) UpdateClient(id uuid.ID, change ClientChange) error {
paddy@50 89 if c.clients == nil {
paddy@50 90 return ErrNoClientStore
paddy@50 91 }
paddy@57 92 return c.clients.updateClient(id, change)
paddy@50 93 }
paddy@50 94
paddy@57 95 // ListClientsByOwner returns a slice of up to num Clients, starting at offset (inclusive)
paddy@57 96 // that have the specified OwnerID in the clientStore associated with the Context.
paddy@50 97 func (c Context) ListClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) {
paddy@50 98 if c.clients == nil {
paddy@50 99 return []Client{}, ErrNoClientStore
paddy@50 100 }
paddy@57 101 return c.clients.listClientsByOwner(ownerID, num, offset)
paddy@50 102 }
paddy@50 103
paddy@164 104 func (c Context) DeleteClientsByOwner(ownerID uuid.ID) error {
paddy@164 105 if c.clients == nil {
paddy@164 106 return ErrNoClientStore
paddy@164 107 }
paddy@164 108 return c.clients.deleteClientsByOwner(ownerID)
paddy@164 109 }
paddy@164 110
paddy@151 111 // AddEndpoints stores the specified Endpoints in the clientStore associated with the Context.
paddy@151 112 func (c Context) AddEndpoints(endpoints []Endpoint) error {
paddy@50 113 if c.clients == nil {
paddy@50 114 return ErrNoClientStore
paddy@50 115 }
paddy@116 116 for pos, endpoint := range endpoints {
paddy@116 117 u, err := normalizeURIString(endpoint.URI)
paddy@116 118 if err != nil {
paddy@116 119 return err
paddy@116 120 }
paddy@116 121 endpoint.NormalizedURI = u
paddy@116 122 endpoints[pos] = endpoint
paddy@116 123 }
paddy@151 124 return c.clients.addEndpoints(endpoints)
paddy@50 125 }
paddy@50 126
paddy@143 127 // GetEndpoint retrieves the Endpoint with the specified ID from the clientStore associated
paddy@143 128 // with the Context, if and only if it belongs to the Client with the specified ID.
paddy@143 129 func (c Context) GetEndpoint(client, endpoint uuid.ID) (Endpoint, error) {
paddy@143 130 if c.clients == nil {
paddy@143 131 return Endpoint{}, ErrNoClientStore
paddy@143 132 }
paddy@143 133 return c.clients.getEndpoint(client, endpoint)
paddy@143 134 }
paddy@143 135
paddy@57 136 // RemoveEndpoint deletes the Endpoint with the specified ID from the clientStore associated
paddy@57 137 // with the Context, and disassociates the Endpoint from the specified Client.
paddy@50 138 func (c Context) RemoveEndpoint(client, endpoint uuid.ID) error {
paddy@50 139 if c.clients == nil {
paddy@50 140 return ErrNoClientStore
paddy@50 141 }
paddy@57 142 return c.clients.removeEndpoint(client, endpoint)
paddy@50 143 }
paddy@50 144
paddy@57 145 // CheckEndpoint finds Endpoints in the clientStore associated with the Context that belong
paddy@58 146 // to the Client specified by the passed ID and match the URI passed. URI matches must be
paddy@58 147 // performed according to RFC 3986 Section 6.
paddy@58 148 func (c Context) CheckEndpoint(client uuid.ID, URI string) (bool, error) {
paddy@50 149 if c.clients == nil {
paddy@50 150 return false, ErrNoClientStore
paddy@50 151 }
paddy@116 152 u, err := normalizeURIString(URI)
paddy@116 153 if err != nil {
paddy@116 154 return false, err
paddy@116 155 }
paddy@116 156 return c.clients.checkEndpoint(client, u)
paddy@50 157 }
paddy@50 158
paddy@57 159 // ListEndpoints finds Endpoints in the clientStore associated with the Context that belong
paddy@57 160 // to the Client specified by the passed ID. It returns up to num endpoints, starting at offset,
paddy@57 161 // exclusive.
paddy@50 162 func (c Context) ListEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) {
paddy@50 163 if c.clients == nil {
paddy@50 164 return []Endpoint{}, ErrNoClientStore
paddy@50 165 }
paddy@57 166 return c.clients.listEndpoints(client, num, offset)
paddy@50 167 }
paddy@50 168
paddy@164 169 func (c Context) RemoveEndpointsByClientID(client uuid.ID) error {
paddy@164 170 if c.clients == nil {
paddy@164 171 return ErrNoClientStore
paddy@164 172 }
paddy@164 173 return c.clients.removeEndpointsByClientID(client)
paddy@164 174 }
paddy@164 175
paddy@57 176 // CountEndpoints returns the number of Endpoints the are associated with the Client specified by the
paddy@57 177 // passed ID in the clientStore associated with the Context.
paddy@55 178 func (c Context) CountEndpoints(client uuid.ID) (int64, error) {
paddy@55 179 if c.clients == nil {
paddy@55 180 return 0, ErrNoClientStore
paddy@55 181 }
paddy@57 182 return c.clients.countEndpoints(client)
paddy@55 183 }
paddy@55 184
paddy@87 185 // GetAuthorizationCode returns the AuthorizationCode specified by the provided code from the authorizationCodeStore associated with the
paddy@57 186 // Context.
paddy@87 187 func (c Context) GetAuthorizationCode(code string) (AuthorizationCode, error) {
paddy@87 188 if c.authCodes == nil {
paddy@87 189 return AuthorizationCode{}, ErrNoAuthorizationCodeStore
paddy@50 190 }
paddy@87 191 return c.authCodes.getAuthorizationCode(code)
paddy@50 192 }
paddy@50 193
paddy@87 194 // SaveAuthorizationCode stores the passed AuthorizationCode in the authorizationCodeStore associated with the Context.
paddy@87 195 func (c Context) SaveAuthorizationCode(authCode AuthorizationCode) error {
paddy@87 196 if c.authCodes == nil {
paddy@87 197 return ErrNoAuthorizationCodeStore
paddy@50 198 }
paddy@87 199 return c.authCodes.saveAuthorizationCode(authCode)
paddy@50 200 }
paddy@50 201
paddy@87 202 // DeleteAuthorizationCode removes the AuthorizationCode specified by the provided code from the authorizationCodeStore associated with
paddy@57 203 // the Context.
paddy@87 204 func (c Context) DeleteAuthorizationCode(code string) error {
paddy@87 205 if c.authCodes == nil {
paddy@87 206 return ErrNoAuthorizationCodeStore
paddy@50 207 }
paddy@87 208 return c.authCodes.deleteAuthorizationCode(code)
paddy@50 209 }
paddy@50 210
paddy@163 211 // DeleteAuthorizationCodesByProfileID removes the AuthorizationCodes associated with the Profile specified by the provided ID from the
paddy@163 212 // authorizationCodeStore associated with the Context.
paddy@163 213 func (c Context) DeleteAuthorizationCodesByProfileID(profileID uuid.ID) error {
paddy@163 214 if c.authCodes == nil {
paddy@163 215 return ErrNoAuthorizationCodeStore
paddy@163 216 }
paddy@163 217 return c.authCodes.deleteAuthorizationCodesByProfileID(profileID)
paddy@163 218 }
paddy@163 219
paddy@164 220 func (c Context) DeleteAuthorizationCodesByClientID(clientID uuid.ID) error {
paddy@164 221 if c.authCodes == nil {
paddy@164 222 return ErrNoAuthorizationCodeStore
paddy@164 223 }
paddy@164 224 return c.authCodes.deleteAuthorizationCodesByClientID(clientID)
paddy@164 225 }
paddy@164 226
paddy@94 227 // UseAuthorizationCode marks the AuthorizationCode specified by the provided code as used in the authorizationCodeStore associated with
paddy@94 228 // the Context. Once an AuthorizationCode is marked as used, its Used property will be set to true when retrieved from the authorizationCodeStore.
paddy@94 229 func (c Context) UseAuthorizationCode(code string) error {
paddy@94 230 if c.authCodes == nil {
paddy@94 231 return ErrNoAuthorizationCodeStore
paddy@94 232 }
paddy@94 233 return c.authCodes.useAuthorizationCode(code)
paddy@94 234 }
paddy@94 235
paddy@57 236 // GetProfileByID returns the Profile specified by the provided ID from the profileStore associated with
paddy@57 237 // the Context.
paddy@50 238 func (c Context) GetProfileByID(id uuid.ID) (Profile, error) {
paddy@50 239 if c.profiles == nil {
paddy@50 240 return Profile{}, ErrNoProfileStore
paddy@50 241 }
paddy@57 242 return c.profiles.getProfileByID(id)
paddy@50 243 }
paddy@50 244
paddy@57 245 // GetProfileByLogin returns the Profile associated with the specified Login from the profileStore associated
paddy@57 246 // with the Context.
paddy@69 247 func (c Context) GetProfileByLogin(value string) (Profile, error) {
paddy@50 248 if c.profiles == nil {
paddy@50 249 return Profile{}, ErrNoProfileStore
paddy@50 250 }
paddy@69 251 return c.profiles.getProfileByLogin(value)
paddy@50 252 }
paddy@50 253
paddy@57 254 // SaveProfile inserts the passed Profile into the profileStore associated with the Context.
paddy@50 255 func (c Context) SaveProfile(profile Profile) error {
paddy@50 256 if c.profiles == nil {
paddy@50 257 return ErrNoProfileStore
paddy@50 258 }
paddy@57 259 return c.profiles.saveProfile(profile)
paddy@50 260 }
paddy@50 261
paddy@57 262 // UpdateProfile applies the supplied ProfileChange to the Profile that matches the specified ID
paddy@57 263 // in the profileStore associated with the Context.
paddy@50 264 func (c Context) UpdateProfile(id uuid.ID, change ProfileChange) error {
paddy@50 265 if c.profiles == nil {
paddy@50 266 return ErrNoProfileStore
paddy@50 267 }
paddy@57 268 return c.profiles.updateProfile(id, change)
paddy@50 269 }
paddy@50 270
paddy@57 271 // UpdateProfiles applies the supplied BulkProfileChange to every Profile that matches one of the
paddy@57 272 // specified IDs in the profileStore associated with the Context.
paddy@50 273 func (c Context) UpdateProfiles(ids []uuid.ID, change BulkProfileChange) error {
paddy@50 274 if c.profiles == nil {
paddy@50 275 return ErrNoProfileStore
paddy@50 276 }
paddy@57 277 return c.profiles.updateProfiles(ids, change)
paddy@50 278 }
paddy@50 279
paddy@161 280 // DeleteProfile removes the specified Profile from the profileStore associated with the Context.
paddy@161 281 func (c Context) DeleteProfile(id uuid.ID) error {
paddy@161 282 if c.profiles == nil {
paddy@161 283 return ErrNoProfileStore
paddy@161 284 }
paddy@161 285 return c.profiles.deleteProfile(id)
paddy@161 286 }
paddy@161 287
paddy@57 288 // AddLogin stores the passed Login in the profileStore associated with the Context. It also associates
paddy@57 289 // the newly-created Login with the Orofile in login.ProfileID.
paddy@50 290 func (c Context) AddLogin(login Login) error {
paddy@50 291 if c.profiles == nil {
paddy@50 292 return ErrNoProfileStore
paddy@50 293 }
paddy@57 294 return c.profiles.addLogin(login)
paddy@50 295 }
paddy@50 296
paddy@172 297 // GetLogin returns the Login specified from the profileStore associated with the Context.
paddy@172 298 func (c Context) GetLogin(value string) (Login, error) {
paddy@172 299 if c.profiles == nil {
paddy@172 300 return Login{}, ErrNoProfileStore
paddy@172 301 }
paddy@172 302 return c.profiles.getLogin(value)
paddy@172 303 }
paddy@172 304
paddy@57 305 // RemoveLogin removes the specified Login from the profileStore associated with the Context, provided
paddy@57 306 // the Login has a ProfileID property that matches the profile ID passed in. It also disassociates the
paddy@57 307 // deleted Login from the Profile in login.ProfileID.
paddy@69 308 func (c Context) RemoveLogin(value string, profile uuid.ID) error {
paddy@50 309 if c.profiles == nil {
paddy@50 310 return ErrNoProfileStore
paddy@50 311 }
paddy@69 312 return c.profiles.removeLogin(value, profile)
paddy@50 313 }
paddy@50 314
paddy@160 315 // RemoveLoginsByProfile removes all Logins connected to the specified Profile in the profileStore
paddy@160 316 // associated with the Context and disassociates them from the Profile.
paddy@160 317 func (c Context) RemoveLoginsByProfile(profile uuid.ID) error {
paddy@160 318 if c.profiles == nil {
paddy@160 319 return ErrNoProfileStore
paddy@160 320 }
paddy@160 321 return c.profiles.removeLoginsByProfile(profile)
paddy@160 322 }
paddy@160 323
paddy@57 324 // RecordLoginUse sets the LastUsed property of the Login specified in the profileStore associated with
paddy@57 325 // the Context to the value passed in as when.
paddy@69 326 func (c Context) RecordLoginUse(value string, when time.Time) error {
paddy@50 327 if c.profiles == nil {
paddy@50 328 return ErrNoProfileStore
paddy@50 329 }
paddy@69 330 return c.profiles.recordLoginUse(value, when)
paddy@50 331 }
paddy@50 332
paddy@172 333 // VerifyLogin sets the Verified property of the Login specied to true in the profileStore associated with
paddy@172 334 // the Context, assuming the Verification property of the Login in the profileStore matches the verification
paddy@172 335 // passed. If the verifications do not match, an ErrLoginVerificationInvalid error is returned.
paddy@172 336 func (c Context) VerifyLogin(value, verification string) error {
paddy@172 337 if c.profiles == nil {
paddy@172 338 return ErrNoProfileStore
paddy@172 339 }
paddy@172 340 return c.profiles.verifyLogin(value, verification)
paddy@172 341 }
paddy@172 342
paddy@57 343 // ListLogins returns a slice of up to num Logins associated with the specified Profile from the profileStore
paddy@57 344 // associated with the Context, skipping offset Profiles.
paddy@50 345 func (c Context) ListLogins(profile uuid.ID, num, offset int) ([]Login, error) {
paddy@50 346 if c.profiles == nil {
paddy@50 347 return []Login{}, ErrNoProfileStore
paddy@50 348 }
paddy@57 349 return c.profiles.listLogins(profile, num, offset)
paddy@50 350 }
paddy@50 351
paddy@57 352 // GetToken returns the Token specified from the tokenStore associated with the Context.
paddy@57 353 // If refresh is true, the token input should be compared against the refresh tokens, not the
paddy@57 354 // access tokens.
paddy@50 355 func (c Context) GetToken(token string, refresh bool) (Token, error) {
paddy@50 356 if c.tokens == nil {
paddy@50 357 return Token{}, ErrNoTokenStore
paddy@50 358 }
paddy@57 359 return c.tokens.getToken(token, refresh)
paddy@50 360 }
paddy@50 361
paddy@57 362 // SaveToken stores the passed Token in the tokenStore associated with the Context.
paddy@50 363 func (c Context) SaveToken(token Token) error {
paddy@50 364 if c.tokens == nil {
paddy@50 365 return ErrNoTokenStore
paddy@50 366 }
paddy@57 367 return c.tokens.saveToken(token)
paddy@50 368 }
paddy@50 369
paddy@93 370 // RevokeToken revokes the Token identfied by the passed token string from the tokenStore associated
paddy@168 371 // with the context.
paddy@168 372 func (c Context) RevokeToken(token string) error {
paddy@93 373 if c.tokens == nil {
paddy@93 374 return ErrNoTokenStore
paddy@93 375 }
paddy@168 376 return c.tokens.revokeToken(token)
paddy@93 377 }
paddy@93 378
paddy@162 379 // RevokeTokensByProfileID revokes the Tokens associated with the Profile identified by the passed ID in
paddy@162 380 // the tokenStore associated with the Context.
paddy@162 381 func (c Context) RevokeTokensByProfileID(profileID uuid.ID) error {
paddy@162 382 if c.tokens == nil {
paddy@162 383 return ErrNoTokenStore
paddy@162 384 }
paddy@162 385 return c.tokens.revokeTokensByProfileID(profileID)
paddy@162 386 }
paddy@162 387
paddy@164 388 func (c Context) RevokeTokensByClientID(clientID uuid.ID) error {
paddy@164 389 if c.tokens == nil {
paddy@164 390 return ErrNoTokenStore
paddy@164 391 }
paddy@164 392 return c.tokens.revokeTokensByClientID(clientID)
paddy@164 393 }
paddy@164 394
paddy@57 395 // GetTokensByProfileID returns a slice of up to num Tokens with a ProfileID that matches the specified
paddy@57 396 // profileID from the tokenStore associated with the Context, skipping offset Tokens.
paddy@50 397 func (c Context) GetTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) {
paddy@50 398 if c.tokens == nil {
paddy@50 399 return []Token{}, ErrNoTokenStore
paddy@50 400 }
paddy@57 401 return c.tokens.getTokensByProfileID(profileID, num, offset)
paddy@50 402 }
paddy@69 403
paddy@69 404 // CreateSession stores the passed Session in the sessionStore associated with the Context.
paddy@69 405 func (c Context) CreateSession(session Session) error {
paddy@69 406 if c.sessions == nil {
paddy@69 407 return ErrNoSessionStore
paddy@69 408 }
paddy@69 409 return c.sessions.createSession(session)
paddy@69 410 }
paddy@69 411
paddy@69 412 // GetSession returns the Session specified from the sessionStore associated with the Context.
paddy@69 413 func (c Context) GetSession(id string) (Session, error) {
paddy@69 414 if c.sessions == nil {
paddy@69 415 return Session{}, ErrNoSessionStore
paddy@69 416 }
paddy@69 417 return c.sessions.getSession(id)
paddy@69 418 }
paddy@69 419
paddy@159 420 // TerminateSession sets the Session identified by the passed ID as inactive in the sessionStore assocated
paddy@159 421 // with the Context.
paddy@159 422 func (c Context) TerminateSession(id string) error {
paddy@159 423 if c.sessions == nil {
paddy@159 424 return ErrNoSessionStore
paddy@159 425 }
paddy@159 426 return c.sessions.terminateSession(id)
paddy@159 427 }
paddy@159 428
paddy@162 429 // TerminateSessionsByProfile sets the Sessions associated with the passed Profile ID as inactive in the
paddy@162 430 // sessionStore associated with the Context.
paddy@162 431 func (c Context) TerminateSessionsByProfile(profile uuid.ID) error {
paddy@162 432 if c.sessions == nil {
paddy@162 433 return ErrNoSessionStore
paddy@162 434 }
paddy@162 435 return c.sessions.terminateSessionsByProfile(profile)
paddy@162 436 }
paddy@162 437
paddy@69 438 // RemoveSession removes the Session identified by the passed ID from the sessionStore associated with
paddy@69 439 // the Context.
paddy@69 440 func (c Context) RemoveSession(id string) error {
paddy@69 441 if c.sessions == nil {
paddy@69 442 return ErrNoSessionStore
paddy@69 443 }
paddy@69 444 return c.sessions.removeSession(id)
paddy@69 445 }
paddy@69 446
paddy@69 447 // ListSessions returns a slice of up to num Sessions from the sessionStore associated with the Context,
paddy@69 448 // ordered by the date they were created, descending. If before.IsZero() returns false, only Sessions
paddy@69 449 // that were created before that time will be returned. If profile is not nil, only Sessions belonging to
paddy@69 450 // that Profile will be returned.
paddy@69 451 func (c Context) ListSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) {
paddy@116 452 if c.sessions == nil {
paddy@69 453 return []Session{}, ErrNoSessionStore
paddy@69 454 }
paddy@69 455 return c.sessions.listSessions(profile, before, num)
paddy@69 456 }
paddy@134 457
paddy@178 458 func (c Context) SendModelEvent(m events.Model, action string) error {
paddy@178 459 if c.eventsPublisher == nil {
paddy@178 460 log.Println("Event publisher not set!")
paddy@172 461 }
paddy@178 462 return events.PublishModelEvent(c.eventsPublisher, m, action)
paddy@172 463 }