Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: SSO Improvement - alter user_sessions table to include access token, implement CRUD ops, GET, POST, PATCH APIs and det token CLIs #9867

Merged
merged 57 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
75fbfda
create table and crud ops
ShreyaLnuHpe Aug 26, 2024
fee92d7
add crud ops in go
ShreyaLnuHpe Aug 28, 2024
5eaa712
integ test wip
ShreyaLnuHpe Aug 28, 2024
0e78f52
integ testing
ShreyaLnuHpe Aug 28, 2024
a93a64a
based on comments
ShreyaLnuHpe Aug 28, 2024
4a85904
API structure
ShreyaLnuHpe Aug 29, 2024
a8031a3
lint go corrections
ShreyaLnuHpe Aug 30, 2024
a8a5ea9
typo
ShreyaLnuHpe Aug 30, 2024
1f689b1
lint check
ShreyaLnuHpe Aug 30, 2024
33c874f
changes per swagger
ShreyaLnuHpe Aug 30, 2024
99b79cf
lint proto
ShreyaLnuHpe Aug 30, 2024
10e2648
add admin nonadmin user auth
ShreyaLnuHpe Aug 31, 2024
feb7d09
add E2E test for POST API
ShreyaLnuHpe Sep 4, 2024
c9ea59b
add GET CRUD ops
ShreyaLnuHpe Sep 4, 2024
293a21d
add GET API structure
ShreyaLnuHpe Sep 4, 2024
a139398
add test-intg for GET API
ShreyaLnuHpe Sep 4, 2024
bc3b7d9
based on comments
ShreyaLnuHpe Sep 6, 2024
cd7dbeb
DELETE API and tests
ShreyaLnuHpe Sep 6, 2024
2c50362
based on brainstoring and building op1
ShreyaLnuHpe Sep 10, 2024
607f0e4
remove llt table
ShreyaLnuHpe Sep 10, 2024
d432aa0
mased on comments
ShreyaLnuHpe Sep 12, 2024
736a9f5
add CLIs
ShreyaLnuHpe Sep 16, 2024
6000477
chore: add revoke token permission
corban-beaird Sep 16, 2024
47d87af
chore: drop WorkspaceCreator from set of roles able to revoke tokens
corban-beaird Sep 16, 2024
7bae276
chore: add rbac token permissions
ShreyaLnuHpe Sep 17, 2024
c2427ba
chore: correct check errors & remove token filter from GetALL
ShreyaLnuHpe Sep 17, 2024
6d9c3ff
refactor: clean up migration for readability
corban-beaird Sep 17, 2024
825af40
Merge branch 'main' into shreya/createTable
ShreyaLnuHpe Sep 18, 2024
05e05a5
fix: lint errors
ShreyaLnuHpe Sep 18, 2024
152f3d3
feat: token description in CLI and authentication using token
corban-beaird Sep 18, 2024
13c4a57
chore: clean up linter issues
corban-beaird Sep 18, 2024
3515ffb
changes to revoke in postgres
ShreyaLnuHpe Sep 18, 2024
a28fbf7
feat: added API support for token description updates & unified revok…
corban-beaird Sep 18, 2024
4d9f9f8
chore: clean up logging
corban-beaird Sep 18, 2024
6f305b5
chore: update table, update cli, getAccessToken, getAllAccessTokens, …
ShreyaLnuHpe Sep 19, 2024
ef03c2a
chore: pretty print cli and unify post api and permissions
ShreyaLnuHpe Sep 19, 2024
c49657e
feat: describe cli to take multiple usernames
ShreyaLnuHpe Sep 20, 2024
c0262e6
fix: authentication of multi login, add tokenType while rendering
ShreyaLnuHpe Sep 24, 2024
f67cffe
fix: change name from long-lived to access token and RBAC access only…
ShreyaLnuHpe Sep 24, 2024
784e69d
chore: refactor Get and Create AccessToken API, add filter option, up…
ShreyaLnuHpe Oct 2, 2024
3fd0e76
chore: Revoke Access Tokens when a User is Deactivated (#10013)
ShreyaLnuHpe Oct 4, 2024
7bd852b
chore: refactor access tokens CLI commands (#10012)
ShreyaLnuHpe Oct 10, 2024
ae46bb6
Merge branch 'main' into shreya/createTable
ShreyaLnuHpe Oct 11, 2024
6d70ed4
chore: changes per merge with main
ShreyaLnuHpe Oct 11, 2024
5d887c4
chore: un-nest CLIs, APIs, permissions and DB layer (#10041)
ShreyaLnuHpe Oct 11, 2024
bde92d2
chore: remove unwanted part from user.py
ShreyaLnuHpe Oct 11, 2024
dcbf014
chore: add release note, minor changes per comments
ShreyaLnuHpe Oct 11, 2024
95e60f2
chore: changes per discussion with ModelDev
ShreyaLnuHpe Oct 14, 2024
b385f1e
remove commented lines
ShreyaLnuHpe Oct 14, 2024
51e1e9d
chore: fix as per comments
ShreyaLnuHpe Oct 15, 2024
72596e7
Merge branch 'main' into shreya/createTable
ShreyaLnuHpe Oct 15, 2024
1b8f9e6
chore: add proto files
ShreyaLnuHpe Oct 15, 2024
23b265a
chore: minor fixes
ShreyaLnuHpe Oct 15, 2024
6af1292
chore: change docs and expiration days
ShreyaLnuHpe Oct 15, 2024
0370c6b
chore: changes as per comments
ShreyaLnuHpe Oct 17, 2024
2e464a3
fix: consider till seconds during lifespan comparison
ShreyaLnuHpe Oct 17, 2024
e386613
chore: create `TokenCreator` role with permissions to VIEW / CREATE /…
ShreyaLnuHpe Oct 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions master/internal/user/postgres_long_lived_tokens.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package user

import (
"context"
"time"

"github.com/o1egl/paseto"
"github.com/uptrace/bun"
"gopkg.in/guregu/null.v3"

"github.com/pkg/errors"

"github.com/determined-ai/determined/master/internal/db"
"github.com/determined-ai/determined/master/pkg/model"
)

const (
// TokenExpirationDuration is how long a newly created long lived token is valid.
ShreyaLnuHpe marked this conversation as resolved.
Show resolved Hide resolved
TokenExpirationDuration = 30 * 24 * time.Hour
)

// LongLivedTokenOption is the return type for WithTokenExpiresAt helper function.
// It takes a pointer to model.LongLivedToken and modifies it.
// It’s used to apply optional settings to the LongLivedToken object.
type LongLivedTokenOption func(f *model.LongLivedToken)

// WithTokenExpiresAt function will add specified expiresAt (if any) to the long lived token table.
func WithTokenExpiresAt(expiresAt *time.Time) LongLivedTokenOption {
corban-beaird marked this conversation as resolved.
Show resolved Hide resolved
return func(s *model.LongLivedToken) {
s.ExpiresAt = *expiresAt
}
}

// CreateLongLivedToken creates a row in the long lived token table.
func CreateLongLivedToken(ctx context.Context, user *model.User, opts ...LongLivedTokenOption) (string, error) {
// Populate the default values in the model.
longLivedToken := &model.LongLivedToken{
UserID: user.ID,
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(TokenExpirationDuration),
}

// Update the optional ExpiresAt field (if passed)
for _, opt := range opts {
opt(longLivedToken)
}

err := db.Bun().RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
// A new row is inserted into the long_lived_token table, and the ID of the
// inserted row is returned and stored in longLivedToken.ID.
_, err := db.Bun().NewInsert().
Model(longLivedToken).
Column("user_id", "expires_at", "created_at").
Returning("id").
Exec(ctx, &longLivedToken.ID)
if err != nil {
return err
}

// A Paseto token is generated using the longLivedToken object and the private key.
v2 := paseto.NewV2()
privateKey := db.GetTokenKeys().PrivateKey
token, err := v2.Sign(privateKey, longLivedToken, nil)
if err != nil {
return errors.Wrap(err, "failed to generate user authentication token")
ShreyaLnuHpe marked this conversation as resolved.
Show resolved Hide resolved
}

// The token is hashed using model.HashPassword
longLivedToken.TokenValue = token
hashedToken, err := model.HashPassword(token)
corban-beaird marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return errors.Wrap(err, "error updating user long lived token")
}
longLivedToken.TokenValueHash = null.StringFrom(hashedToken)

// The TokenValueHash is updated in the database.
_, err = db.Bun().NewUpdate().
Model(longLivedToken).
Column("token_value_hash").
Where("id = (?)", longLivedToken.ID).
Exec(ctx)
if err != nil {
return err
}

return nil
})
if err != nil {
return "", err
}

return longLivedToken.TokenValue, nil
}

// DeleteLongLivenTokenByUserID deletes long lived token if found.
// If not found, the err will be nil, and the number of affected rows will be zero.
func DeleteLongLivenTokenByUserID(ctx context.Context, userID model.UserID) error {
_, err := db.Bun().NewDelete().
Table("long_lived_tokens").
Where("user_id = ?", userID).
Exec(ctx)
return err
}

// DeleteLongLivenTokenByToken deletes long lived token if found.
func DeleteLongLivenTokenByToken(ctx context.Context, token string) error {
v2 := paseto.NewV2()
var longLivedToken model.LongLivedToken
// Verification will fail when using external token (Jwt instead of Paseto).
// Currently passing Paseto token.
if err := v2.Verify(token, db.GetTokenKeys().PublicKey, &longLivedToken, nil); err != nil {
return nil //nolint: nilerr
}
return DeleteLongLivedTokenByTokenID(ctx, longLivedToken.ID)
}

// DeleteLongLivedTokenByTokenID deletes the long lived token with the given token ID.
// If not found, the err will be nil, and the number of affected rows will be zero.
func DeleteLongLivedTokenByTokenID(ctx context.Context, longLivedTokenID model.TokenID) error {
_, err := db.Bun().NewDelete().
Table("long_lived_tokens").
Where("id = ?", longLivedTokenID).
Exec(ctx)
return err
}
ShreyaLnuHpe marked this conversation as resolved.
Show resolved Hide resolved
81 changes: 81 additions & 0 deletions master/internal/user/postgres_long_lived_tokens_intg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//go:build integration
// +build integration

package user

import (
"context"
"testing"
"time"

"github.com/o1egl/paseto"
"github.com/stretchr/testify/require"

"github.com/determined-ai/determined/master/internal/db"
"github.com/determined-ai/determined/master/pkg/model"
)

func TestCreateLongLivedToken(t *testing.T) {
user, err := addTestUser(nil)
require.NoError(t, err)

// Add a LongLivedToken.
token, err := CreateLongLivedToken(context.TODO(), user)
require.NoError(t, err)
require.NotNil(t, token)

exists, err := db.Bun().NewSelect().Table("long_lived_tokens").
Where("user_id = ?", user.ID).Exists(context.TODO())
require.True(t, exists)
require.NoError(t, err)
ShreyaLnuHpe marked this conversation as resolved.
Show resolved Hide resolved
}

func TestCreateLongLivedTokenHasExpiresAt(t *testing.T) {
user, err := addTestUser(nil)
require.NoError(t, err)

// Add a LongLivedToken with custom (Now() + 3 Months) Expiry Time.
expiresAt := time.Now().Add(TokenExpirationDuration * 3)
ShreyaLnuHpe marked this conversation as resolved.
Show resolved Hide resolved
token, err := CreateLongLivedToken(context.TODO(), user, WithTokenExpiresAt(&expiresAt))
require.NoError(t, err)
require.NotNil(t, token)

var restoredSession model.LongLivedToken
v2 := paseto.NewV2()
err = v2.Verify(token, db.GetTokenKeys().PublicKey, &restoredSession, nil)
require.NoError(t, err)

// Strip monotonic clock readings by using time.Equal
require.True(t, restoredSession.ExpiresAt.Equal(expiresAt))

exists, err := db.Bun().NewSelect().Table("long_lived_tokens").
Where("user_id = ?", user.ID).Exists(context.TODO())
require.True(t, exists)
require.NoError(t, err)
}

func TestDeleteLongLivenTokenByUserID(t *testing.T) {
userID, _, _, err := addTestSession()
require.NoError(t, err)

err = DeleteLongLivenTokenByUserID(context.TODO(), userID)
require.NoError(t, err)

exists, err := db.Bun().NewSelect().Table("long_lived_tokens").
Where("user_id = ?", userID).Exists(context.TODO())
require.False(t, exists)
require.NoError(t, err)
}

func TestDeleteLongLivenTokenByToken(t *testing.T) {
userID, _, token, err := addTestSession()
require.NoError(t, err)

err = DeleteLongLivenTokenByToken(context.TODO(), token)
require.NoError(t, err)

exists, err := db.Bun().NewSelect().Table("long_lived_tokens").
Where("user_id = ?", userID).Exists(context.TODO())
require.False(t, exists)
require.NoError(t, err)
}
14 changes: 14 additions & 0 deletions master/pkg/model/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,17 @@ func HashPassword(password string) (string, error) {
}
return string(passwordHash), nil
}

// TokenID is the type for user token IDs.
type TokenID int

// LongLivedToken corresponds to a row in the "long_lived_tokens" DB table.
corban-beaird marked this conversation as resolved.
Show resolved Hide resolved
type LongLivedToken struct {
bun.BaseModel `bun:"table:long_lived_tokens"`
ID TokenID `db:"id" json:"id"`
UserID UserID `db:"user_id" json:"user_id"`
TokenValue string `bun:"-"`
TokenValueHash null.String `db:"token_value_hash" json:"token_value_hash"`
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE TABLE public.long_lived_tokens (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id integer NOT NULL,
token_value_hash text, -- Hash of the token value for secure storage
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES public.users(id) ON DELETE CASCADE
);

-- Adding an index on token_value_hash for faster lookups
CREATE INDEX idx_token_value_hash ON long_lived_tokens(token_value_hash);
Loading