github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/modeluser.go (about) 1 // Copyright 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package state 5 6 import ( 7 "fmt" 8 "strings" 9 "time" 10 11 "github.com/juju/errors" 12 "github.com/juju/mgo/v3" 13 "github.com/juju/mgo/v3/bson" 14 "github.com/juju/mgo/v3/txn" 15 "github.com/juju/names/v5" 16 17 "github.com/juju/juju/core/permission" 18 "github.com/juju/juju/mongo" 19 ) 20 21 // modelUserLastConnectionDoc is updated by the apiserver whenever the user 22 // connects over the API. This update is not done using mgo.txn so the values 23 // could well change underneath a normal transaction and as such, it should 24 // NEVER appear in any transaction asserts. It is really informational only as 25 // far as everyone except the api server is concerned. 26 type modelUserLastConnectionDoc struct { 27 ID string `bson:"_id"` 28 ModelUUID string `bson:"model-uuid"` 29 UserName string `bson:"user"` 30 LastConnection time.Time `bson:"last-connection"` 31 } 32 33 // setModelAccess changes the user's access permissions on the model. 34 func (st *State) setModelAccess(access permission.Access, userGlobalKey, modelUUID string) error { 35 if err := permission.ValidateModelAccess(access); err != nil { 36 return errors.Trace(err) 37 } 38 op := updatePermissionOp(modelKey(modelUUID), userGlobalKey, access) 39 err := st.db().RunTransactionFor(modelUUID, []txn.Op{op}) 40 if err == txn.ErrAborted { 41 return errors.NotFoundf("existing permissions") 42 } 43 return errors.Trace(err) 44 } 45 46 // LastModelConnection returns when this User last connected through the API 47 // in UTC. The resulting time will be nil if the user has never logged in. 48 func (m *Model) LastModelConnection(user names.UserTag) (time.Time, error) { 49 lastConnections, closer := m.st.db().GetRawCollection(modelUserLastConnectionC) 50 defer closer() 51 52 username := user.Id() 53 var lastConn modelUserLastConnectionDoc 54 err := lastConnections.FindId(m.st.docID(username)).Select(bson.D{{"last-connection", 1}}).One(&lastConn) 55 if err != nil { 56 if err == mgo.ErrNotFound { 57 err = errors.Wrap(err, newNeverConnectedError(username)) 58 } 59 return time.Time{}, errors.Trace(err) 60 } 61 62 return lastConn.LastConnection.UTC(), nil 63 } 64 65 // UpdateLastModelConnection updates the last connection time of the model user. 66 func (m *Model) UpdateLastModelConnection(user names.UserTag) error { 67 return m.updateLastModelConnection(user, m.st.nowToTheSecond()) 68 } 69 70 func (m *Model) updateLastModelConnection(user names.UserTag, when time.Time) error { 71 lastConnections, closer := m.st.db().GetCollection(modelUserLastConnectionC) 72 defer closer() 73 74 lastConnectionsW := lastConnections.Writeable() 75 76 // Update the safe mode of the underlying session to not require 77 // write majority, nor sync to disk. 78 session := lastConnectionsW.Underlying().Database.Session 79 session.SetSafe(&mgo.Safe{}) 80 81 lastConn := modelUserLastConnectionDoc{ 82 ID: m.st.docID(strings.ToLower(user.Id())), 83 ModelUUID: m.UUID(), 84 UserName: user.Id(), 85 LastConnection: when, 86 } 87 _, err := lastConnectionsW.UpsertId(lastConn.ID, lastConn) 88 return errors.Trace(err) 89 } 90 91 // ModelUser a model userAccessDoc. 92 func (st *State) modelUser(modelUUID string, user names.UserTag) (userAccessDoc, error) { 93 modelUser := userAccessDoc{} 94 modelUsers, closer := st.db().GetCollectionFor(modelUUID, modelUsersC) 95 defer closer() 96 97 username := strings.ToLower(user.Id()) 98 err := modelUsers.FindId(username).One(&modelUser) 99 if err == mgo.ErrNotFound { 100 return userAccessDoc{}, errors.NotFoundf("model user %q", username) 101 } 102 if err != nil { 103 return userAccessDoc{}, errors.Trace(err) 104 } 105 // DateCreated is inserted as UTC, but read out as local time. So we 106 // convert it back to UTC here. 107 modelUser.DateCreated = modelUser.DateCreated.UTC() 108 return modelUser, nil 109 } 110 111 func createModelUserOps(modelUUID string, user, createdBy names.UserTag, displayName string, dateCreated time.Time, access permission.Access) []txn.Op { 112 creatorname := createdBy.Id() 113 doc := &userAccessDoc{ 114 ID: userAccessID(user), 115 ObjectUUID: modelUUID, 116 UserName: user.Id(), 117 DisplayName: displayName, 118 CreatedBy: creatorname, 119 DateCreated: dateCreated, 120 } 121 122 ops := []txn.Op{ 123 createPermissionOp(modelKey(modelUUID), userGlobalKey(userAccessID(user)), access), 124 { 125 C: modelUsersC, 126 Id: userAccessID(user), 127 Assert: txn.DocMissing, 128 Insert: doc, 129 }, 130 } 131 return ops 132 } 133 134 func removeModelUserOps(modelUUID string, user names.UserTag) []txn.Op { 135 return []txn.Op{ 136 removePermissionOp(modelKey(modelUUID), userGlobalKey(userAccessID(user))), 137 { 138 C: modelUsersC, 139 Id: userAccessID(user), 140 Assert: txn.DocExists, 141 Remove: true, 142 }} 143 } 144 145 func removeModelUserOpsGlobal(modelUUID string, user names.UserTag) []txn.Op { 146 return []txn.Op{ 147 removePermissionOp(modelKey(modelUUID), userGlobalKey(userAccessID(user))), 148 { 149 C: modelUsersC, 150 Id: ensureModelUUID(modelUUID, userAccessID(user)), 151 Assert: txn.DocExists, 152 Remove: true, 153 }} 154 } 155 156 // removeModelUser removes a user from the database. 157 func (st *State) removeModelUser(user names.UserTag) error { 158 ops := removeModelUserOps(st.ModelUUID(), user) 159 err := st.db().RunTransaction(ops) 160 if err == txn.ErrAborted { 161 err = errors.NewNotFound(nil, fmt.Sprintf("model user %q does not exist", user.Id())) 162 } 163 if err != nil { 164 return errors.Trace(err) 165 } 166 return nil 167 } 168 169 func (st *State) ModelSummariesForUser(user names.UserTag, isSuperuser bool) ([]ModelSummary, error) { 170 modelQuery, closer, err := st.modelQueryForUser(user, isSuperuser) 171 if err != nil { 172 return nil, errors.Trace(err) 173 } 174 defer closer() 175 var modelDocs []modelDoc 176 if err := modelQuery.All(&modelDocs); err != nil { 177 return nil, errors.Trace(err) 178 } 179 p := newProcessorFromModelDocs(st, modelDocs, user, isSuperuser) 180 modelDocs = nil 181 if err := p.fillInFromConfig(); err != nil { 182 return nil, errors.Trace(err) 183 } 184 if err := p.fillInFromStatus(); err != nil { 185 return nil, errors.Trace(err) 186 } 187 if err := p.fillInStatusBasedOnCloudCredentialValidity(); err != nil { 188 return nil, errors.Trace(err) 189 } 190 if err := p.fillInJustUser(); err != nil { 191 return nil, errors.Trace(err) 192 } 193 if err := p.fillInLastAccess(); err != nil { 194 return nil, errors.Trace(err) 195 } 196 if err := p.fillInMachineSummary(); err != nil { 197 return nil, errors.Trace(err) 198 } 199 if err := p.fillInApplicationSummary(); err != nil { 200 return nil, errors.Trace(err) 201 } 202 if err := p.fillInMigration(); err != nil { 203 return nil, errors.Trace(err) 204 } 205 return p.summaries, nil 206 } 207 208 // modelsForUser gives you the information about all models that a user has access to. 209 // This includes the name and UUID, as well as the last time the user connected to that model. 210 func (st *State) modelQueryForUser(user names.UserTag, isSuperuser bool) (mongo.Query, SessionCloser, error) { 211 var modelQuery mongo.Query 212 models, closer := st.db().GetCollection(modelsC) 213 if isSuperuser { 214 // Fast path, we just return all the models that aren't Importing 215 modelQuery = models.Find(bson.M{"migration-mode": bson.M{"$ne": MigrationModeImporting}}) 216 } else { 217 // Start by looking up model uuids that the user has access to, and then load only the records that are 218 // included in that set 219 var modelUUID struct { 220 UUID string `bson:"object-uuid"` 221 } 222 modelUsers, userCloser := st.db().GetRawCollection(modelUsersC) 223 defer userCloser() 224 query := modelUsers.Find(bson.D{{"user", user.Id()}}) 225 query.Select(bson.M{"object-uuid": 1, "_id": 0}) 226 query.Batch(100) 227 iter := query.Iter() 228 var modelUUIDs []string 229 for iter.Next(&modelUUID) { 230 modelUUIDs = append(modelUUIDs, modelUUID.UUID) 231 } 232 if err := iter.Close(); err != nil { 233 closer() 234 return nil, nil, errors.Trace(err) 235 } 236 modelQuery = models.Find(bson.M{ 237 "_id": bson.M{"$in": modelUUIDs}, 238 "migration-mode": bson.M{"$ne": MigrationModeImporting}, 239 }) 240 } 241 modelQuery.Sort("name", "owner") 242 return modelQuery, closer, nil 243 } 244 245 type ModelAccessInfo struct { 246 Name string `bson:"name"` 247 UUID string `bson:"_id"` 248 Owner string `bson:"owner"` 249 Type ModelType `bson:"type"` 250 LastConnection time.Time 251 } 252 253 // ModelBasicInfoForUser gives you the information about all models that a user has access to. 254 // This includes the name and UUID, as well as the last time the user connected to that model. 255 func (st *State) ModelBasicInfoForUser(user names.UserTag, isSuperuser bool) ([]ModelAccessInfo, error) { 256 modelQuery, closer1, err := st.modelQueryForUser(user, isSuperuser) 257 if err != nil { 258 return nil, errors.Trace(err) 259 } 260 defer closer1() 261 modelQuery.Select(bson.M{"_id": 1, "name": 1, "owner": 1, "type": 1}) 262 var accessInfo []ModelAccessInfo 263 if err := modelQuery.All(&accessInfo); err != nil { 264 return nil, errors.Trace(err) 265 } 266 // Now we need to find the last-connection time for each model for this user 267 username := user.Id() 268 connDocIds := make([]string, len(accessInfo)) 269 for i, acc := range accessInfo { 270 connDocIds[i] = acc.UUID + ":" + username 271 } 272 lastConnections, closer2 := st.db().GetRawCollection(modelUserLastConnectionC) 273 defer closer2() 274 query := lastConnections.Find(bson.M{"_id": bson.M{"$in": connDocIds}}) 275 query.Select(bson.M{"last-connection": 1, "_id": 0, "model-uuid": 1}) 276 query.Batch(100) 277 iter := query.Iter() 278 lastConns := make(map[string]time.Time, len(connDocIds)) 279 var connInfo modelUserLastConnectionDoc 280 for iter.Next(&connInfo) { 281 lastConns[connInfo.ModelUUID] = connInfo.LastConnection 282 } 283 if err := iter.Close(); err != nil { 284 return nil, errors.Trace(err) 285 } 286 for i := range accessInfo { 287 uuid := accessInfo[i].UUID 288 accessInfo[i].LastConnection = lastConns[uuid] 289 } 290 return accessInfo, nil 291 } 292 293 // ModelUUIDsForUser returns a list of models that the user is able to 294 // access. 295 // Results are sorted by (name, owner). 296 func (st *State) ModelUUIDsForUser(user names.UserTag) ([]string, error) { 297 // Consider the controller permissions overriding Model permission, for 298 // this case the only relevant one is superuser. 299 // The mgo query below wont work for superuser case because it needs at 300 // least one model user per model. 301 access, err := st.UserAccess(user, st.controllerTag) 302 if err != nil && !errors.IsNotFound(err) { 303 return nil, errors.Trace(err) 304 } 305 306 var modelUUIDs []string 307 if access.Access == permission.SuperuserAccess { 308 var err error 309 modelUUIDs, err = st.AllModelUUIDs() 310 if err != nil { 311 return nil, errors.Trace(err) 312 } 313 } else { 314 // Since there are no groups at this stage, the simplest way to get all 315 // the models that a particular user can see is to look through the 316 // model user collection. A raw collection is required to support 317 // queries across multiple models. 318 modelUsers, userCloser := st.db().GetRawCollection(modelUsersC) 319 defer userCloser() 320 321 var userSlice []userAccessDoc 322 err := modelUsers.Find(bson.D{{"user", user.Id()}}).Select(bson.D{{"object-uuid", 1}, {"_id", 1}}).All(&userSlice) 323 if err != nil { 324 return nil, err 325 } 326 for _, doc := range userSlice { 327 modelUUIDs = append(modelUUIDs, doc.ObjectUUID) 328 } 329 } 330 331 modelsColl, close := st.db().GetCollection(modelsC) 332 defer close() 333 query := modelsColl.Find(bson.M{ 334 "_id": bson.M{"$in": modelUUIDs}, 335 "migration-mode": bson.M{"$ne": MigrationModeImporting}, 336 }).Sort("name", "owner").Select(bson.M{"_id": 1}) 337 338 var docs []bson.M 339 err = query.All(&docs) 340 if err != nil { 341 return nil, errors.Trace(err) 342 } 343 out := make([]string, len(docs)) 344 for i, doc := range docs { 345 out[i] = doc["_id"].(string) 346 } 347 return out, nil 348 } 349 350 // IsControllerAdmin returns true if the user specified has Super User Access. 351 func (st *State) IsControllerAdmin(user names.UserTag) (bool, error) { 352 model, err := st.Model() 353 if err != nil { 354 return false, errors.Trace(err) 355 } 356 ua, err := st.UserAccess(user, model.ControllerTag()) 357 if errors.IsNotFound(err) { 358 return false, nil 359 } 360 if err != nil { 361 return false, errors.Trace(err) 362 } 363 return ua.Access == permission.SuperuserAccess, nil 364 } 365 366 func (st *State) isControllerOrModelAdmin(user names.UserTag) (bool, error) { 367 isAdmin, err := st.IsControllerAdmin(user) 368 if err != nil { 369 return false, errors.Trace(err) 370 } 371 if isAdmin { 372 return true, nil 373 } 374 ua, err := st.UserAccess(user, names.NewModelTag(st.ModelUUID())) 375 if errors.IsNotFound(err) { 376 return false, nil 377 } 378 if err != nil { 379 return false, errors.Trace(err) 380 } 381 return ua.Access == permission.AdminAccess, nil 382 }