Skip to content

Commit

Permalink
feat(auth-app): fixed response format
Browse files Browse the repository at this point in the history
Signed-off-by: jkoberg <[email protected]>
  • Loading branch information
kobergj committed Aug 7, 2024
1 parent d483234 commit c090995
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 49 deletions.
5 changes: 5 additions & 0 deletions changelog/unreleased/auth-app-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Enhancement: Add an API to auth-app service

Adds an API to create, list and delete app tokens. Includes an impersonification feature for migration scenarios.

https://github.com/owncloud/ocis/pull/9755
15 changes: 14 additions & 1 deletion services/auth-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,23 @@ PROXY_ENABLE_APP_AUTH=true # mandatory, allow app authentication. In case o

## App Tokens

App Tokens are used to authenticate 3rd party access via https like when using curl (apps) to access an API endpoint. These apps need to authenticate themselves as no logged in user authenticates the request. To be able to use an app token, one must first create a token via the cli. Replace the `user-name` with an existing user. For the `token-expiration`, you can use any time abbreviation from the following list: `h, m, s`. Examples: `72h` or `1h` or `1m` or `1s.` Default is `72h`.
App Tokens are used to authenticate 3rd party access via https like when using curl (apps) to access an API endpoint. These apps need to authenticate themselves as no logged in user authenticates the request. To be able to use an app token, one must first create a token. There are different options of creating a token.

### Via CLI (dev only)

Replace the `user-name` with an existing user. For the `token-expiration`, you can use any time abbreviation from the following list: `h, m, s`. Examples: `72h` or `1h` or `1m` or `1s.` Default is `72h`.

```bash
ocis auth-app create --user-name={user-name} --expiration={token-expiration}
```

Once generated, these tokens can be used to authenticate requests to ocis. They are passed as part of the request as `Basic Auth` header.

### Via API

The `auth-app` service provides an API to create (POST), list (GET) and delete (DELETE) tokens at `/auth-app/tokens`.

### Via Impersonation API

When setting the environment variable `AUTH_APP_ENABLE_IMPERSONATION` to `true`, admins will be able to use the `/auth-app/tokens` endpoint to create tokens for other users. This is crucial for migration scenarios,
but should not be used on a productive system.
122 changes: 74 additions & 48 deletions services/auth-app/pkg/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ import (
"google.golang.org/grpc/metadata"
)

// AuthAppToken represents an app token.
type AuthAppToken struct {
Token string `json:"token"`
ExpirationDate time.Time `json:"expiration_date"`
CreatedDate time.Time `json:"created_date"`
Label string `json:"label"`
}

// AuthAppService defines the service interface.
type AuthAppService struct {
log log.Logger
Expand All @@ -42,7 +50,6 @@ func NewAuthAppService(opts ...Option) (*AuthAppService, error) {
}

r := roles.NewManager(
// TODO: caching?
roles.Logger(o.Logger),
roles.RoleService(o.RoleClient),
)
Expand Down Expand Up @@ -71,153 +78,163 @@ func (a *AuthAppService) ServeHTTP(w http.ResponseWriter, r *http.Request) {

// HandleCreate handles the creation of app tokens
func (a *AuthAppService) HandleCreate(w http.ResponseWriter, r *http.Request) {
ctx := getContext(r)
sublog := a.log.With().Str("actor", ctxpkg.ContextMustGetUser(ctx).GetId().GetOpaqueId()).Logger()

gwc, err := a.gws.Next()
if err != nil {
http.Error(w, "error getting gateway client", http.StatusInternalServerError)
sublog.Error().Err(err).Msg("error getting gateway client")
w.WriteHeader(http.StatusInternalServerError)
return
}

ctx := getContext(r)

q := r.URL.Query()
expiry, err := time.ParseDuration(q.Get("expiry"))
if err != nil {
a.log.Info().Err(err).Msg("error parsing expiry")
sublog.Info().Err(err).Str("duration", q.Get("expiry")).Msg("error parsing expiry")
http.Error(w, "error parsing expiry. Use e.g. 30m or 72h", http.StatusBadRequest)
return
}

label := "Generated via API"
cid := buildClientID(q.Get("userID"), q.Get("userName"))
if cid != "" {
if !a.cfg.AllowImpersonation {
a.log.Error().Msg("impersonation is not allowed")
sublog.Error().Msg("impersonation is not allowed")
http.Error(w, "impersonation is not allowed", http.StatusForbidden)
return
}
ok, err := isAdmin(ctx, a.r)
if err != nil {
a.log.Error().Err(err).Msg("error checking if user is admin")
http.Error(w, "internal server error", http.StatusInternalServerError)
sublog.Error().Err(err).Msg("error checking if user is admin")
w.WriteHeader(http.StatusInternalServerError)
return
}
if !ok {
a.log.Error().Msg("user is not admin")
http.Error(w, "forbidden", http.StatusForbidden)
sublog.Error().Msg("user is not admin")
w.WriteHeader(http.StatusForbidden)
return
}
ctx, err = a.authenticateUser(cid, gwc)
if err != nil {
a.log.Error().Err(err).Msg("error authenticating user")
http.Error(w, "error authenticating user", http.StatusInternalServerError)
sublog.Error().Err(err).Msg("error authenticating user")
w.WriteHeader(http.StatusInternalServerError)
return
}

label = "Generated via Impersonation API"
}

scopes, err := scope.AddOwnerScope(map[string]*authpb.Scope{})
if err != nil {
a.log.Error().Err(err).Msg("error adding owner scope")
http.Error(w, "error adding owner scope", http.StatusInternalServerError)
sublog.Error().Err(err).Msg("error adding owner scope")
w.WriteHeader(http.StatusInternalServerError)
return
}

res, err := gwc.GenerateAppPassword(ctx, &applications.GenerateAppPasswordRequest{
TokenScope: scopes,
Label: "Generated via API",
Label: label,
Expiration: utils.TimeToTS(time.Now().Add(expiry)),
})
if err != nil {
a.log.Error().Err(err).Msg("error generating app password")
http.Error(w, "error generating app password", http.StatusInternalServerError)
sublog.Error().Err(err).Msg("error generating app password")
w.WriteHeader(http.StatusInternalServerError)
return
}
if res.GetStatus().GetCode() != rpc.Code_CODE_OK {
a.log.Error().Str("status", res.GetStatus().GetCode().String()).Msg("error generating app password")
http.Error(w, "error generating app password: "+res.GetStatus().GetMessage(), http.StatusInternalServerError)
sublog.Error().Str("status", res.GetStatus().GetCode().String()).Msg("error generating app password")
w.WriteHeader(http.StatusInternalServerError)
return
}

b, err := json.Marshal(res.GetAppPassword())
b, err := json.Marshal(convert(res.GetAppPassword()))
if err != nil {
a.log.Error().Err(err).Msg("error marshaling app password")
http.Error(w, "error marshaling app password", http.StatusInternalServerError)
sublog.Error().Err(err).Msg("error marshaling app password")
w.WriteHeader(http.StatusInternalServerError)
return
}

if _, err := w.Write(b); err != nil {
a.log.Error().Err(err).Msg("error writing response")
http.Error(w, "error writing response", http.StatusInternalServerError)
return
sublog.Error().Err(err).Msg("error writing response")
}
w.WriteHeader(http.StatusOK)
}

// HandleList handles listing of app tokens
func (a *AuthAppService) HandleList(w http.ResponseWriter, r *http.Request) {
ctx := getContext(r)
sublog := a.log.With().Str("actor", ctxpkg.ContextMustGetUser(ctx).GetId().GetOpaqueId()).Logger()

gwc, err := a.gws.Next()
if err != nil {
a.log.Error().Err(err).Msg("error getting gateway client")
http.Error(w, "error getting gateway client", http.StatusInternalServerError)
sublog.Error().Err(err).Msg("error getting gateway client")
w.WriteHeader(http.StatusInternalServerError)
return
}

ctx := getContext(r)

res, err := gwc.ListAppPasswords(ctx, &applications.ListAppPasswordsRequest{})
if err != nil {
a.log.Error().Err(err).Msg("error listing app passwords")
http.Error(w, "error listing app passwords", http.StatusInternalServerError)
sublog.Error().Err(err).Msg("error listing app passwords")
w.WriteHeader(http.StatusInternalServerError)
return
}

if res.GetStatus().GetCode() != rpc.Code_CODE_OK {
a.log.Error().Str("status", res.GetStatus().GetCode().String()).Msg("error listing app passwords")
http.Error(w, "error listing app passwords: "+res.GetStatus().GetMessage(), http.StatusInternalServerError)
sublog.Error().Str("status", res.GetStatus().GetCode().String()).Msg("error listing app passwords")
w.WriteHeader(http.StatusInternalServerError)
return
}

b, err := json.Marshal(res.GetAppPasswords())
tokens := make([]AuthAppToken, 0, len(res.GetAppPasswords()))
for _, ap := range res.GetAppPasswords() {
tokens = append(tokens, convert(ap))
}

b, err := json.Marshal(tokens)
if err != nil {
a.log.Error().Err(err).Msg("error marshaling app passwords")
http.Error(w, "error marshaling app passwords", http.StatusInternalServerError)
sublog.Error().Err(err).Msg("error marshaling app passwords")
w.WriteHeader(http.StatusInternalServerError)
return
}

if _, err := w.Write(b); err != nil {
a.log.Error().Err(err).Msg("error writing response")
http.Error(w, "error writing response", http.StatusInternalServerError)
sublog.Error().Err(err).Msg("error writing response")
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}

// HandleDelete handles deletion of app tokens
func (a *AuthAppService) HandleDelete(w http.ResponseWriter, r *http.Request) {
ctx := getContext(r)
sublog := a.log.With().Str("actor", ctxpkg.ContextMustGetUser(ctx).GetId().GetOpaqueId()).Logger()

gwc, err := a.gws.Next()
if err != nil {
a.log.Error().Err(err).Msg("error getting gateway client")
http.Error(w, "error getting gateway client", http.StatusInternalServerError)
sublog.Error().Err(err).Msg("error getting gateway client")
w.WriteHeader(http.StatusInternalServerError)
return
}

ctx := getContext(r)

pw := r.URL.Query().Get("token")
if pw == "" {
a.log.Info().Msg("missing token")
http.Error(w, "missing token", http.StatusBadRequest)
sublog.Info().Msg("missing token")
http.Error(w, "missing auth-app token. Set 'token' parameter", http.StatusBadRequest)
return
}

res, err := gwc.InvalidateAppPassword(ctx, &applications.InvalidateAppPasswordRequest{Password: pw})
if err != nil {
a.log.Error().Err(err).Msg("error invalidating app password")
http.Error(w, "error invalidating app password", http.StatusInternalServerError)
sublog.Error().Err(err).Msg("error invalidating app password")
w.WriteHeader(http.StatusInternalServerError)
return
}

if res.GetStatus().GetCode() != rpc.Code_CODE_OK {
a.log.Error().Str("status", res.GetStatus().GetCode().String()).Msg("error invalidating app password")
http.Error(w, "error invalidating app password: "+res.GetStatus().GetMessage(), http.StatusInternalServerError)
sublog.Error().Str("status", res.GetStatus().GetCode().String()).Msg("error invalidating app password")
w.WriteHeader(http.StatusInternalServerError)
return
}

Expand Down Expand Up @@ -289,3 +306,12 @@ func isAdmin(ctx context.Context, rm *roles.Manager) (bool, error) {
// check if permission is present in roles of the authenticated account
return rm.FindPermissionByID(ctx, roleIDs, settings.AccountManagementPermissionID) != nil, nil
}

func convert(ap *applications.AppPassword) AuthAppToken {
return AuthAppToken{
Token: ap.GetPassword(),
ExpirationDate: utils.TSToTime(ap.GetExpiration()),
CreatedDate: utils.TSToTime(ap.GetCtime()),
Label: ap.GetLabel(),
}
}

0 comments on commit c090995

Please sign in to comment.