allow extra domain input for token authentication 90/8590/3
authortierno <alfonso.tiernosepulveda@telefonica.com>
Thu, 13 Feb 2020 16:30:14 +0000 (16:30 +0000)
committertierno <alfonso.tiernosepulveda@telefonica.com>
Fri, 14 Feb 2020 13:54:34 +0000 (13:54 +0000)
Change-Id: I546fa3ead2df453d8cf620cd4a6b828a1667c614
Signed-off-by: tierno <alfonso.tiernosepulveda@telefonica.com>
Dockerfile.local
osm_nbi/auth.py
osm_nbi/authconn.py
osm_nbi/authconn_internal.py
osm_nbi/authconn_keystone.py
osm_nbi/nbi.cfg
osm_nbi/validation.py

index a6658a8..7c91bc6 100644 (file)
@@ -84,6 +84,8 @@ ENV OSMNBI_AUTHENTICATION_BACKEND               internal
 #ENV OSMNBI_AUTHENTICATION_BACKEND               keystone
 #ENV OSMNBI_AUTHENTICATION_AUTH_URL              keystone
 #ENV OSMNBI_AUTHENTICATION_AUTH_PORT             5000
 #ENV OSMNBI_AUTHENTICATION_BACKEND               keystone
 #ENV OSMNBI_AUTHENTICATION_AUTH_URL              keystone
 #ENV OSMNBI_AUTHENTICATION_AUTH_PORT             5000
+
+# DOMAIN_NAME can be a single value or a comma separated list of values
 #ENV OSMNBI_AUTHENTICATION_USER_DOMAIN_NAME      default
 #ENV OSMNBI_AUTHENTICATION_PROJECT_DOMAIN_NAME   default
 #ENV OSMNBI_AUTHENTICATION_SERVICE_USERNAME      nbi
 #ENV OSMNBI_AUTHENTICATION_USER_DOMAIN_NAME      default
 #ENV OSMNBI_AUTHENTICATION_PROJECT_DOMAIN_NAME   default
 #ENV OSMNBI_AUTHENTICATION_SERVICE_USERNAME      nbi
index 1b8fa2b..f756d9d 100644 (file)
@@ -404,10 +404,8 @@ class Authenticator:
 
     def new_token(self, token_info, indata, remote):
         new_token_info = self.backend.authenticate(
 
     def new_token(self, token_info, indata, remote):
         new_token_info = self.backend.authenticate(
-            user=indata.get("username"),
-            password=indata.get("password"),
+            credentials=indata,
             token_info=token_info,
             token_info=token_info,
-            project=indata.get("project_id")
         )
 
         new_token_info["remote_port"] = remote.port
         )
 
         new_token_info["remote_port"] = remote.port
index 2ebbb47..c43c339 100644 (file)
@@ -121,12 +121,14 @@ class Authconn:
         """
         self.config = config
 
         """
         self.config = config
 
-    def authenticate(self, user, password, project=None, token_info=None):
+    def authenticate(self, credentials, token_info=None):
         """
         Authenticate a user using username/password or token_info, plus project
         """
         Authenticate a user using username/password or token_info, plus project
-        :param user: user: name, id or None
-        :param password: password or None
-        :param project: name, id, or None. If None first found project will be used to get an scope token
+        :param credentials: dictionary that contains:
+            username: name, id or None
+            password: password or None
+            project_id: name, id, or None. If None first found project will be used to get an scope token
+            other items are allowed for specific auth backends
         :param token_info: previous token_info to obtain authorization
         :return: the scoped token info or raises an exception. The token is a dictionary with:
             _id:  token string id,
         :param token_info: previous token_info to obtain authorization
         :return: the scoped token info or raises an exception. The token is a dictionary with:
             _id:  token string id,
index 672892d..1977aca 100644 (file)
@@ -118,15 +118,16 @@ class AuthconnInternal(Authconn):
                 self.logger.exception(exmsg)
                 raise AuthException(exmsg, http_code=HTTPStatus.UNAUTHORIZED)
 
                 self.logger.exception(exmsg)
                 raise AuthException(exmsg, http_code=HTTPStatus.UNAUTHORIZED)
 
-    def authenticate(self, user, password, project=None, token_info=None):
+    def authenticate(self, credentials, token_info=None):
         """
         Authenticate a user using username/password or previous token_info plus project; its creates a new token
 
         """
         Authenticate a user using username/password or previous token_info plus project; its creates a new token
 
-        :param user: user: name, id or None
-        :param password: password or None
-        :param project: name, id, or None. If None first found project will be used to get an scope token
+        :param credentials: dictionary that contains:
+            username: name, id or None
+            password: password or None
+            project_id: name, id, or None. If None first found project will be used to get an scope token
+            other items are allowed and ignored
         :param token_info: previous token_info to obtain authorization
         :param token_info: previous token_info to obtain authorization
-        :param remote: remote host information
         :return: the scoped token info or raises an exception. The token is a dictionary with:
             _id:  token string id,
             username: username,
         :return: the scoped token info or raises an exception. The token is a dictionary with:
             _id:  token string id,
             username: username,
@@ -137,6 +138,9 @@ class AuthconnInternal(Authconn):
 
         now = time()
         user_content = None
 
         now = time()
         user_content = None
+        user = credentials.get("username")
+        password = credentials.get("password")
+        project = credentials.get("project_id")
 
         # Try using username/password
         if user:
 
         # Try using username/password
         if user:
index 685773b..748b2e7 100644 (file)
@@ -51,11 +51,18 @@ class AuthconnKeystone(Authconn):
         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.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.user_domain_name_list = config.get("user_domain_name", "default")
+        self.user_domain_name_list = self.user_domain_name_list.split(",")
         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.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")
+        self.project_domain_name_list = config.get("project_domain_name", "default")
+        self.project_domain_name_list = self.project_domain_name_list.split(",")
+        if len(self.user_domain_name_list) != len(self.project_domain_name_list):
+            raise ValueError("Invalid configuration parameter fo authenticate. 'project_domain_name' and "
+                             "'user_domain_name' must be a comma-separated list with the same size. Revise "
+                             "configuration or/and 'OSMNBI_AUTHENTICATION_PROJECT_DOMAIN_NAME', "
+                             "'OSMNBI_AUTHENTICATION_USER_DOMAIN_NAME'  Variables")
 
         # Waiting for Keystone to be up
         available = None
 
         # Waiting for Keystone to be up
         available = None
@@ -70,21 +77,25 @@ class AuthconnKeystone(Authconn):
                 if counter == 0:
                     raise AuthException("Keystone not available after 300s timeout")
 
                 if counter == 0:
                     raise AuthException("Keystone not available after 300s timeout")
 
-        self.auth = v3.Password(user_domain_name=self.user_domain_name,
+        self.auth = v3.Password(user_domain_name=self.user_domain_name_list[0],
                                 username=self.admin_username,
                                 password=self.admin_password,
                                 username=self.admin_username,
                                 password=self.admin_password,
-                                project_domain_name=self.project_domain_name,
+                                project_domain_name=self.project_domain_name_list[0],
                                 project_name=self.admin_project,
                                 auth_url=self.auth_url)
         self.sess = session.Session(auth=self.auth)
         self.keystone = client.Client(session=self.sess)
 
                                 project_name=self.admin_project,
                                 auth_url=self.auth_url)
         self.sess = session.Session(auth=self.auth)
         self.keystone = client.Client(session=self.sess)
 
-    def authenticate(self, user, password, project=None, token_info=None):
+    def authenticate(self, credentials, token_info=None):
         """
         Authenticate a user using username/password or token_info, plus project
         """
         Authenticate a user using username/password or token_info, plus project
-        :param user: user: name, id or None
-        :param password: password or None
-        :param project: name, id, or None. If None first found project will be used to get an scope token
+        :param credentials: dictionary that contains:
+            username: name, id or None
+            password: password or None
+            project_id: name, id, or None. If None first found project will be used to get an scope token
+            project_domain_name: (Optional) To use a concrete domain for the project
+            user_domain_name: (Optional) To use a concrete domain for the project
+            other items are allowed and ignored
         :param token_info: previous token_info to obtain authorization
         :return: the scoped token info or raises an exception. The token is a dictionary with:
             _id:  token string id,
         :param token_info: previous token_info to obtain authorization
         :return: the scoped token info or raises an exception. The token is a dictionary with:
             _id:  token string id,
@@ -94,69 +105,83 @@ class AuthconnKeystone(Authconn):
             expires: epoch time when it expires,
 
         """
             expires: epoch time when it expires,
 
         """
-        try:
-            username = None
-            user_id = None
-            project_id = None
-            project_name = None
-
-            if user:
-                if is_valid_uuid(user):
-                    user_id = user
+        username = None
+        user_id = None
+        project_id = None
+        project_name = None
+        if credentials.get("project_domain_name"):
+            project_domain_name_list = (credentials["project_domain_name"], )
+        else:
+            project_domain_name_list = self.project_domain_name_list
+        if credentials.get("user_domain_name"):
+            user_domain_name_list = (credentials["user_domain_name"], )
+        else:
+            user_domain_name_list = self.user_domain_name_list
+
+        for index, project_domain_name in enumerate(project_domain_name_list):
+            user_domain_name = user_domain_name_list[index]
+            try:
+                if credentials.get("username"):
+                    if is_valid_uuid(credentials["username"]):
+                        user_id = credentials["username"]
+                    else:
+                        username = credentials["username"]
+
+                    # get an unscoped token firstly
+                    unscoped_token = self.keystone.get_raw_token_from_identity_service(
+                        auth_url=self.auth_url,
+                        user_id=user_id,
+                        username=username,
+                        password=credentials.get("password"),
+                        user_domain_name=user_domain_name,
+                        project_domain_name=project_domain_name)
+                elif token_info:
+                    unscoped_token = self.keystone.tokens.validate(token=token_info.get("_id"))
                 else:
                 else:
-                    username = user
-
-                # get an unscoped token firstly
-                unscoped_token = self.keystone.get_raw_token_from_identity_service(
-                    auth_url=self.auth_url,
-                    user_id=user_id,
-                    username=username,
-                    password=password,
-                    user_domain_name=self.user_domain_name,
-                    project_domain_name=self.project_domain_name)
-            elif token_info:
-                unscoped_token = self.keystone.tokens.validate(token=token_info.get("_id"))
-            else:
-                raise AuthException("Provide credentials: username/password or Authorization Bearer token",
-                                    http_code=HTTPStatus.UNAUTHORIZED)
-
-            if not project:
-                # get first project for the user
-                project_list = self.keystone.projects.list(user=unscoped_token["user"]["id"])
-                if not project_list:
-                    raise AuthException("The user {} has not any project and cannot be used for authentication".
-                                        format(user), http_code=HTTPStatus.UNAUTHORIZED)
-                project_id = project_list[0].id
-            else:
-                if is_valid_uuid(project):
-                    project_id = project
+                    raise AuthException("Provide credentials: username/password or Authorization Bearer token",
+                                        http_code=HTTPStatus.UNAUTHORIZED)
+
+                if not credentials.get("project_id"):
+                    # get first project for the user
+                    project_list = self.keystone.projects.list(user=unscoped_token["user"]["id"])
+                    if not project_list:
+                        raise AuthException("The user {} has not any project and cannot be used for authentication".
+                                            format(credentials.get("username")), http_code=HTTPStatus.UNAUTHORIZED)
+                    project_id = project_list[0].id
                 else:
                 else:
-                    project_name = project
-
-            scoped_token = self.keystone.get_raw_token_from_identity_service(
-                auth_url=self.auth_url,
-                project_name=project_name,
-                project_id=project_id,
-                user_domain_name=self.user_domain_name,
-                project_domain_name=self.project_domain_name,
-                token=unscoped_token["auth_token"])
-
-            auth_token = {
-                "_id": scoped_token.auth_token,
-                "id": scoped_token.auth_token,
-                "user_id": scoped_token.user_id,
-                "username": scoped_token.username,
-                "project_id": scoped_token.project_id,
-                "project_name": scoped_token.project_name,
-                "expires": scoped_token.expires.timestamp(),
-                "issued_at": scoped_token.issued.timestamp()
-            }
+                    if is_valid_uuid(credentials["project_id"]):
+                        project_id = credentials["project_id"]
+                    else:
+                        project_name = credentials["project_id"]
 
 
-            return auth_token
-        except ClientException as e:
-            # self.logger.exception("Error during user authentication using keystone. Method: basic: {}".format(e))
-            raise AuthException("Error during user authentication using Keystone: {}".format(e),
-                                http_code=HTTPStatus.UNAUTHORIZED)
+                scoped_token = self.keystone.get_raw_token_from_identity_service(
+                    auth_url=self.auth_url,
+                    project_name=project_name,
+                    project_id=project_id,
+                    user_domain_name=user_domain_name,
+                    project_domain_name=project_domain_name,
+                    token=unscoped_token["auth_token"])
+
+                auth_token = {
+                    "_id": scoped_token.auth_token,
+                    "id": scoped_token.auth_token,
+                    "user_id": scoped_token.user_id,
+                    "username": scoped_token.username,
+                    "project_id": scoped_token.project_id,
+                    "project_name": scoped_token.project_name,
+                    "project_domain_name": scoped_token.project_domain_name,
+                    "user_domain_name": scoped_token.user_domain_name,
+                    "expires": scoped_token.expires.timestamp(),
+                    "issued_at": scoped_token.issued.timestamp()
+                }
+
+                return auth_token
+            except ClientException as e:
+                if index >= len(user_domain_name_list)-1 or index >= len(project_domain_name_list)-1:
+                    # if last try, launch exception
+                    # self.logger.exception("Error during user authentication using keystone: {}".format(e))
+                    raise AuthException("Error during user authentication using Keystone: {}".format(e),
+                                        http_code=HTTPStatus.UNAUTHORIZED)
 
     def validate_token(self, token):
         """
 
     def validate_token(self, token):
         """
@@ -219,8 +244,10 @@ class AuthconnKeystone(Authconn):
         :return: returns the id of the user in keystone.
         """
         try:
         :return: returns the id of the user in keystone.
         """
         try:
-            new_user = self.keystone.users.create(user_info["username"], password=user_info["password"],
-                                                  domain=self.user_domain_name, _admin=user_info["_admin"])
+            new_user = self.keystone.users.create(
+                user_info["username"], password=user_info["password"],
+                domain=user_info.get("user_domain_name", self.user_domain_name_list[0]),
+                _admin=user_info["_admin"])
             if "project_role_mappings" in user_info.keys():
                 for mapping in user_info["project_role_mappings"]:
                     self.assign_role_to_user(new_user.id, mapping["project"], mapping["role"])
             if "project_role_mappings" in user_info.keys():
                 for mapping in user_info["project_role_mappings"]:
                     self.assign_role_to_user(new_user.id, mapping["project"], mapping["role"])
@@ -444,10 +471,12 @@ class AuthconnKeystone(Authconn):
         :raises AuthconnOperationException: if project creation failed.
         """
         try:
         :raises AuthconnOperationException: if project creation failed.
         """
         try:
-            result = self.keystone.projects.create(project_info["name"], self.project_domain_name,
-                                                   _admin=project_info["_admin"],
-                                                   quotas=project_info.get("quotas", {})
-                                                   )
+            result = self.keystone.projects.create(
+                project_info["name"],
+                project_info.get("project_domain_name", self.project_domain_name_list[0]),
+                _admin=project_info["_admin"],
+                quotas=project_info.get("quotas", {})
+            )
             return result.id
         except ClientException as e:
             # self.logger.exception("Error during project creation using keystone: {}".format(e))
             return result.id
         except ClientException as e:
             # self.logger.exception("Error during project creation using keystone: {}".format(e))
index 94d3b26..9255a8c 100644 (file)
@@ -71,7 +71,6 @@ port: 9090
 loglevel:  "DEBUG"
 #logfile: /var/log/osm/nbi-database.log
 
 loglevel:  "DEBUG"
 #logfile: /var/log/osm/nbi-database.log
 
-
 [storage]
 driver: "local"            # local filesystem
 # for local provide file path
 [storage]
 driver: "local"            # local filesystem
 # for local provide file path
@@ -93,6 +92,11 @@ group_id: "nbi-server"
 
 [authentication]
 backend: "internal"         # internal or keystone
 
 [authentication]
 backend: "internal"         # internal or keystone
+# for keystone backend a comma separated list of user adn project _domain_name list can ba provided.
+# NBI will try authenticate with all of then if domain is not provided in the content of a POST token
+# user_domain_name:         "default,ldap"
+# project_domain_name:      "default,ldap"
+
 # Only for test. It works without authorization using the provided user and project:
 # user_not_authorized: "admin"
 # project_not_authorized: "admin"
 # Only for test. It works without authorization using the provided user and project:
 # user_not_authorized: "admin"
 # project_not_authorized: "admin"
index ac0dcd9..e40d0ec 100644 (file)
@@ -719,6 +719,7 @@ user_new_schema = {
     "type": "object",
     "properties": {
         "username": shortname_schema,
     "type": "object",
     "properties": {
         "username": shortname_schema,
+        "user_domain_name": shortname_schema,
         "password": passwd_schema,
         "projects": nameshort_list_schema,
         "project_role_mappings": project_role_mappings,
         "password": passwd_schema,
         "projects": nameshort_list_schema,
         "project_role_mappings": project_role_mappings,
@@ -757,6 +758,7 @@ project_new_schema = {
     "properties": {
         "name": shortname_schema,
         "admin": bool_schema,
     "properties": {
         "name": shortname_schema,
         "admin": bool_schema,
+        "project_domain_name": shortname_schema,
         "quotas": {
             "type": "object",
             "properties": {topic: integer0_schema for topic in topics_with_quota},
         "quotas": {
             "type": "object",
             "properties": {topic: integer0_schema for topic in topics_with_quota},