Add support for Nylas (and other) integration authentication
This commit is contained in:
committed by
Jože Fortun
parent
9bd739cc9c
commit
d53fa26044
@@ -9,6 +9,15 @@
|
||||
{{ $t('enabled') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group label-cols="3">
|
||||
<b-form-checkbox
|
||||
v-model="providerUsage"
|
||||
:value="true"
|
||||
:unchecked-value="false"
|
||||
>
|
||||
{{ $t('apiAccess') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group
|
||||
:label="$t('clientKey')"
|
||||
label-cols="3"
|
||||
@@ -59,5 +68,20 @@ export default {
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
// providerUsage provides a temporary checkbox implementation until something
|
||||
// more proper is done
|
||||
//
|
||||
// When checked (true), we consider it as wanting to use the API
|
||||
providerUsage: {
|
||||
get () {
|
||||
return ((this.value || {}).usage || []).includes('api')
|
||||
},
|
||||
set (value) {
|
||||
this.value.usage = value ? ['api'] : []
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -108,6 +108,7 @@ const idpStandard = [
|
||||
'github',
|
||||
'facebook',
|
||||
'linkedin',
|
||||
'nylas',
|
||||
]
|
||||
|
||||
const idpSecurity = {
|
||||
@@ -140,7 +141,7 @@ function prepareExternal (external) {
|
||||
let out = { ...base }
|
||||
|
||||
for (let k in base) {
|
||||
out[k] = extractKey(`providers.${provider}.${k}`, typeof out[k])
|
||||
out[k] = extractKey(`providers.${provider}.${k}`, Array.isArray(out[k]) ? 'array' : typeof out[k])
|
||||
}
|
||||
|
||||
return out
|
||||
@@ -185,6 +186,7 @@ function prepareExternal (external) {
|
||||
secret: '',
|
||||
key: '',
|
||||
security: {},
|
||||
usage: [],
|
||||
}),
|
||||
security: extractSec(`providers.${handle}`),
|
||||
}))
|
||||
@@ -395,7 +397,7 @@ export default {
|
||||
`${prefix}.${p.handle}`,
|
||||
p,
|
||||
o.standard[i],
|
||||
['key', 'secret', 'enabled', 'security'],
|
||||
['key', 'secret', 'enabled', 'security', 'usage'],
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -620,6 +620,13 @@ func updateAuthSettings(svc authServicer, current *types.AppSettings) {
|
||||
|
||||
for _, p := range current.Auth.External.Providers {
|
||||
if p.ValidConfiguration() {
|
||||
usage := p.Usage
|
||||
|
||||
// By default, use as an identity provider
|
||||
if len(p.Usage) == 0 {
|
||||
p.Usage = []string{types.ExternalProviderUsageIdentity}
|
||||
}
|
||||
|
||||
as.Providers = append(as.Providers, authSettings.Provider{
|
||||
Handle: p.Handle,
|
||||
Label: p.Label,
|
||||
@@ -628,6 +635,7 @@ func updateAuthSettings(svc authServicer, current *types.AppSettings) {
|
||||
RedirectUrl: p.RedirectUrl,
|
||||
Secret: p.Secret,
|
||||
Scope: p.Scope,
|
||||
Usage: usage,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+13
-12
@@ -184,18 +184,19 @@ func New(ctx context.Context, log *zap.Logger, oa2m oauth2def.Manager, s store.S
|
||||
}
|
||||
|
||||
svc.handlers = &handlers.AuthHandlers{
|
||||
Locale: locale.Global(),
|
||||
Log: log,
|
||||
Templates: tpls,
|
||||
SessionManager: sesManager,
|
||||
OAuth2: oauth2Server,
|
||||
AuthService: systemService.DefaultAuth,
|
||||
UserService: systemService.DefaultUser,
|
||||
ClientService: &clientService{s},
|
||||
TokenService: &tokenService{s},
|
||||
DefaultClient: defClient,
|
||||
Opt: svc.opt,
|
||||
Settings: svc.settings,
|
||||
Locale: locale.Global(),
|
||||
Log: log,
|
||||
Templates: tpls,
|
||||
SessionManager: sesManager,
|
||||
OAuth2: oauth2Server,
|
||||
AuthService: systemService.DefaultAuth,
|
||||
CredentialsService: systemService.DefaultCredentials,
|
||||
UserService: systemService.DefaultUser,
|
||||
ClientService: &clientService{s},
|
||||
TokenService: &tokenService{s},
|
||||
DefaultClient: defClient,
|
||||
Opt: svc.opt,
|
||||
Settings: svc.settings,
|
||||
}
|
||||
|
||||
external.Init(sesManager.Store())
|
||||
|
||||
Vendored
+3
@@ -3,6 +3,7 @@ package external
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/auth/external/nylas"
|
||||
"github.com/cortezaproject/corteza/server/auth/settings"
|
||||
"github.com/markbates/goth"
|
||||
"github.com/markbates/goth/providers/facebook"
|
||||
@@ -75,6 +76,8 @@ func SetupGothProviders(log *zap.Logger, redirectUrl string, ep ...settings.Prov
|
||||
provider = google.New(pc.Key, pc.Secret, redirect, "email")
|
||||
case "linkedin":
|
||||
provider = linkedin.New(pc.Key, pc.Secret, redirect, "email")
|
||||
case "nylas":
|
||||
provider = nylas.New(pc.Key, pc.Secret, redirect, "email")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+174
@@ -0,0 +1,174 @@
|
||||
package nylas
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/markbates/goth"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// These vars define the Authentication, Token, and API URLS for Nylas.
|
||||
var (
|
||||
AuthURL = "https://api.nylas.com/oauth/authorize"
|
||||
TokenURL = "https://api.nylas.com/oauth/token"
|
||||
ProfileURL = "https://api.Nylas.com/account"
|
||||
)
|
||||
|
||||
// New creates a new Nylas provider, and sets up important connection details.
|
||||
// You should always call `Nylas.New` to get a new Provider. Never try to create
|
||||
// one manually.
|
||||
func New(clientKey, secret, callbackURL string, scopes ...string) *Provider {
|
||||
return NewCustomisedURL(clientKey, secret, callbackURL, AuthURL, TokenURL, ProfileURL, scopes...)
|
||||
}
|
||||
|
||||
// NewCustomisedURL is similar to New(...) but can be used to set custom URLs to connect to
|
||||
func NewCustomisedURL(clientKey, secret, callbackURL, authURL, tokenURL, profileURL string, scopes ...string) *Provider {
|
||||
p := &Provider{
|
||||
ClientKey: clientKey,
|
||||
Secret: secret,
|
||||
CallbackURL: callbackURL,
|
||||
providerName: "nylas",
|
||||
profileURL: profileURL,
|
||||
}
|
||||
p.config = newConfig(p, authURL, tokenURL, scopes)
|
||||
return p
|
||||
}
|
||||
|
||||
// Provider is the implementation of `goth.Provider` for accessing Nylas.
|
||||
type Provider struct {
|
||||
ClientKey string
|
||||
Secret string
|
||||
CallbackURL string
|
||||
HTTPClient *http.Client
|
||||
config *oauth2.Config
|
||||
providerName string
|
||||
profileURL string
|
||||
}
|
||||
|
||||
// Name is the name used to retrieve this provider later.
|
||||
func (p *Provider) Name() string {
|
||||
return p.providerName
|
||||
}
|
||||
|
||||
// SetName is to update the name of the provider (needed in case of multiple providers of 1 type)
|
||||
func (p *Provider) SetName(name string) {
|
||||
p.providerName = name
|
||||
}
|
||||
|
||||
func (p *Provider) Client() *http.Client {
|
||||
return goth.HTTPClientWithFallBack(p.HTTPClient)
|
||||
}
|
||||
|
||||
// Debug is a no-op for the Nylas package.
|
||||
func (p *Provider) Debug(debug bool) {}
|
||||
|
||||
// BeginAuth asks Nylas for an authentication end-point.
|
||||
func (p *Provider) BeginAuth(state string) (goth.Session, error) {
|
||||
url := p.config.AuthCodeURL(state)
|
||||
session := &Session{
|
||||
AuthURL: url,
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// FetchUser will go to Nylas and access basic information about the user.
|
||||
func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
|
||||
sess := session.(*Session)
|
||||
user := goth.User{
|
||||
AccessToken: sess.AccessToken,
|
||||
Provider: p.Name(),
|
||||
}
|
||||
|
||||
if user.AccessToken == "" {
|
||||
// data is not yet retrieved since accessToken is still empty
|
||||
return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", p.profileURL, nil)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", "Bearer "+sess.AccessToken)
|
||||
response, err := p.Client().Do(req)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return user, fmt.Errorf("Nylas API responded with a %d trying to fetch user information", response.StatusCode)
|
||||
}
|
||||
|
||||
bits, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
err = json.NewDecoder(bytes.NewReader(bits)).Decode(&user.RawData)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
err = userFromReader(bytes.NewReader(bits), &user)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
return user, err
|
||||
}
|
||||
|
||||
func userFromReader(reader io.Reader, user *goth.User) error {
|
||||
u := struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email_address"`
|
||||
Name string `json:"name"`
|
||||
}{}
|
||||
|
||||
err := json.NewDecoder(reader).Decode(&u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.Name = u.Name
|
||||
user.NickName = u.Name
|
||||
user.Email = u.Email
|
||||
user.UserID = u.ID
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func newConfig(provider *Provider, authURL, tokenURL string, scopes []string) *oauth2.Config {
|
||||
c := &oauth2.Config{
|
||||
ClientID: provider.ClientKey,
|
||||
ClientSecret: provider.Secret,
|
||||
RedirectURL: provider.CallbackURL,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: authURL,
|
||||
TokenURL: tokenURL,
|
||||
},
|
||||
Scopes: []string{},
|
||||
}
|
||||
|
||||
for _, scope := range scopes {
|
||||
c.Scopes = append(c.Scopes, scope)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
//RefreshToken refresh token is not provided by Nylas
|
||||
func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
|
||||
return nil, errors.New("Refresh token is not provided by Nylas")
|
||||
}
|
||||
|
||||
//RefreshTokenAvailable refresh token is not provided by Nylas
|
||||
func (p *Provider) RefreshTokenAvailable() bool {
|
||||
return false
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package nylas
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/markbates/goth"
|
||||
)
|
||||
|
||||
// Session stores data during the auth process with Nylas.
|
||||
type Session struct {
|
||||
AuthURL string
|
||||
AccessToken string
|
||||
}
|
||||
|
||||
// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Nylas provider.
|
||||
func (s Session) GetAuthURL() (string, error) {
|
||||
if s.AuthURL == "" {
|
||||
return "", errors.New(goth.NoAuthUrlErrorMessage)
|
||||
}
|
||||
return s.AuthURL, nil
|
||||
}
|
||||
|
||||
// Authorize the session with Nylas and return the access token to be stored for future use.
|
||||
func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) {
|
||||
p := provider.(*Provider)
|
||||
token, err := p.config.Exchange(goth.ContextForClient(p.Client()), params.Get("code"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !token.Valid() {
|
||||
return "", errors.New("Invalid token received from provider")
|
||||
}
|
||||
|
||||
s.AccessToken = token.AccessToken
|
||||
return token.AccessToken, err
|
||||
}
|
||||
|
||||
// Marshal the session into a string
|
||||
func (s Session) Marshal() string {
|
||||
b, _ := json.Marshal(s)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (s Session) String() string {
|
||||
return s.Marshal()
|
||||
}
|
||||
|
||||
// UnmarshalSession will unmarshal a JSON string into a session.
|
||||
func (p *Provider) UnmarshalSession(data string) (goth.Session, error) {
|
||||
sess := &Session{}
|
||||
err := json.NewDecoder(strings.NewReader(data)).Decode(sess)
|
||||
return sess, err
|
||||
}
|
||||
Vendored
-1
@@ -50,7 +50,6 @@ func AddProvider(ctx context.Context, log *zap.Logger, s store.SettingValues, ea
|
||||
}
|
||||
if vv, err := eap.EncodeKV(); err != nil {
|
||||
return fmt.Errorf("could not encode auth provider values: %w", err)
|
||||
return err
|
||||
} else if err = store.UpsertSettingValue(ctx, s, vv...); err != nil {
|
||||
return fmt.Errorf("could not store auth provider values: %w", err)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import (
|
||||
|
||||
"github.com/cortezaproject/corteza/server/auth/external"
|
||||
"github.com/cortezaproject/corteza/server/auth/request"
|
||||
"github.com/cortezaproject/corteza/server/auth/settings"
|
||||
"github.com/cortezaproject/corteza/server/pkg/api"
|
||||
"github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.uber.org/zap"
|
||||
@@ -49,14 +51,25 @@ func (h AuthHandlers) handleSuccessfulExternalAuth(w http.ResponseWriter, r *htt
|
||||
|
||||
h.Log.Info("login successful", zap.String("provider", cred.Provider))
|
||||
|
||||
// Try to login/sign-up external user
|
||||
if user, err = h.AuthService.External(ctx, cred); err != nil {
|
||||
api.Send(w, r, err)
|
||||
// Get the provider config so we can correctly handle the provided values
|
||||
p := h.getProviderConfig(cred.Provider, h.Settings.Providers)
|
||||
if p == nil {
|
||||
api.Send(w, r, fmt.Errorf("credentials for provider %s are not registered in the system", cred.Provider))
|
||||
return
|
||||
}
|
||||
|
||||
h.handle(func(req *request.AuthReq) error {
|
||||
req.AuthUser = request.NewAuthUser(
|
||||
// For later, the request's auth user (no big deal if there isn't one)
|
||||
au := request.GetAuthUser(h.SessionManager.Get(r))
|
||||
|
||||
// Check if we're using it as an identity provider; if so, use it to authenticate
|
||||
if p.HasUsage(types.ExternalProviderUsageIdentity) {
|
||||
// Try to login/sign-up external user
|
||||
if user, err = h.AuthService.External(ctx, cred); err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
au = request.NewAuthUser(
|
||||
h.Settings,
|
||||
user,
|
||||
|
||||
@@ -64,17 +77,89 @@ func (h AuthHandlers) handleSuccessfulExternalAuth(w http.ResponseWriter, r *htt
|
||||
false,
|
||||
)
|
||||
|
||||
// auto-complete EmailOTP and TOTP when authenticating via external identity provider
|
||||
req.AuthUser.CompleteEmailOTP()
|
||||
req.AuthUser.CompleteTOTP()
|
||||
// If that's that, cut the flow here
|
||||
if !p.HasUsage(types.ExternalProviderUsageAPI) {
|
||||
h.handle(func(req *request.AuthReq) error {
|
||||
req.AuthUser = au
|
||||
|
||||
req.AuthUser.Save(req.Session)
|
||||
// auto-complete EmailOTP and TOTP when authenticating via external identity provider
|
||||
req.AuthUser.CompleteEmailOTP()
|
||||
req.AuthUser.CompleteTOTP()
|
||||
|
||||
req.AuthUser.Save(req.Session)
|
||||
|
||||
handleSuccessfulAuth(req)
|
||||
return nil
|
||||
})(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we're using it for an API integration; if so, note the access tokens
|
||||
if p.HasUsage(types.ExternalProviderUsageAPI) {
|
||||
if au.User == nil {
|
||||
api.Send(w, r, fmt.Errorf("could not add credentials for user: not authenticated"))
|
||||
return
|
||||
}
|
||||
ctx = auth.SetIdentityToContext(ctx, au.User)
|
||||
|
||||
// Look for existing
|
||||
cc, err := h.CredentialsService.List(ctx, au.User.ID)
|
||||
if err != nil {
|
||||
api.Send(w, r, fmt.Errorf("couldn't fetch user credentials: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Find the existing one
|
||||
kind := fmt.Sprintf("access-%s", cred.Provider)
|
||||
var current *types.Credential
|
||||
for _, c := range cc {
|
||||
if c.Kind == kind && c.OwnerID == au.User.ID {
|
||||
current = c
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Update
|
||||
if current != nil {
|
||||
current.Credentials = cred.AccessToken
|
||||
_, err = h.CredentialsService.Update(ctx, current)
|
||||
if err != nil {
|
||||
api.Send(w, r, fmt.Errorf("couldn't update user credentials: %w", err))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
_, err = h.CredentialsService.Create(ctx, &types.Credential{
|
||||
Label: fmt.Sprintf("access token %s %s", cred.Provider, au.User.Email),
|
||||
Kind: kind,
|
||||
OwnerID: au.User.ID,
|
||||
Credentials: cred.AccessToken,
|
||||
})
|
||||
if err != nil {
|
||||
api.Send(w, r, fmt.Errorf("couldn't create user credentials: %w", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h.handle(func(req *request.AuthReq) error {
|
||||
req.AuthUser = au
|
||||
|
||||
handleSuccessfulAuth(req)
|
||||
return nil
|
||||
})(w, r)
|
||||
}
|
||||
|
||||
func (h AuthHandlers) getProviderConfig(handle string, set []settings.Provider) *settings.Provider {
|
||||
for _, s := range set {
|
||||
if s.Handle == handle {
|
||||
return &s
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h AuthHandlers) handleFailedExternalAuth(w http.ResponseWriter, _ *http.Request, err error) {
|
||||
if strings.Contains(err.Error(), "Error processing your OAuth request: Invalid oauth_verifier parameter") {
|
||||
// Just take user through the same loop again
|
||||
|
||||
@@ -52,6 +52,13 @@ type (
|
||||
ValidateEmailOTP(ctx context.Context, code string) (err error)
|
||||
}
|
||||
|
||||
credentialsService interface {
|
||||
List(ctx context.Context, userID uint64) (cc types.CredentialSet, err error)
|
||||
Create(ctx context.Context, c *types.Credential) (*types.Credential, error)
|
||||
Update(ctx context.Context, c *types.Credential) (*types.Credential, error)
|
||||
Delete(ctx context.Context, userID, credentialsID uint64) (err error)
|
||||
}
|
||||
|
||||
userService interface {
|
||||
FindByAny(ctx context.Context, identifier interface{}) (*types.User, error)
|
||||
Update(context.Context, *types.User) (*types.User, error)
|
||||
@@ -101,18 +108,19 @@ type (
|
||||
AuthHandlers struct {
|
||||
Log *zap.Logger
|
||||
|
||||
Locale localeService
|
||||
Templates templateExecutor
|
||||
OAuth2 oauth2Service
|
||||
SessionManager *request.SessionManager
|
||||
AuthService authService
|
||||
UserService userService
|
||||
ClientService clientService
|
||||
TokenService tokenService
|
||||
DefaultClient *types.AuthClient
|
||||
Opt options.AuthOpt
|
||||
Settings *settings.Settings
|
||||
SamlSPService *saml.SamlSPService
|
||||
Locale localeService
|
||||
Templates templateExecutor
|
||||
OAuth2 oauth2Service
|
||||
SessionManager *request.SessionManager
|
||||
AuthService authService
|
||||
CredentialsService credentialsService
|
||||
UserService userService
|
||||
ClientService clientService
|
||||
TokenService tokenService
|
||||
DefaultClient *types.AuthClient
|
||||
Opt options.AuthOpt
|
||||
Settings *settings.Settings
|
||||
SamlSPService *saml.SamlSPService
|
||||
}
|
||||
|
||||
handlerFn func(req *request.AuthReq) error
|
||||
@@ -344,6 +352,10 @@ func (h *AuthHandlers) enrichTmplData(req *request.AuthReq) interface{} {
|
||||
if _, err := goth.GetProvider(p.Handle); err != nil {
|
||||
continue
|
||||
}
|
||||
// Skipping the ones we don't use for identity
|
||||
if !p.HasUsage(types.ExternalProviderUsageIdentity) {
|
||||
continue
|
||||
}
|
||||
|
||||
out := provider{
|
||||
Label: p.Label,
|
||||
|
||||
@@ -79,6 +79,7 @@ type (
|
||||
RedirectUrl string
|
||||
Secret string
|
||||
Scope string
|
||||
Usage []string
|
||||
}
|
||||
|
||||
BackgroundUI struct {
|
||||
@@ -86,3 +87,13 @@ type (
|
||||
Styles string
|
||||
}
|
||||
)
|
||||
|
||||
func (p Provider) HasUsage(u string) bool {
|
||||
for _, pu := range p.Usage {
|
||||
if pu == u {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ func authAddExternals(ctx context.Context, log *zap.Logger, s store.SettingValue
|
||||
"google",
|
||||
"linkedin",
|
||||
"oidc",
|
||||
"nylas",
|
||||
}
|
||||
|
||||
key, name string
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/api"
|
||||
@@ -51,6 +52,21 @@ type (
|
||||
Set []*userPayload `json:"set"`
|
||||
}
|
||||
|
||||
credentialPayload struct {
|
||||
ID uint64 `json:"credentialsID,string"`
|
||||
OwnerID uint64 `json:"ownerID,string"`
|
||||
Label string `json:"label"`
|
||||
Kind string `json:"kind"`
|
||||
Credentials string `json:"credentials,omitempty"`
|
||||
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
|
||||
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
|
||||
DeletedAt *time.Time `json:"deletedAt,omitempty"`
|
||||
}
|
||||
|
||||
credentialSetPayload []credentialPayload
|
||||
|
||||
userCredentials interface {
|
||||
List(ctx context.Context, userID uint64) (cc types.CredentialSet, err error)
|
||||
Delete(ctx context.Context, userID, credentialsID uint64) (err error)
|
||||
@@ -299,7 +315,33 @@ func (ctrl *User) SessionsRemove(ctx context.Context, r *request.UserSessionsRem
|
||||
}
|
||||
|
||||
func (ctrl *User) ListCredentials(ctx context.Context, r *request.UserListCredentials) (rsp interface{}, err error) {
|
||||
return ctrl.cred.List(ctx, r.UserID)
|
||||
cc, err := ctrl.cred.List(ctx, r.UserID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// @todo some credentials need to be accessible from the front-end; consider
|
||||
// something less error prone, for now this is ok
|
||||
out := make(credentialSetPayload, len(cc))
|
||||
for i, c := range cc {
|
||||
out[i] = credentialPayload{
|
||||
ID: c.ID,
|
||||
OwnerID: c.OwnerID,
|
||||
Label: c.Label,
|
||||
Kind: c.Kind,
|
||||
Credentials: c.Credentials,
|
||||
LastUsedAt: c.LastUsedAt,
|
||||
ExpiresAt: c.ExpiresAt,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
DeletedAt: c.DeletedAt,
|
||||
}
|
||||
if !strings.HasPrefix(out[i].Kind, "access-") {
|
||||
out[i].Credentials = ""
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (ctrl *User) DeleteCredentials(ctx context.Context, r *request.UserDeleteCredentials) (rsp interface{}, err error) {
|
||||
|
||||
@@ -2,7 +2,9 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
internalAuth "github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
@@ -38,7 +40,9 @@ func (svc *credentials) List(ctx context.Context, userID uint64) (cc types.Crede
|
||||
u, err = loadUser(ctx, svc.store, userID)
|
||||
caProps.setUser(u)
|
||||
|
||||
if !svc.ac.CanManageCredentialsOnUser(ctx, u) {
|
||||
// Allow users to manage their own credentials
|
||||
ci := internalAuth.GetIdentityFromContext(ctx)
|
||||
if ci.Identity() != u.ID && !svc.ac.CanManageCredentialsOnUser(ctx, u) {
|
||||
return CredentialsErrNotAllowedToManage()
|
||||
}
|
||||
|
||||
@@ -49,6 +53,78 @@ func (svc *credentials) List(ctx context.Context, userID uint64) (cc types.Crede
|
||||
return cc, svc.recordAction(ctx, caProps, CredentialsActionSearch, err)
|
||||
}
|
||||
|
||||
func (svc *credentials) Create(ctx context.Context, c *types.Credential) (out *types.Credential, err error) {
|
||||
var (
|
||||
u *types.User
|
||||
caProps = &credentialsActionProps{user: &types.User{ID: c.OwnerID}}
|
||||
)
|
||||
|
||||
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
||||
u, err = loadUser(ctx, svc.store, c.OwnerID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
caProps.setUser(u)
|
||||
|
||||
// Allow users to manage their own credentials
|
||||
ci := internalAuth.GetIdentityFromContext(ctx)
|
||||
if ci.Identity() != u.ID && !svc.ac.CanManageCredentialsOnUser(ctx, u) {
|
||||
return CredentialsErrNotAllowedToManage()
|
||||
}
|
||||
|
||||
c.ID = nextID()
|
||||
c.CreatedAt = *now()
|
||||
|
||||
err = store.CreateCredential(ctx, svc.store, c)
|
||||
return
|
||||
})
|
||||
|
||||
return c, svc.recordAction(ctx, caProps, CredentialsActionCreate, err)
|
||||
}
|
||||
|
||||
func (svc *credentials) Update(ctx context.Context, c *types.Credential) (out *types.Credential, err error) {
|
||||
var (
|
||||
u *types.User
|
||||
caProps = &credentialsActionProps{user: &types.User{ID: c.OwnerID}}
|
||||
)
|
||||
|
||||
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
||||
u, err = loadUser(ctx, svc.store, c.OwnerID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
caProps.setUser(u)
|
||||
|
||||
// Allow users to manage their own credentials
|
||||
ci := internalAuth.GetIdentityFromContext(ctx)
|
||||
if ci.Identity() != u.ID && !svc.ac.CanManageCredentialsOnUser(ctx, u) {
|
||||
return CredentialsErrNotAllowedToManage()
|
||||
}
|
||||
|
||||
old, err := store.LookupCredentialByID(ctx, s, c.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
old.OwnerID = c.OwnerID
|
||||
old.Label = c.Label
|
||||
old.Kind = c.Kind
|
||||
old.Credentials = c.Credentials
|
||||
old.Meta = c.Meta
|
||||
old.LastUsedAt = c.LastUsedAt
|
||||
old.ExpiresAt = c.ExpiresAt
|
||||
old.CreatedAt = c.CreatedAt
|
||||
old.UpdatedAt = now()
|
||||
old.DeletedAt = c.DeletedAt
|
||||
|
||||
err = store.UpdateCredential(ctx, s, old)
|
||||
|
||||
return
|
||||
})
|
||||
|
||||
return c, svc.recordAction(ctx, caProps, CredentialsActionUpdate, err)
|
||||
}
|
||||
|
||||
func (svc *credentials) Delete(ctx context.Context, userID, credentialsID uint64) (err error) {
|
||||
var (
|
||||
c *types.Credential
|
||||
@@ -65,7 +141,9 @@ func (svc *credentials) Delete(ctx context.Context, userID, credentialsID uint64
|
||||
return CredentialsErrNotAllowedToManage()
|
||||
}
|
||||
|
||||
if !svc.ac.CanManageCredentialsOnUser(ctx, u) {
|
||||
// Allow users to manage their own credentials
|
||||
ci := internalAuth.GetIdentityFromContext(ctx)
|
||||
if ci.Identity() != u.ID && !svc.ac.CanManageCredentialsOnUser(ctx, u) {
|
||||
return CredentialsErrNotAllowedToManage()
|
||||
}
|
||||
|
||||
|
||||
+43
-2
@@ -11,12 +11,13 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza/server/pkg/locale"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -215,6 +216,46 @@ func CredentialsActionDelete(props ...*credentialsActionProps) *credentialsActio
|
||||
return a
|
||||
}
|
||||
|
||||
// CredentialsActionCreate returns "system:credentials.create" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func CredentialsActionCreate(props ...*credentialsActionProps) *credentialsAction {
|
||||
a := &credentialsAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "system:credentials",
|
||||
action: "create",
|
||||
log: "created {{user}}",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// CredentialsActionUpdate returns "system:credentials.update" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func CredentialsActionUpdate(props ...*credentialsActionProps) *credentialsAction {
|
||||
a := &credentialsAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "system:credentials",
|
||||
action: "update",
|
||||
log: "updated {{user}}",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Error constructors
|
||||
|
||||
@@ -28,6 +28,12 @@ actions:
|
||||
- action: delete
|
||||
log: "deleted {{user}}"
|
||||
|
||||
- action: create
|
||||
log: "created {{user}}"
|
||||
|
||||
- action: update
|
||||
log: "updated {{user}}"
|
||||
|
||||
errors:
|
||||
- error: notFound
|
||||
message: "credentials not found"
|
||||
|
||||
@@ -7,6 +7,9 @@ import (
|
||||
|
||||
const (
|
||||
oidcProviderPrefix = "openid-connect." // must match const in "github.com/cortezaproject/corteza/server/auth/external" external.go
|
||||
|
||||
ExternalProviderUsageIdentity = "identity"
|
||||
ExternalProviderUsageAPI = "api"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -337,6 +340,9 @@ type (
|
||||
IssuerUrl string `json:"-" kv:"issuer"`
|
||||
Weight int `json:"-"`
|
||||
|
||||
// potentially just use home-brew scopes?
|
||||
Usage []string `json:"usage" kv:"usage"`
|
||||
|
||||
Security ExternalAuthProviderSecurity `json:"-" kv:"security,final"`
|
||||
}
|
||||
|
||||
@@ -434,7 +440,7 @@ func (set *ExternalAuthProviderSet) DecodeKV(kv SettingsKV, prefix string) (err
|
||||
}
|
||||
|
||||
// create standard provider set
|
||||
permanent := map[string]bool{"github": true, "facebook": true, "google": true, "linkedin": true}
|
||||
permanent := map[string]bool{"github": true, "facebook": true, "google": true, "linkedin": true, "nylas": true}
|
||||
// and make a working copy
|
||||
providers := make(map[string]bool)
|
||||
for k, v := range permanent {
|
||||
@@ -482,6 +488,8 @@ func (set *ExternalAuthProviderSet) DecodeKV(kv SettingsKV, prefix string) (err
|
||||
switch p.Handle {
|
||||
case "github":
|
||||
p.Label = "GitHub"
|
||||
case "nylas":
|
||||
p.Label = "Nylas"
|
||||
case "linkedin":
|
||||
p.Label = "LinkedIn"
|
||||
case "corteza-iam", "corteza", "corteza-one":
|
||||
|
||||
Reference in New Issue
Block a user