gitee.com/mysnapcore/mysnapd@v0.1.0/asserts/system_user.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package asserts 21 22 import ( 23 "fmt" 24 "net/mail" 25 "regexp" 26 "strconv" 27 "strings" 28 "time" 29 ) 30 31 var validSystemUserUsernames = regexp.MustCompile(`^[a-z0-9][-a-z0-9+.-_]*$`) 32 33 // SystemUser holds a system-user assertion which allows creating local 34 // system users. 35 type SystemUser struct { 36 assertionBase 37 series []string 38 models []string 39 serials []string 40 sshKeys []string 41 since time.Time 42 until time.Time 43 expiration string 44 45 forcePasswordChange bool 46 } 47 48 // BrandID returns the brand identifier that signed this assertion. 49 func (su *SystemUser) BrandID() string { 50 return su.HeaderString("brand-id") 51 } 52 53 // Email returns the email address that this assertion is valid for. 54 func (su *SystemUser) Email() string { 55 return su.HeaderString("email") 56 } 57 58 // Series returns the series that this assertion is valid for. 59 func (su *SystemUser) Series() []string { 60 return su.series 61 } 62 63 // Models returns the models that this assertion is valid for. 64 func (su *SystemUser) Models() []string { 65 return su.models 66 } 67 68 // Serials returns the serials that this assertion is valid for. 69 func (su *SystemUser) Serials() []string { 70 return su.serials 71 } 72 73 // Name returns the full name of the user (e.g. Random Guy). 74 func (su *SystemUser) Name() string { 75 return su.HeaderString("name") 76 } 77 78 // Username returns the system user name that should be created (e.g. "foo"). 79 func (su *SystemUser) Username() string { 80 return su.HeaderString("username") 81 } 82 83 // Password returns the crypt(3) compatible password for the user. 84 // Note that only ID: $6$ or stronger is supported (sha512crypt). 85 func (su *SystemUser) Password() string { 86 return su.HeaderString("password") 87 } 88 89 // ForcePasswordChange returns true if the user needs to change the password 90 // after the first login. 91 func (su *SystemUser) ForcePasswordChange() bool { 92 return su.forcePasswordChange 93 } 94 95 // SSHKeys returns the ssh keys for the user. 96 func (su *SystemUser) SSHKeys() []string { 97 return su.sshKeys 98 } 99 100 // Since returns the time since the assertion is valid. 101 func (su *SystemUser) Since() time.Time { 102 return su.since 103 } 104 105 // Until returns the time until the assertion is valid. 106 func (su *SystemUser) Until() time.Time { 107 return su.until 108 } 109 110 // UserExpiration returns the expiration or validity duration of the user created. 111 // 112 // If no expiration was specified, this will return an zero time.Time structure. 113 // 114 // If expiration was set to 'until-expiration' then the .Until() time will be 115 // returned. 116 func (su *SystemUser) UserExpiration() time.Time { 117 if su.expiration == "until-expiration" { 118 return su.until 119 } 120 return time.Time{} 121 } 122 123 // ValidAt returns whether the system-user is valid at 'when' time. 124 func (su *SystemUser) ValidAt(when time.Time) bool { 125 valid := when.After(su.since) || when.Equal(su.since) 126 if valid { 127 valid = when.Before(su.until) 128 } 129 return valid 130 } 131 132 // Implement further consistency checks. 133 func (su *SystemUser) checkConsistency(db RODatabase, acck *AccountKey) error { 134 // Do the cross-checks when this assertion is actually used, 135 // i.e. in the create-user code. See also Model.checkConsitency 136 137 return nil 138 } 139 140 // expected interface is implemented 141 var _ consistencyChecker = (*SystemUser)(nil) 142 143 type shadow struct { 144 ID string 145 Rounds string 146 Salt string 147 Hash string 148 } 149 150 // crypt(3) compatible hashes have the forms: 151 // - $id$salt$hash 152 // - $id$rounds=N$salt$hash 153 func parseShadowLine(line string) (*shadow, error) { 154 l := strings.SplitN(line, "$", 5) 155 if len(l) != 4 && len(l) != 5 { 156 return nil, fmt.Errorf(`hashed password must be of the form "$integer-id$salt$hash", see crypt(3)`) 157 } 158 159 // if rounds is the second field, the line must consist of 4 160 if strings.HasPrefix(l[2], "rounds=") && len(l) == 4 { 161 return nil, fmt.Errorf(`missing hash field`) 162 } 163 164 // shadow line without $rounds=N$ 165 if len(l) == 4 { 166 return &shadow{ 167 ID: l[1], 168 Salt: l[2], 169 Hash: l[3], 170 }, nil 171 } 172 // shadow line with rounds 173 return &shadow{ 174 ID: l[1], 175 Rounds: l[2], 176 Salt: l[3], 177 Hash: l[4], 178 }, nil 179 } 180 181 // see crypt(3) for the legal chars 182 var isValidSaltAndHash = regexp.MustCompile(`^[a-zA-Z0-9./]+$`).MatchString 183 184 func checkHashedPassword(headers map[string]interface{}, name string) (string, error) { 185 pw, err := checkOptionalString(headers, name) 186 if err != nil { 187 return "", err 188 } 189 // the pw string is optional, so just return if its empty 190 if pw == "" { 191 return "", nil 192 } 193 194 // parse the shadow line 195 shd, err := parseShadowLine(pw) 196 if err != nil { 197 return "", fmt.Errorf(`%q header invalid: %s`, name, err) 198 } 199 200 // and verify it 201 202 // see crypt(3), ID 6 means SHA-512 (since glibc 2.7) 203 ID, err := strconv.Atoi(shd.ID) 204 if err != nil { 205 return "", fmt.Errorf(`%q header must start with "$integer-id$", got %q`, name, shd.ID) 206 } 207 // double check that we only allow modern hashes 208 if ID < 6 { 209 return "", fmt.Errorf("%q header only supports $id$ values of 6 (sha512crypt) or higher", name) 210 } 211 212 // the $rounds=N$ part is optional 213 if strings.HasPrefix(shd.Rounds, "rounds=") { 214 rounds, err := strconv.Atoi(strings.SplitN(shd.Rounds, "=", 2)[1]) 215 if err != nil { 216 return "", fmt.Errorf("%q header has invalid number of rounds: %s", name, err) 217 } 218 if rounds < 5000 || rounds > 999999999 { 219 return "", fmt.Errorf("%q header rounds parameter out of bounds: %d", name, rounds) 220 } 221 } 222 223 if !isValidSaltAndHash(shd.Salt) { 224 return "", fmt.Errorf("%q header has invalid chars in salt %q", name, shd.Salt) 225 } 226 if !isValidSaltAndHash(shd.Hash) { 227 return "", fmt.Errorf("%q header has invalid chars in hash %q", name, shd.Hash) 228 } 229 230 return pw, nil 231 } 232 233 func checkSystemUserPresence(assert assertionBase) (string, error) { 234 str, err := checkOptionalString(assert.headers, "user-presence") 235 if err != nil || str == "" { 236 return "", err 237 } 238 if assert.Format() < 2 { 239 return "", fmt.Errorf(`the "user-presence" header is only supported for format 2 or greater`) 240 } 241 242 if str != "until-expiration" { 243 return "", fmt.Errorf(`invalid "user-presence" header, only explicit valid value is "until-expiration": %q`, str) 244 } 245 return str, nil 246 } 247 248 func assembleSystemUser(assert assertionBase) (Assertion, error) { 249 // brand-id here can be different from authority-id, 250 // the code using the assertion must use the policy set 251 // by the model assertion system-user-authority header 252 email, err := checkNotEmptyString(assert.headers, "email") 253 if err != nil { 254 return nil, err 255 } 256 if _, err := mail.ParseAddress(email); err != nil { 257 return nil, fmt.Errorf(`"email" header must be a RFC 5322 compliant email address: %s`, err) 258 } 259 260 series, err := checkStringList(assert.headers, "series") 261 if err != nil { 262 return nil, err 263 } 264 models, err := checkStringList(assert.headers, "models") 265 if err != nil { 266 return nil, err 267 } 268 serials, err := checkStringList(assert.headers, "serials") 269 if err != nil { 270 return nil, err 271 } 272 if len(serials) > 0 && assert.Format() < 1 { 273 return nil, fmt.Errorf(`the "serials" header is only supported for format 1 or greater`) 274 } 275 if len(serials) > 0 && len(models) != 1 { 276 return nil, fmt.Errorf(`in the presence of the "serials" header "models" must specify exactly one model`) 277 } 278 279 if _, err := checkOptionalString(assert.headers, "name"); err != nil { 280 return nil, err 281 } 282 if _, err := checkStringMatches(assert.headers, "username", validSystemUserUsernames); err != nil { 283 return nil, err 284 } 285 password, err := checkHashedPassword(assert.headers, "password") 286 if err != nil { 287 return nil, err 288 } 289 forcePasswordChange, err := checkOptionalBool(assert.headers, "force-password-change") 290 if err != nil { 291 return nil, err 292 } 293 if forcePasswordChange && password == "" { 294 return nil, fmt.Errorf(`cannot use "force-password-change" with an empty "password"`) 295 } 296 297 sshKeys, err := checkStringList(assert.headers, "ssh-keys") 298 if err != nil { 299 return nil, err 300 } 301 since, err := checkRFC3339Date(assert.headers, "since") 302 if err != nil { 303 return nil, err 304 } 305 until, err := checkRFC3339Date(assert.headers, "until") 306 if err != nil { 307 return nil, err 308 } 309 if until.Before(since) { 310 return nil, fmt.Errorf("'until' time cannot be before 'since' time") 311 } 312 expiration, err := checkSystemUserPresence(assert) 313 if err != nil { 314 return nil, err 315 } 316 317 // "global" system-user assertion can only be valid for 1y 318 if len(models) == 0 && until.After(since.AddDate(1, 0, 0)) { 319 return nil, fmt.Errorf("'until' time cannot be more than 365 days in the future when no models are specified") 320 } 321 322 return &SystemUser{ 323 assertionBase: assert, 324 series: series, 325 models: models, 326 serials: serials, 327 sshKeys: sshKeys, 328 since: since, 329 until: until, 330 expiration: expiration, 331 forcePasswordChange: forcePasswordChange, 332 }, nil 333 } 334 335 func systemUserFormatAnalyze(headers map[string]interface{}, body []byte) (formatnum int, err error) { 336 formatnum = 0 337 338 serials, err := checkStringList(headers, "serials") 339 if err != nil { 340 return 0, err 341 } 342 if len(serials) > 0 { 343 formatnum = 1 344 } 345 346 presence, err := checkOptionalString(headers, "user-presence") 347 if err != nil { 348 return 0, err 349 } 350 if presence != "" { 351 formatnum = 2 352 } 353 354 return formatnum, nil 355 }