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  }