Merge branch 'develop' into webpack
parent
d1f7e17ee6
commit
cdf880a397
|
@ -1,6 +1,6 @@
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img title="portainer" src='https://portainer.io/images/logo_alt.png' />
|
<img title="portainer" src='https://github.com/portainer/portainer/blob/develop/assets/images/logo_alt.png?raw=true' />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://hub.docker.com/r/portainer/portainer/)
|
[](https://hub.docker.com/r/portainer/portainer/)
|
||||||
|
@ -8,7 +8,6 @@
|
||||||
[](http://portainer.readthedocs.io/en/stable/?badge=stable)
|
[](http://portainer.readthedocs.io/en/stable/?badge=stable)
|
||||||
[](https://portainer.visualstudio.com/Portainer%20CI/_build/latest?definitionId=3&branchName=develop)
|
[](https://portainer.visualstudio.com/Portainer%20CI/_build/latest?definitionId=3&branchName=develop)
|
||||||
[](https://codeclimate.com/github/portainer/portainer)
|
[](https://codeclimate.com/github/portainer/portainer)
|
||||||
[](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
|
||||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6)
|
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6)
|
||||||
|
|
||||||
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your different Docker environments (Docker hosts or Swarm clusters).
|
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your different Docker environments (Docker hosts or Swarm clusters).
|
||||||
|
@ -41,7 +40,6 @@ Unlike the public demo, the playground sessions are deleted after 4 hours. Apart
|
||||||
* Issues: https://github.com/portainer/portainer/issues
|
* Issues: https://github.com/portainer/portainer/issues
|
||||||
* FAQ: https://portainer.readthedocs.io/en/latest/faq.html
|
* FAQ: https://portainer.readthedocs.io/en/latest/faq.html
|
||||||
* Slack (chat): https://portainer.io/slack/
|
* Slack (chat): https://portainer.io/slack/
|
||||||
* Gitter (chat): https://gitter.im/portainer/Lobby
|
|
||||||
|
|
||||||
## Reporting bugs and contributing
|
## Reporting bugs and contributing
|
||||||
|
|
||||||
|
|
|
@ -175,7 +175,7 @@ func loadEndpointSyncSystemSchedule(jobScheduler portainer.JobScheduler, schedul
|
||||||
|
|
||||||
endpointSyncJob := &portainer.EndpointSyncJob{}
|
endpointSyncJob := &portainer.EndpointSyncJob{}
|
||||||
|
|
||||||
endointSyncSchedule := &portainer.Schedule{
|
endpointSyncSchedule := &portainer.Schedule{
|
||||||
ID: portainer.ScheduleID(scheduleService.GetNextIdentifier()),
|
ID: portainer.ScheduleID(scheduleService.GetNextIdentifier()),
|
||||||
Name: "system_endpointsync",
|
Name: "system_endpointsync",
|
||||||
CronExpression: "@every " + *flags.SyncInterval,
|
CronExpression: "@every " + *flags.SyncInterval,
|
||||||
|
@ -186,14 +186,14 @@ func loadEndpointSyncSystemSchedule(jobScheduler portainer.JobScheduler, schedul
|
||||||
}
|
}
|
||||||
|
|
||||||
endpointSyncJobContext := cron.NewEndpointSyncJobContext(endpointService, *flags.ExternalEndpoints)
|
endpointSyncJobContext := cron.NewEndpointSyncJobContext(endpointService, *flags.ExternalEndpoints)
|
||||||
endpointSyncJobRunner := cron.NewEndpointSyncJobRunner(endointSyncSchedule, endpointSyncJobContext)
|
endpointSyncJobRunner := cron.NewEndpointSyncJobRunner(endpointSyncSchedule, endpointSyncJobContext)
|
||||||
|
|
||||||
err = jobScheduler.ScheduleJob(endpointSyncJobRunner)
|
err = jobScheduler.ScheduleJob(endpointSyncJobRunner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return scheduleService.CreateSchedule(endointSyncSchedule)
|
return scheduleService.CreateSchedule(endpointSyncSchedule)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService portainer.JobService, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, fileService portainer.FileService) error {
|
func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService portainer.JobService, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, fileService portainer.FileService) error {
|
||||||
|
@ -260,6 +260,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
|
||||||
portainer.LDAPGroupSearchSettings{},
|
portainer.LDAPGroupSearchSettings{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
OAuthSettings: portainer.OAuthSettings{},
|
||||||
AllowBindMountsForRegularUsers: true,
|
AllowBindMountsForRegularUsers: true,
|
||||||
AllowPrivilegedModeForRegularUsers: true,
|
AllowPrivilegedModeForRegularUsers: true,
|
||||||
EnableHostManagementFeatures: false,
|
EnableHostManagementFeatures: false,
|
||||||
|
|
|
@ -19,6 +19,7 @@ var extensionDownloadBaseURL = "https://portainer-io-assets.sfo2.digitaloceanspa
|
||||||
|
|
||||||
var extensionBinaryMap = map[portainer.ExtensionID]string{
|
var extensionBinaryMap = map[portainer.ExtensionID]string{
|
||||||
portainer.RegistryManagementExtension: "extension-registry-management",
|
portainer.RegistryManagementExtension: "extension-registry-management",
|
||||||
|
portainer.OAuthAuthenticationExtension: "extension-oauth-authentication",
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtensionManager represents a service used to
|
// ExtensionManager represents a service used to
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/asaskevich/govalidator"
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
"github.com/portainer/libhttp/request"
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type oauthPayload struct {
|
||||||
|
Code string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (payload *oauthPayload) Validate(r *http.Request) error {
|
||||||
|
if govalidator.IsNull(payload.Code) {
|
||||||
|
return portainer.Error("Invalid OAuth authorization code")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) authenticateThroughExtension(code, licenseKey string, settings *portainer.OAuthSettings) (string, error) {
|
||||||
|
extensionURL := handler.ProxyManager.GetExtensionURL(portainer.OAuthAuthenticationExtension)
|
||||||
|
|
||||||
|
encodedConfiguration, err := json.Marshal(settings)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", extensionURL+"/validate", nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
req.Header.Set("X-OAuth-Config", string(encodedConfiguration))
|
||||||
|
req.Header.Set("X-OAuth-Code", code)
|
||||||
|
req.Header.Set("X-PortainerExtension-License", licenseKey)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
type extensionResponse struct {
|
||||||
|
Username string `json:"Username,omitempty"`
|
||||||
|
Err string `json:"err,omitempty"`
|
||||||
|
Details string `json:"details,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var extResp extensionResponse
|
||||||
|
err = json.Unmarshal(body, &extResp)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", portainer.Error(extResp.Err + ":" + extResp.Details)
|
||||||
|
}
|
||||||
|
|
||||||
|
return extResp.Username, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
var payload oauthPayload
|
||||||
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings, err := handler.SettingsService.Settings()
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.AuthenticationMethod != 3 {
|
||||||
|
return &httperror.HandlerError{http.StatusForbidden, "OAuth authentication is not enabled", portainer.Error("OAuth authentication is not enabled")}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension, err := handler.ExtensionService.Extension(portainer.OAuthAuthenticationExtension)
|
||||||
|
if err == portainer.ErrObjectNotFound {
|
||||||
|
return &httperror.HandlerError{http.StatusNotFound, "Oauth authentication extension is not enabled", err}
|
||||||
|
} else if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
username, err := handler.authenticateThroughExtension(payload.Code, extension.License.LicenseKey, &settings.OAuthSettings)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[DEBUG] - OAuth authentication error: %s", err)
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate through OAuth", portainer.ErrUnauthorized}
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := handler.UserService.UserByUsername(username)
|
||||||
|
if err != nil && err != portainer.ErrObjectNotFound {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user == nil && !settings.OAuthSettings.OAuthAutoCreateUsers {
|
||||||
|
return &httperror.HandlerError{http.StatusForbidden, "Account not created beforehand in Portainer and automatic user provisioning not enabled", portainer.ErrUnauthorized}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user == nil {
|
||||||
|
user = &portainer.User{
|
||||||
|
Username: username,
|
||||||
|
Role: portainer.StandardUserRole,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.UserService.CreateUser(user)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.OAuthSettings.DefaultTeamID != 0 {
|
||||||
|
membership := &portainer.TeamMembership{
|
||||||
|
UserID: user.ID,
|
||||||
|
TeamID: settings.OAuthSettings.DefaultTeamID,
|
||||||
|
Role: portainer.TeamMember,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.TeamMembershipService.CreateTeamMembership(membership)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team membership inside the database", err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler.writeToken(w, user)
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
|
"github.com/portainer/portainer/http/proxy"
|
||||||
"github.com/portainer/portainer/http/security"
|
"github.com/portainer/portainer/http/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -28,6 +29,8 @@ type Handler struct {
|
||||||
SettingsService portainer.SettingsService
|
SettingsService portainer.SettingsService
|
||||||
TeamService portainer.TeamService
|
TeamService portainer.TeamService
|
||||||
TeamMembershipService portainer.TeamMembershipService
|
TeamMembershipService portainer.TeamMembershipService
|
||||||
|
ExtensionService portainer.ExtensionService
|
||||||
|
ProxyManager *proxy.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage authentication operations.
|
// NewHandler creates a handler to manage authentication operations.
|
||||||
|
@ -36,6 +39,9 @@ func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimi
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
authDisabled: authDisabled,
|
authDisabled: authDisabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.Handle("/auth/oauth/validate",
|
||||||
|
rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.validateOAuth)))).Methods(http.MethodPost)
|
||||||
h.Handle("/auth",
|
h.Handle("/auth",
|
||||||
rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.authenticate)))).Methods(http.MethodPost)
|
rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.authenticate)))).Methods(http.MethodPost)
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
h.Handle("/extensions",
|
h.Handle("/extensions",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet)
|
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet)
|
||||||
h.Handle("/extensions",
|
h.Handle("/extensions",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionCreate))).Methods(http.MethodPost)
|
bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionCreate))).Methods(http.MethodPost)
|
||||||
h.Handle("/extensions/{id}",
|
h.Handle("/extensions/{id}",
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type motdResponse struct {
|
type motdResponse struct {
|
||||||
|
Title string `json:"Title"`
|
||||||
Message string `json:"Message"`
|
Message string `json:"Message"`
|
||||||
Hash []byte `json:"Hash"`
|
Hash []byte `json:"Hash"`
|
||||||
}
|
}
|
||||||
|
@ -22,6 +23,12 @@ func (handler *Handler) motd(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hash := crypto.HashFromBytes(motd)
|
title, err := client.Get(portainer.MessageOfTheDayTitleURL, 0)
|
||||||
response.JSON(w, &motdResponse{Message: string(motd), Hash: hash})
|
if err != nil {
|
||||||
|
response.JSON(w, &motdResponse{Message: ""})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := crypto.HashFromBytes(motd)
|
||||||
|
response.JSON(w, &motdResponse{Title: string(title), Message: string(motd), Hash: hash})
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ func hideFields(registry *portainer.Registry) {
|
||||||
// Handler is the HTTP handler used to handle registry operations.
|
// Handler is the HTTP handler used to handle registry operations.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
|
requestBouncer *security.RequestBouncer
|
||||||
RegistryService portainer.RegistryService
|
RegistryService portainer.RegistryService
|
||||||
ExtensionService portainer.ExtensionService
|
ExtensionService portainer.ExtensionService
|
||||||
FileService portainer.FileService
|
FileService portainer.FileService
|
||||||
|
@ -28,6 +29,7 @@ type Handler struct {
|
||||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
|
requestBouncer: bouncer,
|
||||||
}
|
}
|
||||||
|
|
||||||
h.Handle("/registries",
|
h.Handle("/registries",
|
||||||
|
@ -35,7 +37,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
h.Handle("/registries",
|
h.Handle("/registries",
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryList))).Methods(http.MethodGet)
|
bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryList))).Methods(http.MethodGet)
|
||||||
h.Handle("/registries/{id}",
|
h.Handle("/registries/{id}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryInspect))).Methods(http.MethodGet)
|
bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryInspect))).Methods(http.MethodGet)
|
||||||
h.Handle("/registries/{id}",
|
h.Handle("/registries/{id}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryUpdate))).Methods(http.MethodPut)
|
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryUpdate))).Methods(http.MethodPut)
|
||||||
h.Handle("/registries/{id}/access",
|
h.Handle("/registries/{id}/access",
|
||||||
|
@ -45,7 +47,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
h.Handle("/registries/{id}",
|
h.Handle("/registries/{id}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete)
|
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete)
|
||||||
h.PathPrefix("/registries/{id}/v2").Handler(
|
h.PathPrefix("/registries/{id}/v2").Handler(
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI)))
|
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI)))
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,11 @@ func (handler *Handler) proxyRequestsToRegistryAPI(w http.ResponseWriter, r *htt
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = handler.requestBouncer.RegistryAccess(r, registry)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", portainer.ErrEndpointAccessDenied}
|
||||||
|
}
|
||||||
|
|
||||||
extension, err := handler.ExtensionService.Extension(portainer.RegistryManagementExtension)
|
extension, err := handler.ExtensionService.Extension(portainer.RegistryManagementExtension)
|
||||||
if err == portainer.ErrObjectNotFound {
|
if err == portainer.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Registry management extension is not enabled", err}
|
return &httperror.HandlerError{http.StatusNotFound, "Registry management extension is not enabled", err}
|
||||||
|
|
|
@ -23,6 +23,11 @@ func (handler *Handler) registryInspect(w http.ResponseWriter, r *http.Request)
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = handler.requestBouncer.RegistryAccess(r, registry)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", portainer.ErrEndpointAccessDenied}
|
||||||
|
}
|
||||||
|
|
||||||
hideFields(registry)
|
hideFields(registry)
|
||||||
return response.JSON(w, registry)
|
return response.JSON(w, registry)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
|
|
||||||
func hideFields(settings *portainer.Settings) {
|
func hideFields(settings *portainer.Settings) {
|
||||||
settings.LDAPSettings.Password = ""
|
settings.LDAPSettings.Password = ""
|
||||||
|
settings.OAuthSettings.ClientSecret = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler is the HTTP handler used to handle settings operations.
|
// Handler is the HTTP handler used to handle settings operations.
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
@ -15,6 +16,7 @@ type publicSettingsResponse struct {
|
||||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||||
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
|
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
|
||||||
ExternalTemplates bool `json:"ExternalTemplates"`
|
ExternalTemplates bool `json:"ExternalTemplates"`
|
||||||
|
OAuthLoginURI string `json:"OAuthLoginURI"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET request on /api/settings/public
|
// GET request on /api/settings/public
|
||||||
|
@ -31,6 +33,11 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
|
||||||
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
|
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
|
||||||
EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
|
EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
|
||||||
ExternalTemplates: false,
|
ExternalTemplates: false,
|
||||||
|
OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login",
|
||||||
|
settings.OAuthSettings.AuthorizationURI,
|
||||||
|
settings.OAuthSettings.ClientID,
|
||||||
|
settings.OAuthSettings.RedirectURI,
|
||||||
|
settings.OAuthSettings.Scopes),
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.TemplatesURL != "" {
|
if settings.TemplatesURL != "" {
|
||||||
|
|
|
@ -16,6 +16,7 @@ type settingsUpdatePayload struct {
|
||||||
BlackListedLabels []portainer.Pair
|
BlackListedLabels []portainer.Pair
|
||||||
AuthenticationMethod *int
|
AuthenticationMethod *int
|
||||||
LDAPSettings *portainer.LDAPSettings
|
LDAPSettings *portainer.LDAPSettings
|
||||||
|
OAuthSettings *portainer.OAuthSettings
|
||||||
AllowBindMountsForRegularUsers *bool
|
AllowBindMountsForRegularUsers *bool
|
||||||
AllowPrivilegedModeForRegularUsers *bool
|
AllowPrivilegedModeForRegularUsers *bool
|
||||||
EnableHostManagementFeatures *bool
|
EnableHostManagementFeatures *bool
|
||||||
|
@ -24,8 +25,8 @@ type settingsUpdatePayload struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
||||||
if *payload.AuthenticationMethod != 1 && *payload.AuthenticationMethod != 2 {
|
if *payload.AuthenticationMethod != 1 && *payload.AuthenticationMethod != 2 && *payload.AuthenticationMethod != 3 {
|
||||||
return portainer.Error("Invalid authentication method value. Value must be one of: 1 (internal) or 2 (LDAP/AD)")
|
return portainer.Error("Invalid authentication method value. Value must be one of: 1 (internal), 2 (LDAP/AD) or 3 (OAuth)")
|
||||||
}
|
}
|
||||||
if payload.LogoURL != nil && *payload.LogoURL != "" && !govalidator.IsURL(*payload.LogoURL) {
|
if payload.LogoURL != nil && *payload.LogoURL != "" && !govalidator.IsURL(*payload.LogoURL) {
|
||||||
return portainer.Error("Invalid logo URL. Must correspond to a valid URL format")
|
return portainer.Error("Invalid logo URL. Must correspond to a valid URL format")
|
||||||
|
@ -74,6 +75,15 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
settings.LDAPSettings.Password = ldapPassword
|
settings.LDAPSettings.Password = ldapPassword
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if payload.OAuthSettings != nil {
|
||||||
|
clientSecret := payload.OAuthSettings.ClientSecret
|
||||||
|
if clientSecret == "" {
|
||||||
|
clientSecret = settings.OAuthSettings.ClientSecret
|
||||||
|
}
|
||||||
|
settings.OAuthSettings = *payload.OAuthSettings
|
||||||
|
settings.OAuthSettings.ClientSecret = clientSecret
|
||||||
|
}
|
||||||
|
|
||||||
if payload.AllowBindMountsForRegularUsers != nil {
|
if payload.AllowBindMountsForRegularUsers != nil {
|
||||||
settings.AllowBindMountsForRegularUsers = *payload.AllowBindMountsForRegularUsers
|
settings.AllowBindMountsForRegularUsers = *payload.AllowBindMountsForRegularUsers
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,10 @@ func (handler *Handler) userDelete(w http.ResponseWriter, r *http.Request) *http
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) deleteAdminUser(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
|
func (handler *Handler) deleteAdminUser(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
|
||||||
|
if user.Password == "" {
|
||||||
|
return handler.deleteUser(w, user)
|
||||||
|
}
|
||||||
|
|
||||||
users, err := handler.UserService.Users()
|
users, err := handler.UserService.Users()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve users from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve users from the database", err}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
|
|
||||||
var extensionPorts = map[portainer.ExtensionID]string{
|
var extensionPorts = map[portainer.ExtensionID]string{
|
||||||
portainer.RegistryManagementExtension: "7001",
|
portainer.RegistryManagementExtension: "7001",
|
||||||
|
portainer.OAuthAuthenticationExtension: "7002",
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -103,6 +104,11 @@ func (manager *Manager) CreateExtensionProxy(extensionID portainer.ExtensionID)
|
||||||
return proxy, nil
|
return proxy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetExtensionURL retrieves the URL of an extension running locally based on the extension port table
|
||||||
|
func (manager *Manager) GetExtensionURL(extensionID portainer.ExtensionID) string {
|
||||||
|
return "http://localhost:" + extensionPorts[extensionID]
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteExtensionProxy deletes the extension proxy associated to an extension identifier
|
// DeleteExtensionProxy deletes the extension proxy associated to an extension identifier
|
||||||
func (manager *Manager) DeleteExtensionProxy(extensionID portainer.ExtensionID) {
|
func (manager *Manager) DeleteExtensionProxy(extensionID portainer.ExtensionID) {
|
||||||
manager.extensionProxies.Remove(strconv.Itoa(int(extensionID)))
|
manager.extensionProxies.Remove(strconv.Itoa(int(extensionID)))
|
||||||
|
|
|
@ -153,10 +153,10 @@ func authorizedEndpointAccess(endpoint *portainer.Endpoint, endpointGroup *porta
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizedEndpointGroupAccess ensure that the user can access the specified endpoint group.
|
// authorizedEndpointGroupAccess ensure that the user can access the specified endpoint group.
|
||||||
// It will check if the user is part of the authorized users or part of a team that is
|
// It will check if the user is part of the authorized users or part of a team that is
|
||||||
// listed in the authorized teams.
|
// listed in the authorized teams.
|
||||||
func AuthorizedEndpointGroupAccess(endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
func authorizedEndpointGroupAccess(endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
||||||
return authorizedAccess(userID, memberships, endpointGroup.AuthorizedUsers, endpointGroup.AuthorizedTeams)
|
return authorizedAccess(userID, memberships, endpointGroup.AuthorizedUsers, endpointGroup.AuthorizedTeams)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -111,6 +111,31 @@ func (bouncer *RequestBouncer) EndpointAccess(r *http.Request, endpoint *portain
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegistryAccess retrieves the JWT token from the request context and verifies
|
||||||
|
// that the user can access the specified registry.
|
||||||
|
// An error is returned when access is denied.
|
||||||
|
func (bouncer *RequestBouncer) RegistryAccess(r *http.Request, registry *portainer.Registry) error {
|
||||||
|
tokenData, err := RetrieveTokenData(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenData.Role == portainer.AdministratorRole {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
memberships, err := bouncer.teamMembershipService.TeamMembershipsByUserID(tokenData.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !AuthorizedRegistryAccess(registry, tokenData.ID, memberships) {
|
||||||
|
return portainer.ErrEndpointAccessDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// mwSecureHeaders provides secure headers middleware for handlers.
|
// mwSecureHeaders provides secure headers middleware for handlers.
|
||||||
func mwSecureHeaders(next http.Handler) http.Handler {
|
func mwSecureHeaders(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
@ -124,7 +124,7 @@ func FilterEndpointGroups(endpointGroups []portainer.EndpointGroup, context *Res
|
||||||
filteredEndpointGroups = make([]portainer.EndpointGroup, 0)
|
filteredEndpointGroups = make([]portainer.EndpointGroup, 0)
|
||||||
|
|
||||||
for _, group := range endpointGroups {
|
for _, group := range endpointGroups {
|
||||||
if AuthorizedEndpointGroupAccess(&group, context.UserID, context.UserMemberships) {
|
if authorizedEndpointGroupAccess(&group, context.UserID, context.UserMemberships) {
|
||||||
filteredEndpointGroups = append(filteredEndpointGroups, group)
|
filteredEndpointGroups = append(filteredEndpointGroups, group)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,6 +107,8 @@ func (server *Server) Start() error {
|
||||||
authHandler.SettingsService = server.SettingsService
|
authHandler.SettingsService = server.SettingsService
|
||||||
authHandler.TeamService = server.TeamService
|
authHandler.TeamService = server.TeamService
|
||||||
authHandler.TeamMembershipService = server.TeamMembershipService
|
authHandler.TeamMembershipService = server.TeamMembershipService
|
||||||
|
authHandler.ExtensionService = server.ExtensionService
|
||||||
|
authHandler.ProxyManager = proxyManager
|
||||||
|
|
||||||
var dockerHubHandler = dockerhub.NewHandler(requestBouncer)
|
var dockerHubHandler = dockerhub.NewHandler(requestBouncer)
|
||||||
dockerHubHandler.DockerHubService = server.DockerHubService
|
dockerHubHandler.DockerHubService = server.DockerHubService
|
||||||
|
|
|
@ -56,6 +56,20 @@ type (
|
||||||
AutoCreateUsers bool `json:"AutoCreateUsers"`
|
AutoCreateUsers bool `json:"AutoCreateUsers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OAuthSettings represents the settings used to authorize with an authorization server
|
||||||
|
OAuthSettings struct {
|
||||||
|
ClientID string `json:"ClientID"`
|
||||||
|
ClientSecret string `json:"ClientSecret,omitempty"`
|
||||||
|
AccessTokenURI string `json:"AccessTokenURI"`
|
||||||
|
AuthorizationURI string `json:"AuthorizationURI"`
|
||||||
|
ResourceURI string `json:"ResourceURI"`
|
||||||
|
RedirectURI string `json:"RedirectURI"`
|
||||||
|
UserIdentifier string `json:"UserIdentifier"`
|
||||||
|
Scopes string `json:"Scopes"`
|
||||||
|
OAuthAutoCreateUsers bool `json:"OAuthAutoCreateUsers"`
|
||||||
|
DefaultTeamID TeamID `json:"DefaultTeamID"`
|
||||||
|
}
|
||||||
|
|
||||||
// TLSConfiguration represents a TLS configuration
|
// TLSConfiguration represents a TLS configuration
|
||||||
TLSConfiguration struct {
|
TLSConfiguration struct {
|
||||||
TLS bool `json:"TLS"`
|
TLS bool `json:"TLS"`
|
||||||
|
@ -85,6 +99,7 @@ type (
|
||||||
BlackListedLabels []Pair `json:"BlackListedLabels"`
|
BlackListedLabels []Pair `json:"BlackListedLabels"`
|
||||||
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
|
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
|
||||||
LDAPSettings LDAPSettings `json:"LDAPSettings"`
|
LDAPSettings LDAPSettings `json:"LDAPSettings"`
|
||||||
|
OAuthSettings OAuthSettings `json:"OAuthSettings"`
|
||||||
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
||||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||||
SnapshotInterval string `json:"SnapshotInterval"`
|
SnapshotInterval string `json:"SnapshotInterval"`
|
||||||
|
@ -779,15 +794,17 @@ type (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// APIVersion is the version number of the Portainer API
|
// APIVersion is the version number of the Portainer API
|
||||||
APIVersion = "1.20.1"
|
APIVersion = "1.20.2"
|
||||||
// DBVersion is the version number of the Portainer database
|
// DBVersion is the version number of the Portainer database
|
||||||
DBVersion = 17
|
DBVersion = 17
|
||||||
// AssetsServerURL represents the URL of the Portainer asset server
|
// AssetsServerURL represents the URL of the Portainer asset server
|
||||||
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
|
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
|
||||||
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
||||||
MessageOfTheDayURL = AssetsServerURL + "/motd.html"
|
MessageOfTheDayURL = AssetsServerURL + "/motd.html"
|
||||||
|
// MessageOfTheDayTitleURL represents the URL where Portainer MOTD title can be retrieved
|
||||||
|
MessageOfTheDayTitleURL = AssetsServerURL + "/motd-title.txt"
|
||||||
// ExtensionDefinitionsURL represents the URL where Portainer extension definitions can be retrieved
|
// ExtensionDefinitionsURL represents the URL where Portainer extension definitions can be retrieved
|
||||||
ExtensionDefinitionsURL = AssetsServerURL + "/extensions.json"
|
ExtensionDefinitionsURL = AssetsServerURL + "/extensions-1.20.2.json"
|
||||||
// PortainerAgentHeader represents the name of the header available in any agent response
|
// PortainerAgentHeader represents the name of the header available in any agent response
|
||||||
PortainerAgentHeader = "Portainer-Agent"
|
PortainerAgentHeader = "Portainer-Agent"
|
||||||
// PortainerAgentTargetHeader represent the name of the header containing the target node name
|
// PortainerAgentTargetHeader represent the name of the header containing the target node name
|
||||||
|
@ -834,6 +851,8 @@ const (
|
||||||
AuthenticationInternal
|
AuthenticationInternal
|
||||||
// AuthenticationLDAP represents the LDAP authentication method (authentication against a LDAP server)
|
// AuthenticationLDAP represents the LDAP authentication method (authentication against a LDAP server)
|
||||||
AuthenticationLDAP
|
AuthenticationLDAP
|
||||||
|
//AuthenticationOAuth represents the OAuth authentication method (authentication against a authorization server)
|
||||||
|
AuthenticationOAuth
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -912,6 +931,8 @@ const (
|
||||||
_ ExtensionID = iota
|
_ ExtensionID = iota
|
||||||
// RegistryManagementExtension represents the registry management extension
|
// RegistryManagementExtension represents the registry management extension
|
||||||
RegistryManagementExtension
|
RegistryManagementExtension
|
||||||
|
// OAuthAuthenticationExtension represents the OAuth authentication extension
|
||||||
|
OAuthAuthenticationExtension
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -54,7 +54,7 @@ info:
|
||||||
|
|
||||||
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
|
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
|
||||||
|
|
||||||
version: "1.20.1"
|
version: "1.20.2"
|
||||||
title: "Portainer API"
|
title: "Portainer API"
|
||||||
contact:
|
contact:
|
||||||
email: "info@portainer.io"
|
email: "info@portainer.io"
|
||||||
|
@ -3018,7 +3018,7 @@ definitions:
|
||||||
description: "Is analytics enabled"
|
description: "Is analytics enabled"
|
||||||
Version:
|
Version:
|
||||||
type: "string"
|
type: "string"
|
||||||
example: "1.20.1"
|
example: "1.20.2"
|
||||||
description: "Portainer API version"
|
description: "Portainer API version"
|
||||||
PublicSettingsInspectResponse:
|
PublicSettingsInspectResponse:
|
||||||
type: "object"
|
type: "object"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"packageName": "portainer",
|
"packageName": "portainer",
|
||||||
"packageVersion": "1.20.1",
|
"packageVersion": "1.20.2",
|
||||||
"projectName": "portainer"
|
"projectName": "portainer"
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ function initAuthentication(authManager, Authentication, $rootScope, $state) {
|
||||||
// to have more controls on which URL should trigger the unauthenticated state.
|
// to have more controls on which URL should trigger the unauthenticated state.
|
||||||
$rootScope.$on('unauthenticated', function (event, data) {
|
$rootScope.$on('unauthenticated', function (event, data) {
|
||||||
if (!_.includes(data.config.url, '/v2/')) {
|
if (!_.includes(data.config.url, '/v2/')) {
|
||||||
$state.go('portainer.auth', {error: 'Your session has expired', redirect: $state.current.name});
|
$state.go('portainer.auth', { error: 'Your session has expired' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,4 +20,5 @@ angular.module('portainer')
|
||||||
.constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json')
|
.constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json')
|
||||||
.constant('PAGINATION_MAX_ITEMS', 10)
|
.constant('PAGINATION_MAX_ITEMS', 10)
|
||||||
.constant('APPLICATION_CACHE_VALIDITY', 3600)
|
.constant('APPLICATION_CACHE_VALIDITY', 3600)
|
||||||
.constant('CONSOLE_COMMANDS_LABEL_PREFIX', 'io.portainer.commands.');
|
.constant('CONSOLE_COMMANDS_LABEL_PREFIX', 'io.portainer.commands.')
|
||||||
|
.constant('PREDEFINED_NETWORKS', ['host', 'bridge', 'none']);
|
||||||
|
|
|
@ -28,14 +28,14 @@
|
||||||
<i class="fa fa-info-circle space-right" aria-hidden="true"></i>
|
<i class="fa fa-info-circle space-right" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
ng-if="$ctrl.state.showQuickActionStats && ['starting', 'running', 'healthy', 'unhealthy'].indexOf($ctrl.status) !== -1"
|
ng-if="$ctrl.state.showQuickActionStats && ['starting', 'running', 'healthy', 'unhealthy'].indexOf($ctrl.status) !== -1 && $ctrl.taskId === undefined"
|
||||||
style="margin: 0 2.5px;"
|
style="margin: 0 2.5px;"
|
||||||
ui-sref="docker.containers.container.stats({id: $ctrl.containerId, nodeName: $ctrl.nodeName})"
|
ui-sref="docker.containers.container.stats({id: $ctrl.containerId, nodeName: $ctrl.nodeName})"
|
||||||
title="Stats">
|
title="Stats">
|
||||||
<i class="fa fa-chart-area space-right" aria-hidden="true"></i>
|
<i class="fa fa-chart-area space-right" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
ng-if="$ctrl.state.showQuickActionConsole && ['starting', 'running', 'healthy', 'unhealthy'].indexOf($ctrl.status) !== -1"
|
ng-if="$ctrl.state.showQuickActionConsole && ['starting', 'running', 'healthy', 'unhealthy'].indexOf($ctrl.status) !== -1 && $ctrl.taskId === undefined"
|
||||||
style="margin: 0 2.5px;"
|
style="margin: 0 2.5px;"
|
||||||
ui-sref="docker.containers.container.console({id: $ctrl.containerId, nodeName: $ctrl.nodeName})"
|
ui-sref="docker.containers.container.console({id: $ctrl.containerId, nodeName: $ctrl.nodeName})"
|
||||||
title="Console">
|
title="Console">
|
||||||
|
|
|
@ -110,7 +110,7 @@
|
||||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)" ng-disabled="$ctrl.disableRemove(item)"/>
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</span>
|
||||||
<a ng-if="!$ctrl.offlineMode" ui-sref="docker.networks.network({ id: item.Id, nodeName: item.NodeName })" title="{{ item.Name }}">{{ item.Name | truncate:40 }}</a>
|
<a ng-if="!$ctrl.offlineMode" ui-sref="docker.networks.network({ id: item.Id, nodeName: item.NodeName })" title="{{ item.Name }}">{{ item.Name | truncate:40 }}</a>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.docker').component('networksDatatable', {
|
angular.module('portainer.docker').component('networksDatatable', {
|
||||||
templateUrl: './networksDatatable.html',
|
templateUrl: './networksDatatable.html',
|
||||||
controller: 'GenericDatatableController',
|
controller: 'NetworksDatatableController',
|
||||||
bindings: {
|
bindings: {
|
||||||
titleText: '@',
|
titleText: '@',
|
||||||
titleIcon: '@',
|
titleIcon: '@',
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
angular.module('portainer.docker')
|
||||||
|
.controller('NetworksDatatableController', ['$scope', '$controller', 'PREDEFINED_NETWORKS',
|
||||||
|
function ($scope, $controller, PREDEFINED_NETWORKS) {
|
||||||
|
angular.extend(this, $controller('GenericDatatableController', {$scope: $scope}));
|
||||||
|
|
||||||
|
this.disableRemove = function(item) {
|
||||||
|
return PREDEFINED_NETWORKS.includes(item.Name);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.selectAll = function() {
|
||||||
|
for (var i = 0; i < this.state.filteredDataSet.length; i++) {
|
||||||
|
var item = this.state.filteredDataSet[i];
|
||||||
|
if (!this.disableRemove(item) && item.Checked !== this.state.selectAll) {
|
||||||
|
item.Checked = this.state.selectAll;
|
||||||
|
this.selectItem(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
]);
|
|
@ -60,8 +60,8 @@
|
||||||
<span class="label label-{{ item.Status.State | taskstatusbadge }} space-right">{{ item.Status.State }}</span>
|
<span class="label label-{{ item.Status.State | taskstatusbadge }} space-right">{{ item.Status.State }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a ng-if="!$ctrl.agentProxy || !item.Container" ui-sref="docker.tasks.task({id: item.Id})" class="monospaced">{{ item.Id }}Roz</a>
|
<a ng-if="!$ctrl.agentProxy || !item.Container" ui-sref="docker.tasks.task({id: item.Id})" class="monospaced">{{ item.Id }}</a>
|
||||||
<a ng-if="$ctrl.agentProxy && item.Container" ui-sref="docker.containers.container({ id: item.Container.Id, nodeName: item.Container.NodeName })" class="monospaced">{{ item.Id }}Doz</a>
|
<a ng-if="$ctrl.agentProxy && item.Container" ui-sref="docker.containers.container({ id: item.Container.Id, nodeName: item.Container.NodeName })" class="monospaced">{{ item.Id }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<container-quick-actions ng-if="!$ctrl.agentProxy || !item.Container" container-id="item.ContainerId" task-id="item.Id" status="item.Status.State" state="$ctrl.state"></container-quick-actions>
|
<container-quick-actions ng-if="!$ctrl.agentProxy || !item.Container" container-id="item.ContainerId" task-id="item.Id" status="item.Status.State" state="$ctrl.state"></container-quick-actions>
|
||||||
|
|
|
@ -79,6 +79,13 @@
|
||||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Mountpoint' && $ctrl.state.reverseOrder"></i>
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Mountpoint' && $ctrl.state.reverseOrder"></i>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
|
<th>
|
||||||
|
<a ng-click="$ctrl.changeOrderBy('CreatedAt')">
|
||||||
|
Created
|
||||||
|
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CreatedAt' && !$ctrl.state.reverseOrder"></i>
|
||||||
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CreatedAt' && $ctrl.state.reverseOrder"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
<th ng-if="$ctrl.showHostColumn">
|
<th ng-if="$ctrl.showHostColumn">
|
||||||
<a ng-click="$ctrl.changeOrderBy('NodeName')">
|
<a ng-click="$ctrl.changeOrderBy('NodeName')">
|
||||||
Host
|
Host
|
||||||
|
@ -112,6 +119,7 @@
|
||||||
<td>{{ item.StackName ? item.StackName : '-' }}</td>
|
<td>{{ item.StackName ? item.StackName : '-' }}</td>
|
||||||
<td>{{ item.Driver }}</td>
|
<td>{{ item.Driver }}</td>
|
||||||
<td>{{ item.Mountpoint | truncatelr }}</td>
|
<td>{{ item.Mountpoint | truncatelr }}</td>
|
||||||
|
<td>{{ item.CreatedAt | getisodate }}</td>
|
||||||
<td ng-if="$ctrl.showHostColumn">{{ item.NodeName ? item.NodeName : '-' }}</td>
|
<td ng-if="$ctrl.showHostColumn">{{ item.NodeName ? item.NodeName : '-' }}</td>
|
||||||
<td ng-if="$ctrl.showOwnershipColumn">
|
<td ng-if="$ctrl.showOwnershipColumn">
|
||||||
<span>
|
<span>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { ResourceControlViewModel } from "../../portainer/models/resourceControl
|
||||||
|
|
||||||
export function VolumeViewModel(data) {
|
export function VolumeViewModel(data) {
|
||||||
this.Id = data.Name;
|
this.Id = data.Name;
|
||||||
|
this.CreatedAt = data.CreatedAt;
|
||||||
this.Driver = data.Driver;
|
this.Driver = data.Driver;
|
||||||
this.Options = data.Options;
|
this.Options = data.Options;
|
||||||
this.Labels = data.Labels;
|
this.Labels = data.Labels;
|
||||||
|
|
|
@ -56,6 +56,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
||||||
PortBindings: [],
|
PortBindings: [],
|
||||||
PublishAllPorts: false,
|
PublishAllPorts: false,
|
||||||
Binds: [],
|
Binds: [],
|
||||||
|
AutoRemove: false,
|
||||||
NetworkMode: 'bridge',
|
NetworkMode: 'bridge',
|
||||||
Privileged: false,
|
Privileged: false,
|
||||||
Runtime: '',
|
Runtime: '',
|
||||||
|
|
|
@ -124,6 +124,19 @@
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Actions
|
Actions
|
||||||
</div>
|
</div>
|
||||||
|
<!-- autoremove -->
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<label for="ownership" class="control-label text-left">
|
||||||
|
Auto remove
|
||||||
|
<portainer-tooltip position="bottom" message="When enabled, Portainer will automatically remove the container when it exits. This is useful when you want to use the container only once."></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<label class="switch" style="margin-left: 20px;">
|
||||||
|
<input type="checkbox" ng-model="config.HostConfig.AutoRemove"><i></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !autoremove -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !config.Image || (!formValues.Registry && fromContainer)" ng-click="create()" button-spinner="state.actionInProgress">
|
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !config.Image || (!formValues.Registry && fromContainer)" ng-click="create()" button-spinner="state.actionInProgress">
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
<td>ID</td>
|
<td>ID</td>
|
||||||
<td>
|
<td>
|
||||||
{{ network.Id }}
|
{{ network.Id }}
|
||||||
<button class="btn btn-xs btn-danger" ng-click="removeNetwork(network.Id)"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Delete this network</button>
|
<button ng-if="allowRemove(network)" class="btn btn-xs btn-danger" ng-click="removeNetwork(network.Id)"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Delete this network</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('NetworkController', ['$scope', '$state', '$transition$', '$filter', 'NetworkService', 'Container', 'Notifications', 'HttpRequestHelper',
|
.controller('NetworkController', ['$scope', '$state', '$transition$', '$filter', 'NetworkService', 'Container', 'Notifications', 'HttpRequestHelper', 'PREDEFINED_NETWORKS',
|
||||||
function ($scope, $state, $transition$, $filter, NetworkService, Container, Notifications, HttpRequestHelper) {
|
function ($scope, $state, $transition$, $filter, NetworkService, Container, Notifications, HttpRequestHelper, PREDEFINED_NETWORKS) {
|
||||||
|
|
||||||
$scope.removeNetwork = function removeNetwork() {
|
$scope.removeNetwork = function removeNetwork() {
|
||||||
NetworkService.remove($transition$.params().id, $transition$.params().id)
|
NetworkService.remove($transition$.params().id, $transition$.params().id)
|
||||||
|
@ -25,6 +25,10 @@ function ($scope, $state, $transition$, $filter, NetworkService, Container, Noti
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.allowRemove = function allowRemove(item) {
|
||||||
|
return !PREDEFINED_NETWORKS.includes(item.Name);
|
||||||
|
};
|
||||||
|
|
||||||
function filterContainersInNetwork(network, containers) {
|
function filterContainersInNetwork(network, containers) {
|
||||||
var containersInNetwork = [];
|
var containersInNetwork = [];
|
||||||
containers.forEach(function(container) {
|
containers.forEach(function(container) {
|
||||||
|
|
|
@ -19,6 +19,10 @@
|
||||||
<button class="btn btn-xs btn-danger" ng-click="removeVolume()"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove this volume</button>
|
<button class="btn btn-xs btn-danger" ng-click="removeVolume()"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove this volume</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Created</td>
|
||||||
|
<td>{{ volume.CreatedAt | getisodate }}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Mount path</td>
|
<td>Mount path</td>
|
||||||
<td>{{ volume.Mountpoint }}</td>
|
<td>{{ volume.Mountpoint }}</td>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
angular.module('portainer.extensions', [
|
angular.module('portainer.extensions', [
|
||||||
'portainer.extensions.registrymanagement'
|
'portainer.extensions.registrymanagement',
|
||||||
|
'portainer.extensions.oauth'
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
angular.module('portainer.extensions.oauth', ['ngResource'])
|
||||||
|
.constant('API_ENDPOINT_OAUTH', 'api/auth/oauth');
|
|
@ -0,0 +1,63 @@
|
||||||
|
angular.module('portainer.extensions.oauth')
|
||||||
|
.controller('OAuthProviderSelectorController', function OAuthProviderSelectorController() {
|
||||||
|
var ctrl = this;
|
||||||
|
|
||||||
|
this.providers = [
|
||||||
|
{
|
||||||
|
authUrl: 'https://login.microsoftonline.com/TENANT_ID/oauth2/authorize',
|
||||||
|
accessTokenUrl: 'https://login.microsoftonline.com/TENANT_ID/oauth2/token',
|
||||||
|
resourceUrl: 'https://graph.windows.net/TENANT_ID/me?api-version=2013-11-08',
|
||||||
|
userIdentifier: 'userPrincipalName',
|
||||||
|
scopes: 'id,email,name',
|
||||||
|
name: 'microsoft'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authUrl: 'https://accounts.google.com/o/oauth2/auth',
|
||||||
|
accessTokenUrl: 'https://accounts.google.com/o/oauth2/token',
|
||||||
|
resourceUrl: 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json',
|
||||||
|
userIdentifier: 'email',
|
||||||
|
scopes: 'profile email',
|
||||||
|
name: 'google'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authUrl: 'https://github.com/login/oauth/authorize',
|
||||||
|
accessTokenUrl: 'https://github.com/login/oauth/access_token',
|
||||||
|
resourceUrl: 'https://api.github.com/user',
|
||||||
|
userIdentifier: 'login',
|
||||||
|
scopes: 'id email name',
|
||||||
|
name: 'github'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authUrl: '',
|
||||||
|
accessTokenUrl: '',
|
||||||
|
resourceUrl: '',
|
||||||
|
userIdentifier: '',
|
||||||
|
scopes: '',
|
||||||
|
name: 'custom'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
this.$onInit = onInit;
|
||||||
|
|
||||||
|
function onInit() {
|
||||||
|
if (ctrl.provider.authUrl) {
|
||||||
|
ctrl.provider = getProviderByURL(ctrl.provider.authUrl);
|
||||||
|
} else {
|
||||||
|
ctrl.provider = ctrl.providers[0];
|
||||||
|
}
|
||||||
|
ctrl.onSelect(ctrl.provider, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProviderByURL(providerAuthURL) {
|
||||||
|
if (providerAuthURL.indexOf('login.microsoftonline.com') !== -1) {
|
||||||
|
return ctrl.providers[0];
|
||||||
|
}
|
||||||
|
else if (providerAuthURL.indexOf('accounts.google.com') !== -1) {
|
||||||
|
return ctrl.providers[1];
|
||||||
|
}
|
||||||
|
else if (providerAuthURL.indexOf('github.com') !== -1) {
|
||||||
|
return ctrl.providers[2];
|
||||||
|
}
|
||||||
|
return ctrl.providers[3];
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,49 @@
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Provider
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group"></div>
|
||||||
|
<div class="form-group" style="margin-bottom: 0">
|
||||||
|
<div class="boxselector_wrapper">
|
||||||
|
<div ng-click="$ctrl.onSelect($ctrl.provider, true)">
|
||||||
|
<input type="radio" id="oauth_provider_microsoft" ng-model="$ctrl.provider" ng-value="$ctrl.providers[0]">
|
||||||
|
<label for="oauth_provider_microsoft">
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fab fa-microsoft" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
Microsoft
|
||||||
|
</div>
|
||||||
|
<p>Microsoft OAuth provider</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div ng-click="$ctrl.onSelect($ctrl.provider, true)">
|
||||||
|
<input type="radio" id="oauth_provider_google" ng-model="$ctrl.provider" ng-value="$ctrl.providers[1]">
|
||||||
|
<label for="oauth_provider_google">
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fab fa-google" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
Google
|
||||||
|
</div>
|
||||||
|
<p>Google OAuth provider</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div ng-click="$ctrl.onSelect($ctrl.provider, true)">
|
||||||
|
<input type="radio" id="oauth_provider_github" ng-model="$ctrl.provider" ng-value="$ctrl.providers[2]">
|
||||||
|
<label for="oauth_provider_github">
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fab fa-github" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
Github
|
||||||
|
</div>
|
||||||
|
<p>Github OAuth provider</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div ng-click="$ctrl.onSelect($ctrl.provider, true)">
|
||||||
|
<input type="radio" id="oauth_provider_custom" ng-model="$ctrl.provider" ng-value="$ctrl.providers[3]">
|
||||||
|
<label for="oauth_provider_custom">
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fa fa-user-check" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
Custom
|
||||||
|
</div>
|
||||||
|
<p>Custom OAuth provider</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,8 @@
|
||||||
|
angular.module('portainer.extensions.oauth').component('oauthProvidersSelector', {
|
||||||
|
templateUrl: './oauth-providers-selector.html',
|
||||||
|
bindings: {
|
||||||
|
onSelect: '<',
|
||||||
|
provider: '='
|
||||||
|
},
|
||||||
|
controller: 'OAuthProviderSelectorController'
|
||||||
|
});
|
|
@ -0,0 +1,76 @@
|
||||||
|
import _ from 'lodash-es';
|
||||||
|
|
||||||
|
angular.module('portainer.extensions.oauth')
|
||||||
|
.controller('OAuthSettingsController', function OAuthSettingsController() {
|
||||||
|
var ctrl = this;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
provider: {},
|
||||||
|
overrideConfiguration: false,
|
||||||
|
microsoftTenantID: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$onInit = onInit;
|
||||||
|
this.onSelectProvider = onSelectProvider;
|
||||||
|
this.onMicrosoftTenantIDChange = onMicrosoftTenantIDChange;
|
||||||
|
this.useDefaultProviderConfiguration = useDefaultProviderConfiguration;
|
||||||
|
|
||||||
|
function onMicrosoftTenantIDChange() {
|
||||||
|
var tenantID = ctrl.state.microsoftTenantID;
|
||||||
|
|
||||||
|
ctrl.settings.AuthorizationURI = _.replace('https://login.microsoftonline.com/TENANT_ID/oauth2/authorize', 'TENANT_ID', tenantID);
|
||||||
|
ctrl.settings.AccessTokenURI = _.replace('https://login.microsoftonline.com/TENANT_ID/oauth2/token', 'TENANT_ID', tenantID);
|
||||||
|
ctrl.settings.ResourceURI = _.replace('https://graph.windows.net/TENANT_ID/me?api-version=2013-11-08', 'TENANT_ID', tenantID);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useDefaultProviderConfiguration() {
|
||||||
|
ctrl.settings.AuthorizationURI = ctrl.state.provider.authUrl;
|
||||||
|
ctrl.settings.AccessTokenURI = ctrl.state.provider.accessTokenUrl;
|
||||||
|
ctrl.settings.ResourceURI = ctrl.state.provider.resourceUrl;
|
||||||
|
ctrl.settings.UserIdentifier = ctrl.state.provider.userIdentifier;
|
||||||
|
ctrl.settings.Scopes = ctrl.state.provider.scopes;
|
||||||
|
|
||||||
|
if (ctrl.state.provider.name === 'microsoft' && ctrl.state.microsoftTenantID !== '') {
|
||||||
|
onMicrosoftTenantIDChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useExistingConfiguration() {
|
||||||
|
var provider = ctrl.state.provider;
|
||||||
|
ctrl.settings.AuthorizationURI = ctrl.settings.AuthorizationURI === '' ? provider.authUrl : ctrl.settings.AuthorizationURI;
|
||||||
|
ctrl.settings.AccessTokenURI = ctrl.settings.AccessTokenURI === '' ? provider.accessTokenUrl : ctrl.settings.AccessTokenURI;
|
||||||
|
ctrl.settings.ResourceURI = ctrl.settings.ResourceURI === '' ? provider.resourceUrl : ctrl.settings.ResourceURI;
|
||||||
|
ctrl.settings.UserIdentifier = ctrl.settings.UserIdentifier === '' ? provider.userIdentifier : ctrl.settings.UserIdentifier;
|
||||||
|
ctrl.settings.Scopes = ctrl.settings.Scopes === '' ? provider.scopes : ctrl.settings.Scopes;
|
||||||
|
|
||||||
|
if (provider.name === 'microsoft' && ctrl.state.microsoftTenantID !== '') {
|
||||||
|
onMicrosoftTenantIDChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSelectProvider(provider, overrideConfiguration) {
|
||||||
|
ctrl.state.provider = provider;
|
||||||
|
|
||||||
|
if (overrideConfiguration) {
|
||||||
|
useDefaultProviderConfiguration();
|
||||||
|
} else {
|
||||||
|
useExistingConfiguration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInit() {
|
||||||
|
if (ctrl.settings.RedirectURI === '') {
|
||||||
|
ctrl.settings.RedirectURI = window.location.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctrl.settings.AuthorizationURI !== '') {
|
||||||
|
ctrl.state.provider.authUrl = ctrl.settings.AuthorizationURI;
|
||||||
|
|
||||||
|
if (ctrl.settings.AuthorizationURI.indexOf('login.microsoftonline.com') > -1) {
|
||||||
|
var tenantID = ctrl.settings.AuthorizationURI.match(/login.microsoftonline.com\/(.*?)\//)[1];
|
||||||
|
ctrl.state.microsoftTenantID = tenantID;
|
||||||
|
onMicrosoftTenantIDChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,215 @@
|
||||||
|
<div>
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Automatic user provisioning
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
With automatic user provisioning enabled, Portainer will create user(s) automatically with standard user role. If
|
||||||
|
disabled, users must be created beforehand in Portainer in order to login.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-3 col-lg-2 control-label text-left">Automatic user provisioning</label>
|
||||||
|
<label class="switch" style="margin-left: 20px">
|
||||||
|
<input type="checkbox" ng-model="$ctrl.settings.OAuthAutoCreateUsers" /><i></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-if="$ctrl.settings.OAuthAutoCreateUsers">
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
<p>The users created by the automatic provisioning feature can be added to a default team on creation.</p>
|
||||||
|
<p>By assigning newly created users to a team they will be able to access the environments associated to that team. This setting is optional and if not set newly created users won't be able to access any environments.</p>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-3 col-lg-2 control-label text-left">Default team</label>
|
||||||
|
<span class="small text-muted" style="margin-left: 20px;" ng-if="$ctrl.teams.length === 0">
|
||||||
|
You have not yet created any team. Head over the <a ui-sref="portainer.teams">teams view</a> to manage user teams.
|
||||||
|
</span>
|
||||||
|
<button type="button" class="btn btn-sm btn-danger" ng-click="$ctrl.settings.DefaultTeamID = null" ng-disabled="!$ctrl.settings.DefaultTeamID" ng-if="$ctrl.teams.length > 0"><i class="fa fa-times" aria-hidden="true"></i></button>
|
||||||
|
<div class="col-sm-9 col-lg-9" ng-if="$ctrl.teams.length > 0">
|
||||||
|
<select class="form-control" ng-model="$ctrl.settings.DefaultTeamID" ng-options="team.Id as team.Name for team in $ctrl.teams">
|
||||||
|
<option value="">No team</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<oauth-providers-selector on-select="$ctrl.onSelectProvider" provider="$ctrl.state.provider"></oauth-providers-selector>
|
||||||
|
|
||||||
|
<div class="col-sm-12 form-section-title">OAuth Configuration</div>
|
||||||
|
|
||||||
|
<div class="form-group" ng-if="$ctrl.state.provider.name == 'microsoft'">
|
||||||
|
<label for="oauth_microsoft_tenant_id" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
Tenant ID
|
||||||
|
<portainer-tooltip position="bottom" message="ID of the Azure Directory you wish to authenticate against. Also known as the Directory ID"></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="oauth_microsoft_tenant_id"
|
||||||
|
placeholder="xxxxxxxxxxxxxxxxxxxx"
|
||||||
|
ng-model="$ctrl.state.microsoftTenantID"
|
||||||
|
ng-change="$ctrl.onMicrosoftTenantIDChange()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="oauth_client_id" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
{{ $ctrl.state.provider.name == 'microsoft' ? 'Application ID' : 'Client ID' }}
|
||||||
|
<portainer-tooltip position="bottom" message="Public identifier of the OAuth application"></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="oauth_client_id"
|
||||||
|
ng-model="$ctrl.settings.ClientID"
|
||||||
|
placeholder="xxxxxxxxxxxxxxxxxxxx"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="oauth_client_secret" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
{{ $ctrl.state.provider.name == 'microsoft' ? 'Application key' : 'Client secret' }}
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="oauth_client_secret"
|
||||||
|
ng-model="$ctrl.settings.ClientSecret"
|
||||||
|
placeholder="xxxxxxxxxxxxxxxxxxxx"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
|
||||||
|
<label for="oauth_authorization_uri" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
Authorization URL
|
||||||
|
<portainer-tooltip
|
||||||
|
position="bottom"
|
||||||
|
message="URL used to authenticate against the OAuth provider. Will redirect the user to the OAuth provider login view"
|
||||||
|
></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="oauth_authorization_uri"
|
||||||
|
ng-model="$ctrl.settings.AuthorizationURI"
|
||||||
|
placeholder="https://example.com/oauth/authorize"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
|
||||||
|
<label for="oauth_access_token_uri" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
Access token URL
|
||||||
|
<portainer-tooltip
|
||||||
|
position="bottom"
|
||||||
|
message="URL used by Portainer to exchange a valid OAuth authentication code for an access token"
|
||||||
|
></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="oauth_access_token_uri"
|
||||||
|
ng-model="$ctrl.settings.AccessTokenURI"
|
||||||
|
placeholder="https://example.com/oauth/token"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
|
||||||
|
<label for="oauth_resource_uri" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
Resource URL
|
||||||
|
<portainer-tooltip
|
||||||
|
position="bottom"
|
||||||
|
message="URL used by Portainer to retrieve information about the authenticated user"
|
||||||
|
></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="oauth_resource_uri"
|
||||||
|
ng-model="$ctrl.settings.ResourceURI"
|
||||||
|
placeholder="https://example.com/user"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
|
||||||
|
<label for="oauth_redirect_uri" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
Redirect URL
|
||||||
|
<portainer-tooltip
|
||||||
|
position="bottom"
|
||||||
|
message="URL used by the OAuth provider to redirect the user after successful authentication. Should be set to your Portainer instance URL"
|
||||||
|
></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="oauth_redirect_uri"
|
||||||
|
ng-model="$ctrl.settings.RedirectURI"
|
||||||
|
placeholder="http://yourportainer.com/"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
|
||||||
|
<label for="oauth_user_identifier" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
User identifier
|
||||||
|
<portainer-tooltip
|
||||||
|
position="bottom"
|
||||||
|
message="Identifier that will be used by Portainer to create an account for the authenticated user. Retrieved from the resource server specified via the Resource URL field"
|
||||||
|
></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="oauth_user_identifier"
|
||||||
|
ng-model="$ctrl.settings.UserIdentifier"
|
||||||
|
placeholder="id"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
|
||||||
|
<label for="oauth_scopes" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
Scopes
|
||||||
|
<portainer-tooltip
|
||||||
|
position="bottom"
|
||||||
|
message="Scopes required by the OAuth provider to retrieve information about the authenticated user. Refer to your OAuth provider documentation for more information about this"
|
||||||
|
></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="oauth_scopes"
|
||||||
|
ng-model="$ctrl.settings.Scopes"
|
||||||
|
placeholder="id,email,name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" ng-if="$ctrl.state.provider.name != 'custom'">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<a class="small interactive" ng-if="!$ctrl.state.overrideConfiguration" ng-click="$ctrl.state.overrideConfiguration = true;">
|
||||||
|
<i class="fa fa-wrench space-right" aria-hidden="true"></i> Override default configuration
|
||||||
|
</a>
|
||||||
|
<a class="small interactive" ng-if="$ctrl.state.overrideConfiguration" ng-click="$ctrl.state.overrideConfiguration = false; $ctrl.useDefaultProviderConfiguration()">
|
||||||
|
<i class="fa fa-cogs space-right" aria-hidden="true"></i> Use default configuration
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,8 @@
|
||||||
|
angular.module('portainer.extensions.oauth').component('oauthSettings', {
|
||||||
|
templateUrl: './oauth-settings.html',
|
||||||
|
bindings: {
|
||||||
|
settings: '=',
|
||||||
|
teams: '<'
|
||||||
|
},
|
||||||
|
controller: 'OAuthSettingsController'
|
||||||
|
});
|
|
@ -0,0 +1,13 @@
|
||||||
|
angular.module('portainer.extensions.oauth')
|
||||||
|
.factory('OAuth', ['$resource', 'API_ENDPOINT_OAUTH', function OAuthFactory($resource, API_ENDPOINT_OAUTH) {
|
||||||
|
'use strict';
|
||||||
|
return $resource(API_ENDPOINT_OAUTH + '/:action', {}, {
|
||||||
|
validate: {
|
||||||
|
method: 'POST',
|
||||||
|
ignoreLoadingBar: true,
|
||||||
|
params: {
|
||||||
|
action: 'validate'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}]);
|
|
@ -5,7 +5,7 @@
|
||||||
</a>
|
</a>
|
||||||
</rd-header-title>
|
</rd-header-title>
|
||||||
<rd-header-content>
|
<rd-header-content>
|
||||||
<a ui-sref="portainer.registries">Registries</a> > <a ui-sref="portainer.registries.registry({id: registry.Id})">{{ registry.Name }}</a> > Repositories
|
<a ui-sref="portainer.registries">Registries</a> > <a ng-if="isAdmin" ui-sref="portainer.registries.registry({id: registry.Id})">{{ registry.Name }}</a><span ng-if="!isAdmin">{{ registry.Name}}</span> > Repositories
|
||||||
</rd-header-content>
|
</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.extensions.registrymanagement')
|
angular.module('portainer.extensions.registrymanagement')
|
||||||
.controller('RegistryRepositoriesController', ['$transition$', '$scope', 'RegistryService', 'RegistryV2Service', 'Notifications',
|
.controller('RegistryRepositoriesController', ['$transition$', '$scope', 'RegistryService', 'RegistryV2Service', 'Notifications', 'Authentication',
|
||||||
function ($transition$, $scope, RegistryService, RegistryV2Service, Notifications) {
|
function ($transition$, $scope, RegistryService, RegistryV2Service, Notifications, Authentication) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
displayInvalidConfigurationMessage: false
|
displayInvalidConfigurationMessage: false
|
||||||
|
@ -9,6 +9,13 @@ function ($transition$, $scope, RegistryService, RegistryV2Service, Notification
|
||||||
function initView() {
|
function initView() {
|
||||||
var registryId = $transition$.params().id;
|
var registryId = $transition$.params().id;
|
||||||
|
|
||||||
|
var authenticationEnabled = $scope.applicationState.application.authentication;
|
||||||
|
if (authenticationEnabled) {
|
||||||
|
var userDetails = Authentication.getUserDetails();
|
||||||
|
var isAdmin = userDetails.role === 1;
|
||||||
|
$scope.isAdmin = isAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
RegistryService.registry(registryId)
|
RegistryService.registry(registryId)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.registry = data;
|
$scope.registry = data;
|
||||||
|
|
|
@ -48,7 +48,7 @@ angular.module('portainer.app', [])
|
||||||
|
|
||||||
var authentication = {
|
var authentication = {
|
||||||
name: 'portainer.auth',
|
name: 'portainer.auth',
|
||||||
url: '/auth?redirect',
|
url: '/auth',
|
||||||
params: {
|
params: {
|
||||||
logout: false,
|
logout: false,
|
||||||
error: ''
|
error: ''
|
||||||
|
@ -457,6 +457,16 @@ angular.module('portainer.app', [])
|
||||||
var templates = {
|
var templates = {
|
||||||
name: 'portainer.templates',
|
name: 'portainer.templates',
|
||||||
url: '/templates',
|
url: '/templates',
|
||||||
|
resolve: {
|
||||||
|
endpointID: ['EndpointProvider', '$state',
|
||||||
|
function (EndpointProvider, $state) {
|
||||||
|
var id = EndpointProvider.endpointID();
|
||||||
|
if (!id) {
|
||||||
|
return $state.go('portainer.home');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
templateUrl: './views/templates/templates.html',
|
templateUrl: './views/templates/templates.html',
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actionBar">
|
<div class="actionBar" ng-if="$ctrl.accessManagement">
|
||||||
<button type="button" class="btn btn-sm btn-danger"
|
<button type="button" class="btn btn-sm btn-danger"
|
||||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox" ng-if="$ctrl.accessManagement">
|
||||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||||
<label for="select_all"></label>
|
<label for="select_all"></label>
|
||||||
</span>
|
</span>
|
||||||
|
@ -47,11 +47,12 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox" ng-if="$ctrl.accessManagement">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</span>
|
||||||
<a ui-sref="portainer.registries.registry({id: item.Id})">{{ item.Name }}</a>
|
<a ui-sref="portainer.registries.registry({id: item.Id})" ng-if="$ctrl.accessManagement">{{ item.Name }}</a>
|
||||||
|
<span ng-if="!$ctrl.accessManagement">{{ item.Name }}</span>
|
||||||
<span ng-if="item.Authentication" style="margin-left: 5px;" class="label label-info image-tag">authentication-enabled</span>
|
<span ng-if="item.Authentication" style="margin-left: 5px;" class="label label-info image-tag">authentication-enabled</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
@ -65,8 +65,9 @@
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span ng-if="item.Id === 1 || $ctrl.authenticationMethod !== 2">Internal</span>
|
<span ng-if="item.Id === 1 || $ctrl.authenticationMethod !== 2 && $ctrl.authenticationMethod !== 3">Internal</span>
|
||||||
<span ng-if="item.Id !== 1 && $ctrl.authenticationMethod === 2">LDAP</span>
|
<span ng-if="item.Id !== 1 && $ctrl.authenticationMethod === 2">LDAP</span>
|
||||||
|
<span ng-if="item.Id !== 1 && $ctrl.authenticationMethod === 3">OAuth</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="!$ctrl.dataset">
|
<tr ng-if="!$ctrl.dataset">
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
angular.module('portainer.app')
|
||||||
|
.factory('URLHelper', ['$window', function URLHelperFactory($window) {
|
||||||
|
'use strict';
|
||||||
|
var helper = {};
|
||||||
|
|
||||||
|
helper.getParameter = getParameter;
|
||||||
|
helper.cleanParameters = cleanParameters;
|
||||||
|
|
||||||
|
function getParameter(param) {
|
||||||
|
var parameters = extractParameters();
|
||||||
|
return parameters[param];
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractParameters() {
|
||||||
|
var queryString = $window.location.search.replace(/.*?\?/,'').split('&');
|
||||||
|
return queryString.reduce(function(acc, keyValStr) {
|
||||||
|
var keyVal = keyValStr.split('=');
|
||||||
|
var key = keyVal[0];
|
||||||
|
var val = keyVal[1];
|
||||||
|
acc[key] = val;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanParameters() {
|
||||||
|
$window.location.search = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return helper;
|
||||||
|
}]);
|
|
@ -1,4 +1,5 @@
|
||||||
export function MotdViewModel(data) {
|
export function MotdViewModel(data) {
|
||||||
|
this.Title = data.Title;
|
||||||
this.Message = data.Message;
|
this.Message = data.Message;
|
||||||
this.Hash = data.Hash;
|
this.Hash = data.Hash;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ export function SettingsViewModel(data) {
|
||||||
this.BlackListedLabels = data.BlackListedLabels;
|
this.BlackListedLabels = data.BlackListedLabels;
|
||||||
this.AuthenticationMethod = data.AuthenticationMethod;
|
this.AuthenticationMethod = data.AuthenticationMethod;
|
||||||
this.LDAPSettings = data.LDAPSettings;
|
this.LDAPSettings = data.LDAPSettings;
|
||||||
|
this.OAuthSettings = new OAuthSettingsViewModel(data.OAuthSettings);
|
||||||
this.AllowBindMountsForRegularUsers = data.AllowBindMountsForRegularUsers;
|
this.AllowBindMountsForRegularUsers = data.AllowBindMountsForRegularUsers;
|
||||||
this.AllowPrivilegedModeForRegularUsers = data.AllowPrivilegedModeForRegularUsers;
|
this.AllowPrivilegedModeForRegularUsers = data.AllowPrivilegedModeForRegularUsers;
|
||||||
this.SnapshotInterval = data.SnapshotInterval;
|
this.SnapshotInterval = data.SnapshotInterval;
|
||||||
|
@ -11,6 +12,16 @@ export function SettingsViewModel(data) {
|
||||||
this.EnableHostManagementFeatures = data.EnableHostManagementFeatures;
|
this.EnableHostManagementFeatures = data.EnableHostManagementFeatures;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function PublicSettingsViewModel(settings) {
|
||||||
|
this.AllowBindMountsForRegularUsers = settings.AllowBindMountsForRegularUsers;
|
||||||
|
this.AllowPrivilegedModeForRegularUsers = settings.AllowPrivilegedModeForRegularUsers;
|
||||||
|
this.AuthenticationMethod = settings.AuthenticationMethod;
|
||||||
|
this.EnableHostManagementFeatures = settings.EnableHostManagementFeatures;
|
||||||
|
this.ExternalTemplates = settings.ExternalTemplates;
|
||||||
|
this.LogoURL = settings.LogoURL;
|
||||||
|
this.OAuthLoginURI = settings.OAuthLoginURI;
|
||||||
|
}
|
||||||
|
|
||||||
export function LDAPSettingsViewModel(data) {
|
export function LDAPSettingsViewModel(data) {
|
||||||
this.ReaderDN = data.ReaderDN;
|
this.ReaderDN = data.ReaderDN;
|
||||||
this.Password = data.Password;
|
this.Password = data.Password;
|
||||||
|
@ -31,3 +42,16 @@ export function LDAPGroupSearchSettings(GroupBaseDN, GroupAttribute, GroupFilter
|
||||||
this.GroupAttribute = GroupAttribute;
|
this.GroupAttribute = GroupAttribute;
|
||||||
this.GroupFilter = GroupFilter;
|
this.GroupFilter = GroupFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function OAuthSettingsViewModel(data) {
|
||||||
|
this.ClientID = data.ClientID;
|
||||||
|
this.ClientSecret = data.ClientSecret;
|
||||||
|
this.AccessTokenURI = data.AccessTokenURI;
|
||||||
|
this.AuthorizationURI = data.AuthorizationURI;
|
||||||
|
this.ResourceURI = data.ResourceURI;
|
||||||
|
this.RedirectURI = data.RedirectURI;
|
||||||
|
this.UserIdentifier = data.UserIdentifier;
|
||||||
|
this.Scopes = data.Scopes;
|
||||||
|
this.OAuthAutoCreateUsers = data.OAuthAutoCreateUsers;
|
||||||
|
this.DefaultTeamID = data.DefaultTeamID;
|
||||||
|
}
|
|
@ -65,5 +65,20 @@ angular.module('portainer.app')
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.OAuthAuthenticationEnabled = function() {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
service.extensions(false)
|
||||||
|
.then(function onSuccess(extensions) {
|
||||||
|
var extensionAvailable = _.find(extensions, { Id: 2, Enabled: true }) ? true : false;
|
||||||
|
deferred.resolve(extensionAvailable);
|
||||||
|
})
|
||||||
|
.catch(function onError(err) {
|
||||||
|
deferred.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { SettingsViewModel } from "../../models/settings";
|
import { SettingsViewModel, PublicSettingsViewModel } from "../../models/settings";
|
||||||
|
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.factory('SettingsService', ['$q', 'Settings', function SettingsServiceFactory($q, Settings) {
|
.factory('SettingsService', ['$q', 'Settings', function SettingsServiceFactory($q, Settings) {
|
||||||
|
@ -29,7 +29,7 @@ angular.module('portainer.app')
|
||||||
|
|
||||||
Settings.publicSettings().$promise
|
Settings.publicSettings().$promise
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var settings = new SettingsViewModel(data);
|
var settings = new PublicSettingsViewModel(data);
|
||||||
deferred.resolve(settings);
|
deferred.resolve(settings);
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.factory('Authentication', ['$q', 'Auth', 'jwtHelper', 'LocalStorage', 'StateManager', 'EndpointProvider', function AuthenticationFactory($q, Auth, jwtHelper, LocalStorage, StateManager, EndpointProvider) {
|
.factory('Authentication', [
|
||||||
|
'Auth', 'OAuth', 'jwtHelper', 'LocalStorage', 'StateManager', 'EndpointProvider',
|
||||||
|
function AuthenticationFactory(Auth, OAuth, jwtHelper, LocalStorage, StateManager, EndpointProvider) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var service = {};
|
var service = {};
|
||||||
var user = {};
|
var user = {};
|
||||||
|
|
||||||
service.init = init;
|
service.init = init;
|
||||||
|
service.OAuthLogin = OAuthLogin;
|
||||||
service.login = login;
|
service.login = login;
|
||||||
service.logout = logout;
|
service.logout = logout;
|
||||||
service.isAuthenticated = isAuthenticated;
|
service.isAuthenticated = isAuthenticated;
|
||||||
|
@ -15,30 +18,22 @@ angular.module('portainer.app')
|
||||||
var jwt = LocalStorage.getJWT();
|
var jwt = LocalStorage.getJWT();
|
||||||
|
|
||||||
if (jwt) {
|
if (jwt) {
|
||||||
var tokenPayload = jwtHelper.decodeToken(jwt);
|
setUser(jwt);
|
||||||
user.username = tokenPayload.username;
|
|
||||||
user.ID = tokenPayload.id;
|
|
||||||
user.role = tokenPayload.role;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function OAuthLogin(code) {
|
||||||
|
return OAuth.validate({ code: code }).$promise
|
||||||
|
.then(function onLoginSuccess(response) {
|
||||||
|
return setUser(response.jwt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function login(username, password) {
|
function login(username, password) {
|
||||||
var deferred = $q.defer();
|
return Auth.login({ username: username, password: password }).$promise
|
||||||
|
.then(function onLoginSuccess(response) {
|
||||||
Auth.login({username: username, password: password}).$promise
|
return setUser(response.jwt);
|
||||||
.then(function success(data) {
|
|
||||||
LocalStorage.storeJWT(data.jwt);
|
|
||||||
var tokenPayload = jwtHelper.decodeToken(data.jwt);
|
|
||||||
user.username = username;
|
|
||||||
user.ID = tokenPayload.id;
|
|
||||||
user.role = tokenPayload.role;
|
|
||||||
deferred.resolve();
|
|
||||||
})
|
|
||||||
.catch(function error() {
|
|
||||||
deferred.reject();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
|
@ -56,5 +51,13 @@ angular.module('portainer.app')
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setUser(jwt) {
|
||||||
|
LocalStorage.storeJWT(jwt);
|
||||||
|
var tokenPayload = jwtHelper.decodeToken(jwt);
|
||||||
|
user.username = tokenPayload.username;
|
||||||
|
user.ID = tokenPayload.id;
|
||||||
|
user.role = tokenPayload.role;
|
||||||
|
}
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -18,7 +18,13 @@ angular.module('portainer.app')
|
||||||
var codeMirrorYAMLOptions = {
|
var codeMirrorYAMLOptions = {
|
||||||
mode: 'text/x-yaml',
|
mode: 'text/x-yaml',
|
||||||
gutters: ['CodeMirror-lint-markers'],
|
gutters: ['CodeMirror-lint-markers'],
|
||||||
lint: true
|
lint: true,
|
||||||
|
extraKeys: {
|
||||||
|
Tab: function(cm) {
|
||||||
|
var spaces = Array(cm.getOption('indentUnit') + 1).join(' ');
|
||||||
|
cm.replaceSelection(spaces);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
service.applyCodeMirrorOnElement = function(element, yamlLint, readOnly) {
|
service.applyCodeMirrorOnElement = function(element, yamlLint, readOnly) {
|
||||||
|
|
|
@ -56,6 +56,10 @@
|
||||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||||
You cannot change your password when using LDAP authentication.
|
You cannot change your password when using LDAP authentication.
|
||||||
</span>
|
</span>
|
||||||
|
<span class="text-muted small" style="margin-left: 5px;" ng-if="AuthenticationMethod === 3 && userID !== 1">
|
||||||
|
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||||
|
You cannot change your password when using OAuth authentication.
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !login box logo -->
|
<!-- !login box logo -->
|
||||||
<!-- login panel -->
|
<!-- login panel -->
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default" ng-show="!state.isInOAuthProcess">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<!-- login form -->
|
<!-- login form -->
|
||||||
<form class="simple-box-form form-horizontal">
|
<form class="simple-box-form form-horizontal">
|
||||||
|
@ -28,19 +28,43 @@
|
||||||
<!-- login button -->
|
<!-- login button -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12" >
|
<div class="col-sm-12" >
|
||||||
|
<a ng-href="{{OAuthLoginURI}}" ng-if="AuthenticationMethod === 3">
|
||||||
|
<div class="btn btn-primary btn-sm pull-left" style="margin-left:2px" ng-if="state.OAuthProvider === 'Microsoft'">
|
||||||
|
<i class="fab fa-microsoft" aria-hidden="true"></i> Login with Microsoft
|
||||||
|
</div>
|
||||||
|
<div class="btn btn-primary btn-sm pull-left" style="margin-left:2px" ng-if="state.OAuthProvider === 'Google'">
|
||||||
|
<i class="fab fa-google" aria-hidden="true" ></i> Login with Google
|
||||||
|
</div>
|
||||||
|
<div class="btn btn-primary btn-sm pull-left" style="margin-left:2px" ng-if="state.OAuthProvider === 'Github'">
|
||||||
|
<i class="fab fa-github" aria-hidden="true" ></i> Login with Github
|
||||||
|
</div>
|
||||||
|
<div class="btn btn-primary btn-sm pull-left" style="margin-left:2px" ng-if="state.OAuthProvider === 'OAuth'">
|
||||||
|
<i class="fa fa-sign-in-alt" aria-hidden="true" ></i> Login with OAuth
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary btn-sm pull-right" ng-click="authenticateUser()"><i class="fa fa-sign-in-alt" aria-hidden="true"></i> Login</button>
|
<button type="submit" class="btn btn-primary btn-sm pull-right" ng-click="authenticateUser()"><i class="fa fa-sign-in-alt" aria-hidden="true"></i> Login</button>
|
||||||
<span class="pull-left" style="margin: 5px;" ng-if="state.AuthenticationError">
|
|
||||||
|
<span class="pull-right" style="margin: 5px;" ng-if="state.AuthenticationError">
|
||||||
<i class="fa fa-exclamation-triangle red-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i class="fa fa-exclamation-triangle red-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
<span class="small text-danger">{{ state.AuthenticationError }}</span>
|
<span class="small text-danger">{{ state.AuthenticationError }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- !login button -->
|
<!-- !login button -->
|
||||||
</form>
|
</form>
|
||||||
<!-- !login form -->
|
<!-- !login form -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !login panel -->
|
<!-- !login panel -->
|
||||||
|
<div class="panel panel-default" ng-show="state.isInOAuthProcess">
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="form-group text-center">
|
||||||
|
<span class="small text-muted">OAuth authentication in progress... <span button-spinner="true"></span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !login box -->
|
<!-- !login box -->
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('AuthenticationController', ['$q', '$scope', '$state', '$transition$', '$sanitize', 'Authentication', 'UserService', 'EndpointService', 'StateManager', 'Notifications', 'SettingsService', '$stateParams',
|
.controller('AuthenticationController', ['$q', '$scope', '$state', '$stateParams', '$sanitize', 'Authentication', 'UserService', 'EndpointService', 'StateManager', 'Notifications', 'SettingsService', 'URLHelper',
|
||||||
function ($q, $scope, $state, $transition$, $sanitize, Authentication, UserService, EndpointService, StateManager, Notifications, SettingsService, $stateParams) {
|
function($q, $scope, $state, $stateParams, $sanitize, Authentication, UserService, EndpointService, StateManager, Notifications, SettingsService, URLHelper) {
|
||||||
|
|
||||||
$scope.logo = StateManager.getState().application.logo;
|
$scope.logo = StateManager.getState().application.logo;
|
||||||
|
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
|
@ -10,7 +9,9 @@ function ($q, $scope, $state, $transition$, $sanitize, Authentication, UserServi
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
AuthenticationError: ''
|
AuthenticationError: '',
|
||||||
|
isInOAuthProcess: true,
|
||||||
|
OAuthProvider: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.authenticateUser = function() {
|
$scope.authenticateUser = function() {
|
||||||
|
@ -44,7 +45,7 @@ function ($q, $scope, $state, $transition$, $sanitize, Authentication, UserServi
|
||||||
if (endpoints.length === 0) {
|
if (endpoints.length === 0) {
|
||||||
$state.go('portainer.init.endpoint');
|
$state.go('portainer.init.endpoint');
|
||||||
} else {
|
} else {
|
||||||
$state.go($stateParams.redirect ||'portainer.home');
|
$state.go('portainer.home');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
@ -73,7 +74,7 @@ function ($q, $scope, $state, $transition$, $sanitize, Authentication, UserServi
|
||||||
if (endpoints.length === 0 && userDetails.role === 1) {
|
if (endpoints.length === 0 && userDetails.role === 1) {
|
||||||
$state.go('portainer.init.endpoint');
|
$state.go('portainer.init.endpoint');
|
||||||
} else {
|
} else {
|
||||||
$state.go($stateParams.redirect || 'portainer.home');
|
$state.go('portainer.home');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
@ -81,10 +82,31 @@ function ($q, $scope, $state, $transition$, $sanitize, Authentication, UserServi
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function determineOauthProvider(LoginURI) {
|
||||||
|
if (LoginURI.indexOf('login.microsoftonline.com') !== -1) {
|
||||||
|
return 'Microsoft';
|
||||||
|
}
|
||||||
|
else if (LoginURI.indexOf('accounts.google.com') !== -1) {
|
||||||
|
return 'Google';
|
||||||
|
}
|
||||||
|
else if (LoginURI.indexOf('github.com') !== -1) {
|
||||||
|
return 'Github';
|
||||||
|
}
|
||||||
|
return 'OAuth';
|
||||||
|
}
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
if ($transition$.params().logout || $transition$.params().error) {
|
SettingsService.publicSettings()
|
||||||
|
.then(function success(settings) {
|
||||||
|
$scope.AuthenticationMethod = settings.AuthenticationMethod;
|
||||||
|
$scope.OAuthLoginURI = settings.OAuthLoginURI;
|
||||||
|
$scope.state.OAuthProvider = determineOauthProvider(settings.OAuthLoginURI);
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($stateParams.logout || $stateParams.error) {
|
||||||
Authentication.logout();
|
Authentication.logout();
|
||||||
$scope.state.AuthenticationError = $transition$.params().error;
|
$scope.state.AuthenticationError = $stateParams.error;
|
||||||
|
$scope.state.isInOAuthProcess = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,7 +120,26 @@ function ($q, $scope, $state, $transition$, $sanitize, Authentication, UserServi
|
||||||
} else {
|
} else {
|
||||||
authenticatedFlow();
|
authenticatedFlow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var code = URLHelper.getParameter('code');
|
||||||
|
if (code) {
|
||||||
|
oAuthLogin(code);
|
||||||
|
} else {
|
||||||
|
$scope.state.isInOAuthProcess = false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function oAuthLogin(code) {
|
||||||
|
return Authentication.OAuthLogin(code)
|
||||||
|
.then(function success() {
|
||||||
|
URLHelper.cleanParameters();
|
||||||
|
})
|
||||||
|
.catch(function error() {
|
||||||
|
$scope.state.AuthenticationError = 'Unable to login via OAuth';
|
||||||
|
$scope.state.isInOAuthProcess = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
initView();
|
initView();
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -22,7 +22,7 @@ function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService,
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.copyAgentCommand = function() {
|
$scope.copyAgentCommand = function() {
|
||||||
clipboard.copyText('curl -L https://portainer.io/download/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent');
|
clipboard.copyText('curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent');
|
||||||
$('#copyNotification').show();
|
$('#copyNotification').show();
|
||||||
$('#copyNotification').fadeOut(2000);
|
$('#copyNotification').fadeOut(2000);
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
<span class="text-muted" style="font-size: 90%;">
|
<span class="text-muted" style="font-size: 90%;">
|
||||||
<p>
|
<p>
|
||||||
Portainer CE is a great way of managing clusters, provisioning containers and services and
|
Portainer CE is a great way of managing clusters, provisioning containers and services and
|
||||||
managing container environment lifecycles. To extend the benefit of Portainer CE even
|
managing container environment lifecycles. To extend the benefit of Portainer CE even more,
|
||||||
more, and to address the needs of larger, complex or critical environments, the Portainer
|
and to address the needs of larger, complex or critical environments, the Portainer
|
||||||
team provides a growing range of low-cost Extensions.
|
team provides a growing range of low-cost Extensions.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -39,19 +39,18 @@
|
||||||
<p>
|
<p>
|
||||||
The advantage of an extensible design is clear: While a range of capability is available, only
|
The advantage of an extensible design is clear: While a range of capability is available, only
|
||||||
necessary functionality is added as and when needed.
|
necessary functionality is added as and when needed.
|
||||||
|
To ensure that Portainer remains the best choice for managing production container platforms,
|
||||||
|
the Portainer team have chosen a modular, extensible design approach, where additional capability
|
||||||
|
can be added to the Portainer CE core as needed, and at very low cost.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Our first extension is <a ui-sref="portainer.extensions.extension({id: 1})">Registry Manager</a>, available now. Others (such as
|
Available through a simple subscription process from the list below, Portainer Extensions
|
||||||
Single Sign On and Operations Management) are scheduled for the early part of 2019.
|
provide a simple way to enhance Portainer CE’s core functionality through incremental capability in important areas.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Portainer CE is the core of the Portainer management environments. Portainer CE will
|
For additional information on Portainer Extensions, see our website <a href="https://www.portainer.io/products-services/portainer-extension-software/" target="_blank">here</a>.
|
||||||
continue to be developed and made freely available as part of our deep commitment to our
|
|
||||||
Open Source heritage and our user community. Portainer CE will always deliver great
|
|
||||||
functionality and remain the industry standard toolset for managing container-based
|
|
||||||
platforms.
|
|
||||||
</p>
|
</p>
|
||||||
</span>
|
</span>
|
||||||
</information-panel>
|
</information-panel>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
<information-panel
|
<information-panel
|
||||||
ng-if="motd && motd.Message !== '' && applicationState.UI.dismissedInfoHash !== motd.Hash"
|
ng-if="motd && motd.Message !== '' && applicationState.UI.dismissedInfoHash !== motd.Hash"
|
||||||
title-text="Important message"
|
title-text="{{ motd.Title }}"
|
||||||
dismiss-action="dismissImportantInformation(motd.Hash)">
|
dismiss-action="dismissImportantInformation(motd.Hash)">
|
||||||
<span class="text-muted">
|
<span class="text-muted">
|
||||||
<p ng-bind-html="motd.Message"></p>
|
<p ng-bind-html="motd.Message"></p>
|
||||||
|
|
|
@ -66,7 +66,7 @@ function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, G
|
||||||
EndpointProvider.setEndpointID(endpoint.Id);
|
EndpointProvider.setEndpointID(endpoint.Id);
|
||||||
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
||||||
EndpointProvider.setOfflineModeFromStatus(endpoint.Status);
|
EndpointProvider.setOfflineModeFromStatus(endpoint.Status);
|
||||||
StateManager.updateEndpointState(endpoint.Name, endpoint.Type, [])
|
StateManager.updateEndpointState(endpoint, [])
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
$state.go('azure.dashboard');
|
$state.go('azure.dashboard');
|
||||||
})
|
})
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<rd-header-content>Registry management</rd-header-content>
|
<rd-header-content>Registry management</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
|
||||||
<div class="row" ng-if="dockerhub">
|
<div class="row" ng-if="dockerhub && isAdmin">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-header icon="fa-database" title-text="DockerHub">
|
<rd-widget-header icon="fa-database" title-text="DockerHub">
|
||||||
|
@ -74,7 +74,7 @@
|
||||||
title-text="Registries" title-icon="fa-database"
|
title-text="Registries" title-icon="fa-database"
|
||||||
dataset="registries" table-key="registries"
|
dataset="registries" table-key="registries"
|
||||||
order-by="Name"
|
order-by="Name"
|
||||||
access-management="applicationState.application.authentication"
|
access-management="applicationState.application.authentication && isAdmin"
|
||||||
remove-action="removeAction"
|
remove-action="removeAction"
|
||||||
registry-management="registryManagementAvailable"
|
registry-management="registryManagementAvailable"
|
||||||
></registries-datatable>
|
></registries-datatable>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('RegistriesController', ['$q', '$scope', '$state', 'RegistryService', 'DockerHubService', 'ModalService', 'Notifications', 'ExtensionService',
|
.controller('RegistriesController', ['$q', '$scope', '$state', 'RegistryService', 'DockerHubService', 'ModalService', 'Notifications', 'ExtensionService', 'Authentication',
|
||||||
function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, Notifications, ExtensionService) {
|
function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, Notifications, ExtensionService, Authentication) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
actionInProgress: false
|
actionInProgress: false
|
||||||
|
@ -67,6 +67,12 @@ function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, N
|
||||||
$scope.registries = data.registries;
|
$scope.registries = data.registries;
|
||||||
$scope.dockerhub = data.dockerhub;
|
$scope.dockerhub = data.dockerhub;
|
||||||
$scope.registryManagementAvailable = data.registryManagement;
|
$scope.registryManagementAvailable = data.registryManagement;
|
||||||
|
var authenticationEnabled = $scope.applicationState.application.authentication;
|
||||||
|
if (authenticationEnabled) {
|
||||||
|
var userDetails = Authentication.getUserDetails();
|
||||||
|
var isAdmin = userDetails.role === 1;
|
||||||
|
$scope.isAdmin = isAdmin;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
$scope.registries = [];
|
$scope.registries = [];
|
||||||
|
|
|
@ -37,23 +37,49 @@
|
||||||
<p>LDAP authentication</p>
|
<p>LDAP authentication</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div ng-if="oauthAuthenticationAvailable">
|
||||||
|
<input type="radio" id="registry_auth" ng-model="settings.AuthenticationMethod" ng-value="3">
|
||||||
|
<label for="registry_auth">
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fa fa-users" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
OAuth
|
||||||
|
</div>
|
||||||
|
<p>OAuth authentication</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div style="color: #767676;" ng-click="goToOAuthExtensionView()" ng-if="!oauthAuthenticationAvailable">
|
||||||
|
<input type="radio" id="registry_auth" ng-model="settings.AuthenticationMethod" ng-value="3" disabled>
|
||||||
|
<label for="registry_auth" tooltip-append-to-body="true" tooltip-placement="bottom" tooltip-class="portainer-tooltip" uib-tooltip="Feature available via an extension" style="cursor:pointer; border-color: #767676">
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fa fa-users" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
OAuth (extension)
|
||||||
|
</div>
|
||||||
|
<p>OAuth authentication</p>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-if="settings.AuthenticationMethod === 1">
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Information
|
Information
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" ng-if="settings.AuthenticationMethod === 1">
|
<div class="form-group col-sm-12 text-muted small">
|
||||||
<span class="col-sm-12 text-muted small">
|
|
||||||
When using internal authentication, Portainer will encrypt user passwords and store credentials locally.
|
When using internal authentication, Portainer will encrypt user passwords and store credentials locally.
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" ng-if="settings.AuthenticationMethod === 2">
|
|
||||||
<span class="col-sm-12 text-muted small">
|
|
||||||
When using LDAP authentication, Portainer will delegate user authentication to a LDAP server and fallback to internal authentication if LDAP authentication fails.
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div ng-if="settings.AuthenticationMethod === 2">
|
<div ng-if="settings.AuthenticationMethod === 2">
|
||||||
|
<div>
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Information
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-sm-12 text-muted small">
|
||||||
|
When using LDAP authentication, Portainer will delegate user authentication to a LDAP server and fallback to internal authentication if LDAP authentication fails.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
LDAP configuration
|
LDAP configuration
|
||||||
</div>
|
</div>
|
||||||
|
@ -306,7 +332,12 @@
|
||||||
<!-- !group-search-settings -->
|
<!-- !group-search-settings -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<oauth-settings ng-if="isOauthEnabled()" settings="OAuthSettings" teams="teams"></oauth-settings>
|
||||||
|
|
||||||
<!-- actions -->
|
<!-- actions -->
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Actions
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="button" class="btn btn-primary btn-sm" ng-click="saveSettings()" ng-disabled="state.actionInProgress" button-spinner="state.actionInProgress">
|
<button type="button" class="btn btn-primary btn-sm" ng-click="saveSettings()" ng-disabled="state.actionInProgress" button-spinner="state.actionInProgress">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('SettingsAuthenticationController', ['$q', '$scope', 'Notifications', 'SettingsService', 'FileUploadService',
|
.controller('SettingsAuthenticationController', ['$q', '$scope', '$state', 'Notifications', 'SettingsService', 'FileUploadService', 'TeamService', 'ExtensionService',
|
||||||
function ($q, $scope, Notifications, SettingsService, FileUploadService) {
|
function($q, $scope, $state, Notifications, SettingsService, FileUploadService, TeamService, ExtensionService) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
successfulConnectivityCheck: false,
|
successfulConnectivityCheck: false,
|
||||||
|
@ -14,6 +14,14 @@ function ($q, $scope, Notifications, SettingsService, FileUploadService) {
|
||||||
TLSCACert: ''
|
TLSCACert: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.goToOAuthExtensionView = function() {
|
||||||
|
$state.go('portainer.extensions.extension', { id: 2 });
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.isOauthEnabled = function isOauthEnabled() {
|
||||||
|
return $scope.settings && $scope.settings.AuthenticationMethod === 3;
|
||||||
|
};
|
||||||
|
|
||||||
$scope.addSearchConfiguration = function() {
|
$scope.addSearchConfiguration = function() {
|
||||||
$scope.LDAPSettings.SearchSettings.push({ BaseDN: '', UserNameAttribute: '', Filter: '' });
|
$scope.LDAPSettings.SearchSettings.push({ BaseDN: '', UserNameAttribute: '', Filter: '' });
|
||||||
};
|
};
|
||||||
|
@ -92,12 +100,19 @@ function ($q, $scope, Notifications, SettingsService, FileUploadService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
SettingsService.settings()
|
$q.all({
|
||||||
|
settings: SettingsService.settings(),
|
||||||
|
teams: TeamService.teams(),
|
||||||
|
oauthAuthentication: ExtensionService.OAuthAuthenticationEnabled()
|
||||||
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var settings = data;
|
var settings = data.settings;
|
||||||
|
$scope.teams = data.teams;
|
||||||
$scope.settings = settings;
|
$scope.settings = settings;
|
||||||
$scope.LDAPSettings = settings.LDAPSettings;
|
$scope.LDAPSettings = settings.LDAPSettings;
|
||||||
|
$scope.OAuthSettings = settings.OAuthSettings;
|
||||||
$scope.formValues.TLSCACert = settings.LDAPSettings.TLSConfig.TLSCACert;
|
$scope.formValues.TLSCACert = settings.LDAPSettings.TLSConfig.TLSCACert;
|
||||||
|
$scope.oauthAuthenticationAvailable = data.oauthAuthentication;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve application settings');
|
Notifications.error('Failure', err, 'Unable to retrieve application settings');
|
||||||
|
|
|
@ -63,7 +63,7 @@
|
||||||
<a ui-sref="portainer.tags" ui-sref-active="active">Tags</a>
|
<a ui-sref="portainer.tags" ui-sref-active="active">Tags</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
|
<li class="sidebar-list" ng-if="applicationState.application.authentication">
|
||||||
<a ui-sref="portainer.registries" ui-sref-active="active">Registries <span class="menu-icon fa fa-database fa-fw"></span></a>
|
<a ui-sref="portainer.registries" ui-sref-active="active">Registries <span class="menu-icon fa fa-database fa-fw"></span></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
|
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
Name: portainer
|
Name: portainer
|
||||||
Version: 1.20.1
|
Version: 1.20.2
|
||||||
Release: 0
|
Release: 0
|
||||||
License: Zlib
|
License: Zlib
|
||||||
Summary: A lightweight docker management UI
|
Summary: A lightweight docker management UI
|
||||||
|
|
12
gruntfile.js
12
gruntfile.js
|
@ -62,6 +62,8 @@ module.exports = function(grunt) {
|
||||||
});
|
});
|
||||||
|
|
||||||
grunt.registerTask('lint', ['eslint']);
|
grunt.registerTask('lint', ['eslint']);
|
||||||
|
grunt.registerTask('run-dev', ['build', 'shell:run', 'watch:build']);
|
||||||
|
grunt.registerTask('clear', ['clean:app']);
|
||||||
|
|
||||||
grunt.registerTask('run-dev', [
|
grunt.registerTask('run-dev', [
|
||||||
'config:dev',
|
'config:dev',
|
||||||
|
@ -74,7 +76,7 @@ module.exports = function(grunt) {
|
||||||
grunt.initConfig({
|
grunt.initConfig({
|
||||||
root: 'dist',
|
root: 'dist',
|
||||||
distdir: 'dist/public',
|
distdir: 'dist/public',
|
||||||
shippedDockerVersion: '18.09.0',
|
shippedDockerVersion: '18.09.3',
|
||||||
shippedDockerVersionWindows: '17.09.0-ce',
|
shippedDockerVersionWindows: '17.09.0-ce',
|
||||||
pkg: grunt.file.readJSON('package.json'),
|
pkg: grunt.file.readJSON('package.json'),
|
||||||
config: gruntfile_cfg.config,
|
config: gruntfile_cfg.config,
|
||||||
|
@ -153,7 +155,7 @@ gruntfile_cfg.copy = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function shell_buildBinary(p, a) {
|
function shell_buildBinary(p, a) {
|
||||||
var binfile = 'dist/portainer-' + p + '-' + a;
|
var binfile = 'dist/portainer';
|
||||||
if (p === 'linux') {
|
if (p === 'linux') {
|
||||||
return [
|
return [
|
||||||
'if [ -f ' + (binfile) + ' ]; then',
|
'if [ -f ' + (binfile) + ' ]; then',
|
||||||
|
@ -181,12 +183,10 @@ function shell_buildBinaryOnDevOps(p, a) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function shell_run(arch) {
|
function shell_run() {
|
||||||
return [
|
return [
|
||||||
'docker rm -f portainer',
|
'docker rm -f portainer',
|
||||||
'docker run -d -p 9000:9000 -v $(pwd)/dist:/app -v /tmp/portainer:/data -v /var/run/docker.sock:/var/run/docker.sock:z --name portainer portainer/base /app/portainer-linux-' +
|
'docker run -d -p 9000:9000 -v $(pwd)/dist:/app -v /tmp/portainer:/data -v /var/run/docker.sock:/var/run/docker.sock:z --name portainer portainer/base /app/portainer --no-analytics --template-file /app/templates.json'
|
||||||
arch +
|
|
||||||
' --no-analytics --template-file /app/templates.json'
|
|
||||||
].join(';');
|
].join(';');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
11
package.json
11
package.json
|
@ -2,7 +2,7 @@
|
||||||
"author": "Portainer.io",
|
"author": "Portainer.io",
|
||||||
"name": "portainer",
|
"name": "portainer",
|
||||||
"homepage": "http://portainer.io",
|
"homepage": "http://portainer.io",
|
||||||
"version": "1.20.1",
|
"version": "1.20.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git@github.com:portainer/portainer.git"
|
"url": "git@github.com:portainer/portainer.git"
|
||||||
|
@ -20,13 +20,12 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"grunt": "grunt",
|
|
||||||
"start": "grunt run-dev",
|
"start": "grunt run-dev",
|
||||||
"start:client": "webpack-dev-server",
|
"start:client": "webpack-dev-server",
|
||||||
"start:server": "yarn build:server:offline && grunt shell:run:amd64",
|
"start:server": "yarn build:server:offline && grunt shell:run:amd64",
|
||||||
"clean:all": "grunt clean:all",
|
"clean:all": "grunt clean:all",
|
||||||
"build": "NODE_ENV=production grunt build",
|
"build": "NODE_ENV=production grunt build",
|
||||||
"build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer-linux-amd64",
|
"build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer",
|
||||||
"build:client": "NODE_ENV=production grunt build-webapp"
|
"build:client": "NODE_ENV=production grunt build-webapp"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -39,7 +38,7 @@
|
||||||
"angular-clipboard": "^1.6.2",
|
"angular-clipboard": "^1.6.2",
|
||||||
"angular-cookies": "~1.5.0",
|
"angular-cookies": "~1.5.0",
|
||||||
"angular-file-saver": "^1.1.3",
|
"angular-file-saver": "^1.1.3",
|
||||||
"angular-google-analytics": "github:revolunet/angular-google-analytics#~1.1.9",
|
"angular-google-analytics": "github:revolunet/angular-google-analytics#semver:~1.1.9",
|
||||||
"angular-json-tree": "1.0.1",
|
"angular-json-tree": "1.0.1",
|
||||||
"angular-jwt": "~0.1.8",
|
"angular-jwt": "~0.1.8",
|
||||||
"angular-loading-bar": "~0.9.0",
|
"angular-loading-bar": "~0.9.0",
|
||||||
|
@ -66,8 +65,8 @@
|
||||||
"moment": "^2.21.0",
|
"moment": "^2.21.0",
|
||||||
"ng-file-upload": "~12.2.13",
|
"ng-file-upload": "~12.2.13",
|
||||||
"rdash-ui": "1.0.*",
|
"rdash-ui": "1.0.*",
|
||||||
"splitargs": "github:deviantony/splitargs#~0.2.0",
|
"splitargs": "github:deviantony/splitargs#semver:~0.2.0",
|
||||||
"toastr": "^2.1.4",
|
"toastr": "github:CodeSeven/toastr#semver:~2.1.3",
|
||||||
"ui-select": "^0.19.8",
|
"ui-select": "^0.19.8",
|
||||||
"xterm": "^3.8.0"
|
"xterm": "^3.8.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -914,7 +914,7 @@ angular-file-saver@^1.1.3:
|
||||||
blob-tmp "^1.0.0"
|
blob-tmp "^1.0.0"
|
||||||
file-saver "^1.3.3"
|
file-saver "^1.3.3"
|
||||||
|
|
||||||
"angular-google-analytics@github:revolunet/angular-google-analytics#~1.1.9":
|
"angular-google-analytics@github:revolunet/angular-google-analytics#semver:~1.1.9":
|
||||||
version "1.1.8"
|
version "1.1.8"
|
||||||
resolved "https://codeload.github.com/revolunet/angular-google-analytics/tar.gz/92768a525870bc066dcf85fbe9d9f115358a6d91"
|
resolved "https://codeload.github.com/revolunet/angular-google-analytics/tar.gz/92768a525870bc066dcf85fbe9d9f115358a6d91"
|
||||||
|
|
||||||
|
@ -10206,7 +10206,7 @@ split@0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
through "2"
|
through "2"
|
||||||
|
|
||||||
"splitargs@github:deviantony/splitargs#~0.2.0":
|
"splitargs@github:deviantony/splitargs#semver:~0.2.0":
|
||||||
version "0.0.7"
|
version "0.0.7"
|
||||||
resolved "https://codeload.github.com/deviantony/splitargs/tar.gz/2a87a1dfb1f9698b94e28e3106ad34057841dbd1"
|
resolved "https://codeload.github.com/deviantony/splitargs/tar.gz/2a87a1dfb1f9698b94e28e3106ad34057841dbd1"
|
||||||
|
|
||||||
|
@ -10781,10 +10781,9 @@ to-regex@^3.0.1, to-regex@^3.0.2:
|
||||||
regex-not "^1.0.2"
|
regex-not "^1.0.2"
|
||||||
safe-regex "^1.1.0"
|
safe-regex "^1.1.0"
|
||||||
|
|
||||||
toastr@^2.1.4:
|
"toastr@github:CodeSeven/toastr#semver:~2.1.3":
|
||||||
version "2.1.4"
|
version "2.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/toastr/-/toastr-2.1.4.tgz#8b43be64fb9d0c414871446f2db8e8ca4e95f181"
|
resolved "https://codeload.github.com/CodeSeven/toastr/tar.gz/1ef00d723691b563b610077a08539391386826b3"
|
||||||
integrity sha1-i0O+ZPudDEFIcURvLbjoyk6V8YE=
|
|
||||||
dependencies:
|
dependencies:
|
||||||
jquery ">=1.12.0"
|
jquery ">=1.12.0"
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue