artifacts: expose a special auth-token based artifacts download urls.
This will allow sharing download to external locations used new generated artifact download tokens. This feature allows also serving downloads using secret urls with all the fancy logic of our auth tokens.
This commit is contained in:
parent
a73fb57d29
commit
c6f649e139
5 changed files with 78 additions and 18 deletions
|
|
@ -44,6 +44,9 @@ def includeme(config):
|
|||
config.add_route(
|
||||
name='download_file',
|
||||
pattern='/_file_store/download/{fid}')
|
||||
config.add_route(
|
||||
name='download_file_by_token',
|
||||
pattern='/_file_store/token-download/{_auth_token}/{fid}')
|
||||
|
||||
# Scan module for configuration decorators.
|
||||
config.scan('.views', ignore='.tests')
|
||||
|
|
|
|||
|
|
@ -30,8 +30,10 @@ from rhodecode.apps.file_store.exceptions import (
|
|||
|
||||
from rhodecode.lib import helpers as h
|
||||
from rhodecode.lib import audit_logger
|
||||
from rhodecode.lib.auth import (CSRFRequired, NotAnonymous, HasRepoPermissionAny, HasRepoGroupPermissionAny)
|
||||
from rhodecode.model.db import Session, FileStore
|
||||
from rhodecode.lib.auth import (
|
||||
CSRFRequired, NotAnonymous, HasRepoPermissionAny, HasRepoGroupPermissionAny,
|
||||
LoginRequired)
|
||||
from rhodecode.model.db import Session, FileStore, UserApiKeys
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -44,6 +46,7 @@ class FileStoreView(BaseAppView):
|
|||
self.storage = utils.get_file_storage(self.request.registry.settings)
|
||||
return c
|
||||
|
||||
@LoginRequired()
|
||||
@NotAnonymous()
|
||||
@CSRFRequired()
|
||||
@view_config(route_name='upload_file', request_method='POST', renderer='json_ext')
|
||||
|
|
@ -99,11 +102,7 @@ class FileStoreView(BaseAppView):
|
|||
return {'store_fid': store_uid,
|
||||
'access_path': h.route_path('download_file', fid=store_uid)}
|
||||
|
||||
@view_config(route_name='download_file')
|
||||
def download_file(self):
|
||||
self.load_default_context()
|
||||
file_uid = self.request.matchdict['fid']
|
||||
log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
|
||||
def _serve_file(self, file_uid):
|
||||
|
||||
if not self.storage.exists(file_uid):
|
||||
store_path = self.storage.store_path(file_uid)
|
||||
|
|
@ -128,7 +127,7 @@ class FileStoreView(BaseAppView):
|
|||
perm_set = ['repository.read', 'repository.write', 'repository.admin']
|
||||
has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'FileStore check')
|
||||
if not has_perm:
|
||||
log.warning('Access to file store object forbidden')
|
||||
log.warning('Access to file store object `%s` forbidden', file_uid)
|
||||
raise HTTPNotFound()
|
||||
|
||||
# scoped to repository group permissions
|
||||
|
|
@ -137,10 +136,31 @@ class FileStoreView(BaseAppView):
|
|||
perm_set = ['group.read', 'group.write', 'group.admin']
|
||||
has_perm = HasRepoGroupPermissionAny(*perm_set)(repo_group.group_name, 'FileStore check')
|
||||
if not has_perm:
|
||||
log.warning('Access to file store object forbidden')
|
||||
log.warning('Access to file store object `%s` forbidden', file_uid)
|
||||
raise HTTPNotFound()
|
||||
|
||||
FileStore.bump_access_counter(file_uid)
|
||||
|
||||
file_path = self.storage.store_path(file_uid)
|
||||
return FileResponse(file_path)
|
||||
|
||||
# ACL is checked by scopes, if no scope the file is accessible to all
|
||||
@view_config(route_name='download_file')
|
||||
def download_file(self):
|
||||
self.load_default_context()
|
||||
file_uid = self.request.matchdict['fid']
|
||||
log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
|
||||
return self._serve_file(file_uid)
|
||||
|
||||
@LoginRequired(auth_token_access=[UserApiKeys.ROLE_ARTIFACT_DOWNLOAD])
|
||||
@view_config(route_name='download_file_by_token')
|
||||
def download_file_by_token(self):
|
||||
"""
|
||||
Special view that allows to access the download file by special URL that
|
||||
is stored inside the URL.
|
||||
|
||||
http://example.com/_file_store/token-download/TOKEN/FILE_UID
|
||||
"""
|
||||
self.load_default_context()
|
||||
file_uid = self.request.matchdict['fid']
|
||||
return self._serve_file(file_uid)
|
||||
|
|
|
|||
|
|
@ -1140,7 +1140,7 @@ class AuthUser(object):
|
|||
|
||||
# try go get user by api key
|
||||
elif self._api_key and self._api_key != anon_user.api_key:
|
||||
log.debug('Trying Auth User lookup by API KEY: `%s`', self._api_key)
|
||||
log.debug('Trying Auth User lookup by API KEY: `...%s`', self._api_key[-4:])
|
||||
is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
|
||||
|
||||
# lookup by username
|
||||
|
|
@ -1366,6 +1366,10 @@ class AuthUser(object):
|
|||
def feed_token(self):
|
||||
return self.get_instance().feed_token
|
||||
|
||||
@LazyProperty
|
||||
def artifact_token(self):
|
||||
return self.get_instance().artifact_token
|
||||
|
||||
@classmethod
|
||||
def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
|
||||
allowed_ips = AuthUser.get_allowed_ips(
|
||||
|
|
@ -1597,6 +1601,11 @@ class LoginRequired(object):
|
|||
"""
|
||||
def __init__(self, auth_token_access=None):
|
||||
self.auth_token_access = auth_token_access
|
||||
if self.auth_token_access:
|
||||
valid_type = set(auth_token_access).intersection(set(UserApiKeys.ROLES))
|
||||
if not valid_type:
|
||||
raise ValueError('auth_token_access must be on of {}, got {}'.format(
|
||||
UserApiKeys.ROLES, auth_token_access))
|
||||
|
||||
def __call__(self, func):
|
||||
return get_cython_compat_decorator(self.__wrapper, func)
|
||||
|
|
@ -1616,19 +1625,25 @@ class LoginRequired(object):
|
|||
# check if our IP is allowed
|
||||
ip_access_valid = True
|
||||
if not user.ip_allowed:
|
||||
h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))),
|
||||
h.flash(h.literal(_('IP {} not allowed'.format(user.ip_addr))),
|
||||
category='warning')
|
||||
ip_access_valid = False
|
||||
|
||||
# check if we used an APIKEY and it's a valid one
|
||||
# we used stored token that is extract from GET or URL param (if any)
|
||||
_auth_token = request.user_auth_token
|
||||
|
||||
# check if we used an AUTH_TOKEN and it's a valid one
|
||||
# defined white-list of controllers which API access will be enabled
|
||||
_auth_token = request.GET.get(
|
||||
'auth_token', '') or request.GET.get('api_key', '')
|
||||
whitelist = None
|
||||
if self.auth_token_access:
|
||||
# since this location is allowed by @LoginRequired decorator it's our
|
||||
# only whitelist
|
||||
whitelist = [loc]
|
||||
auth_token_access_valid = allowed_auth_token_access(
|
||||
loc, auth_token=_auth_token)
|
||||
loc, whitelist=whitelist, auth_token=_auth_token)
|
||||
|
||||
# explicit controller is enabled or API is in our whitelist
|
||||
if self.auth_token_access or auth_token_access_valid:
|
||||
if auth_token_access_valid:
|
||||
log.debug('Checking AUTH TOKEN access for %s', cls)
|
||||
db_user = user.get_instance()
|
||||
|
||||
|
|
@ -1637,6 +1652,8 @@ class LoginRequired(object):
|
|||
roles = self.auth_token_access
|
||||
else:
|
||||
roles = [UserApiKeys.ROLE_HTTP]
|
||||
log.debug('AUTH TOKEN: checking auth for user %s and roles %s',
|
||||
db_user, roles)
|
||||
token_match = db_user.authenticate_by_token(
|
||||
_auth_token, roles=roles)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -718,6 +718,23 @@ class User(Base, BaseModel):
|
|||
return feed_tokens[0].api_key
|
||||
return 'NO_FEED_TOKEN_AVAILABLE'
|
||||
|
||||
@LazyProperty
|
||||
def artifact_token(self):
|
||||
return self.get_artifact_token()
|
||||
|
||||
def get_artifact_token(self, cache=True):
|
||||
artifacts_tokens = UserApiKeys.query()\
|
||||
.filter(UserApiKeys.user == self)\
|
||||
.filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
|
||||
if cache:
|
||||
artifacts_tokens = artifacts_tokens.options(
|
||||
FromCache("sql_cache_short", "get_user_artifact_token_%s" % self.user_id))
|
||||
|
||||
artifacts_tokens = artifacts_tokens.all()
|
||||
if artifacts_tokens:
|
||||
return artifacts_tokens[0].api_key
|
||||
return 'NO_ARTIFACT_TOKEN_AVAILABLE'
|
||||
|
||||
@classmethod
|
||||
def get(cls, user_id, cache=False):
|
||||
if not user_id:
|
||||
|
|
@ -765,7 +782,7 @@ class User(Base, BaseModel):
|
|||
else:
|
||||
plain_token_map[token.api_key] = token
|
||||
log.debug(
|
||||
'Found %s plain and %s encrypted user tokens to check for authentication',
|
||||
'Found %s plain and %s encrypted tokens to check for authentication for this user',
|
||||
len(plain_token_map), len(enc_token_map))
|
||||
|
||||
# plain token match comes first
|
||||
|
|
@ -1075,9 +1092,10 @@ class UserApiKeys(Base, BaseModel):
|
|||
ROLE_VCS = 'token_role_vcs'
|
||||
ROLE_API = 'token_role_api'
|
||||
ROLE_FEED = 'token_role_feed'
|
||||
ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
|
||||
ROLE_PASSWORD_RESET = 'token_password_reset'
|
||||
|
||||
ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
|
||||
ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
|
||||
|
||||
user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
|
||||
user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
|
||||
|
|
@ -1139,6 +1157,7 @@ class UserApiKeys(Base, BaseModel):
|
|||
cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
|
||||
cls.ROLE_API: _('api calls'),
|
||||
cls.ROLE_FEED: _('feed access'),
|
||||
cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
|
||||
}.get(role, role)
|
||||
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ function registerRCRoutes() {
|
|||
pyroutes.register('channelstream_proxy', '/_channelstream', []);
|
||||
pyroutes.register('upload_file', '/_file_store/upload', []);
|
||||
pyroutes.register('download_file', '/_file_store/download/%(fid)s', ['fid']);
|
||||
pyroutes.register('download_file_by_token', '/_file_store/token-download/%(_auth_token)s/%(fid)s', ['_auth_token', 'fid']);
|
||||
pyroutes.register('logout', '/_admin/logout', []);
|
||||
pyroutes.register('reset_password', '/_admin/password_reset', []);
|
||||
pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue