##// 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 config.add_route(
44 config.add_route(
45 name='download_file',
45 name='download_file',
46 pattern='/_file_store/download/{fid}')
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 # Scan module for configuration decorators.
51 # Scan module for configuration decorators.
49 config.scan('.views', ignore='.tests')
52 config.scan('.views', ignore='.tests')
@@ -30,8 +30,10 b' from rhodecode.apps.file_store.exception'
30
30
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.lib import audit_logger
32 from rhodecode.lib import audit_logger
33 from rhodecode.lib.auth import (CSRFRequired, NotAnonymous, HasRepoPermissionAny, HasRepoGroupPermissionAny)
33 from rhodecode.lib.auth import (
34 from rhodecode.model.db import Session, FileStore
34 CSRFRequired, NotAnonymous, HasRepoPermissionAny, HasRepoGroupPermissionAny,
35 LoginRequired)
36 from rhodecode.model.db import Session, FileStore, UserApiKeys
35
37
36 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
37
39
@@ -44,6 +46,7 b' class FileStoreView(BaseAppView):'
44 self.storage = utils.get_file_storage(self.request.registry.settings)
46 self.storage = utils.get_file_storage(self.request.registry.settings)
45 return c
47 return c
46
48
49 @LoginRequired()
47 @NotAnonymous()
50 @NotAnonymous()
48 @CSRFRequired()
51 @CSRFRequired()
49 @view_config(route_name='upload_file', request_method='POST', renderer='json_ext')
52 @view_config(route_name='upload_file', request_method='POST', renderer='json_ext')
@@ -99,11 +102,7 b' class FileStoreView(BaseAppView):'
99 return {'store_fid': store_uid,
102 return {'store_fid': store_uid,
100 'access_path': h.route_path('download_file', fid=store_uid)}
103 'access_path': h.route_path('download_file', fid=store_uid)}
101
104
102 @view_config(route_name='download_file')
105 def _serve_file(self, file_uid):
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)
107
106
108 if not self.storage.exists(file_uid):
107 if not self.storage.exists(file_uid):
109 store_path = self.storage.store_path(file_uid)
108 store_path = self.storage.store_path(file_uid)
@@ -128,7 +127,7 b' class FileStoreView(BaseAppView):'
128 perm_set = ['repository.read', 'repository.write', 'repository.admin']
127 perm_set = ['repository.read', 'repository.write', 'repository.admin']
129 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'FileStore check')
128 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'FileStore check')
130 if not has_perm:
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 raise HTTPNotFound()
131 raise HTTPNotFound()
133
132
134 # scoped to repository group permissions
133 # scoped to repository group permissions
@@ -137,10 +136,31 b' class FileStoreView(BaseAppView):'
137 perm_set = ['group.read', 'group.write', 'group.admin']
136 perm_set = ['group.read', 'group.write', 'group.admin']
138 has_perm = HasRepoGroupPermissionAny(*perm_set)(repo_group.group_name, 'FileStore check')
137 has_perm = HasRepoGroupPermissionAny(*perm_set)(repo_group.group_name, 'FileStore check')
139 if not has_perm:
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 raise HTTPNotFound()
140 raise HTTPNotFound()
142
141
143 FileStore.bump_access_counter(file_uid)
142 FileStore.bump_access_counter(file_uid)
144
143
145 file_path = self.storage.store_path(file_uid)
144 file_path = self.storage.store_path(file_uid)
146 return FileResponse(file_path)
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 # try go get user by api key
1141 # try go get user by api key
1142 elif self._api_key and self._api_key != anon_user.api_key:
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 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
1144 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
1145
1145
1146 # lookup by username
1146 # lookup by username
@@ -1366,6 +1366,10 b' class AuthUser(object):'
1366 def feed_token(self):
1366 def feed_token(self):
1367 return self.get_instance().feed_token
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 @classmethod
1373 @classmethod
1370 def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
1374 def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
1371 allowed_ips = AuthUser.get_allowed_ips(
1375 allowed_ips = AuthUser.get_allowed_ips(
@@ -1597,6 +1601,11 b' class LoginRequired(object):'
1597 """
1601 """
1598 def __init__(self, auth_token_access=None):
1602 def __init__(self, auth_token_access=None):
1599 self.auth_token_access = auth_token_access
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 def __call__(self, func):
1610 def __call__(self, func):
1602 return get_cython_compat_decorator(self.__wrapper, func)
1611 return get_cython_compat_decorator(self.__wrapper, func)
@@ -1616,19 +1625,25 b' class LoginRequired(object):'
1616 # check if our IP is allowed
1625 # check if our IP is allowed
1617 ip_access_valid = True
1626 ip_access_valid = True
1618 if not user.ip_allowed:
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 category='warning')
1629 category='warning')
1621 ip_access_valid = False
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 # defined white-list of controllers which API access will be enabled
1636 # defined white-list of controllers which API access will be enabled
1625 _auth_token = request.GET.get(
1637 whitelist = None
1626 'auth_token', '') or request.GET.get('api_key', '')
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 auth_token_access_valid = allowed_auth_token_access(
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 # explicit controller is enabled or API is in our whitelist
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 log.debug('Checking AUTH TOKEN access for %s', cls)
1647 log.debug('Checking AUTH TOKEN access for %s', cls)
1633 db_user = user.get_instance()
1648 db_user = user.get_instance()
1634
1649
@@ -1637,6 +1652,8 b' class LoginRequired(object):'
1637 roles = self.auth_token_access
1652 roles = self.auth_token_access
1638 else:
1653 else:
1639 roles = [UserApiKeys.ROLE_HTTP]
1654 roles = [UserApiKeys.ROLE_HTTP]
1655 log.debug('AUTH TOKEN: checking auth for user %s and roles %s',
1656 db_user, roles)
1640 token_match = db_user.authenticate_by_token(
1657 token_match = db_user.authenticate_by_token(
1641 _auth_token, roles=roles)
1658 _auth_token, roles=roles)
1642 else:
1659 else:
@@ -718,6 +718,23 b' class User(Base, BaseModel):'
718 return feed_tokens[0].api_key
718 return feed_tokens[0].api_key
719 return 'NO_FEED_TOKEN_AVAILABLE'
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 @classmethod
738 @classmethod
722 def get(cls, user_id, cache=False):
739 def get(cls, user_id, cache=False):
723 if not user_id:
740 if not user_id:
@@ -765,7 +782,7 b' class User(Base, BaseModel):'
765 else:
782 else:
766 plain_token_map[token.api_key] = token
783 plain_token_map[token.api_key] = token
767 log.debug(
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 len(plain_token_map), len(enc_token_map))
786 len(plain_token_map), len(enc_token_map))
770
787
771 # plain token match comes first
788 # plain token match comes first
@@ -1075,9 +1092,10 b' class UserApiKeys(Base, BaseModel):'
1075 ROLE_VCS = 'token_role_vcs'
1092 ROLE_VCS = 'token_role_vcs'
1076 ROLE_API = 'token_role_api'
1093 ROLE_API = 'token_role_api'
1077 ROLE_FEED = 'token_role_feed'
1094 ROLE_FEED = 'token_role_feed'
1095 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1078 ROLE_PASSWORD_RESET = 'token_password_reset'
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 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1100 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1083 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
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 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1157 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1140 cls.ROLE_API: _('api calls'),
1158 cls.ROLE_API: _('api calls'),
1141 cls.ROLE_FEED: _('feed access'),
1159 cls.ROLE_FEED: _('feed access'),
1160 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1142 }.get(role, role)
1161 }.get(role, role)
1143
1162
1144 @property
1163 @property
@@ -140,6 +140,7 b' function registerRCRoutes() {'
140 pyroutes.register('channelstream_proxy', '/_channelstream', []);
140 pyroutes.register('channelstream_proxy', '/_channelstream', []);
141 pyroutes.register('upload_file', '/_file_store/upload', []);
141 pyroutes.register('upload_file', '/_file_store/upload', []);
142 pyroutes.register('download_file', '/_file_store/download/%(fid)s', ['fid']);
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 pyroutes.register('logout', '/_admin/logout', []);
144 pyroutes.register('logout', '/_admin/logout', []);
144 pyroutes.register('reset_password', '/_admin/password_reset', []);
145 pyroutes.register('reset_password', '/_admin/password_reset', []);
145 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
146 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
General Comments 0
You need to be logged in to leave comments. Login now