Beispiel #1
0
 def _internal_get_token(self, session, token_id):
     token_value = self.db.get_one("tokens", {"_id": token_id}, fail_on_empty=False)
     if not token_value:
         raise AuthException("token not found", http_code=HTTPStatus.NOT_FOUND)
     if token_value["username"] != session["username"] and not session["admin"]:
         raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
     return token_value
Beispiel #2
0
    def _internal_authorize(self, token_id):
        try:
            if not token_id:
                raise AuthException("Needed a token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED)
            # try to get from cache first
            now = time()
            session = self.tokens_cache.get(token_id)
            if session and session["expires"] < now:
                del self.tokens_cache[token_id]
                session = None
            if session:
                return session

            # get from database if not in cache
            session = self.db.get_one("tokens", {"_id": token_id})
            if session["expires"] < now:
                raise AuthException("Expired Token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED)
            self.tokens_cache[token_id] = session
            return session
        except DbException as e:
            if e.http_code == HTTPStatus.NOT_FOUND:
                raise AuthException("Invalid Token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED)
            else:
                raise

        except AuthException:
            if self.config["global"].get("test.user_not_authorized"):
                return {"id": "fake-token-id-for-test",
                        "project_id": self.config["global"].get("test.project_not_authorized", "admin"),
                        "username": self.config["global"]["test.user_not_authorized"]}
            else:
                raise
Beispiel #3
0
    def start(self, config):
        """
        Method to configure the Authenticator object. This method should be called
        after object creation. It is responsible by initializing the selected backend,
        as well as the initialization of the database connection.

        :param config: dictionary containing the relevant parameters for this object.
        """
        self.config = config

        try:
            if not self.db:
                if config["database"]["driver"] == "mongo":
                    self.db = dbmongo.DbMongo()
                    self.db.db_connect(config["database"])
                elif config["database"]["driver"] == "memory":
                    self.db = dbmemory.DbMemory()
                    self.db.db_connect(config["database"])
                else:
                    raise AuthException(
                        "Invalid configuration param '{}' at '[database]':'driver'"
                        .format(config["database"]["driver"]))
            if not self.backend:
                if config["authentication"]["backend"] == "keystone":
                    self.backend = AuthconnKeystone(
                        self.config["authentication"])
                elif config["authentication"]["backend"] == "internal":
                    self._internal_tokens_prune()
                else:
                    raise AuthException(
                        "Unknown authentication backend: {}".format(
                            config["authentication"]["backend"]))
        except Exception as e:
            raise AuthException(str(e))
Beispiel #4
0
 def get_token(self, session, token):
     if self.config["authentication"]["backend"] == "internal":
         return self._internal_get_token(session, token)
     else:
         # TODO: check if this can be avoided. Backend may provide enough information
         token_value = self.tokens_cache.get(token)
         if not token_value:
             raise AuthException("token not found", http_code=HTTPStatus.NOT_FOUND)
         if token_value["username"] != session["username"] and not session["admin"]:
             raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
         return token_value
Beispiel #5
0
    def start(self, config):
        """
        Method to configure the Authenticator object. This method should be called
        after object creation. It is responsible by initializing the selected backend,
        as well as the initialization of the database connection.

        :param config: dictionary containing the relevant parameters for this object.
        """
        self.config = config

        try:
            if not self.db:
                if config["database"]["driver"] == "mongo":
                    self.db = dbmongo.DbMongo()
                    self.db.db_connect(config["database"])
                elif config["database"]["driver"] == "memory":
                    self.db = dbmemory.DbMemory()
                    self.db.db_connect(config["database"])
                else:
                    raise AuthException("Invalid configuration param '{}' at '[database]':'driver'"
                                        .format(config["database"]["driver"]))
            if not self.backend:
                if config["authentication"]["backend"] == "keystone":
                    self.backend = AuthconnKeystone(self.config["authentication"])
                elif config["authentication"]["backend"] == "internal":
                    self._internal_tokens_prune()
                else:
                    raise AuthException("Unknown authentication backend: {}"
                                        .format(config["authentication"]["backend"]))
            if not self.resources_to_operations_file:
                if "resources_to_operations" in config["rbac"]:
                    self.resources_to_operations_file = config["rbac"]["resources_to_operations"]
                else:
                    for config_file in (__file__[:__file__.rfind("auth.py")] + "resources_to_operations.yml",
                                        "./resources_to_operations.yml"):
                        if path.isfile(config_file):
                            self.resources_to_operations_file = config_file
                            break
                    if not self.resources_to_operations_file:
                        raise AuthException("Invalid permission configuration: resources_to_operations file missing")
            if not self.roles_to_operations_file:
                if "roles_to_operations" in config["rbac"]:
                    self.roles_to_operations_file = config["rbac"]["roles_to_operations"]
                else:
                    for config_file in (__file__[:__file__.rfind("auth.py")] + "roles_to_operations.yml",
                                        "./roles_to_operations.yml"):
                        if path.isfile(config_file):
                            self.roles_to_operations_file = config_file
                            break
                    if not self.roles_to_operations_file:
                        raise AuthException("Invalid permission configuration: roles_to_operations file missing")
        except Exception as e:
            raise AuthException(str(e))
Beispiel #6
0
    def _internal_new_token(self, session, indata, remote):
        now = time()
        user_content = None

        # Try using username/password
        if indata.get("username"):
            user_rows = self.db.get_list("users", {"username": indata.get("username")})
            if user_rows:
                user_content = user_rows[0]
                salt = user_content["_admin"]["salt"]
                shadow_password = sha256(indata.get("password", "").encode('utf-8') + salt.encode('utf-8')).hexdigest()
                if shadow_password != user_content["password"]:
                    user_content = None
            if not user_content:
                raise AuthException("Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED)
        elif session:
            user_rows = self.db.get_list("users", {"username": session["username"]})
            if user_rows:
                user_content = user_rows[0]
            else:
                raise AuthException("Invalid token", http_code=HTTPStatus.UNAUTHORIZED)
        else:
            raise AuthException("Provide credentials: username/password or Authorization Bearer token",
                                http_code=HTTPStatus.UNAUTHORIZED)

        token_id = ''.join(random_choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
                           for _ in range(0, 32))
        if indata.get("project_id"):
            project_id = indata.get("project_id")
            if project_id not in user_content["projects"]:
                raise AuthException("project {} not allowed for this user"
                                    .format(project_id), http_code=HTTPStatus.UNAUTHORIZED)
        else:
            project_id = user_content["projects"][0]
        if project_id == "admin":
            session_admin = True
        else:
            project = self.db.get_one("projects", {"_id": project_id})
            session_admin = project.get("admin", False)
        new_session = {"issued_at": now, "expires": now + 3600,
                       "_id": token_id, "id": token_id, "project_id": project_id, "username": user_content["username"],
                       "remote_port": remote.port, "admin": session_admin}
        if remote.name:
            new_session["remote_host"] = remote.name
        elif remote.ip:
            new_session["remote_host"] = remote.ip

        self.tokens_cache[token_id] = new_session
        self.db.create("tokens", new_session)
        # check if database must be prune
        self._internal_tokens_prune(now)
        return deepcopy(new_session)
Beispiel #7
0
    def new_token(self, session, indata, remote):
        if self.config["authentication"]["backend"] == "internal":
            return self._internal_new_token(session, indata, remote)
        else:
            if indata.get("username"):
                token, projects = self.backend.authenticate_with_user_password(
                    indata.get("username"), indata.get("password"))
            elif session:
                token, projects = self.backend.authenticate_with_token(
                    session.get("id"), indata.get("project_id"))
            else:
                raise AuthException("Provide credentials: username/password or Authorization Bearer token",
                                    http_code=HTTPStatus.UNAUTHORIZED)

            if indata.get("project_id"):
                project_id = indata.get("project_id")
                if project_id not in projects:
                    raise AuthException("Project {} not allowed for this user".format(project_id),
                                        http_code=HTTPStatus.UNAUTHORIZED)
            else:
                project_id = projects[0]

            if not session:
                token, projects = self.backend.authenticate_with_token(token, project_id)

            if project_id == "admin":
                session_admin = True
            else:
                session_admin = reduce(lambda x, y: x or (True if y == "admin" else False),
                                       projects, False)

            now = time()
            new_session = {
                "_id": token,
                "id": token,
                "issued_at": now,
                "expires": now + 3600,
                "project_id": project_id,
                "username": indata.get("username") if not session else session.get("username"),
                "remote_port": remote.port,
                "admin": session_admin
            }

            if remote.name:
                new_session["remote_host"] = remote.name
            elif remote.ip:
                new_session["remote_host"] = remote.ip

            # TODO: check if this can be avoided. Backend may provide enough information
            self.tokens_cache[token] = new_session

            return deepcopy(new_session)
Beispiel #8
0
 def authorize(self):
     token = None
     user_passwd64 = None
     try:
         # 1. Get token Authorization bearer
         auth = cherrypy.request.headers.get("Authorization")
         if auth:
             auth_list = auth.split(" ")
             if auth_list[0].lower() == "bearer":
                 token = auth_list[-1]
             elif auth_list[0].lower() == "basic":
                 user_passwd64 = auth_list[-1]
         if not token:
             if cherrypy.session.get("Authorization"):
                 # 2. Try using session before request a new token. If not, basic authentication will generate
                 token = cherrypy.session.get("Authorization")
                 if token == "logout":
                     token = None  # force Unauthorized response to insert user password again
             elif user_passwd64 and cherrypy.request.config.get("auth.allow_basic_authentication"):
                 # 3. Get new token from user password
                 user = None
                 passwd = None
                 try:
                     user_passwd = standard_b64decode(user_passwd64).decode()
                     user, _, passwd = user_passwd.partition(":")
                 except Exception:
                     pass
                 outdata = self.new_token(None, {"username": user, "password": passwd})
                 token = outdata["id"]
                 cherrypy.session['Authorization'] = token
         if self.config["authentication"]["backend"] == "internal":
             return self._internal_authorize(token)
         else:
             if not token:
                 raise AuthException("Needed a token or Authorization http header",
                                     http_code=HTTPStatus.UNAUTHORIZED)
             try:
                 self.backend.validate_token(token)
                 self.check_permissions(self.tokens_cache[token], cherrypy.request.path_info,
                                        cherrypy.request.method)
                 # TODO: check if this can be avoided. Backend may provide enough information
                 return deepcopy(self.tokens_cache[token])
             except AuthException:
                 self.del_token(token)
                 raise
     except AuthException as e:
         if cherrypy.session.get('Authorization'):
             del cherrypy.session['Authorization']
         cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="{}"'.format(e)
         raise AuthException(str(e))
Beispiel #9
0
    def _normalize_url(self, url, method):
        # Removing query strings
        normalized_url = url if '?' not in url else url[:url.find("?")]
        normalized_url_splitted = normalized_url.split("/")
        parameters = {}

        filtered_keys = [key for key in self.resources_to_operations_mapping.keys()
                         if method in key.split()[0]]

        for idx, path_part in enumerate(normalized_url_splitted):
            tmp_keys = []
            for tmp_key in filtered_keys:
                splitted = tmp_key.split()[1].split("/")
                if "<" in splitted[idx] and ">" in splitted[idx]:
                    if splitted[idx] == "<artifactPath>":
                        tmp_keys.append(tmp_key)
                        continue
                    elif idx == len(normalized_url_splitted) - 1 and \
                            len(normalized_url_splitted) != len(splitted):
                        continue
                    else:
                        tmp_keys.append(tmp_key)
                elif splitted[idx] == path_part:
                    if idx == len(normalized_url_splitted) - 1 and \
                            len(normalized_url_splitted) != len(splitted):
                        continue
                    else:
                        tmp_keys.append(tmp_key)
            filtered_keys = tmp_keys
            if len(filtered_keys) == 1 and \
                    filtered_keys[0].split("/")[-1] == "<artifactPath>":
                break

        if len(filtered_keys) == 0:
            raise AuthException("Cannot make an authorization decision. URL not found. URL: {0}".format(url))
        elif len(filtered_keys) > 1:
            raise AuthException("Cannot make an authorization decision. Multiple URLs found. URL: {0}".format(url))

        filtered_key = filtered_keys[0]

        for idx, path_part in enumerate(filtered_key.split()[1].split("/")):
            if "<" in path_part and ">" in path_part:
                if path_part == "<artifactPath>":
                    parameters[path_part[1:-1]] = "/".join(normalized_url_splitted[idx:])
                else:
                    parameters[path_part[1:-1]] = normalized_url_splitted[idx]

        return filtered_key, parameters
Beispiel #10
0
    def decode_token_OIDC(self, token):
        if not token:
            raise AuthException("Needed a token or Authorization http header",
                                http_code=HTTPStatus.UNAUTHORIZED)

        oidc_resource = self.config["authentication"]["oidc_resource"]
        oidc_well_known_url = self.config["authentication"][
            "oidc_well_known_url"]
        response = urllib.request.urlopen(oidc_well_known_url)
        oidc_well_known_json = json.loads(
            urllib.request.urlopen(oidc_well_known_url).read().decode('utf-8'))
        oidc_jwks_uri = oidc_well_known_json["jwks_uri"]
        oidc_key_json = json.loads(
            urllib.request.urlopen(oidc_jwks_uri).read().decode('utf-8'))
        key_json = json.dumps(oidc_key_json["keys"][0])

        public_key = jwt.algorithms.RSAAlgorithm.from_jwk(key_json)
        decoded = jwt.decode(token,
                             public_key,
                             algorithms='RS256',
                             audience=oidc_resource)

        localuser = decoded.get("resource_access").get(oidc_resource).get(
            "roles")[0]
        self.logger.info("localuser: " + localuser)
        return localuser
    def authenticate_with_token(self, token, project=None):
        """
        Authenticate a user using a token. Can be used to revalidate the token
        or to get a scoped token.

        :param token: a valid token.
        :param project: (optional) project for a scoped token.
        :return: return a revalidated token, scoped if a project was passed or
        the previous token was already scoped.
        """
        try:
            token_info = self.keystone.tokens.validate(token=token)
            projects = self.keystone.projects.list(
                user=token_info["user"]["id"])
            project_names = [project.name for project in projects]

            token = self.keystone.get_raw_token_from_identity_service(
                auth_url=self.auth_url,
                token=token,
                project_name=project,
                user_domain_name=self.user_domain_name,
                project_domain_name=self.project_domain_name)

            return token["auth_token"], project_names
        except ClientException:
            self.logger.exception(
                "Error during user authentication using keystone. Method: bearer"
            )
            raise AuthException(
                "Error during user authentication using Keystone",
                http_code=HTTPStatus.UNAUTHORIZED)
    def authenticate_with_user_password(self, user, password):
        """
        Authenticate a user using username and password.

        :param user: username
        :param password: password
        :return: an unscoped token that grants access to project list
        """
        try:
            user_id = list(
                filter(lambda x: x.name == user,
                       self.keystone.users.list()))[0].id
            project_names = [
                project.name
                for project in self.keystone.projects.list(user=user_id)
            ]

            token = self.keystone.get_raw_token_from_identity_service(
                auth_url=self.auth_url,
                username=user,
                password=password,
                user_domain_name=self.user_domain_name,
                project_domain_name=self.project_domain_name)

            return token["auth_token"], project_names
        except ClientException:
            self.logger.exception(
                "Error during user authentication using keystone. Method: basic"
            )
            raise AuthException(
                "Error during user authentication using Keystone",
                http_code=HTTPStatus.UNAUTHORIZED)
Beispiel #13
0
    def authorize_OIDC(self, access_token, remote):
        user = self.decode_token_OIDC(access_token)

        now = time()
        user_content = None

        if user:
            user_rows = self.db.get_list("users", {"username": user})
            user_content = None
            if user_rows:
                user_content = user_rows[0]
                if access_token == None:
                    user_content = None
            if not user_content:
                raise AuthException("Invalid username/password",
                                    http_code=HTTPStatus.UNAUTHORIZED)
        else:
            raise AuthException(
                "Provide credentials: username/password or Authorization Bearer token",
                http_code=HTTPStatus.UNAUTHORIZED)

        token_id = str(access_token)
        project_id = user_content["projects"][0]

        if project_id == "admin":
            session_admin = True
        else:
            project = self.db.get_one("projects", {"_id": project_id})
            session_admin = project.get("admin", False)
        new_session = {
            "issued_at": now,
            "expires": now + 3600,
            "_id": token_id,
            "id": token_id,
            "project_id": project_id,
            "username": user_content["username"],
            "remote_port": remote.port,
            "admin": session_admin
        }
        if remote.name:
            new_session["remote_host"] = remote.name
        elif remote.ip:
            new_session["remote_host"] = remote.ip

        self.tokens_cache[token_id] = new_session
        return deepcopy(new_session)
Beispiel #14
0
 def del_token(self, token):
     if self.config["authentication"]["backend"] == "internal":
         return self._internal_del_token(token)
     else:
         try:
             self.backend.revoke_token(token)
             del self.tokens_cache[token]
             return "token '{}' deleted".format(token)
         except KeyError:
             raise AuthException("Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND)
Beispiel #15
0
 def _internal_del_token(self, token_id):
     try:
         self.tokens_cache.pop(token_id, None)
         self.db.del_one("tokens", {"_id": token_id})
         return "token '{}' deleted".format(token_id)
     except DbException as e:
         if e.http_code == HTTPStatus.NOT_FOUND:
             raise AuthException("Token '{}' not found".format(token_id), http_code=HTTPStatus.NOT_FOUND)
         else:
             raise
    def revoke_token(self, token):
        """
        Invalidate a token.

        :param token: token to be revoked
        """
        try:
            self.keystone.tokens.revoke_token(token=token)

            return True
        except ClientException:
            self.logger.exception(
                "Error during token revocation using keystone")
            raise AuthException("Error during token revocation using Keystone",
                                http_code=HTTPStatus.UNAUTHORIZED)
    def validate_token(self, token):
        """
        Check if the token is valid.

        :param token: token to validate
        :return: dictionary with information associated with the token. If the
        token is not valid, returns None.
        """
        if not token:
            return

        try:
            token_info = self.keystone.tokens.validate(token=token)

            return token_info
        except ClientException:
            self.logger.exception(
                "Error during token validation using keystone")
            raise AuthException("Error during token validation using Keystone",
                                http_code=HTTPStatus.UNAUTHORIZED)
    def get_project_list(self, token):
        """
        Get all the projects associated with a user.

        :param token: valid token
        :return: list of projects
        """
        try:
            token_info = self.keystone.tokens.validate(token=token)
            projects = self.keystone.projects.list(
                user=token_info["user"]["id"])
            project_names = [project.name for project in projects]

            return project_names
        except ClientException:
            self.logger.exception(
                "Error during user project listing using keystone")
            raise AuthException(
                "Error during user project listing using Keystone",
                http_code=HTTPStatus.UNAUTHORIZED)
Beispiel #19
0
    def check_permissions(self, session, url, method):
        self.logger.info("Session: {}".format(session))
        self.logger.info("URL: {}".format(url))
        self.logger.info("Method: {}".format(method))

        key, parameters = self._normalize_url(url, method)

        # TODO: Check if parameters might be useful for the decision

        operation = self.resources_to_operations_mapping[key]
        roles_required = self.operation_to_allowed_roles[operation]
        roles_allowed = self.backend.get_role_list(session["id"])

        if "anonymous" in roles_required:
            return

        for role in roles_allowed:
            if role in roles_required:
                return

        raise AuthException("Access denied: lack of permissions.")
    def get_role_list(self, token):
        """
        Get role list for a scoped project.

        :param token: scoped token.
        :return: returns the list of roles for the user in that project. If
        the token is unscoped it returns None.
        """
        try:
            token_info = self.keystone.tokens.validate(token=token)
            roles = self.keystone.roles.list(
                user=token_info["user"]["id"],
                project=token_info["project"]["id"])

            return roles
        except ClientException:
            self.logger.exception(
                "Error during user role listing using keystone")
            raise AuthException(
                "Error during user role listing using Keystone",
                http_code=HTTPStatus.UNAUTHORIZED)
Beispiel #21
0
    def __init__(self, config):
        Authconn.__init__(self, config)

        self.logger = logging.getLogger("nbi.authenticator.keystone")

        self.auth_url = "http://{0}:{1}/v3".format(
            config.get("auth_url", "keystone"),
            config.get("auth_port", "5000"))
        self.user_domain_name = config.get("user_domain_name", "default")
        self.admin_project = config.get("service_project", "service")
        self.admin_username = config.get("service_username", "nbi")
        self.admin_password = config.get("service_password", "nbi")
        self.project_domain_name = config.get("project_domain_name", "default")

        # Waiting for Keystone to be up
        available = None
        counter = 300
        while available is None:
            time.sleep(1)
            try:
                result = requests.get(self.auth_url)
                available = True if result.status_code == 200 else None
            except Exception:
                counter -= 1
                if counter == 0:
                    raise AuthException(
                        "Keystone not available after 300s timeout")

        self.auth = v3.Password(user_domain_name=self.user_domain_name,
                                username=self.admin_username,
                                password=self.admin_password,
                                project_domain_name=self.project_domain_name,
                                project_name=self.admin_project,
                                auth_url=self.auth_url)
        self.sess = session.Session(auth=self.auth)
        self.keystone = client.Client(session=self.sess)
Beispiel #22
0
    def new_token_OIDC(self, indata, remote):
        oidc_resource = self.config["authentication"]["oidc_resource"]
        oidc_secret = self.config["authentication"]["oidc_secret"]
        oidc_callback_url = self.config["authentication"]["oidc_callback_url"]

        oidc_well_known_url = self.config["authentication"][
            "oidc_well_known_url"]
        oidc_well_known_json = json.loads(
            urllib.request.urlopen(oidc_well_known_url).read().decode('utf-8'))
        oidc_token_endpoint = oidc_well_known_json["token_endpoint"]
        code = indata.get("code")

        url = oidc_token_endpoint
        payload = "code=" + code + "&redirect_uri=" + urllib.parse.quote(
            oidc_callback_url) + "&grant_type=authorization_code"

        basic = base64.b64encode(
            (oidc_resource + ':' + oidc_secret).encode()).decode("utf-8")
        headers = {
            'Content-Type': "application/x-www-form-urlencoded",
            'cache-control': "no-cache",
            'Authorization': "Basic " + basic
        }
        login_response = requests.request("POST",
                                          url,
                                          data=payload,
                                          headers=headers)
        access_token = login_response.json()['access_token']

        now = time()
        user_content = None

        user = self.decode_token_OIDC(access_token)

        if user:
            user_rows = self.db.get_list("users", {"username": user})
            user_content = None
            if user_rows:
                user_content = user_rows[0]
                if access_token == None:
                    user_content = None
            if not user_content:
                raise AuthException("Invalid username/password",
                                    http_code=HTTPStatus.UNAUTHORIZED)
        else:
            raise AuthException(
                "Provide credentials: username/password or Authorization Bearer token",
                http_code=HTTPStatus.UNAUTHORIZED)

        token_id = str(access_token)
        if indata.get("project_id"):
            project_id = indata.get("project_id")
            if project_id not in user_content["projects"]:
                raise AuthException(
                    "project {} not allowed for this user".format(project_id),
                    http_code=HTTPStatus.UNAUTHORIZED)
        else:

            project_id = user_content["projects"][0]
        if project_id == "admin":
            session_admin = True
        else:
            project = self.db.get_one("projects", {"_id": project_id})
            session_admin = project.get("admin", False)
        new_session = {
            "issued_at": now,
            "expires": now + 3600,
            "_id": token_id,
            "id": token_id,
            "project_id": project_id,
            "username": user_content["username"],
            "remote_port": remote.port,
            "admin": session_admin
        }
        if remote.name:
            new_session["remote_host"] = remote.name
        elif remote.ip:
            new_session["remote_host"] = remote.ip

        self.tokens_cache[token_id] = new_session
        return deepcopy(new_session)
Beispiel #23
0
 def stop(self):
     try:
         if self.db:
             self.db.db_disconnect()
     except DbException as e:
         raise AuthException(str(e), http_code=e.http_code)