[#8450] make /rest/auth/tools endpoint, refactor repo fields, remove old/confusing tool fields
diff --git a/Allura/allura/app.py b/Allura/allura/app.py
index e2e34db..d40668e 100644
--- a/Allura/allura/app.py
+++ b/Allura/allura/app.py
@@ -786,15 +786,15 @@
 
         Returns dict that will be included in project's API under tools key.
         """
-        return {
+        json = {
             'name': self.config.tool_name,
             'mount_point': self.config.options.mount_point,
-            'url': self.config.url(),
-            'icons': self.icons,
-            'installable': self.installable,
-            'tool_label': self.tool_label,
+            'url': h.absurl(self.config.url()),
             'mount_label': self.config.options.mount_label
         }
+        if self.api_root:
+            json['api_url'] = h.absurl('/rest' + self.config.url())
+        return json
 
     def get_attachment_export_path(self, path='', *args):
         return os.path.join(path, self.config.options.mount_point, *args)
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index ac0c7b4..e5154f0 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -554,6 +554,21 @@
             redirect('/')
 
 
+class AuthRestController:
+
+    @expose('json:')
+    def tools(self, tool_type: str):
+        apps = []
+        for p in c.user.my_projects():
+            for ac in p.app_configs:
+                if ac.tool_name == tool_type and h.has_access(ac, 'read'):
+                    apps.append(p.app_instance(ac))
+
+        return {
+            'tools': apps,  # TG automatically runs their __json__ methods
+        }
+
+
 def select_new_primary_addr(user, ignore_emails=[]):
     for obj_e in user.email_addresses:
         obj = user.address_object(obj_e)
diff --git a/Allura/allura/controllers/repository.py b/Allura/allura/controllers/repository.py
index 1f76940..ab98ec1 100644
--- a/Allura/allura/controllers/repository.py
+++ b/Allura/allura/controllers/repository.py
@@ -313,6 +313,11 @@
     def index(self, **kw):
         app: Application = c.app
         repo: M.Repository = app.repo
+
+        # core fields, shared in other API endpoints
+        resp = app.__json__()
+
+        # more expensive fields that we only show in this individual API endpoint
         try:
             all_commits = repo._impl.new_commits(all_commits=True)
         except Exception:
@@ -320,16 +325,8 @@
             commit_count = None
         else:
             commit_count = len(all_commits)
-        resp = dict(
-            commit_count=commit_count,
-            name=app.config.options.mount_label,
-            type=app.tool_label,
-        )
-        for clone_cat in repo.clone_command_categories(anon=c.user.is_anonymous()):
-            respkey = 'clone_url_' + clone_cat['key']
-            resp[respkey] = repo.clone_url(clone_cat['key'],
-                                           username='' if c.user.is_anonymous() else c.user.username,
-                                           )
+        resp['commit_count'] = commit_count
+
         return resp
 
     @expose('json:')
diff --git a/Allura/allura/controllers/rest.py b/Allura/allura/controllers/rest.py
index c36b454..5bf4c78 100644
--- a/Allura/allura/controllers/rest.py
+++ b/Allura/allura/controllers/rest.py
@@ -31,6 +31,7 @@
 from ming.orm import session
 
 from allura import model as M
+from allura.controllers.auth import AuthRestController
 from allura.lib import helpers as h
 from allura.lib import security
 from allura.lib import plugin
@@ -48,6 +49,7 @@
 
     def __init__(self):
         self.oauth = OAuthNegotiator()
+        self.auth = AuthRestController()
 
     def _check_security(self):
         if not request.path.startswith('/rest/oauth/'):  # everything but OAuthNegotiator
diff --git a/Allura/allura/lib/repository.py b/Allura/allura/lib/repository.py
index 24789f9..8444191 100644
--- a/Allura/allura/lib/repository.py
+++ b/Allura/allura/lib/repository.py
@@ -250,6 +250,16 @@
     def uninstall(self, project):
         allura.tasks.repo_tasks.uninstall.post()
 
+    def __json__(self):
+        data = super().__json__()
+        repo: M.Repository = self.repo
+        for clone_cat in repo.clone_command_categories(anon=c.user.is_anonymous()):
+            respkey = 'clone_url_' + clone_cat['key']
+            data[respkey] = repo.clone_url(clone_cat['key'],
+                                           username='' if c.user.is_anonymous() else c.user.username,
+                                           )
+        return data
+
 
 class RepoAdminController(DefaultAdminController):
 
diff --git a/Allura/allura/model/auth.py b/Allura/allura/model/auth.py
index ae4b0b4..915b887 100644
--- a/Allura/allura/model/auth.py
+++ b/Allura/allura/model/auth.py
@@ -15,6 +15,8 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
+from __future__ import annotations
+
 import logging
 import calendar
 import typing
@@ -56,6 +58,7 @@
 
 if typing.TYPE_CHECKING:
     from ming.odm.mapper import Query
+    from allura.model.project import Project
 
 
 log = logging.getLogger(__name__)
@@ -768,9 +771,9 @@
     def script_name(self):
         return '/u/' + self.username + '/'
 
-    def my_projects(self):
+    def my_projects(self) -> typing.Iterable[Project]:
         if self.is_anonymous():
-            return
+            return []
         roles = g.credentials.user_roles(user_id=self._id)
         # filter out projects to which the user belongs to no named groups (i.e., role['roles'] is empty)
         projects = [r['project_id'] for r in roles if r['roles']]
diff --git a/Allura/allura/model/project.py b/Allura/allura/model/project.py
index 0e35ae3..d384b5c 100644
--- a/Allura/allura/model/project.py
+++ b/Allura/allura/model/project.py
@@ -15,9 +15,12 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
+from __future__ import annotations
+
 import logging
 from calendar import timegm
 from collections import Counter, OrderedDict
+from collections.abc import Iterable
 from hashlib import sha256
 import typing
 from datetime import datetime
@@ -67,6 +70,7 @@
 
 if typing.TYPE_CHECKING:
     from ming.odm.mapper import Query
+    from allura.model import AppConfig
 
 
 log = logging.getLogger(__name__)
@@ -250,7 +254,7 @@
     acl = FieldProperty(ACL(permissions=_perms_init))
     neighborhood_invitations = FieldProperty([S.ObjectId])
     neighborhood = RelationProperty(Neighborhood)
-    app_configs = RelationProperty('AppConfig')
+    app_configs: Iterable[AppConfig] = RelationProperty('AppConfig')
     category_id = FieldProperty(S.ObjectId, if_missing=None)
     deleted = FieldProperty(bool, if_missing=False)
     labels = FieldProperty([str])
@@ -899,7 +903,7 @@
             app.install(self)
         return app
 
-    def uninstall_app(self, mount_point):
+    def uninstall_app(self, mount_point: str):
         app = self.app_instance(mount_point)
         if app is None:
             return
@@ -908,7 +912,7 @@
         with h.push_config(c, project=self, app=app):
             app.uninstall(self)
 
-    def app_instance(self, mount_point_or_config):
+    def app_instance(self, mount_point_or_config: AppConfig | str):
         if isinstance(mount_point_or_config, AppConfig):
             app_config = mount_point_or_config
         else:
@@ -921,12 +925,12 @@
         else:
             return App(self, app_config)
 
-    def app_config(self, mount_point):
+    def app_config(self, mount_point: str):
         return AppConfig.query.find({
             'project_id': self._id,
             'options.mount_point': mount_point}).first()
 
-    def app_config_by_tool_type(self, tool_type):
+    def app_config_by_tool_type(self, tool_type: str):
         for ac in self.app_configs:
             if ac.tool_name == tool_type:
                 return ac
diff --git a/Allura/allura/model/repository.py b/Allura/allura/model/repository.py
index 0c2239c..7ebbffd 100644
--- a/Allura/allura/model/repository.py
+++ b/Allura/allura/model/repository.py
@@ -365,7 +365,6 @@
     fs_path = FieldProperty(str)
     url_path = FieldProperty(str)
     status = FieldProperty(str)
-    email_address = ''
     additional_viewable_extensions = FieldProperty(str)
     heads = FieldProperty(S.Deprecated)
     branches = FieldProperty(S.Deprecated)
diff --git a/Allura/allura/tests/functional/test_auth.py b/Allura/allura/tests/functional/test_auth.py
index e555cf8..7766ece 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -45,7 +45,7 @@
 from allura.tests import TestController
 from allura.tests import decorators as td
 from allura.tests.decorators import audits, out_audits
-from alluratest.controller import setup_trove_categories
+from alluratest.controller import setup_trove_categories, TestRestApiBase
 from allura import model as M
 from allura.lib import plugin
 from allura.lib import helpers as h
@@ -1138,6 +1138,43 @@
         assert_not_equal(r.content_length, 777)
 
 
+class TestAuthRest(TestRestApiBase):
+
+    def test_tools_list_anon(self):
+        resp = self.api_get('/rest/auth/tools/wiki', user='*anonymous')
+        assert_equal(resp.json, {
+            'tools': []
+        })
+
+    def test_tools_list_invalid_tool(self):
+        resp = self.api_get('/rest/auth/tools/af732q9547235')
+        assert_equal(resp.json, {
+            'tools': []
+        })
+
+    @td.with_tool('test', 'Wiki', mount_point='docs', mount_label='Documentation')
+    def test_tools_list_wiki(self):
+        resp = self.api_get('/rest/auth/tools/wiki')
+        assert_equal(resp.json, {
+            'tools': [
+                {
+                    'mount_label': 'Wiki',
+                    'mount_point': 'wiki',
+                    'name': 'wiki',
+                    'url': 'http://localhost/adobe/wiki/',
+                    'api_url': 'http://localhost/rest/adobe/wiki/',
+                },
+                {
+                    'mount_label': 'Documentation',
+                    'mount_point': 'docs',
+                    'name': 'wiki',
+                    'url': 'http://localhost/p/test/docs/',
+                    'api_url': 'http://localhost/rest/p/test/docs/',
+                },
+            ]
+        })
+
+
 class TestPreferences(TestController):
     @td.with_user_project('test-admin')
     def test_personal_data(self):
diff --git a/Allura/docs/api-rest/api.raml b/Allura/docs/api-rest/api.raml
index 8167cc4..8574205 100755
--- a/Allura/docs/api-rest/api.raml
+++ b/Allura/docs/api-rest/api.raml
@@ -17,7 +17,9 @@
 #       under the License.
 #
 #
-# http://apiworkbench.com/ is a useful tool to edit this file
+# https://github.com/mulesoft/api-designer is a useful tool to edit this file
+# web version http://mulesoft.github.io/api-designer/
+# version that works well with local files: https://github.com/sichvoge/api-designer-fs but you must change git:// to https:// in its package.json before `npm install`
 ---
 title: Apache Allura
 version: 1
@@ -38,6 +40,25 @@
   - title: API Overview
     content: !include docs.md
 
+/auth:
+    description: |
+      Authorization related APIs.  See also OAuth
+
+    /tools/{tool_type}:
+      uriParameters:
+        tool_type:
+          type: string
+          example: git
+      type: {
+        generic: {
+          example: !include examples/auth-tools.json,
+          schema: !include schemas/auth-tools.json
+        }
+      }
+      get:
+        description: |
+          List tools (e.g. "git" repos) that the current user is associated with
+
 /oauth:
     description: |
       See separate docs section for authenticating with the OAuth 1.0 APIs
@@ -263,7 +284,7 @@
                       description: ticket description
                       type: string
                       required: false
-                    ticket_form.assigned_to::
+                    ticket_form.assigned_to:
                       type: string
                       required: false
                       description: username of ticket assignee
@@ -311,7 +332,7 @@
                     description: ticket description
                     type: string
                     required: false
-                  ticket_form.assigned_to::
+                  ticket_form.assigned_to:
                     type: string
                     required: false
                     description: username of ticket assignee
diff --git a/Allura/docs/api-rest/examples/auth-tools.json b/Allura/docs/api-rest/examples/auth-tools.json
new file mode 100755
index 0000000..cdd569f
--- /dev/null
+++ b/Allura/docs/api-rest/examples/auth-tools.json
@@ -0,0 +1,22 @@
+{
+  "tools": [
+    {
+      "url": "https://forge-allura.apache.org/p/test/code/",
+      "api_url": "https://forge-allura.apache.org/rest/p/test/code/",
+      "mount_label": "Code",
+      "mount_point": "code",
+      "name": "git",
+      "clone_url_https_anon": "https://forge-allura.apache.org/git/p/test/code/",
+      "clone_url_ro": "git://forge-allura.apache.org/git/p/test/code"
+    },
+    {
+      "url": "https://forge-allura.apache.org/u/someuser/widgets-fork/",
+      "api_url": "https://forge-allura.apache.org/rest/u/someuser/widgets-fork/",
+      "mount_label": "Widgets - Fork",
+      "mount_point": "widgets-fork",
+      "name": "git",
+      "clone_url_https_anon": "https://forge-allura.apache.org/git/u/someuser/widgets-fork",
+      "clone_url_ro": "git://forge-allura.apache.org/git/u/someuser/widgets-fork"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/Allura/docs/api-rest/examples/scm.json b/Allura/docs/api-rest/examples/scm.json
index a5a2f4b..7718a39 100755
--- a/Allura/docs/api-rest/examples/scm.json
+++ b/Allura/docs/api-rest/examples/scm.json
@@ -1,6 +1,9 @@
 {
-  "name": "Code",
-  "type": "Git",
+  "url": "https://forge-allura.apache.org/p/test/code/",
+  "api_url": "https://forge-allura.apache.org/rest/p/test/code/",
+  "mount_label": "Code",
+  "mount_point": "code",
+  "name": "git",
   "commit_count": 17,
   "clone_url_https_anon": "https://forge-allura.apache.org/git/p/test/code",
   "clone_url_ro": "git://forge-allura.apache.org/git/p/test/code"
diff --git a/Allura/docs/api-rest/schemas/auth-tools.json b/Allura/docs/api-rest/schemas/auth-tools.json
new file mode 100755
index 0000000..5de2ae5
--- /dev/null
+++ b/Allura/docs/api-rest/schemas/auth-tools.json
@@ -0,0 +1,50 @@
+{
+    "$schema": "http://json-schema.org/draft-04/schema",
+    "type": "object",
+    "id": "#",
+    "properties": {
+        "tools": {
+            "items": {
+                "type": "object",
+                "id": "0",
+                "properties": {
+                    "url": {
+                        "type": "string",
+                        "id": "url"
+                    },
+                    "api_url": {
+                        "type": "string",
+                        "id": "api_url"
+                    },
+                    "mount_label": {
+                        "type": "string",
+                        "id": "mount_label"
+                    },
+                    "mount_point": {
+                        "type": "string",
+                        "id": "mount_point"
+                    },
+                    "name": {
+                        "type": "string",
+                        "id": "name"
+                    },
+                    "clone_url_https_anon": {
+                        "type": "string",
+                        "id": "clone_url_https_anon"
+                    },
+                    "clone_url_ro": {
+                        "type": "string",
+                        "id": "clone_url_ro"
+                    },
+                    "clone_url_*": {
+                        "type": "string",
+                        "id": "clone_url_*"
+                    }
+                }
+            },
+            "type": "array",
+            "id": "tools"
+        }
+    }
+}
+	
\ No newline at end of file
diff --git a/Allura/docs/api-rest/schemas/page.json b/Allura/docs/api-rest/schemas/page.json
index d961145..c54240e 100755
--- a/Allura/docs/api-rest/schemas/page.json
+++ b/Allura/docs/api-rest/schemas/page.json
@@ -4,7 +4,7 @@
   "properties": {
     "related_artifacts": {
       "items": {
-        "type": ["null", "string"],
+        "type": ["null", "string"]
       },
       "type": ["null", "array"],
       "id": "related_artifacts"
diff --git a/Allura/docs/api-rest/schemas/scm.json b/Allura/docs/api-rest/schemas/scm.json
index 0a69b65..b686e79 100755
--- a/Allura/docs/api-rest/schemas/scm.json
+++ b/Allura/docs/api-rest/schemas/scm.json
@@ -3,29 +3,41 @@
     "type": "object",
     "id": "#",
     "properties": {
+        "url": {
+            "type": "string",
+            "id": "url"
+        },
+        "api_url": {
+            "type": "string",
+            "id": "api_url"
+        },
+        "mount_label": {
+            "type": "string",
+            "id": "mount_label"
+        },
+        "mount_point": {
+            "type": "string",
+            "id": "mount_point"
+        },
         "name": {
             "type": "string",
             "id": "name"
         },
-        "type": {
-            "type": "string",
-            "id": "type"
-        },
         "commit_count": {
             "type": "integer",
             "id": "commit_count"
         },
         "clone_url_https_anon": {
             "type": "string",
-            "id": "name"
+            "id": "clone_url_https_anon"
         },
         "clone_url_ro": {
             "type": "string",
-            "id": "name"
+            "id": "clone_url_ro"
         },
         "clone_url_*": {
             "type": "string",
-            "id": "name"
+            "id": "clone_url_*"
         }
     }
 }
diff --git a/Allura/docs/api-rest/schemas/tickets.json b/Allura/docs/api-rest/schemas/tickets.json
index 6a68c2c..a30e01c 100755
--- a/Allura/docs/api-rest/schemas/tickets.json
+++ b/Allura/docs/api-rest/schemas/tickets.json
@@ -46,7 +46,7 @@
                 },
                 "default": {
                   "id": "default",
-                  "type": ""
+                  "type": "string"
                 },
                 "description": {
                   "id": "description",
diff --git a/ForgeGit/forgegit/tests/functional/test_controllers.py b/ForgeGit/forgegit/tests/functional/test_controllers.py
index f4854db..2bc49ed 100644
--- a/ForgeGit/forgegit/tests/functional/test_controllers.py
+++ b/ForgeGit/forgegit/tests/functional/test_controllers.py
@@ -556,10 +556,13 @@
     def test_index(self):
         resp = self.app.get('/rest/p/test/src-git/', status=200)
         assert_equal(resp.json, {
+            'api_url': 'http://localhost/rest/p/test/src-git/',
+            'url': 'http://localhost/p/test/src-git/',
+            'mount_label': 'Git',
+            'mount_point': 'src-git',
+            'name': 'git',
+            'clone_url_file': '/srv/git/p/test/testgit',  # should be "src-git" but test data is weird?
             'commit_count': 5,
-            'name': 'Git',
-            'type': 'Git',
-            'clone_url_file': '/srv/git/p/test/testgit',
         })
 
     def test_commits(self):