Example #1
0
 def test_first_service_user(self):
     assert requests.get(Url(
         '/uiconfig')).json()['clusterConfiguration']['firstUser'] is True
     self.put_service()
     # A service user account is not expected to change the outcome.
     assert requests.get(Url(
         '/uiconfig')).json()['clusterConfiguration']['firstUser'] is True
Example #2
0
    def test_put_service_wrong_pem_pubkey_2(self):
        # Present an Elliptic Curve public key in X.509 PEM format. It has the
        # same header (dashy lines) as an RSA type X.509 pubkey would have,
        # but encodes a different key type in its payload. See
        # http://stackoverflow.com/a/29707204/145400

        # Define pre-generated key. Created with the cryptography module with
        # the OpenSSL back-end:
        # >>> from cryptography.hazmat.backends import default_backend
        # >>> from cryptography.hazmat.primitives.asymmetric import ec
        # >>> from cryptography.hazmat.primitives import serialization
        # >>> c = ec.EllipticCurve
        # >>> c.name = "secp256r1">>> c.key_size = 256
        # >>> privkey = ec.generate_private_key(curve=c, backend=default_backend())
        # >>> pubkey = privkey.public_key()
        # >>> pubkey.public_bytes(
        #    encoding=serialization.Encoding.PEM,
        #    format=serialization.PublicFormat.SubjectPublicKeyInfo)

        elliptic_curve_pubkey_pem_x509 = dedent("""
            -----BEGIN PUBLIC KEY-----
            MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/wMXOeN0OLzsO22VUsOhwwhaMaxk
            fPBF/7ZXjTv86oBCbnqThDnvpDE/8kp7Y8OQ6I4UO72f9HiA3NZLtiPWSg==
            -----END PUBLIC KEY-----
            """).lstrip()

        uid = 'servicea'
        data = {
            'description': 'Service A',
            'public_key': elliptic_curve_pubkey_pem_x509
        }
        url = Url('/users/%s' % uid)
        r = requests.put(url, json=data)
        assert r.status_code == 400
        assert 'must be of type RSA' in r.text
Example #3
0
 def test_put_service_empty_pubkey(self):
     uid = 'servicea'
     data = {'description': 'Service A', 'public_key': ''}
     url = Url('/users/%s' % uid)
     r = requests.put(url, json=data)
     assert r.status_code == 400
     assert '`public_key` must not be empty when provided' in r.text
Example #4
0
 def test_put_uid_validation_invalid_strings(self, uid):
     url = Url('/users/{uid}'.format(uid=uid))
     data = {'description': 'User A', 'password': '******'}
     r = requests.put(url, json=data)
     assert r.status_code == 400
     assert r.json()['code'] == 'ERR_INVALID_USER_ID'
     assert 'Invalid user ID' in r.json()['description']
Example #5
0
 def test_request_authtoken_with_custom_expiration(self):
     # Include the `exp` key in the `data` dictionary, with a non-zero
     # integer value. Note(JP): this is a private DC/OS interface introduced
     # as a workaround for the problem of reliable authentication token
     # refresh. It must never be publicly documented, and should be removed
     # in a future version, together with this test.
     url = Url('/auth/login')
     data = {
         'uid':
         self.service1_uid,
         'exp':
         84600,
         'token':
         jwt.encode(
             {
                 'exp': int(time.time() + 5 * 60),
                 'uid': self.service1_uid
             },
             self.service1_private_key,
             algorithm='RS256').decode('ascii')
     }
     r = requests.post(url, json=data)
     assert r.status_code == 200
     token = r.json()['token']
     _, payload_bytes, _ = [
         base64url_decode(_.encode('ascii')) for _ in token.split(".")
     ]
     payload_dict = json.loads(payload_bytes.decode('ascii'))
     assert payload_dict['exp'] == 84600
Example #6
0
 def test_patch_unknown_user(self):
     data = {'description': 'new'}
     r = requests.patch(Url('/users/unknownid'), json=data)
     assert r.status_code == 400
     d = r.json()
     assert d['code'] == 'ERR_UNKNOWN_USER_ID'
     assert 'User with uid `unknownid` not known' in d['description']
Example #7
0
 def test_default_providers(self):
     providers_url = Url('/auth/providers')
     r = requests.get(providers_url)
     assert r.status_code == 200
     d = r.json()
     assert set(d.keys()) == set(
         ['dcos-users', 'dcos-services', 'dcos-oidc-auth0'])
Example #8
0
    def test_request_authtoken_without_expiration(self):
        # Include the `exp` key in the `data` dictionary, with the value 0.
        # Note(JP): this is a private DC/OS interface introduced as a workaround
        # for the problem of reliable authentication token refresh. It must
        # never be publicly documented, and should be removed in a future
        # version.
        login_token = jwt.encode(
            {
                'exp': int(time.time() + 5 * 60),
                'uid': self.service1_uid
            },
            self.service1_private_key,
            algorithm='RS256').decode('ascii')

        r = requests.post(url=Url('/auth/login'),
                          json={
                              'exp': 0,
                              'uid': self.service1_uid,
                              'token': login_token
                          })
        assert r.status_code == 200
        token = r.json()['token']
        _, payload_bytes, _ = [
            base64url_decode(_.encode('ascii')) for _ in token.split(".")
        ]
        payload_dict = json.loads(payload_bytes.decode('ascii'))
        assert 'exp' not in payload_dict
Example #9
0
    def test_authtoken_rs256_verification(self):
        # Verify that the auth token (signed by Bouncer's private key)
        # can be validated using Bouncer's public key. Obtain/construct
        # Bouncer's public key from it's JSON Web Key Set (jwks) entpoint

        # Obtain authentication token.
        token, _ = self.test_authtoken_rs256_anatomy()

        # Obtain the JSON Web Key Set.
        r = requests.get(Url('/auth/jwks'))
        keys = r.json()['keys'][0]

        # Extract the public modulus and exponent from the data.
        exponent_bytes = base64url_decode(keys['e'].encode('ascii'))
        exponent_int = bytes_to_number(exponent_bytes)

        modulus_bytes = base64url_decode(keys['n'].encode('ascii'))
        modulus_int = bytes_to_number(modulus_bytes)

        # Generate a public key instance from these numbers.
        public_numbers = rsa.RSAPublicNumbers(n=modulus_int, e=exponent_int)
        public_key = public_numbers.public_key(backend=cryptography_backend)

        # Verify token signature using that public key.
        payload = jwt.decode(token, public_key, algorithms='RS256')
        assert payload['uid'] == self.user1_uid
Example #10
0
 def test_login_user(self):
     url = Url('/auth/login')
     credentials = {'uid': self.user1_uid, 'password': self.user1_password}
     r = requests.post(url, json=credentials)
     assert r.status_code == 200
     d = r.json()
     assert 'token' in d
Example #11
0
 def test_get_valid_but_unexpected_accept_header(self):
     url = Url('/users')
     req = urllib.request.Request(str(url), headers={'Accept': 'text/html'})
     try:
         urllib.request.urlopen(req)
     except urllib.error.HTTPError as e:
         # We require application/json, so expect 406 Not Acceptable
         assert e.code == 406
    def test_login_first_user_wins(self, dex):
        """
        DC/OS uses the dead-simple "first user wins" approach, where the first
        user that presents a valid ID Token from one of the white-listed
        providers is accepted and imported by the IAM. A subsequent login
        attempt with valid ID Token results in a 401 if the user is not known
        in the database.
        """

        id_token_1 = self._get_id_token(dex, '*****@*****.**', 'password')
        r = requests.post(Url('/auth/login'), json={'token': id_token_1})
        assert r.status_code == 200

        id_token_2 = self._get_id_token(dex, '*****@*****.**', 'password')
        r = requests.post(Url('/auth/login'), json={'token': id_token_2})
        assert r.status_code == 401
        assert 'user unknown' in r.text
Example #13
0
 def test_login_invalid_uid(self):
     url = Url('/auth/login')
     credentials = {'uid': 'wronguid', 'password': '******'}
     r = requests.post(url, json=credentials)
     assert r.status_code == 401
     d = r.json()
     assert d['code'] == 'ERR_INVALID_CREDENTIALS'
     assert r.headers['WWW-Authenticate'] == 'acsjwt'
    def test_login_first_user_and_ui_config_change(self, dex):
        """
        Build on the `test_login_first_user_wins` test and confirm that the
        dynamically generated UI config contains the expected value for the
        `firstUser` key. This implements the initial phase of an (open) DC/OS
        cluster lifecycle.
        """

        assert requests.get(Url(
            '/uiconfig')).json()['clusterConfiguration']['firstUser'] is True

        id_token_1 = self._get_id_token(dex, '*****@*****.**', 'password')
        r = requests.post(Url('/auth/login'), json={'token': id_token_1})
        assert r.status_code == 200

        assert requests.get(Url(
            '/uiconfig')).json()['clusterConfiguration']['firstUser'] is False
Example #15
0
 def test_put_user_invalid_id(self):
     uid = 'user!'
     data = {'description': 'User A', 'password': '******'}
     url = Url('/users/%s' % uid)
     r = requests.put(url, json=data)
     assert r.status_code == 400
     assert r.json()['code'] == 'ERR_INVALID_USER_ID'
     assert 'Invalid user ID' in r.text
Example #16
0
 def test_putget_users_two_users(self):
     uid1 = 'usera'
     data1 = {'description': 'User A', 'password': '******'}
     url1 = Url('/users/%s' % uid1)
     r = requests.put(url1, json=data1)
     assert r.status_code == 201
     uid2 = 'userb'
     data2 = {'description': 'User B', 'password': '******'}
     url2 = Url('/users/%s' % uid2)
     r = requests.put(url2, json=data2)
     assert r.status_code == 201
     r = requests.get(Url('/users'))
     assert r.status_code == 200
     d = r.json()
     assert isinstance(d['array'], list)
     correct = sorted([{
         'uid': uid1,
         'description': 'User A',
         'url': url1.rel(),
         'is_remote': False,
         'is_service': False,
         'provider_type': 'internal',
         'provider_id': '',
     }, {
         'uid': uid2,
         'description': 'User B',
         'url': url2.rel(),
         'is_remote': False,
         'is_service': False,
         'provider_type': 'internal',
         'provider_id': '',
     }],
                      key=lambda x: x['uid'])
     assert correct == sorted(d['array'], key=lambda x: x['uid'])
Example #17
0
 def test_with_servicefilter(self):
     r = requests.get(Url('/users?type=service'))
     assert r.status_code == 200
     # Expect one service, no users.
     services = r.json()['array']
     assert len(services) == 2
     uids = [d['uid'] for d in services]
     assert self.service1_uid in uids
     assert self.service2_uid in uids
Example #18
0
 def test_wo_servicefilter(self):
     r = requests.get(Url('/users'))
     assert r.status_code == 200
     # Expect two users, no service.
     users = r.json()['array']
     assert len(users) == 2
     uids = [d['uid'] for d in users]
     assert self.user1_uid in uids
     assert self.user2_uid in uids
Example #19
0
 def test_put_user_twice(self):
     uid = 'userx'
     data = {'description': 'User X', 'password': '******'}
     url = Url('/users/%s' % uid)
     r = requests.put(url, json=data)
     assert r.status_code == 201
     r = requests.put(url, json=data)
     assert r.status_code == 409
     assert r.json()['code'] == 'ERR_USER_EXISTS'
Example #20
0
 def test_putget_one_service_public_key(self):
     uid = 'servicea'
     data = {'description': 'Service A', 'public_key': default_rsa_pubkey}
     url = Url('/users/%s' % uid)
     r = requests.put(url, json=data)
     assert r.status_code == 201
     r = requests.get(Url('/users/%s' % uid))
     assert r.status_code == 200
     d = r.json()
     assert d == {
         'uid': uid,
         'description': 'Service A',
         'url': url.rel(),
         'is_remote': False,
         'is_service': True,
         'provider_type': 'internal',
         'provider_id': '',
         'public_key': default_rsa_pubkey
     }
Example #21
0
 def test_yaml_spec_is_served(self):
     r = requests.get(Url('/internal/openapispec.yaml'))
     r.raise_for_status()
     assert r.headers['Content-Type'] == 'application/x-yaml; charset=utf-8'
     yaml_bytes = r.content
     # Confirm that a byte sequence of non-zero length was served.
     assert yaml_bytes
     # Confirm that the byte sequences can be decoded using UTF-8, and that
     # the resulting text is a serialized YAML document.
     yaml.safe_load(yaml_bytes.decode('utf-8'))
Example #22
0
 def test_put_service_wrong_pem_pubkey_1(self):
     # Make dashed lines indicate that this is a PKCS#1 PEM key format
     # instead of the X.509 pubkey format.
     wrongkey = default_rsa_pubkey.replace('PUBLIC KEY', 'RSA PUBLIC KEY')
     uid = 'servicea'
     data = {'description': 'Service A', 'public_key': wrongkey}
     url = Url('/users/%s' % uid)
     r = requests.put(url, json=data)
     assert r.status_code == 400
     assert 'Invalid public key' in r.text
Example #23
0
 def test_get_blank_accept_header(self):
     # From RFC 7231: A request without any Accept header field implies
     # that the user agent will accept any media type in response.
     url = Url('/users')
     # The requests module sends a couple of header fields
     # by default, such as Accept: */* -- urllib.request is more raw.
     # Do not send any Accept header here.
     req = urllib.request.Request(str(url), headers={})
     resp = urllib.request.urlopen(req)
     assert resp.status == 200
Example #24
0
 def test_put_user_invalid_password_properties(self):
     uid = 'userx'
     data = {
         'description': 'User X',
         'password': '******',
     }
     url = Url('/users/%s' % uid)
     r = requests.put(url, json=data)
     assert r.status_code == 400
     assert 'Password does not match rules' in r.text
 def setup(self):
     """Executed before every test in this class. Skip if this Bouncer
     variant does not offer the OIDC ID Token login.
     """
     providers_url = Url('/auth/providers')
     r = requests.get(providers_url)
     assert r.status_code == 200
     d = r.json()
     if 'dcos-oidc-auth0' not in d:
         pytest.skip('OIDC ID Token login not activated')
Example #26
0
 def test_putget_users_one_user(self):
     uid = 'usera'
     data = {'description': 'User A', 'password': '******'}
     url = Url('/users/%s' % uid)
     r = requests.put(url, json=data)
     assert r.status_code == 201
     r = requests.get(Url('/users'))
     assert r.status_code == 200
     d = r.json()
     assert isinstance(d['array'], list)
     assert d['array'] == [{
         'uid': uid,
         'description': 'User A',
         'url': url.rel(),
         'is_remote': False,
         'is_service': False,
         'provider_type': 'internal',
         'provider_id': '',
     }]
Example #27
0
    def test_legacy_user_creation_with_meaningless_request_body(self):
        """Test for a special property of the `dcos-oidc-auth0` auth provider.

        Legacy HTTP clients, such as the web UI, might insert users in the
        following way and expect those users to be usable with the legacy OIDC
        ID Token login method through the 'https://dcos.auth0.com/' provider.
        """
        r = requests.put(Url('/users/[email protected]'), json={})
        assert r.status_code == 201

        r = requests.get(Url('/users/[email protected]'))
        assert r.json()['provider_type'] == 'oidc'
        assert r.json()['provider_id'] == 'https://dcos.auth0.com/'

        # Make sure that the above magic depends on the uid looking like an
        # email address.
        r = requests.put(Url('/users/user1'), json={})
        assert r.status_code == 400
        assert 'One of `password` or `public_key` must be provided' in r.text
    def test_login_succeeds(self, dex):

        id_token = self._get_id_token(dex, '*****@*****.**', 'password')

        # With the OIDC ID token (text) at hand the goal of this test is to
        # confirm that it can be exchanged into a DC/OS authentication token.
        # This login method was built for (open) DC/OS in April 2016, and the
        # goal here is to resemble that implementation. Sadly, the entry point
        # is not really explicit (POST request JSON body with a 'token' key).
        r = requests.post(Url('/auth/login'), json={'token': id_token})
        assert r.status_code == 200
        assert 'token' in r.json()

        # Confirm that user was imported implicitly.
        users = {
            u['uid']: u
            for u in requests.get(Url('/users')).json()['array']
        }
        assert '*****@*****.**' in users
        assert users['*****@*****.**']['is_remote']
Example #29
0
 def test_put_service_too_many_args(self):
     uid = 'servicea'
     data = {
         'description': 'Service A',
         'password': '******',
         'public_key': 'secret',
     }
     url = Url('/users/%s' % uid)
     r = requests.put(url, json=data)
     assert r.status_code == 400
     assert 'One of `password` or `public_key` must be provided' in r.text
    def test_login_succeeds_twice_for_same_user(self, dex):
        """
        So far we allow a reply attack. Hence, technically we could use the same
        `id_token`. However, don't do this here in hopefully anticipation of us
        fixing the replay vulnerability.
        """

        id_token_1 = self._get_id_token(dex, '*****@*****.**', 'password')
        r = requests.post(Url('/auth/login'), json={'token': id_token_1})
        assert r.status_code == 200

        id_token_2 = self._get_id_token(dex, '*****@*****.**', 'password')
        r = requests.post(Url('/auth/login'), json={'token': id_token_2})
        assert r.status_code == 200

        # When one understands the inner workings of _get_id_token() then it is
        # obvious that the two ID tokens differ. The following assertion proves
        # that fact to the reader (however, testing this is not essential for
        # this test).
        assert id_token_1 != id_token_2