github.com/rigado/snapd@v2.42.5-go-mod+incompatible/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  	sshKeys []string
    40  	since   time.Time
    41  	until   time.Time
    42  
    43  	forcePasswordChange bool
    44  }
    45  
    46  // BrandID returns the brand identifier that signed this assertion.
    47  func (su *SystemUser) BrandID() string {
    48  	return su.HeaderString("brand-id")
    49  }
    50  
    51  // Email returns the email address that this assertion is valid for.
    52  func (su *SystemUser) Email() string {
    53  	return su.HeaderString("email")
    54  }
    55  
    56  // Series returns the series that this assertion is valid for.
    57  func (su *SystemUser) Series() []string {
    58  	return su.series
    59  }
    60  
    61  // Models returns the models that this assertion is valid for.
    62  func (su *SystemUser) Models() []string {
    63  	return su.models
    64  }
    65  
    66  // Name returns the full name of the user (e.g. Random Guy).
    67  func (su *SystemUser) Name() string {
    68  	return su.HeaderString("name")
    69  }
    70  
    71  // Username returns the system user name that should be created (e.g. "foo").
    72  func (su *SystemUser) Username() string {
    73  	return su.HeaderString("username")
    74  }
    75  
    76  // Password returns the crypt(3) compatible password for the user.
    77  // Note that only ID: $6$ or stronger is supported (sha512crypt).
    78  func (su *SystemUser) Password() string {
    79  	return su.HeaderString("password")
    80  }
    81  
    82  // ForcePasswordChange returns true if the user needs to change the password
    83  // after the first login.
    84  func (su *SystemUser) ForcePasswordChange() bool {
    85  	return su.forcePasswordChange
    86  }
    87  
    88  // SSHKeys returns the ssh keys for the user.
    89  func (su *SystemUser) SSHKeys() []string {
    90  	return su.sshKeys
    91  }
    92  
    93  // Since returns the time since the assertion is valid.
    94  func (su *SystemUser) Since() time.Time {
    95  	return su.since
    96  }
    97  
    98  // Until returns the time until the assertion is valid.
    99  func (su *SystemUser) Until() time.Time {
   100  	return su.until
   101  }
   102  
   103  // ValidAt returns whether the system-user is valid at 'when' time.
   104  func (su *SystemUser) ValidAt(when time.Time) bool {
   105  	valid := when.After(su.since) || when.Equal(su.since)
   106  	if valid {
   107  		valid = when.Before(su.until)
   108  	}
   109  	return valid
   110  }
   111  
   112  // Implement further consistency checks.
   113  func (su *SystemUser) checkConsistency(db RODatabase, acck *AccountKey) error {
   114  	// Do the cross-checks when this assertion is actually used,
   115  	// i.e. in the create-user code. See also Model.checkConsitency
   116  
   117  	return nil
   118  }
   119  
   120  // sanity
   121  var _ consistencyChecker = (*SystemUser)(nil)
   122  
   123  type shadow struct {
   124  	ID     string
   125  	Rounds string
   126  	Salt   string
   127  	Hash   string
   128  }
   129  
   130  // crypt(3) compatible hashes have the forms:
   131  // - $id$salt$hash
   132  // - $id$rounds=N$salt$hash
   133  func parseShadowLine(line string) (*shadow, error) {
   134  	l := strings.SplitN(line, "$", 5)
   135  	if len(l) != 4 && len(l) != 5 {
   136  		return nil, fmt.Errorf(`hashed password must be of the form "$integer-id$salt$hash", see crypt(3)`)
   137  	}
   138  
   139  	// if rounds is the second field, the line must consist of 4
   140  	if strings.HasPrefix(l[2], "rounds=") && len(l) == 4 {
   141  		return nil, fmt.Errorf(`missing hash field`)
   142  	}
   143  
   144  	// shadow line without $rounds=N$
   145  	if len(l) == 4 {
   146  		return &shadow{
   147  			ID:   l[1],
   148  			Salt: l[2],
   149  			Hash: l[3],
   150  		}, nil
   151  	}
   152  	// shadow line with rounds
   153  	return &shadow{
   154  		ID:     l[1],
   155  		Rounds: l[2],
   156  		Salt:   l[3],
   157  		Hash:   l[4],
   158  	}, nil
   159  }
   160  
   161  // see crypt(3) for the legal chars
   162  var isValidSaltAndHash = regexp.MustCompile(`^[a-zA-Z0-9./]+$`).MatchString
   163  
   164  func checkHashedPassword(headers map[string]interface{}, name string) (string, error) {
   165  	pw, err := checkOptionalString(headers, name)
   166  	if err != nil {
   167  		return "", err
   168  	}
   169  	// the pw string is optional, so just return if its empty
   170  	if pw == "" {
   171  		return "", nil
   172  	}
   173  
   174  	// parse the shadow line
   175  	shd, err := parseShadowLine(pw)
   176  	if err != nil {
   177  		return "", fmt.Errorf(`%q header invalid: %s`, name, err)
   178  	}
   179  
   180  	// and verify it
   181  
   182  	// see crypt(3), ID 6 means SHA-512 (since glibc 2.7)
   183  	ID, err := strconv.Atoi(shd.ID)
   184  	if err != nil {
   185  		return "", fmt.Errorf(`%q header must start with "$integer-id$", got %q`, name, shd.ID)
   186  	}
   187  	// double check that we only allow modern hashes
   188  	if ID < 6 {
   189  		return "", fmt.Errorf("%q header only supports $id$ values of 6 (sha512crypt) or higher", name)
   190  	}
   191  
   192  	// the $rounds=N$ part is optional
   193  	if strings.HasPrefix(shd.Rounds, "rounds=") {
   194  		rounds, err := strconv.Atoi(strings.SplitN(shd.Rounds, "=", 2)[1])
   195  		if err != nil {
   196  			return "", fmt.Errorf("%q header has invalid number of rounds: %s", name, err)
   197  		}
   198  		if rounds < 5000 || rounds > 999999999 {
   199  			return "", fmt.Errorf("%q header rounds parameter out of bounds: %d", name, rounds)
   200  		}
   201  	}
   202  
   203  	if !isValidSaltAndHash(shd.Salt) {
   204  		return "", fmt.Errorf("%q header has invalid chars in salt %q", name, shd.Salt)
   205  	}
   206  	if !isValidSaltAndHash(shd.Hash) {
   207  		return "", fmt.Errorf("%q header has invalid chars in hash %q", name, shd.Hash)
   208  	}
   209  
   210  	return pw, nil
   211  }
   212  
   213  func assembleSystemUser(assert assertionBase) (Assertion, error) {
   214  	// brand-id here can be different from authority-id,
   215  	// the code using the assertion must use the policy set
   216  	// by the model assertion system-user-authority header
   217  	email, err := checkNotEmptyString(assert.headers, "email")
   218  	if err != nil {
   219  		return nil, err
   220  	}
   221  	if _, err := mail.ParseAddress(email); err != nil {
   222  		return nil, fmt.Errorf(`"email" header must be a RFC 5322 compliant email address: %s`, err)
   223  	}
   224  
   225  	series, err := checkStringList(assert.headers, "series")
   226  	if err != nil {
   227  		return nil, err
   228  	}
   229  	models, err := checkStringList(assert.headers, "models")
   230  	if err != nil {
   231  		return nil, err
   232  	}
   233  	if _, err := checkOptionalString(assert.headers, "name"); err != nil {
   234  		return nil, err
   235  	}
   236  	if _, err := checkStringMatches(assert.headers, "username", validSystemUserUsernames); err != nil {
   237  		return nil, err
   238  	}
   239  	password, err := checkHashedPassword(assert.headers, "password")
   240  	if err != nil {
   241  		return nil, err
   242  	}
   243  	forcePasswordChange, err := checkOptionalBool(assert.headers, "force-password-change")
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  	if forcePasswordChange && password == "" {
   248  		return nil, fmt.Errorf(`cannot use "force-password-change" with an empty "password"`)
   249  	}
   250  
   251  	sshKeys, err := checkStringList(assert.headers, "ssh-keys")
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  	since, err := checkRFC3339Date(assert.headers, "since")
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  	until, err := checkRFC3339Date(assert.headers, "until")
   260  	if err != nil {
   261  		return nil, err
   262  	}
   263  	if until.Before(since) {
   264  		return nil, fmt.Errorf("'until' time cannot be before 'since' time")
   265  	}
   266  
   267  	// "global" system-user assertion can only be valid for 1y
   268  	if len(models) == 0 && until.After(since.AddDate(1, 0, 0)) {
   269  		return nil, fmt.Errorf("'until' time cannot be more than 365 days in the future when no models are specified")
   270  	}
   271  
   272  	return &SystemUser{
   273  		assertionBase:       assert,
   274  		series:              series,
   275  		models:              models,
   276  		sshKeys:             sshKeys,
   277  		since:               since,
   278  		until:               until,
   279  		forcePasswordChange: forcePasswordChange,
   280  	}, nil
   281  }