github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/apiserver/common/crossmodel/auth.go (about) 1 // Copyright 2017 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package crossmodel 5 6 import ( 7 "time" 8 9 "github.com/juju/clock" 10 "github.com/juju/errors" 11 "gopkg.in/errgo.v1" 12 "gopkg.in/juju/names.v2" 13 "gopkg.in/macaroon-bakery.v2-unstable/bakery" 14 "gopkg.in/macaroon-bakery.v2-unstable/bakery/checkers" 15 "gopkg.in/macaroon.v2-unstable" 16 "gopkg.in/yaml.v2" 17 18 "github.com/juju/juju/apiserver/authentication" 19 "github.com/juju/juju/apiserver/common" 20 "github.com/juju/juju/apiserver/params" 21 "github.com/juju/juju/permission" 22 ) 23 24 const ( 25 usernameKey = "username" 26 offeruuidKey = "offer-uuid" 27 sourcemodelKey = "source-model-uuid" 28 relationKey = "relation-key" 29 30 offerPermissionCaveat = "has-offer-permission" 31 32 // localOfferPermissionExpiryTime is used to expire offer macaroons. 33 // It should be long enough to allow machines hosting workloads to 34 // be provisioned so that the macaroon is still valid when the macaroon 35 // is next used. If a machine takes longer, that's ok, a new discharge 36 // will be obtained. 37 localOfferPermissionExpiryTime = 3 * time.Minute 38 ) 39 40 // AuthContext is used to validate macaroons used to access 41 // application offers. 42 type AuthContext struct { 43 pool StatePool 44 45 clock clock.Clock 46 localOfferThirdPartyBakeryService authentication.BakeryService 47 localOfferBakeryService authentication.ExpirableStorageBakeryService 48 49 offerAccessEndpoint string 50 } 51 52 // NewAuthContext creates a new authentication context for checking 53 // macaroons used with application offer requests. 54 func NewAuthContext( 55 pool StatePool, 56 localOfferThirdPartyBakeryService authentication.BakeryService, 57 localOfferBakeryService authentication.ExpirableStorageBakeryService, 58 ) (*AuthContext, error) { 59 ctxt := &AuthContext{ 60 pool: pool, 61 clock: clock.WallClock, 62 localOfferBakeryService: localOfferBakeryService, 63 localOfferThirdPartyBakeryService: localOfferThirdPartyBakeryService, 64 } 65 return ctxt, nil 66 } 67 68 // WithClock creates a new authentication context 69 // using the specified clock. 70 func (a *AuthContext) WithClock(clock clock.Clock) *AuthContext { 71 ctxtCopy := *a 72 ctxtCopy.clock = clock 73 return &ctxtCopy 74 } 75 76 // WithDischargeURL create an auth context based on this context and used 77 // to perform third party discharges at the specified URL. 78 func (a *AuthContext) WithDischargeURL(offerAccessEndpoint string) *AuthContext { 79 ctxtCopy := *a 80 ctxtCopy.offerAccessEndpoint = offerAccessEndpoint 81 return &ctxtCopy 82 } 83 84 // ThirdPartyBakeryService returns the third party bakery service. 85 func (a *AuthContext) ThirdPartyBakeryService() authentication.BakeryService { 86 return a.localOfferThirdPartyBakeryService 87 } 88 89 // CheckOfferAccessCaveat checks that the specified caveat required to be satisfied 90 // to gain access to an offer is valid, and returns the attributes return to check 91 // that the caveat is satisfied. 92 func (a *AuthContext) CheckOfferAccessCaveat(caveat string) (*offerPermissionCheck, error) { 93 op, rest, err := checkers.ParseCaveat(caveat) 94 if err != nil { 95 return nil, errors.Annotatef(err, "cannot parse caveat %q", caveat) 96 } 97 if op != offerPermissionCaveat { 98 return nil, checkers.ErrCaveatNotRecognized 99 } 100 var details offerPermissionCheck 101 err = yaml.Unmarshal([]byte(rest), &details) 102 if err != nil { 103 return nil, errors.Trace(err) 104 } 105 logger.Debugf("offer access caveat details: %+v", details) 106 if !names.IsValidModel(details.SourceModelUUID) { 107 return nil, errors.NotValidf("source-model-uuid %q", details.SourceModelUUID) 108 } 109 if !names.IsValidUser(details.User) { 110 return nil, errors.NotValidf("username %q", details.User) 111 } 112 if err := permission.ValidateOfferAccess(permission.Access(details.Permission)); err != nil { 113 return nil, errors.NotValidf("permission %q", details.Permission) 114 } 115 return &details, nil 116 } 117 118 // CheckLocalAccessRequest checks that the user in the specified permission 119 // check details has consume access to the offer in the details. 120 // It returns an error with a *bakery.VerificationError cause if the macaroon 121 // verification failed. If the macaroon is valid, CheckLocalAccessRequest 122 // returns a list of caveats to add to the discharge macaroon. 123 func (a *AuthContext) CheckLocalAccessRequest(details *offerPermissionCheck) ([]checkers.Caveat, error) { 124 logger.Debugf("authenticate local offer access: %+v", details) 125 st, releaser, err := a.pool.Get(details.SourceModelUUID) 126 if err != nil { 127 return nil, errors.Trace(err) 128 } 129 defer releaser() 130 if err := a.checkOfferAccess(st, details.User, details.OfferUUID); err != nil { 131 return nil, errors.Trace(err) 132 } 133 134 firstPartyCaveats := []checkers.Caveat{ 135 checkers.DeclaredCaveat(sourcemodelKey, details.SourceModelUUID), 136 checkers.DeclaredCaveat(offeruuidKey, details.OfferUUID), 137 checkers.DeclaredCaveat(usernameKey, details.User), 138 checkers.TimeBeforeCaveat(a.clock.Now().Add(localOfferPermissionExpiryTime)), 139 } 140 if details.Relation != "" { 141 firstPartyCaveats = append(firstPartyCaveats, checkers.DeclaredCaveat(relationKey, details.Relation)) 142 } 143 return firstPartyCaveats, nil 144 } 145 146 func (a *AuthContext) checkOfferAccess(st Backend, username, offerUUID string) error { 147 userTag := names.NewUserTag(username) 148 isAdmin, err := a.hasControllerAdminAccess(st, userTag) 149 if err != nil { 150 return common.ErrPerm 151 } 152 if isAdmin { 153 return nil 154 } 155 isAdmin, err = a.hasModelAdminAccess(st, userTag) 156 if err != nil { 157 return common.ErrPerm 158 } 159 if isAdmin { 160 return nil 161 } 162 access, err := st.GetOfferAccess(offerUUID, userTag) 163 if err != nil && !errors.IsNotFound(err) { 164 return common.ErrPerm 165 } 166 if !access.EqualOrGreaterOfferAccessThan(permission.ConsumeAccess) { 167 return common.ErrPerm 168 } 169 return nil 170 } 171 172 func (api *AuthContext) hasControllerAdminAccess(st Backend, userTag names.UserTag) (bool, error) { 173 isAdmin, err := common.HasPermission(st.UserPermission, userTag, permission.SuperuserAccess, st.ControllerTag()) 174 if errors.IsNotFound(err) { 175 return false, nil 176 } 177 return isAdmin, err 178 } 179 180 func (api *AuthContext) hasModelAdminAccess(st Backend, userTag names.UserTag) (bool, error) { 181 isAdmin, err := common.HasPermission(st.UserPermission, userTag, permission.AdminAccess, st.ModelTag()) 182 if errors.IsNotFound(err) { 183 return false, nil 184 } 185 return isAdmin, err 186 } 187 188 func (a *AuthContext) offerPermissionYaml(sourceModelUUID, username, offerURL, relationKey string, permission permission.Access) (string, error) { 189 out, err := yaml.Marshal(offerPermissionCheck{ 190 SourceModelUUID: sourceModelUUID, 191 User: username, 192 OfferUUID: offerURL, 193 Relation: relationKey, 194 Permission: string(permission), 195 }) 196 if err != nil { 197 return "", err 198 } 199 return string(out), nil 200 } 201 202 // CreateConsumeOfferMacaroon creates a macaroon that authorises access to the specified offer. 203 func (a *AuthContext) CreateConsumeOfferMacaroon(offer *params.ApplicationOfferDetails, username string) (*macaroon.Macaroon, error) { 204 sourceModelTag, err := names.ParseModelTag(offer.SourceModelTag) 205 if err != nil { 206 return nil, errors.Trace(err) 207 } 208 expiryTime := a.clock.Now().Add(localOfferPermissionExpiryTime) 209 bakery, err := a.localOfferBakeryService.ExpireStorageAfter(localOfferPermissionExpiryTime) 210 if err != nil { 211 return nil, errors.Trace(err) 212 } 213 214 return bakery.NewMacaroon( 215 []checkers.Caveat{ 216 checkers.TimeBeforeCaveat(expiryTime), 217 checkers.DeclaredCaveat(sourcemodelKey, sourceModelTag.Id()), 218 checkers.DeclaredCaveat(offeruuidKey, offer.OfferUUID), 219 checkers.DeclaredCaveat(usernameKey, username), 220 }) 221 222 } 223 224 // CreateRemoteRelationMacaroon creates a macaroon that authorises access to the specified relation. 225 func (a *AuthContext) CreateRemoteRelationMacaroon(sourceModelUUID, offerUUID string, username string, rel names.Tag) (*macaroon.Macaroon, error) { 226 expiryTime := a.clock.Now().Add(localOfferPermissionExpiryTime) 227 bakery, err := a.localOfferBakeryService.ExpireStorageAfter(localOfferPermissionExpiryTime) 228 if err != nil { 229 return nil, errors.Trace(err) 230 } 231 232 offerMacaroon, err := bakery.NewMacaroon( 233 []checkers.Caveat{ 234 checkers.TimeBeforeCaveat(expiryTime), 235 checkers.DeclaredCaveat(sourcemodelKey, sourceModelUUID), 236 checkers.DeclaredCaveat(offeruuidKey, offerUUID), 237 checkers.DeclaredCaveat(usernameKey, username), 238 checkers.DeclaredCaveat(relationKey, rel.Id()), 239 }) 240 241 return offerMacaroon, err 242 } 243 244 type offerPermissionCheck struct { 245 SourceModelUUID string `yaml:"source-model-uuid"` 246 User string `yaml:"username"` 247 OfferUUID string `yaml:"offer-uuid"` 248 Relation string `yaml:"relation-key"` 249 Permission string `yaml:"permission"` 250 } 251 252 type authenticator struct { 253 clock clock.Clock 254 bakery authentication.ExpirableStorageBakeryService 255 ctxt *AuthContext 256 257 sourceModelUUID string 258 offerUUID string 259 260 // offerAccessEndpoint holds the URL of the trusted third party 261 // that is used to address the has-offer-permission third party caveat. 262 offerAccessEndpoint string 263 } 264 265 // Authenticator returns an instance used to authenticate macaroons used to 266 // access the specified offer. 267 func (a *AuthContext) Authenticator(sourceModelUUID, offerUUID string) *authenticator { 268 auth := &authenticator{ 269 clock: a.clock, 270 bakery: a.localOfferBakeryService, 271 ctxt: a, 272 sourceModelUUID: sourceModelUUID, 273 offerUUID: offerUUID, 274 offerAccessEndpoint: a.offerAccessEndpoint, 275 } 276 return auth 277 } 278 279 func (a *authenticator) checkMacaroons(mac macaroon.Slice, requiredValues map[string]string) (map[string]string, error) { 280 logger.Debugf("check %d macaroons with required attrs: %v", len(mac), requiredValues) 281 for _, m := range mac { 282 if m == nil { 283 logger.Warningf("unexpected nil cross model macaroon") 284 continue 285 } 286 logger.Debugf("- mac %s", m.Id()) 287 } 288 declared := checkers.InferDeclared(mac) 289 logger.Debugf("check macaroons with declared attrs: %v", declared) 290 username, ok := declared[usernameKey] 291 if !ok { 292 return nil, common.ErrPerm 293 } 294 relation := declared[relationKey] 295 attrs, err := a.bakery.CheckAny([]macaroon.Slice{mac}, requiredValues, checkers.TimeBefore) 296 if err == nil { 297 logger.Debugf("macaroon check ok, attr: %v", attrs) 298 return attrs, nil 299 } 300 301 if _, ok := errgo.Cause(err).(*bakery.VerificationError); !ok { 302 logger.Debugf("macaroon verification failed: %+v", err) 303 return nil, common.ErrPerm 304 } 305 306 logger.Debugf("generating discharge macaroon because: %v", err) 307 cause := err 308 authYaml, err := a.ctxt.offerPermissionYaml(a.sourceModelUUID, username, a.offerUUID, relation, permission.ConsumeAccess) 309 if err != nil { 310 return nil, errors.Trace(err) 311 } 312 bakery, err := a.bakery.ExpireStorageAfter(localOfferPermissionExpiryTime) 313 if err != nil { 314 return nil, errors.Trace(err) 315 } 316 keys := []string{usernameKey} 317 for k := range requiredValues { 318 keys = append(keys, k) 319 } 320 m, err := bakery.NewMacaroon([]checkers.Caveat{ 321 checkers.NeedDeclaredCaveat( 322 checkers.Caveat{ 323 Location: a.offerAccessEndpoint, 324 Condition: offerPermissionCaveat + " " + authYaml, 325 }, 326 keys..., 327 ), 328 checkers.TimeBeforeCaveat(a.clock.Now().Add(localOfferPermissionExpiryTime)), 329 }) 330 331 if err != nil { 332 return nil, errors.Annotate(err, "cannot create macaroon") 333 } 334 return nil, &common.DischargeRequiredError{ 335 Cause: cause, 336 Macaroon: m, 337 } 338 } 339 340 // CheckOfferMacaroons verifies that the specified macaroons allow access to the offer. 341 func (a *authenticator) CheckOfferMacaroons(offerUUID string, mac macaroon.Slice) (map[string]string, error) { 342 requiredValues := map[string]string{ 343 sourcemodelKey: a.sourceModelUUID, 344 offeruuidKey: offerUUID, 345 } 346 return a.checkMacaroons(mac, requiredValues) 347 } 348 349 // CheckRelationMacaroons verifies that the specified macaroons allow access to the relation. 350 func (a *authenticator) CheckRelationMacaroons(relationTag names.Tag, mac macaroon.Slice) error { 351 requiredValues := map[string]string{ 352 sourcemodelKey: a.sourceModelUUID, 353 relationKey: relationTag.Id(), 354 } 355 _, err := a.checkMacaroons(mac, requiredValues) 356 return err 357 }