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 |
|
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 |
|
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 |
|
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