mirror of
https://github.com/Joxit/docker-registry-ui.git
synced 2025-04-27 23:50:01 +03:00
feat(token-auth-keycloak): remove custom dialog and use browser basic auth with nginx configuration
This commit is contained in:
parent
58b1486c81
commit
bc5082dcf9
7 changed files with 53 additions and 98 deletions
|
@ -34,6 +34,17 @@ server {
|
||||||
proxy_pass http://keycloak:8080;
|
proxy_pass http://keycloak:8080;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /auth/realms/master/protocol/docker-v2/auth {
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
if ($http_authorization = "") {
|
||||||
|
add_header WWW-Authenticate 'Basic realm="Keycloak login"' always;
|
||||||
|
return 401;
|
||||||
|
}
|
||||||
|
proxy_pass http://keycloak:8080;
|
||||||
|
}
|
||||||
|
|
||||||
location /ui {
|
location /ui {
|
||||||
proxy_pass http://ui/;
|
proxy_pass http://ui/;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,71 +0,0 @@
|
||||||
<!--
|
|
||||||
Copyright (C) 2016-2021 Jones Magloire @Joxit
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
<registry-authentication>
|
|
||||||
<material-popup opened="{ props.opened }" onClick="{ props.onClose }">
|
|
||||||
<div slot="title">Sign In to your registry</div>
|
|
||||||
<div slot="content">
|
|
||||||
<material-input placeholder="Username" id="username"></material-input>
|
|
||||||
<material-input placeholder="Password" id="password" onkeyup="{ onKeyUp }" type="password"></material-input>
|
|
||||||
</div>
|
|
||||||
<div slot="action">
|
|
||||||
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ signIn }">
|
|
||||||
Sign In
|
|
||||||
</material-button>
|
|
||||||
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ props.onClose }">
|
|
||||||
Cancel
|
|
||||||
</material-button>
|
|
||||||
</div>
|
|
||||||
</material-popup>
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
signIn() {
|
|
||||||
const {
|
|
||||||
realm,
|
|
||||||
service,
|
|
||||||
scope,
|
|
||||||
onAuthenticated,
|
|
||||||
onClose
|
|
||||||
} = this.props;
|
|
||||||
const username = this.$('#username input').value;
|
|
||||||
const password = this.$('#password input').value;
|
|
||||||
const req = new XMLHttpRequest()
|
|
||||||
req.addEventListener('loadend', () => {
|
|
||||||
try {
|
|
||||||
const bearer = JSON.parse(req.responseText);
|
|
||||||
onAuthenticated(bearer)
|
|
||||||
localStorage.setItem('registry.token', bearer.token);
|
|
||||||
localStorage.setItem('registry.issued_at', bearer.issued_at);
|
|
||||||
localStorage.setItem('registry.expires_in', bearer.expires_in);
|
|
||||||
onClose();
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
req.open('GET', `${realm}?service=${service}&scope=${scope}`)
|
|
||||||
req.setRequestHeader('Authorization', `Basic ${btoa(`${username}:${password}`)}`)
|
|
||||||
req.send()
|
|
||||||
},
|
|
||||||
onKeyUp(event) {
|
|
||||||
// if keyCode is Enter
|
|
||||||
if (event.keyCode === 13) {
|
|
||||||
this.signIn();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</registry-authentication>
|
|
|
@ -39,7 +39,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
<route path="{baseRoute}taghistory/(.*)">
|
<route path="{baseRoute}taghistory/(.*)">
|
||||||
<tag-history registry-url="{ state.registryUrl }" registry-name="{ state.name }" pull-url="{ state.pullUrl }"
|
<tag-history registry-url="{ state.registryUrl }" registry-name="{ state.name }" pull-url="{ state.pullUrl }"
|
||||||
image="{ router.getTagHistoryImage() }" tag="{ router.getTagHistoryTag() }"
|
image="{ router.getTagHistoryImage() }" tag="{ router.getTagHistoryTag() }"
|
||||||
is-image-remove-activated="{ props.isImageRemoveActivated }" on-notify="{ notifySnackbar }" on-authentication="{ onAuthentication }"></tag-history>
|
is-image-remove-activated="{ props.isImageRemoveActivated }" on-notify="{ notifySnackbar }"
|
||||||
|
on-authentication="{ onAuthentication }"></tag-history>
|
||||||
</route>
|
</route>
|
||||||
</router>
|
</router>
|
||||||
<registry-authentication realm="{ state.realm }" scope="{ state.scope }" service="{ state.service }"
|
<registry-authentication realm="{ state.realm }" scope="{ state.scope }" service="{ state.service }"
|
||||||
|
@ -73,7 +74,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
import TagHistory from './tag-history/tag-history.riot';
|
import TagHistory from './tag-history/tag-history.riot';
|
||||||
import DialogsMenu from './dialogs/dialogs-menu.riot';
|
import DialogsMenu from './dialogs/dialogs-menu.riot';
|
||||||
import SearchBar from './search-bar.riot'
|
import SearchBar from './search-bar.riot'
|
||||||
import RegistryAuthentication from './dialogs/registry-authentication.riot';
|
|
||||||
import {
|
import {
|
||||||
stripHttps,
|
stripHttps,
|
||||||
getRegistryServers
|
getRegistryServers
|
||||||
|
@ -87,7 +87,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
TagHistory,
|
TagHistory,
|
||||||
DialogsMenu,
|
DialogsMenu,
|
||||||
SearchBar,
|
SearchBar,
|
||||||
RegistryAuthentication,
|
|
||||||
Router,
|
Router,
|
||||||
Route
|
Route
|
||||||
},
|
},
|
||||||
|
@ -113,20 +112,23 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onAuthentication(opts, onAuthenticated) {
|
onAuthentication(opts, onAuthenticated) {
|
||||||
const bearer = {
|
if (opts && opts.realm && opts.service && opts.scope) {
|
||||||
token: localStorage.getItem('registry.token'),
|
const {
|
||||||
issued_at: localStorage.getItem('registry.issued_at'),
|
realm,
|
||||||
expires_in: localStorage.getItem('registry.expires_in')
|
service,
|
||||||
}
|
scope,
|
||||||
if (bearer.token && bearer.issued_at && bearer.expires_in &&
|
} = opts;
|
||||||
(new Date().getTime() - new Date(bearer.issued_at).getTime()) < (bearer.expires_in * 1000)) {
|
const req = new XMLHttpRequest()
|
||||||
onAuthenticated(bearer)
|
req.addEventListener('loadend', () => {
|
||||||
} else if (opts) {
|
try {
|
||||||
this.update({
|
const bearer = JSON.parse(req.responseText);
|
||||||
authenticationDialogOpened: true,
|
onAuthenticated(bearer)
|
||||||
onAuthenticated,
|
} catch (e) {
|
||||||
...opts
|
this.notifySnackbar(`Failed to log in: ${e.message}`, true)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
req.open('GET', `${realm}?service=${service}&scope=${scope}`)
|
||||||
|
req.send()
|
||||||
} else {
|
} else {
|
||||||
onAuthenticated()
|
onAuthenticated()
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,21 +43,32 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
state.checked = props.checked;
|
state.checked = props.checked;
|
||||||
},
|
},
|
||||||
deleteImage(ignoreError) {
|
deleteImage(ignoreError) {
|
||||||
deleteImage(this.props.image, this.props.registryUrl, ignoreError, this.props.onNotify)
|
deleteImage(this.props.image, {
|
||||||
|
...this.props,
|
||||||
|
ignoreError
|
||||||
|
})
|
||||||
},
|
},
|
||||||
handleCheckboxChange(checked) {
|
handleCheckboxChange(checked) {
|
||||||
this.props.handleCheckboxChange(checked, this.props.image);
|
this.props.handleCheckboxChange(checked, this.props.image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteImage(image, registryUrl, ignoreError, onNotify) {
|
export function deleteImage(image, opts) {
|
||||||
|
const {
|
||||||
|
registryUrl,
|
||||||
|
ignoreError,
|
||||||
|
onNotify,
|
||||||
|
onAuthentication
|
||||||
|
} = opts;
|
||||||
if (!image.digest) {
|
if (!image.digest) {
|
||||||
onNotify(`Information for ${name}:${tag} are not yet loaded.`);
|
onNotify(`Information for ${name}:${tag} are not yet loaded.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const name = image.name;
|
const name = image.name;
|
||||||
const tag = image.tag;
|
const tag = image.tag;
|
||||||
const oReq = new Http();
|
const oReq = new Http({
|
||||||
|
onAuthentication: onAuthentication
|
||||||
|
});
|
||||||
oReq.addEventListener('loadend', function () {
|
oReq.addEventListener('loadend', function () {
|
||||||
if (this.status == 200 || this.status == 202) {
|
if (this.status == 200 || this.status == 202) {
|
||||||
router.taglist(name);
|
router.taglist(name);
|
||||||
|
@ -71,7 +82,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
onNotify(this.responseText);
|
onNotify(this.responseText);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
oReq.open('DELETE', registryUrl + '/v2/' + name + '/manifests/' + image.digest);
|
oReq.open('DELETE', `${registryUrl}/v2/${name}/manifests/${image.digest}`);
|
||||||
oReq.setRequestHeader('Accept',
|
oReq.setRequestHeader('Accept',
|
||||||
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json');
|
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json');
|
||||||
oReq.addEventListener('error', function () {
|
oReq.addEventListener('error', function () {
|
||||||
|
|
|
@ -39,7 +39,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
<tag-table if="{ state.loadend }" tags="{state.tags}" asc="{state.asc}" page="{ state.page }"
|
<tag-table if="{ state.loadend }" tags="{state.tags}" asc="{state.asc}" page="{ state.page }"
|
||||||
show-content-digest="{props.showContentDigest}" is-image-remove-activated="{props.isImageRemoveActivated}"
|
show-content-digest="{props.showContentDigest}" is-image-remove-activated="{props.isImageRemoveActivated}"
|
||||||
onReverseOrder="{ onReverseOrder }" registry-url="{ props.registryUrl }" pull-url="{ props.pullUrl }"
|
onReverseOrder="{ onReverseOrder }" registry-url="{ props.registryUrl }" pull-url="{ props.pullUrl }"
|
||||||
on-notify="{ props.onNotify }" filter-results="{ props.filterResults }"></tag-table>
|
on-notify="{ props.onNotify }" filter-results="{ props.filterResults }"
|
||||||
|
on-authentication="{ props.onAuthentication }">
|
||||||
|
</tag-table>
|
||||||
|
|
||||||
<pagination pages="{ getPageLabels(state.page, getNumPages(state.tags)) }" onPageUpdate="{onPageUpdate}"></pagination>
|
<pagination pages="{ getPageLabels(state.page, getNumPages(state.tags)) }" onPageUpdate="{onPageUpdate}"></pagination>
|
||||||
|
|
||||||
|
|
|
@ -63,16 +63,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
<td if="{ props.isImageRemoveActivated }" class="remove-tag">
|
<td if="{ props.isImageRemoveActivated }" class="remove-tag">
|
||||||
<remove-image multi-delete="{ state.multiDelete }" image="{ image }" registry-url="{ props.registryUrl }"
|
<remove-image multi-delete="{ state.multiDelete }" image="{ image }" registry-url="{ props.registryUrl }"
|
||||||
handleCheckboxChange="{ onRemoveImageChange }" checked="{ state.toDelete.has(image) }"
|
handleCheckboxChange="{ onRemoveImageChange }" checked="{ state.toDelete.has(image) }"
|
||||||
on-notify="{ props.onNotify }" />
|
on-notify="{ props.onNotify }" on-authentication="{ props.onAuthentication }" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</material-card>
|
</material-card>
|
||||||
<script>
|
<script>
|
||||||
import {
|
|
||||||
Http
|
|
||||||
} from '../../scripts/http';
|
|
||||||
import {
|
import {
|
||||||
getPage,
|
getPage,
|
||||||
} from '../../scripts/utils';
|
} from '../../scripts/utils';
|
||||||
|
@ -112,7 +109,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
state.page = props.page
|
state.page = props.page
|
||||||
},
|
},
|
||||||
bulkDelete() {
|
bulkDelete() {
|
||||||
this.state.toDelete.forEach(image => deleteImage(image, this.props.registryUrl, true, this.props.onNotify))
|
this.state.toDelete.forEach(image => deleteImage(image, {
|
||||||
|
...this.props,
|
||||||
|
ignoreError: true
|
||||||
|
}))
|
||||||
},
|
},
|
||||||
onRemoveImageHeaderChange(checked, event) {
|
onRemoveImageHeaderChange(checked, event) {
|
||||||
if (event.altKey === true) {
|
if (event.altKey === true) {
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<!-- build:keep production -->
|
<!-- build:keep production -->
|
||||||
<docker-registry-ui registry-url="${URL}" name="${REGISTRY_TITLE}" pull-url="${PULL_URL}"
|
<docker-registry-ui registry-url="${REGISTRY_URL}" name="${REGISTRY_TITLE}" pull-url="${PULL_URL}"
|
||||||
show-content-digest="${SHOW_CONTENT_DIGEST}" is-image-remove-activated="${DELETE_IMAGES}"
|
show-content-digest="${SHOW_CONTENT_DIGEST}" is-image-remove-activated="${DELETE_IMAGES}"
|
||||||
catalog-elements-limit="${CATALOG_ELEMENTS_LIMIT}" single-registry="${SINGLE_REGISTRY}">
|
catalog-elements-limit="${CATALOG_ELEMENTS_LIMIT}" single-registry="${SINGLE_REGISTRY}">
|
||||||
</docker-registry-ui>
|
</docker-registry-ui>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue