All files / src/impl PrincipalAuthority.js

96.83% Statements 122/126
89.55% Branches 60/67
91.67% Functions 11/12
96.83% Lines 122/126
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274                              3x 3x 3x 3x   3x 3x 3x 3x                 129x   129x 129x 129x 129x   129x 129x 129x 129x 129x     129x                             18x       3x       18x   18x 18x 18x   3x   3x             15x 3x   3x     12x 12x 12x 12x     12x     12x 12x 3x   3x           9x 9x 9x 9x 3x   3x           6x 3x   3x         3x 3x 3x 3x 3x 3x 3x 3x       27x 27x   9x 6x   9x         12x 6x 3x     12x   3x   3x 3x   27x           24x 24x               24x 6x 3x 3x 3x 3x 3x   18x 6x 6x     24x       27x 3x   24x 21x   3x               12x 12x 12x 6x 3x 3x 3x   3x   6x 3x 3x 3x     6x             27x 27x 27x 6x 6x 3x   3x 3x   3x         24x 24x 6x     6x 6x     18x     18x 6x 6x 6x   12x       42x       3x  
/**
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
'use strict';
 
var winston = require('winston');
var PrincipalToken = require('../token/PrincipalToken');
var SimplePrincipal = require('./SimplePrincipal');
var config = require('../../config/config')();
 
var USER_DOMAIN = 'user';
var SYS_AUTH_DOMAIN = 'sys.auth';
var ZMS_SERVICE = 'zms';
var ZTS_SERVICE = 'zts';
 
var ATHENZ_PROP_TOKEN_OFFSET;
var ATHENZ_PROP_IP_CHECK_MODE;
var ATHENZ_PROP_USER_DOMAIN;
var ATHENZ_PROP_PRINCIPAL_HEADER;
 
class PrincipalAuthority {
  constructor() {
    winston.level = config.logLevel;
 
    ATHENZ_PROP_TOKEN_OFFSET = Number(config.principalTokenAllowedOffset);
    ATHENZ_PROP_IP_CHECK_MODE = config.principalIpCheckMode;
    ATHENZ_PROP_USER_DOMAIN = config.principalUserDomain;
    ATHENZ_PROP_PRINCIPAL_HEADER = config.principalHeader;
 
    this._keyStore = null;
    this._allowedOffset = (ATHENZ_PROP_TOKEN_OFFSET) ? Number(ATHENZ_PROP_TOKEN_OFFSET) : 300;
    this._ipCheckMode = ATHENZ_PROP_IP_CHECK_MODE || 'OPS_WRITE';
    this._userDomain = ATHENZ_PROP_USER_DOMAIN || 'user';
    this._headerName = ATHENZ_PROP_PRINCIPAL_HEADER || 'Athenz-Principal-Auth';
 
    // case of invalid value, we'll default back to 5 minutes
    Iif (this._allowedOffset < 0) {
      this._allowedOffset = 300;
    }
  }
 
  static setConfig(c) {
    config = Object.assign({}, config, c.auth_core);
    PrincipalToken.setConfig(c);
    SimplePrincipal.setConfig(c);
  }
 
  initialize() {
  }
 
  getDomain() {
    return null;
  }
 
  getHeader() {
    return this._headerName;
  }
 
  authenticate(signedToken, remoteAddr, httpMethod) {
    winston.debug('Authenticating PrincipalToken: ' + signedToken);
 
    var serviceToken = null;
    try {
      serviceToken = new PrincipalToken(signedToken);
    } catch (e) {
      winston.error('PrincipalAuthority:authenticate: Invalid token: exc=' + e.message +
        ' : credential=' + PrincipalToken.getUnsignedToken(signedToken));
      return null;
    }
 
    /* before authenticating verify that if this is a valid
     * authorized service token or not and if required
     * components are provided (the method already logs
     * all error messages) */
    if (!serviceToken.isValidAuthorizedServiceToken()) {
      winston.error('PrincipalAuthority:authenticate: Invalid authorized service token: credential=' +
        PrincipalToken.getUnsignedToken(signedToken));
      return null;
    }
 
    var tokenDomain = serviceToken.getDomain().toString().toLowerCase();
    var tokenName = serviceToken.getName().toString().toLowerCase();
    var keyService = serviceToken.getKeyService();
    var userToken = (tokenDomain === this._userDomain);
 
    /* get the public key for this token to validate signature */
    var publicKey = this._getPublicKey(tokenDomain, tokenName, keyService, serviceToken.getKeyId(), userToken);
 
    /* the validate method logs all error messages */
    var writeOp = this._isWriteOperation(httpMethod);
    if (serviceToken.validate(publicKey, this._allowedOffset, !writeOp) === false) {
      winston.error('PrincipalAuthority:authenticate: service token validation failure: credential=' +
        PrincipalToken.getUnsignedToken(signedToken));
      return null;
    }
 
    /* if an authorized service signature is available then we're going to validate
     * that signature as well to support token chaining in Athenz and, if necessary,
     * bypass IP address mismatch for users */
    var authorizedServiceName = null;
    Eif (serviceToken.getAuthorizedServiceSignature()) {
      authorizedServiceName = this._validateAuthorizeService(serviceToken);
      if (!authorizedServiceName) {
        winston.error('PrincipalAuthority:authenticate: validation of authorized service failure: credential=' +
          PrincipalToken.getUnsignedToken(signedToken));
        return null;
      }
    }
 
    /* if we have a usertoken and our remote ip check enabled, verify that the IP address
     * matches before allowing the operation go through */
    if (userToken && !this._remoteIpCheck(remoteAddr, writeOp, serviceToken, authorizedServiceName)) {
      winston.error('PrincipalAuthority:authenticate: IP Mismatch - token (' + serviceToken.getIP() +
        ') request (' + remoteAddr + ')');
      return null;
    }
 
    /* all the role members in Athenz are normalized to lower case so we need to make
     * sure our principal's name and domain are created with lower case as well */
    var princ = SimplePrincipal.createByUserIdentity(tokenDomain, tokenName, signedToken, serviceToken.getTimestamp(), this);
    princ.setUnsignedCreds(serviceToken.getUnsignedToken());
    princ.setAuthorizedService(authorizedServiceName);
    princ.setOriginalRequestor(serviceToken.getOriginalRequestor());
    princ.setKeyService(keyService);
    princ.setIP(serviceToken.getIP());
    princ.setKeyId(serviceToken.getKeyId());
    return princ;
  }
 
  _remoteIpCheck(remoteAddr, writeOp, serviceToken, authorizedServiceName) {
    var checkResult = true;
    switch (this._ipCheckMode) {
      case 'OPS_ALL':
        if (remoteAddr !== serviceToken.getIP()) {
          checkResult = false;
        }
        break;
      case 'OPS_WRITE':
        /* if we have a user token for a write operation and we have an IP address
         * mismatch then we'll allow this authenticate request to proceed only if it's
         * been configured with authorized user only. */
        if (writeOp && remoteAddr !== serviceToken.getIP()) {
          if (!authorizedServiceName) {
            checkResult = false;
          }
        }
        break;
      case 'OPS_NONE':
        break;
      default:
        checkResult = false;
        break;
    }
    return checkResult;
  }
 
  _getPublicKey(tokenDomain, tokenName, keyService, keyId, userToken) {
    /* by default we're going to look for the public key for the domain
     * and service defined in the token */
    var publicKeyDomain = tokenDomain;
    var publicKeyService = tokenName;
 
    /* now let's handle the exceptions:
     * 1) if the token has a key service field set then only supported values are
     * either zms or zts, so we use sys.auth.zms or sys.auth.zts services
     * 2) if the token's domain is user then it's a user token or if it's sd then
     * it's our special project token so for those cases we are going to ask for
     * zms's own public key. */
    if (keyService) {
      if (keyService === ZMS_SERVICE) {
        publicKeyDomain = SYS_AUTH_DOMAIN;
        publicKeyService = ZMS_SERVICE;
      } else Eif (keyService === ZTS_SERVICE) {
        publicKeyDomain = SYS_AUTH_DOMAIN;
        publicKeyService = ZTS_SERVICE;
      }
    } else if (userToken) {
      publicKeyDomain = SYS_AUTH_DOMAIN;
      publicKeyService = ZMS_SERVICE;
    }
 
    return this._keyStore.getPublicKey(publicKeyDomain, publicKeyService, keyId);
  }
 
  _isWriteOperation(httpMethod) {
    if (!httpMethod) {
      return false;
    }
    if (httpMethod.toString().toUpperCase() === 'PUT' || httpMethod.toString().toUpperCase() === 'POST' || httpMethod.toString().toUpperCase() === 'DELETE') {
      return true;
    } else {
      return false;
    }
  }
 
  _getAuthorizedServiceName(authorizedServices, authorizedServiceName) {
    /* if we have an authorized service name specified then it must be
     * present in the authorized services list or if it's null then the
     * list must contain a single element only */
    var serviceName = authorizedServiceName;
    var err = null;
    if (!serviceName) {
      if (authorizedServices.length !== 1) {
        err = new Error('getAuthorizedServiceName() failed: No authorized service name specified');
        winston.error(err);
        return null;
      }
      serviceName = authorizedServices[0];
    } else {
      if (authorizedServices.indexOf(serviceName) === -1) {
        err = new Error('getAuthorizedServiceName() failed: Invalid authorized service name specified:' + serviceName);
        winston.error(err);
        return null;
      }
    }
    return serviceName;
  }
 
  _validateAuthorizeService(userToken) {
    /* if we have an authorized service name specified then it must be
     * present in the authorized services list or if it's null then the
     * list must contain a single element only */
    var authorizedServiceName = userToken.getAuthorizedServiceName();
    var err = null;
    if (!authorizedServiceName) {
      var authorizedServices = userToken.getAuthorizedServices();
      if (!authorizedServices || authorizedServices.length !== 1) {
        err = new Error('PrincipalAuthority:validateAuthorizeService: ' +
          'No service name and services list empty OR contains multiple entries: token=' + userToken.getUnsignedToken());
        winston.error(err);
        return null;
      } else {
        authorizedServiceName = authorizedServices[0];
      }
    }
 
    /* need to extract domain and service name from our full service name value */
    var idx = authorizedServiceName.lastIndexOf('.');
    if (idx <= 0 || idx === authorizedServiceName.length - 1) {
      err = new Error('PrincipalAuthority:validateAuthorizeService: ' +
        'failed: token=' + userToken.getUnsignedToken() +
        ' : Invalid authorized service name specified=' + authorizedServiceName);
      winston.error(err);
      return null;
    }
 
    var publicKey = this._keyStore.getPublicKey(authorizedServiceName.substring(0, idx), authorizedServiceName.substring(idx + 1), userToken.getAuthorizedServiceKeyId());
 
    /* the token method reports all error messages */
    if (!userToken.validateForAuthorizedService(publicKey)) {
      err = new Error('PrincipalAuthority:validateAuthorizeService: token validation for authorized service failed');
      winston.error(err);
      return null;
    }
    return authorizedServiceName;
  }
 
  setKeyStore(keyStore) {
    this._keyStore = keyStore;
  }
}
 
module.exports = PrincipalAuthority;