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