##// END OF EJS Templates
artifacts: expose a special auth-token based artifacts download urls....
marcink -
r4003:09f31efc default
parent child Browse files
Show More
@@ -44,6 +44,9 b' def includeme(config):'
44 44 config.add_route(
45 45 name='download_file',
46 46 pattern='/_file_store/download/{fid}')
47 config.add_route(
48 name='download_file_by_token',
49 pattern='/_file_store/token-download/{_auth_token}/{fid}')
47 50
48 51 # Scan module for configuration decorators.
49 52 config.scan('.views', ignore='.tests')
@@ -30,8 +30,10 b' from rhodecode.apps.file_store.exception'
30 30
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib import audit_logger
33 from rhodecode.lib.auth import (CSRFRequired, NotAnonymous, HasRepoPermissionAny, HasRepoGroupPermissionAny)
34 from rhodecode.model.db import Session, FileStore
33 from rhodecode.lib.auth import (
34 CSRFRequired, NotAnonymous, HasRepoPermissionAny, HasRepoGroupPermissionAny,
35 LoginRequired)
36 from rhodecode.model.db import Session, FileStore, UserApiKeys
35 37
36 38 log = logging.getLogger(__name__)
37 39
@@ -44,6 +46,7 b' class FileStoreView(BaseAppView):'
44 46 self.storage = utils.get_file_storage(self.request.registry.settings)
45 47 return c
46 48
49 @LoginRequired()
47 50 @NotAnonymous()
48 51 @CSRFRequired()
49 52 @view_config(route_name='upload_file', request_method='POST', renderer='json_ext')
@@ -99,11 +102,7 b' class FileStoreView(BaseAppView):'
99 102 return {'store_fid': store_uid,
100 103 'access_path': h.route_path('download_file', fid=store_uid)}
101 104
102 @view_config(route_name='download_file')
103 def download_file(self):
104 self.load_default_context()
105 file_uid = self.request.matchdict['fid']
106 log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
105 def _serve_file(self, file_uid):
107 106
108 107 if not self.storage.exists(file_uid):
109 108 store_path = self.storage.store_path(file_uid)
@@ -128,7 +127,7 b' class FileStoreView(BaseAppView):'
128 127 perm_set = ['repository.read', 'repository.write', 'repository.admin']
129 128 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'FileStore check')
130 129 if not has_perm:
131 log.warning('Access to file store object forbidden')
130 log.warning('Access to file store object `%s` forbidden', file_uid)
132 131 raise HTTPNotFound()
133 132
134 133 # scoped to repository group permissions
@@ -137,10 +136,31 b' class FileStoreView(BaseAppView):'
137 136 perm_set = ['group.read', 'group.write', 'group.admin']
138 137 has_perm = HasRepoGroupPermissionAny(*perm_set)(repo_group.group_name, 'FileStore check')
139 138 if not has_perm:
140 log.warning('Access to file store object forbidden')
139 log.warning('Access to file store object `%s` forbidden', file_uid)
141 140 raise HTTPNotFound()
142 141
143 142 FileStore.bump_access_counter(file_uid)
144 143
145 144 file_path = self.storage.store_path(file_uid)
146 145 return FileResponse(file_path)
146
147 # ACL is checked by scopes, if no scope the file is accessible to all
148 @view_config(route_name='download_file')
149 def download_file(self):
150 self.load_default_context()
151 file_uid = self.request.matchdict['fid']
152 log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
153 return self._serve_file(file_uid)
154
155 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_ARTIFACT_DOWNLOAD])
156 @view_config(route_name='download_file_by_token')
157 def download_file_by_token(self):
158 """
159 Special view that allows to access the download file by special URL that
160 is stored inside the URL.
161
162 http://example.com/_file_store/token-download/TOKEN/FILE_UID
163 """
164 self.load_default_context()
165 file_uid = self.request.matchdict['fid']
166 return self._serve_file(file_uid)
@@ -1140,7 +1140,7 b' class AuthUser(object):'
1140 1140
1141 1141 # try go get user by api key
1142 1142 elif self._api_key and self._api_key != anon_user.api_key:
1143 log.debug('Trying Auth User lookup by API KEY: `%s`', self._api_key)
1143 log.debug('Trying Auth User lookup by API KEY: `...%s`', self._api_key[-4:])
1144 1144 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
1145 1145
1146 1146 # lookup by username
@@ -1366,6 +1366,10 b' class AuthUser(object):'
1366 1366 def feed_token(self):
1367 1367 return self.get_instance().feed_token
1368 1368
1369 @LazyProperty
1370 def artifact_token(self):
1371 return self.get_instance().artifact_token
1372
1369 1373 @classmethod
1370 1374 def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
1371 1375 allowed_ips = AuthUser.get_allowed_ips(
@@ -1597,6 +1601,11 b' class LoginRequired(object):'
1597 1601 """
1598 1602 def __init__(self, auth_token_access=None):
1599 1603 self.auth_token_access = auth_token_access
1604 if self.auth_token_access:
1605 valid_type = set(auth_token_access).intersection(set(UserApiKeys.ROLES))
1606 if not valid_type:
1607 raise ValueError('auth_token_access must be on of {}, got {}'.format(
1608 UserApiKeys.ROLES, auth_token_access))
1600 1609
1601 1610 def __call__(self, func):
1602 1611 return get_cython_compat_decorator(self.__wrapper, func)
@@ -1616,19 +1625,25 b' class LoginRequired(object):'
1616 1625 # check if our IP is allowed
1617 1626 ip_access_valid = True
1618 1627 if not user.ip_allowed:
1619 h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))),
1628 h.flash(h.literal(_('IP {} not allowed'.format(user.ip_addr))),
1620 1629 category='warning')
1621 1630 ip_access_valid = False
1622 1631
1623 # check if we used an APIKEY and it's a valid one
1632 # we used stored token that is extract from GET or URL param (if any)
1633 _auth_token = request.user_auth_token
1634
1635 # check if we used an AUTH_TOKEN and it's a valid one
1624 1636 # defined white-list of controllers which API access will be enabled
1625 _auth_token = request.GET.get(
1626 'auth_token', '') or request.GET.get('api_key', '')
1637 whitelist = None
1638 if self.auth_token_access:
1639 # since this location is allowed by @LoginRequired decorator it's our
1640 # only whitelist
1641 whitelist = [loc]
1627 1642 auth_token_access_valid = allowed_auth_token_access(
1628 loc, auth_token=_auth_token)
1643 loc, whitelist=whitelist, auth_token=_auth_token)
1629 1644
1630 1645 # explicit controller is enabled or API is in our whitelist
1631 if self.auth_token_access or auth_token_access_valid:
1646 if auth_token_access_valid:
1632 1647 log.debug('Checking AUTH TOKEN access for %s', cls)
1633 1648 db_user = user.get_instance()
1634 1649
@@ -1637,6 +1652,8 b' class LoginRequired(object):'
1637 1652 roles = self.auth_token_access
1638 1653 else:
1639 1654 roles = [UserApiKeys.ROLE_HTTP]
1655 log.debug('AUTH TOKEN: checking auth for user %s and roles %s',
1656 db_user, roles)
1640 1657 token_match = db_user.authenticate_by_token(
1641 1658 _auth_token, roles=roles)
1642 1659 else:
@@ -718,6 +718,23 b' class User(Base, BaseModel):'
718 718 return feed_tokens[0].api_key
719 719 return 'NO_FEED_TOKEN_AVAILABLE'
720 720
721 @LazyProperty
722 def artifact_token(self):
723 return self.get_artifact_token()
724
725 def get_artifact_token(self, cache=True):
726 artifacts_tokens = UserApiKeys.query()\
727 .filter(UserApiKeys.user == self)\
728 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
729 if cache:
730 artifacts_tokens = artifacts_tokens.options(
731 FromCache("sql_cache_short", "get_user_artifact_token_%s" % self.user_id))
732
733 artifacts_tokens = artifacts_tokens.all()
734 if artifacts_tokens:
735 return artifacts_tokens[0].api_key
736 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
737
721 738 @classmethod
722 739 def get(cls, user_id, cache=False):
723 740 if not user_id:
@@ -765,7 +782,7 b' class User(Base, BaseModel):'
765 782 else:
766 783 plain_token_map[token.api_key] = token
767 784 log.debug(
768 'Found %s plain and %s encrypted user tokens to check for authentication',
785 'Found %s plain and %s encrypted tokens to check for authentication for this user',
769 786 len(plain_token_map), len(enc_token_map))
770 787
771 788 # plain token match comes first
@@ -1075,9 +1092,10 b' class UserApiKeys(Base, BaseModel):'
1075 1092 ROLE_VCS = 'token_role_vcs'
1076 1093 ROLE_API = 'token_role_api'
1077 1094 ROLE_FEED = 'token_role_feed'
1095 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1078 1096 ROLE_PASSWORD_RESET = 'token_password_reset'
1079 1097
1080 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
1098 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1081 1099
1082 1100 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1083 1101 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
@@ -1139,6 +1157,7 b' class UserApiKeys(Base, BaseModel):'
1139 1157 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1140 1158 cls.ROLE_API: _('api calls'),
1141 1159 cls.ROLE_FEED: _('feed access'),
1160 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1142 1161 }.get(role, role)
1143 1162
1144 1163 @property
@@ -140,6 +140,7 b' function registerRCRoutes() {'
140 140 pyroutes.register('channelstream_proxy', '/_channelstream', []);
141 141 pyroutes.register('upload_file', '/_file_store/upload', []);
142 142 pyroutes.register('download_file', '/_file_store/download/%(fid)s', ['fid']);
143 pyroutes.register('download_file_by_token', '/_file_store/token-download/%(_auth_token)s/%(fid)s', ['_auth_token', 'fid']);
143 144 pyroutes.register('logout', '/_admin/logout', []);
144 145 pyroutes.register('reset_password', '/_admin/password_reset', []);
145 146 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
General Comments 0
You need to be logged in to leave comments. Login now